use procfs::process::Process;
use reverie::Error;
use reverie::Guest;
use reverie::syscalls;
use reverie::syscalls::Errno;
use reverie::syscalls::MemoryAccess;
use crate::Detcore;
use crate::RecordOrReplay;
use crate::tool_global::thread_observe_time;
use crate::tool_local::ResourceLimit;
const CLOCK_TICKS_PER_SECOND: u64 = 100;
const NANOS_PER_CLOCK_TICK: u64 = 1_000_000_000 / CLOCK_TICKS_PER_SECOND;
fn clock_ticks(duration: crate::types::LogicalTime) -> u64 {
duration.as_nanos() / NANOS_PER_CLOCK_TICK
}
fn clock_t_from_ticks(ticks: u64) -> libc::clock_t {
ticks as libc::clock_t
}
const NANOS_PER_SECOND: u64 = 1_000_000_000;
const NANOS_PER_MICROSECOND: u64 = 1_000;
fn timeval_from_logical(duration: crate::types::LogicalTime) -> libc::timeval {
let nanos = duration.as_nanos();
libc::timeval {
tv_sec: (nanos / NANOS_PER_SECOND) as libc::time_t,
tv_usec: ((nanos % NANOS_PER_SECOND) / NANOS_PER_MICROSECOND) as libc::suseconds_t,
}
}
fn logical_clock_ticks(
now: crate::types::LogicalTime,
boot: crate::types::LogicalTime,
uptime_offset_seconds: u64,
) -> libc::clock_t {
let ticks = uptime_offset_seconds
.wrapping_mul(CLOCK_TICKS_PER_SECOND)
.wrapping_add(clock_ticks(now - boot));
clock_t_from_ticks(ticks)
}
fn sysinfo_uptime_seconds(
now: crate::types::LogicalTime,
epoch: crate::types::LogicalTime,
uptime_offset_seconds: u64,
) -> Result<u64, Error> {
let now_ns = now.as_nanos();
let epoch_ns = epoch.as_nanos();
let elapsed_ns = now_ns.checked_sub(epoch_ns).ok_or_else(|| {
Error::Tool(anyhow::anyhow!(
"sysinfo observed logical time {now_ns} ns before epoch {epoch_ns} ns"
))
})?;
let seconds =
elapsed_ns / NANOS_PER_SECOND + u64::from(!elapsed_ns.is_multiple_of(NANOS_PER_SECOND));
Ok(uptime_offset_seconds.wrapping_add(seconds))
}
fn procfs_uptime_seconds(
now: crate::types::LogicalTime,
boot: crate::types::LogicalTime,
uptime_offset_seconds: u64,
) -> u64 {
uptime_offset_seconds + (now - boot).as_secs()
}
fn procfs_boot_time_seconds(
boot: crate::types::LogicalTime,
uptime_offset_seconds: u64,
) -> Option<i64> {
i64::try_from(i128::from(boot.as_secs()) - i128::from(uptime_offset_seconds)).ok()
}
fn prlimit_targets_current_process(
target_pid: i32,
deterministic_pid: Option<i32>,
physical_pid: i32,
) -> bool {
target_pid == 0 || target_pid == deterministic_pid.unwrap_or(physical_pid)
}
fn validate_resource_limit_mutation(
resource: u32,
previous: ResourceLimit,
requested: ResourceLimit,
) -> Result<(), Errno> {
if requested.current > requested.maximum {
return Err(Errno::EINVAL);
}
if requested == previous {
return Ok(());
}
if resource != libc::RLIMIT_STACK
&& resource != libc::RLIMIT_NOFILE
&& resource != libc::RLIMIT_CORE
{
return Err(Errno::EPERM);
}
if requested.maximum > previous.maximum {
return Err(Errno::EPERM);
}
Ok(())
}
impl<T: RecordOrReplay> Detcore<T> {
pub async fn handle_getrlimit<G: Guest<Self>>(
&self,
guest: &mut G,
call: syscalls::Getrlimit,
) -> Result<i64, Error> {
let resource = u32::try_from(call.resource()).map_err(|_| Errno::EINVAL)?;
let address = call.rlim().ok_or(Errno::EFAULT)?;
let limit = guest
.thread_state()
.resource_limits
.lock()
.expect("resource limits mutex poisoned")
.get(resource)
.ok_or(Errno::EINVAL)?;
let result = libc::rlimit {
rlim_cur: limit.current,
rlim_max: limit.maximum,
};
guest.memory().write_value(address, &result)?;
Ok(0)
}
pub async fn handle_setrlimit<G: Guest<Self>>(
&self,
guest: &mut G,
call: syscalls::Setrlimit,
) -> Result<i64, Error> {
let resource = u32::try_from(call.resource()).map_err(|_| Errno::EINVAL)?;
let address = call.rlim().ok_or(Errno::EFAULT)?;
let requested: libc::rlimit = guest.memory().read_value(address)?;
let requested = ResourceLimit {
current: requested.rlim_cur,
maximum: requested.rlim_max,
};
let resource_limits = guest.thread_state().resource_limits.clone();
let mut limits = resource_limits
.lock()
.expect("resource limits mutex poisoned");
let previous = limits.get(resource).ok_or(Errno::EINVAL)?;
validate_resource_limit_mutation(resource, previous, requested)?;
if requested != previous {
limits.set(resource, requested);
}
Ok(0)
}
pub async fn handle_prlimit64<G: Guest<Self>>(
&self,
guest: &mut G,
call: syscalls::Prlimit64,
) -> Result<i64, Error> {
let resource = call.resource();
let resource_limits = guest.thread_state().resource_limits.clone();
if resource_limits
.lock()
.expect("resource limits mutex poisoned")
.get(resource)
.is_none()
{
return Err(Errno::EINVAL.into());
}
let requested = if let Some(address) = call.new_rlim() {
let limit: libc::rlimit64 = guest.memory().read_value(address)?;
Some(ResourceLimit {
current: limit.rlim_cur,
maximum: limit.rlim_max,
})
} else {
None
};
let pid = call.pid();
let deterministic_pid = guest.thread_state().detpid.map(|detpid| detpid.as_raw());
if !prlimit_targets_current_process(pid, deterministic_pid, guest.pid().as_raw()) {
return Err(Errno::EPERM.into());
}
let previous = {
let mut limits = resource_limits
.lock()
.expect("resource limits mutex poisoned");
let previous = limits
.get(resource)
.expect("resource validity changed while handling prlimit64");
if let Some(requested) = requested {
validate_resource_limit_mutation(resource, previous, requested)?;
if requested != previous {
limits.set(resource, requested);
}
}
previous
};
if let Some(address) = call.old_rlim() {
let previous = libc::rlimit64 {
rlim_cur: previous.current,
rlim_max: previous.maximum,
};
guest.memory().write_value(address, &previous)?;
}
crate::detlog!(
"prlimit64: pid={pid}, resource={resource}, mutation={}, old={}:{}",
requested.is_some(),
previous.current,
previous.maximum
);
Ok(0)
}
pub async fn handle_getrusage<G: Guest<Self>>(
&self,
guest: &mut G,
call: syscalls::Getrusage,
) -> Result<i64, Error> {
let who = call.who();
match who {
libc::RUSAGE_SELF | libc::RUSAGE_CHILDREN | libc::RUSAGE_THREAD => {}
_ => return Err(Errno::EINVAL.into()),
}
let usage_addr = call.usage().ok_or(Errno::EFAULT)?;
let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
let (user, system) = match who {
libc::RUSAGE_THREAD => guest.thread_state_mut().thread_cpu_time(),
libc::RUSAGE_CHILDREN => {
let cpu = guest.thread_state_mut().process_cpu_time();
(cpu.children_user, cpu.children_system)
}
_ => {
let cpu = guest.thread_state_mut().process_cpu_time();
(cpu.user, cpu.system)
}
};
usage.ru_utime = timeval_from_logical(user);
usage.ru_stime = timeval_from_logical(system);
if matches!(who, libc::RUSAGE_SELF | libc::RUSAGE_THREAD) {
usage.ru_maxrss = self.guest_peak_rss_kb(guest) as libc::c_long;
}
guest.memory().write_value(usage_addr, &usage)?;
Ok(0)
}
pub async fn handle_times<G: Guest<Self>>(
&self,
guest: &mut G,
call: syscalls::Times,
) -> Result<i64, Error> {
let now = thread_observe_time(guest).await;
let boot = crate::types::DetTime::new(&self.cfg).as_nanos();
let ticks = logical_clock_ticks(now, boot, self.cfg.sysinfo_uptime_offset);
let cpu = guest.thread_state_mut().process_cpu_time();
if let Some(address) = call.buf() {
let usage = libc::tms {
tms_utime: clock_t_from_ticks(clock_ticks(cpu.user)),
tms_stime: clock_t_from_ticks(clock_ticks(cpu.system)),
tms_cutime: clock_t_from_ticks(clock_ticks(cpu.children_user)),
tms_cstime: clock_t_from_ticks(clock_ticks(cpu.children_system)),
};
guest.memory().write_value(address, &usage)?;
}
Ok(ticks as i64)
}
fn guest_peak_rss_kb<G: Guest<Self>>(&self, guest: &G) -> u64 {
Process::new(guest.pid().as_raw())
.and_then(|process| process.status())
.ok()
.and_then(|status| status.vmhwm.or(status.vmrss))
.unwrap_or(0)
.max(1)
}
pub async fn handle_sysinfo<G: Guest<Self>>(
&self,
guest: &mut G,
call: syscalls::Sysinfo,
) -> Result<i64, Error> {
let info_addr = call.info().ok_or(Errno::EFAULT)?;
let sys_info = self.collect_sysinfo(guest).await?;
let mut memory = guest.memory();
memory.write_value(info_addr, &sys_info.into())?;
Ok(0)
}
pub(super) async fn calculate_procfs_uptime<G: Guest<Self>>(
&self,
guest: &mut G,
) -> Result<u64, Error> {
let global_time = thread_observe_time(guest).await;
Ok(procfs_uptime_seconds(
global_time,
crate::types::DetTime::new(&self.cfg).as_nanos(),
self.cfg.sysinfo_uptime_offset,
))
}
pub(super) fn calculate_procfs_boot_time(&self) -> Result<i64, Error> {
procfs_boot_time_seconds(
crate::types::DetTime::new(&self.cfg).as_nanos(),
self.cfg.sysinfo_uptime_offset,
)
.ok_or_else(|| Errno::EOVERFLOW.into())
}
async fn collect_sysinfo<G: Guest<Self>>(
&self,
guest: &mut G,
) -> Result<syscalls::SysInfo, Error> {
let memory = configured_memory(self.cfg.memory);
let now = thread_observe_time(guest).await;
let epoch = crate::types::DetTime::new(&self.cfg).as_nanos();
Ok(syscalls::SysInfo {
uptime: sysinfo_uptime_seconds(now, epoch, self.cfg.sysinfo_uptime_offset)?,
loads_1: 1,
loads_5: 1,
loads_15: 1,
total_ram: memory.total_ram,
free_ram: memory.free_ram,
buffer_ram: memory.buffer_ram,
shared_ram: memory.shared_ram,
total_swap: memory.total_swap,
free_swap: memory.free_swap,
procs: 1,
total_high: memory.total_high,
free_high: memory.free_high,
mem_unit: memory.mem_unit,
})
}
}
#[derive(Debug, PartialEq, Eq)]
struct ConfiguredMemory {
total_ram: u64,
free_ram: u64,
buffer_ram: u64,
shared_ram: u64,
total_swap: u64,
free_swap: u64,
total_high: u64,
free_high: u64,
mem_unit: u32,
}
fn configured_memory(memory: u64) -> ConfiguredMemory {
ConfiguredMemory {
total_ram: memory,
free_ram: memory,
buffer_ram: 0,
shared_ram: 0,
total_swap: 0,
free_swap: 0,
total_high: 0,
free_high: 0,
mem_unit: 1,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::LogicalTime;
#[test]
fn logical_clock_ticks_include_boot_offset_and_fractional_seconds() {
let boot = LogicalTime::from_secs(1_000);
let now = boot + LogicalTime::from_millis(25);
assert_eq!(logical_clock_ticks(now, boot, 120), 12_002);
}
#[test]
fn sysinfo_uptime_rounds_positive_elapsed_up() {
let epoch = LogicalTime::from_nanos(1_000_000_000_000);
for (elapsed_ns, expected_zero, expected_offset) in [
(0, 0, 120),
(1, 1, 121),
(999_999_999, 1, 121),
(1_000_000_000, 1, 121),
(1_000_000_001, 2, 122),
(1_200_000_000, 2, 122),
] {
let now = epoch + LogicalTime::from_nanos(elapsed_ns);
assert_eq!(
sysinfo_uptime_seconds(now, epoch, 0).unwrap(),
expected_zero,
"elapsed {elapsed_ns} ns without boot offset"
);
assert_eq!(
sysinfo_uptime_seconds(now, epoch, 120).unwrap(),
expected_offset,
"elapsed {elapsed_ns} ns with boot offset"
);
}
}
#[test]
fn sysinfo_uptime_ignores_epoch_fraction() {
for fraction_ns in [0, 1, 1_000, 999_999_000, 999_999_999] {
let epoch = LogicalTime::from_nanos(1_000_000_000_000 + fraction_ns);
for (elapsed_ns, expected) in [
(0, 120),
(1, 121),
(999_999_999, 121),
(1_000_000_000, 121),
(1_000_000_001, 122),
(1_200_000_000, 122),
] {
let now = epoch + LogicalTime::from_nanos(elapsed_ns);
assert_eq!(
sysinfo_uptime_seconds(now, epoch, 120).unwrap(),
expected,
"epoch fraction {fraction_ns} ns, elapsed {elapsed_ns} ns"
);
}
}
}
#[test]
fn sysinfo_uptime_rounds_maximum_duration_without_overflow() {
let epoch = LogicalTime::from_nanos(0);
assert_eq!(
sysinfo_uptime_seconds(LogicalTime::MAX, epoch, 0).unwrap(),
18_446_744_074
);
assert_eq!(
sysinfo_uptime_seconds(
LogicalTime::from_nanos(18_446_744_073_000_000_000),
epoch,
120,
)
.unwrap(),
18_446_744_193
);
assert_eq!(
sysinfo_uptime_seconds(LogicalTime::MAX, LogicalTime::from_nanos(u64::MAX - 1), 120,)
.unwrap(),
121
);
}
#[test]
fn sysinfo_uptime_preserves_wrapping_offset_extension() {
let epoch = LogicalTime::from_nanos(0);
for (elapsed_ns, offset, expected) in [
(0, u64::MAX, u64::MAX),
(1, u64::MAX, 0),
(1_000_000_000, u64::MAX, 0),
(1_000_000_001, u64::MAX, 1),
(1, u64::MAX - 1, u64::MAX),
(1_000_000_001, u64::MAX - 1, 0),
] {
assert_eq!(
sysinfo_uptime_seconds(LogicalTime::from_nanos(elapsed_ns), epoch, offset).unwrap(),
expected,
"elapsed {elapsed_ns} ns, offset {offset}"
);
}
}
#[test]
fn sysinfo_uptime_preserves_signed_abi_conversion() {
let epoch = LogicalTime::from_nanos(0);
for (elapsed_ns, offset, expected) in [
(0, i64::MAX as u64, i64::MAX),
(1, i64::MAX as u64, i64::MIN),
(0, u64::MAX, -1),
(1, u64::MAX, 0),
] {
let uptime =
sysinfo_uptime_seconds(LogicalTime::from_nanos(elapsed_ns), epoch, offset).unwrap();
let info: libc::sysinfo = syscalls::SysInfo {
uptime,
loads_1: 0,
loads_5: 0,
loads_15: 0,
total_ram: 0,
free_ram: 0,
shared_ram: 0,
buffer_ram: 0,
total_swap: 0,
free_swap: 0,
procs: 0,
total_high: 0,
free_high: 0,
mem_unit: 1,
}
.into();
assert_eq!(info.uptime, expected);
}
}
#[test]
fn sysinfo_uptime_rejects_time_before_epoch() {
let error = sysinfo_uptime_seconds(
LogicalTime::from_nanos(999),
LogicalTime::from_nanos(1_000),
120,
)
.unwrap_err();
let Error::Tool(error) = error else {
panic!("an impossible clock must be a tool failure, got {error:?}");
};
assert_eq!(
error.to_string(),
"sysinfo observed logical time 999 ns before epoch 1000 ns"
);
}
#[test]
fn procfs_uptime_subtracts_fractional_boot_before_truncating() {
let boot = LogicalTime::from_nanos(1_000_999_999_999);
assert_eq!(
procfs_uptime_seconds(boot + LogicalTime::from_nanos(1), boot, 120),
120
);
assert_eq!(
procfs_uptime_seconds(boot + LogicalTime::from_secs(1), boot, 120),
121
);
}
#[test]
fn procfs_boot_time_is_the_boot_instant_not_now_minus_uptime() {
let boot = LogicalTime::from_nanos(1_000_750_000_000);
assert_eq!(procfs_boot_time_seconds(boot, 120), Some(880));
let old_btime =
|now: LogicalTime| now.as_secs() as i64 - procfs_uptime_seconds(now, boot, 120) as i64;
let samples = [100, 400, 1_100].map(|millis| boot + LogicalTime::from_millis(millis));
assert_eq!(samples.map(old_btime), [880, 881, 880]);
let integral_boot = LogicalTime::from_secs(1_000);
let integral_now = integral_boot + LogicalTime::from_millis(1_400);
assert_eq!(
procfs_boot_time_seconds(integral_boot, 120),
Some(
integral_now.as_secs() as i64
- procfs_uptime_seconds(integral_now, integral_boot, 120) as i64
)
);
assert_eq!(procfs_boot_time_seconds(boot, u64::MAX), None);
}
#[test]
fn procfs_boot_time_is_exact_for_every_offset_whose_result_fits() {
let boot = LogicalTime::from_secs(1_767_225_600);
assert_eq!(procfs_boot_time_seconds(boot, 0), Some(1_767_225_600));
assert_eq!(
procfs_boot_time_seconds(boot, 1 << 63),
Some(-9_223_372_035_087_550_208)
);
assert_eq!(
procfs_boot_time_seconds(boot, 1_767_225_600 + (1 << 63)),
Some(i64::MIN)
);
assert_eq!(
procfs_boot_time_seconds(boot, 1_767_225_600 + (1 << 63) + 1),
None
);
assert_eq!(procfs_boot_time_seconds(boot, u64::MAX), None);
}
#[test]
fn sysinfo_uptime_rounds_fractional_elapsed_up_like_linux() {
let boot = LogicalTime::from_nanos(1_000_999_999_999);
assert_eq!(sysinfo_uptime_seconds(boot, boot, 120).unwrap(), 120);
assert_eq!(
sysinfo_uptime_seconds(boot + LogicalTime::from_nanos(1), boot, 120).unwrap(),
121
);
assert_eq!(
sysinfo_uptime_seconds(boot + LogicalTime::from_millis(999), boot, 120).unwrap(),
121
);
assert_eq!(
sysinfo_uptime_seconds(boot + LogicalTime::from_secs(1), boot, 120).unwrap(),
121
);
assert_eq!(
sysinfo_uptime_seconds(
boot + LogicalTime::from_secs(1) + LogicalTime::from_nanos(1),
boot,
120
)
.unwrap(),
122
);
let fractional = boot + LogicalTime::from_millis(1_500);
assert_eq!(procfs_uptime_seconds(fractional, boot, 120), 121);
assert_eq!(sysinfo_uptime_seconds(fractional, boot, 120).unwrap(), 122);
}
#[test]
fn sysinfo_memory_matches_configured_memory() {
assert_eq!(
configured_memory(1_000_000_000),
ConfiguredMemory {
total_ram: 1_000_000_000,
free_ram: 1_000_000_000,
buffer_ram: 0,
shared_ram: 0,
total_swap: 0,
free_swap: 0,
total_high: 0,
free_high: 0,
mem_unit: 1,
},
);
}
#[test]
fn prlimit_self_target_prefers_deterministic_process_identity() {
assert!(prlimit_targets_current_process(3, Some(3), 10_003));
assert!(prlimit_targets_current_process(0, Some(3), 10_003));
assert!(!prlimit_targets_current_process(10_003, Some(3), 10_003));
assert!(!prlimit_targets_current_process(4, Some(3), 10_003));
}
#[test]
fn prlimit_self_target_falls_back_to_physical_identity_before_init() {
assert!(prlimit_targets_current_process(10_003, None, 10_003));
assert!(!prlimit_targets_current_process(3, None, 10_003));
}
#[test]
fn prlimit_accepts_exact_noop_for_restricted_resource() {
let limit = ResourceLimit {
current: 0,
maximum: 0,
};
assert_eq!(
validate_resource_limit_mutation(libc::RLIMIT_CPU, limit, limit),
Ok(())
);
}
#[test]
fn prlimit_accepts_core_soft_limit_change() {
let previous = ResourceLimit {
current: 1,
maximum: 1,
};
let requested = ResourceLimit {
current: 0,
maximum: 1,
};
assert_eq!(
validate_resource_limit_mutation(libc::RLIMIT_CORE, previous, requested),
Ok(())
);
}
#[test]
fn prlimit_rejects_actual_change_to_restricted_resource() {
let previous = ResourceLimit {
current: 1,
maximum: 1,
};
let requested = ResourceLimit {
current: 0,
maximum: 1,
};
assert_eq!(
validate_resource_limit_mutation(libc::RLIMIT_CPU, previous, requested),
Err(Errno::EPERM)
);
}
#[test]
fn prlimit_rejects_invalid_soft_limit_before_noop_policy() {
let previous = ResourceLimit {
current: 1,
maximum: 1,
};
let requested = ResourceLimit {
current: 2,
maximum: 1,
};
assert_eq!(
validate_resource_limit_mutation(libc::RLIMIT_CORE, previous, requested),
Err(Errno::EINVAL)
);
}
#[test]
fn prlimit_rejects_core_hard_limit_raise() {
let previous = ResourceLimit {
current: 1,
maximum: 1,
};
let requested = ResourceLimit {
current: 1,
maximum: 2,
};
assert_eq!(
validate_resource_limit_mutation(libc::RLIMIT_CORE, previous, requested),
Err(Errno::EPERM)
);
}
#[test]
fn logical_cpu_ticks_exclude_boot_epoch() {
assert_eq!(clock_ticks(LogicalTime::from_millis(25)), 2);
}
#[test]
fn rusage_timeval_splits_seconds_and_microseconds() {
let tv = timeval_from_logical(LogicalTime::from_millis(2_500));
assert_eq!(tv.tv_sec, 2);
assert_eq!(tv.tv_usec, 500_000);
}
#[test]
fn rusage_timeval_truncates_sub_microsecond_rather_than_rounding() {
let tv = timeval_from_logical(LogicalTime::from_nanos(1_999));
assert_eq!(tv.tv_sec, 0);
assert_eq!(tv.tv_usec, 1);
}
#[test]
fn rusage_timeval_is_monotonic_in_the_logical_duration() {
let mut previous = (0_i64, 0_i64);
for nanos in (0..3_000_000u64).step_by(997) {
let tv = timeval_from_logical(LogicalTime::from_nanos(nanos));
let current = (tv.tv_sec, tv.tv_usec);
assert!(
current >= previous,
"rusage timeval went backwards at {nanos}ns: {previous:?} -> {current:?}"
);
previous = current;
}
}
#[test]
fn rusage_zero_cpu_time_renders_as_zero() {
let tv = timeval_from_logical(LogicalTime::ZERO);
assert_eq!(tv.tv_sec, 0);
assert_eq!(tv.tv_usec, 0);
}
#[test]
fn rusage_and_times_agree_within_one_clock_tick() {
for nanos in [0u64, 1_000_000, 300_484_000, 7_000_000_000, 12_345_678_901] {
let duration = LogicalTime::from_nanos(nanos);
let tv = timeval_from_logical(duration);
let rusage_micros = tv.tv_sec as u64 * 1_000_000 + tv.tv_usec as u64;
let times_micros = clock_ticks(duration) * (NANOS_PER_CLOCK_TICK / 1_000);
assert!(rusage_micros >= times_micros);
assert!(rusage_micros - times_micros < NANOS_PER_CLOCK_TICK / 1_000);
if nanos % NANOS_PER_CLOCK_TICK == 0 {
assert_eq!(rusage_micros, times_micros);
}
}
}
#[test]
fn logical_clock_ticks_wrap_configured_offset_like_linux_clock_t() {
let boot = LogicalTime::from_secs(1_000);
let before = logical_clock_ticks(boot, boot, u64::MAX);
let after = logical_clock_ticks(boot + LogicalTime::from_millis(10), boot, u64::MAX);
assert_eq!(before, -100);
assert_eq!(after, -99);
}
}