use std::{
fs::File,
io::{Seek, Write},
os::fd::OwnedFd,
time::Instant,
};
use libseccomp::ScmpArch;
use nix::errno::Errno;
use crate::{
config::*,
fs::{create_memfd, seal_memfd},
rng::{fillrandom, randint},
};
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub(crate) struct sysinfo32 {
uptime: i32,
loads: [u32; 3],
totalram: u32,
freeram: u32,
sharedram: u32,
bufferram: u32,
totalswap: u32,
freeswap: u32,
procs: u16,
pad: u16,
totalhigh: u32,
freehigh: u32,
mem_unit: u32,
_f: [u8; 8],
}
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub(crate) struct sysinfo64 {
uptime: i64,
loads: [u64; 3],
totalram: u64,
freeram: u64,
sharedram: u64,
bufferram: u64,
totalswap: u64,
freeswap: u64,
procs: u16,
pad: u16,
totalhigh: u64,
freehigh: u64,
mem_unit: u32,
_f: [u8; 0],
}
pub(crate) enum SysInfo {
S32(sysinfo32),
S64(sysinfo64),
}
#[inline]
fn fill_pod_random<T>(pod: &mut T) -> Result<(), Errno> {
let siz = std::mem::size_of::<T>();
let ptr = (pod as *mut T) as *mut u8;
let bytes = unsafe { std::slice::from_raw_parts_mut(ptr, siz) };
fillrandom(bytes)
}
macro_rules! init_sysinfo {
($info:ident, $U:ty, $S:ty) => {{
fill_pod_random(&mut $info)?;
$info.mem_unit = 1;
$info.totalhigh = 0 as $U;
$info.freehigh = 0 as $U;
$info.totalswap = 0 as $U;
$info.freeswap = 0 as $U;
const MIN_RAM: $U = 0x0080_0000 as $U; const MAX_RAM: $U = 0xFFFF_FFFF as $U;
let mut totalram: $U = $info.totalram % (MAX_RAM - MIN_RAM + 1 as $U) + MIN_RAM;
totalram = if totalram.is_power_of_two() {
totalram
} else {
totalram.checked_next_power_of_two().unwrap_or(MAX_RAM) >> 1
};
totalram = totalram.clamp(MIN_RAM, MAX_RAM);
$info.totalram = totalram;
let mut freeram: $U = $info.freeram % ($info.totalram + 1 as $U);
freeram = if freeram.is_power_of_two() {
freeram
} else {
freeram
.checked_next_power_of_two()
.unwrap_or($info.totalram)
>> 1
};
$info.freeram = freeram.min($info.totalram);
let mut sharedram: $U = $info.sharedram % ($info.totalram + 1 as $U);
sharedram = if sharedram.is_power_of_two() {
sharedram
} else {
sharedram
.checked_next_power_of_two()
.unwrap_or($info.totalram)
>> 1
};
$info.sharedram = sharedram.min($info.totalram);
let mut bufferram: $U = $info.bufferram % ($info.totalram + 1 as $U);
bufferram = if bufferram.is_power_of_two() {
bufferram
} else {
bufferram
.checked_next_power_of_two()
.unwrap_or($info.totalram)
>> 1
};
$info.bufferram = bufferram.min($info.totalram);
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_possible_wrap)]
{
$info.uptime = RAND_TIMER().uptime() as $S;
}
const LOAD_SCALE: $U = 0x1_0000 as $U;
const MAX_LOAD: $U = (LOAD_SCALE * 16) as $U;
for ld in &mut $info.loads {
*ld %= MAX_LOAD + 1 as $U;
}
const MIN_PROCS: u16 = 2;
const MAX_PROCS: u16 = 0x8000;
$info.procs = ($info.procs % (MAX_PROCS - MIN_PROCS + 1)) + MIN_PROCS;
Ok::<(), Errno>(())
}};
}
impl SysInfo {
#[inline]
pub fn new(arch: ScmpArch) -> Result<Self, Errno> {
match arch {
ScmpArch::X8664
| ScmpArch::X32 | ScmpArch::Aarch64
| ScmpArch::Mips64
| ScmpArch::Mipsel64
| ScmpArch::Ppc64
| ScmpArch::Ppc64Le
| ScmpArch::Riscv64
| ScmpArch::S390X
| ScmpArch::Loongarch64 => Self::new64(),
ScmpArch::X86
| ScmpArch::Arm
| ScmpArch::M68k
| ScmpArch::Mips
| ScmpArch::Mipsel
| ScmpArch::Mips64N32 | ScmpArch::Mipsel64N32 | ScmpArch::S390 => Self::new32(),
_ => Err(Errno::ENOSYS),
}
}
pub(crate) fn new32() -> Result<Self, Errno> {
let mut info: sysinfo32 = unsafe { std::mem::zeroed() };
init_sysinfo!(info, u32, i32)?;
Ok(SysInfo::S32(info))
}
pub fn new64() -> Result<Self, Errno> {
let mut info: sysinfo64 = unsafe { std::mem::zeroed() };
init_sysinfo!(info, u64, i64)?;
Ok(SysInfo::S64(info))
}
#[inline]
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::S32(s) => unsafe {
std::slice::from_raw_parts(
(s as *const _) as *const u8,
std::mem::size_of::<sysinfo32>(),
)
},
Self::S64(s) => unsafe {
std::slice::from_raw_parts(
(s as *const _) as *const u8,
std::mem::size_of::<sysinfo64>(),
)
},
}
}
}
pub struct RandTimer {
pub start: Instant,
pub uptime_offset: u64,
pub idle_offset: u64,
}
impl RandTimer {
pub fn new() -> Result<Self, Errno> {
Ok(Self {
start: Instant::now(),
uptime_offset: randint(1..=0xFF_FFFF)?,
idle_offset: randint(1..=0xFF_FFFF)?,
})
}
pub fn uptime(&self) -> u64 {
let elapsed = self.start.elapsed().as_secs();
elapsed.wrapping_add(self.uptime_offset)
}
pub fn idle(&self) -> u64 {
let elapsed = self.start.elapsed().as_secs();
elapsed.wrapping_add(self.idle_offset)
}
pub fn proc(&self) -> String {
let elapsed = self.start.elapsed().as_secs();
format!(
"{}.{:02} {}.{:02}\n",
elapsed.wrapping_add(self.uptime_offset),
self.uptime_offset % 100,
elapsed.wrapping_add(self.idle_offset),
self.idle_offset % 100
)
}
pub fn proc_fd(&self) -> Result<OwnedFd, Errno> {
let repr = self.proc();
let data = repr.as_bytes();
let fd = create_memfd(b"syd-proc-uptime\0", *SAFE_MFD_FLAGS)?;
let mut file = File::from(fd);
file.write_all(data).or(Err(Errno::EIO))?;
file.rewind().or(Err(Errno::EIO))?;
seal_memfd(&file)?;
Ok(file.into())
}
}
#[cfg(test)]
mod tests {
use std::{thread, time::Duration};
use super::RandTimer;
#[test]
fn test_basic_creation() {
let rt = RandTimer::new().expect("RandTimer creation failed");
let _ = rt.uptime();
let _ = rt.idle();
}
#[test]
fn test_monotonic_increase() {
let rt = RandTimer::new().expect("RandTimer creation failed");
let before_uptime = rt.uptime();
let before_idle = rt.idle();
thread::sleep(Duration::from_millis(10));
let after_uptime = rt.uptime();
let after_idle = rt.idle();
assert!(
after_uptime >= before_uptime,
"Uptime decreased from {} to {}",
before_uptime,
after_uptime
);
assert!(
after_idle >= before_idle,
"Idle time decreased from {} to {}",
before_idle,
after_idle
);
}
#[test]
fn test_rapid_fire() {
let rt = RandTimer::new().expect("RandTimer creation failed");
for _ in 0..10_000 {
let _ = rt.uptime();
let _ = rt.idle();
}
}
#[test]
fn test_repeated_creation() {
for _ in 0..1000 {
let rt = RandTimer::new().expect("RandTimer creation failed");
assert_ne!(
rt.uptime(),
0,
"Uptime offset might be zero too often, suspicious RNG."
);
assert_ne!(
rt.idle(),
0,
"Idle offset might be zero too often, suspicious RNG."
);
}
}
#[test]
fn test_concurrency() {
let threads = 8;
let iterations = 2000;
let mut handles = Vec::new();
for _ in 0..threads {
handles.push(thread::spawn(move || {
for _ in 0..iterations {
let rt = RandTimer::new().unwrap();
let _ = rt.uptime();
let _ = rt.idle();
}
}));
}
for handle in handles {
handle.join().expect("Thread panic in concurrency test");
}
}
#[test]
fn test_uptime_offset_variability() {
let iterations = 30;
let mut offsets = Vec::new();
for _ in 0..iterations {
let rt = RandTimer::new().expect("RandTimer creation failed");
let elapsed = rt.start.elapsed().as_secs();
let offset_guess = rt.uptime().wrapping_sub(elapsed);
offsets.push(offset_guess);
}
let all_same = offsets.windows(2).all(|w| w[0] == w[1]);
assert!(
!all_same,
"All uptime offsets identical over {} RandTimer creations, suspicious RNG!",
iterations
);
}
#[test]
fn test_idle_offset_variability() {
let iterations = 30;
let mut offsets = Vec::new();
for _ in 0..iterations {
let rt = RandTimer::new().expect("RandTimer creation failed");
let elapsed = rt.start.elapsed().as_secs();
let offset_guess = rt.idle().wrapping_sub(elapsed);
offsets.push(offset_guess);
}
let all_same = offsets.windows(2).all(|w| w[0] == w[1]);
assert!(!all_same, "All idle offsets identical, suspicious RNG!");
}
#[test]
fn test_uptime_wrapping() {
let mut rt = RandTimer::new().expect("RandTimer creation failed");
rt.uptime_offset = u64::MAX - 1;
let before = rt.uptime();
thread::sleep(Duration::from_secs(1));
let after = rt.uptime();
assert!(
after != before,
"No change in uptime after forcing near-max offset + 1s sleep!"
);
}
#[test]
fn test_idle_wrapping() {
let mut rt = RandTimer::new().expect("RandTimer creation failed");
rt.idle_offset = u64::MAX - 1;
let before = rt.idle();
thread::sleep(Duration::from_secs(1));
let after = rt.idle();
assert!(
after != before,
"No change in idle time after forcing near-max offset + 1s sleep!"
);
}
#[test]
fn test_force_offsets_zero() {
let mut rt = RandTimer::new().expect("RandTimer creation failed");
rt.uptime_offset = 0;
rt.idle_offset = 0;
let t1_up = rt.uptime();
let t1_idle = rt.idle();
thread::sleep(Duration::from_millis(5));
let t2_up = rt.uptime();
let t2_idle = rt.idle();
assert!(
t2_up >= t1_up,
"Uptime decreased with zero offset: {} to {}",
t1_up,
t2_up
);
assert!(
t2_idle >= t1_idle,
"Idle decreased with zero offset: {} to {}",
t1_idle,
t2_idle
);
}
#[test]
fn test_large_idle_offset_small_sleep() {
let mut rt = RandTimer::new().expect("RandTimer creation failed");
rt.idle_offset = u64::MAX / 2;
let before = rt.idle();
thread::sleep(Duration::from_secs(1));
let after = rt.idle();
assert_ne!(
before, after,
"Idle unchanged after short sleep with large offset!"
);
}
#[test]
fn test_big_loop_creation() {
for i in 0..10_000 {
let rt = RandTimer::new().expect("RandTimer creation failed");
if i % 1000 == 0 {
let _ = rt.uptime();
let _ = rt.idle();
}
}
}
#[test]
fn test_various_forced_offsets() {
let test_offsets = [
(1, 1),
(42, 999_999_999),
(0x0000FFFF_FFFFFFFF, 0xFFFFAAAA_FFFFFFFF),
(0xFFFFFFFF_FFFFFFFF, 0x55555555_55555555),
];
for &(u_off, i_off) in &test_offsets {
let mut rt = RandTimer::new().expect("RandTimer creation failed");
rt.uptime_offset = u_off;
rt.idle_offset = i_off;
let up1 = rt.uptime();
let idle1 = rt.idle();
thread::sleep(Duration::from_millis(2));
let up2 = rt.uptime();
let idle2 = rt.idle();
assert!(
up2 >= up1,
"Uptime offset {} yields invalid progression: {} -> {}",
u_off,
up1,
up2
);
assert!(
idle2 >= idle1,
"Idle offset {} yields invalid progression: {} -> {}",
i_off,
idle1,
idle2
);
}
}
}