Skip to main content

detcore/syscalls/
sysinfo.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9use procfs::process::Process;
10use reverie::Error;
11use reverie::Guest;
12use reverie::syscalls;
13use reverie::syscalls::Errno;
14use reverie::syscalls::MemoryAccess;
15use reverie::syscalls::Sysno;
16
17use crate::Detcore;
18use crate::RecordOrReplay;
19use crate::tool_global::thread_observe_time;
20use crate::tool_local::ResourceLimit;
21
22// Linux exposes USER_HZ, not the kernel's configurable scheduler HZ, through times(2).
23const CLOCK_TICKS_PER_SECOND: u64 = 100;
24const NANOS_PER_CLOCK_TICK: u64 = 1_000_000_000 / CLOCK_TICKS_PER_SECOND;
25
26/// `duration` in clock ticks (`USER_HZ`), truncated, as Linux's
27/// `nsec_to_clock_t` converts the CPU times `times(2)` and a process's `stat`
28/// report (kernel/time/time.c:733-736 at v6.16).
29pub(crate) fn clock_ticks(duration: crate::types::LogicalTime) -> u64 {
30    duration.as_nanos() / NANOS_PER_CLOCK_TICK
31}
32
33fn clock_t_from_ticks(ticks: u64) -> libc::clock_t {
34    ticks as libc::clock_t
35}
36
37const NANOS_PER_SECOND: u64 = 1_000_000_000;
38const NANOS_PER_MICROSECOND: u64 = 1_000;
39
40/// Render a logical CPU duration as the `timeval` `getrusage(2)` reports.
41///
42/// Linux truncates rusage CPU times to microsecond granularity, so the sub-microsecond
43/// remainder of the logical duration is discarded rather than rounded. Truncation (not
44/// rounding) is what keeps the value monotonic: a duration that grows by less than a
45/// microsecond must never make the reported total go backwards, and rounding-to-nearest
46/// on a shrinking remainder can do exactly that.
47fn timeval_from_logical(duration: crate::types::LogicalTime) -> libc::timeval {
48    let nanos = duration.as_nanos();
49    libc::timeval {
50        tv_sec: (nanos / NANOS_PER_SECOND) as libc::time_t,
51        tv_usec: ((nanos % NANOS_PER_SECOND) / NANOS_PER_MICROSECOND) as libc::suseconds_t,
52    }
53}
54
55fn logical_clock_ticks(
56    now: crate::types::LogicalTime,
57    boot: crate::types::LogicalTime,
58    uptime_offset_seconds: u64,
59) -> libc::clock_t {
60    let ticks = uptime_offset_seconds
61        .wrapping_mul(CLOCK_TICKS_PER_SECOND)
62        .wrapping_add(clock_ticks(now - boot));
63    clock_t_from_ticks(ticks)
64}
65
66/// Project elapsed logical time into Linux's integer `sysinfo(2)` uptime.
67///
68/// Linux rounds a positive fractional boottime up to the next second. Subtract
69/// the exact internal epoch first so its calendar fraction cannot affect the
70/// result. This projection does not alter the underlying logical clock.
71fn sysinfo_uptime_seconds(
72    now: crate::types::LogicalTime,
73    epoch: crate::types::LogicalTime,
74    uptime_offset_seconds: u64,
75) -> Result<u64, Error> {
76    let now_ns = now.as_nanos();
77    let epoch_ns = epoch.as_nanos();
78    let elapsed_ns = now_ns.checked_sub(epoch_ns).ok_or_else(|| {
79        Error::Tool(anyhow::anyhow!(
80            "sysinfo observed logical time {now_ns} ns before epoch {epoch_ns} ns"
81        ))
82    })?;
83    // Divide before rounding: adding NANOS_PER_SECOND - 1 to elapsed_ns can
84    // overflow. The rounded quotient of any u64 nanosecond duration fits u64.
85    let seconds =
86        elapsed_ns / NANOS_PER_SECOND + u64::from(!elapsed_ns.is_multiple_of(NANOS_PER_SECOND));
87    // Config accepts the full u64 offset; preserve its existing release-build
88    // wrapping extension, including the subsequent SysInfo -> c_long ABI cast.
89    // Ordinary Linux uptime semantics apply within the nonnegative c_long range.
90    Ok(uptime_offset_seconds.wrapping_add(seconds))
91}
92
93/// Render the whole-second part of `/proc/uptime` from an absolute logical clock.
94///
95/// Linux's `uptime_proc_show` prints `tv_sec` followed by truncated
96/// centiseconds, so the integer part is the floor of the elapsed time.
97/// Subtract before truncating. Truncating `now` and `boot` separately makes a
98/// sub-second run appear one second old whenever it crosses an absolute-second
99/// boundary, even though less than one logical second elapsed.
100fn procfs_uptime_seconds(
101    now: crate::types::LogicalTime,
102    boot: crate::types::LogicalTime,
103    uptime_offset_seconds: u64,
104) -> u64 {
105    uptime_offset_seconds + (now - boot).as_secs()
106}
107
108/// Render `/proc/stat`'s `btime` from the logical boot instant.
109///
110/// Linux's `show_stat` prints the seconds of `getboottime64`, a fixed instant
111/// that moves only when the realtime clock or time-namespace offset changes.
112/// Derive it from the boot instant itself, never as `floor(now) - uptime`: for
113/// a fractional boot those two floors round independently, so ordinary
114/// elapsed time would move `btime` back and forth by one second.
115///
116/// Config accepts every u64 offset, and an offset above `i64::MAX` can still
117/// place the boot instant inside time64_t. Subtract in i128, where every such
118/// difference is exact, and range-check only the result. `None` means the
119/// boot instant itself precedes `i64::MIN` seconds.
120fn procfs_boot_time_seconds(
121    boot: crate::types::LogicalTime,
122    uptime_offset_seconds: u64,
123) -> Option<i64> {
124    i64::try_from(i128::from(boot.as_secs()) - i128::from(uptime_offset_seconds)).ok()
125}
126
127fn prlimit_targets_current_process(
128    target_pid: i32,
129    deterministic_pid: Option<i32>,
130    physical_pid: i32,
131) -> bool {
132    target_pid == 0 || target_pid == deterministic_pid.unwrap_or(physical_pid)
133}
134
135fn validate_resource_limit_mutation(
136    resource: u32,
137    previous: ResourceLimit,
138    requested: ResourceLimit,
139) -> Result<(), Errno> {
140    if requested.current > requested.maximum {
141        return Err(Errno::EINVAL);
142    }
143    // Linux accepts an exact no-op for every valid resource, including limits
144    // that an unprivileged process could not otherwise change. Recognize that
145    // case before applying Detcore's narrower virtual-mutation policy.
146    if requested == previous {
147        return Ok(());
148    }
149    // CORE is virtual compatibility state too: changing it cannot enable host
150    // core dumps, while Linux sanitizers routinely lower its soft limit.
151    if resource != reverie::syscalls::linux_abi::RLIMIT_STACK
152        && resource != reverie::syscalls::linux_abi::RLIMIT_NOFILE
153        && resource != reverie::syscalls::linux_abi::RLIMIT_CORE
154    {
155        return Err(Errno::EPERM);
156    }
157    if requested.maximum > previous.maximum {
158        return Err(Errno::EPERM);
159    }
160    Ok(())
161}
162
163/// On an in-guest run's default route
164/// (`Config::in_guest_refuses_host_timed_producers`), a WARN for a change to
165/// `RLIMIT_CPU` refused with `errno` EPERM (as it is on every backend: the
166/// limit is virtual, [`validate_resource_limit_mutation`]): a physical CPU-time
167/// limit would send SIGXCPU at a moment host CPU time decides, a host-timed
168/// producer A5 step 3c refuses there by name. Nothing on any other backend,
169/// for another resource, or for another errno (EINVAL, for a soft limit above
170/// the hard one, comes first and arms nothing).
171fn warn_refused_cpu_limit<G: Guest<Detcore<T>>, T: RecordOrReplay>(
172    guest: &G,
173    call: &str,
174    resource: u32,
175    errno: Errno,
176) {
177    if resource == reverie::syscalls::linux_abi::RLIMIT_CPU
178        && errno == Errno::EPERM
179        && guest.config().in_guest_refuses_host_timed_producers
180    {
181        tracing::warn!(
182            "{}",
183            crate::host_timed_producers::rlimit_cpu_refused_warning(
184                call,
185                guest.thread_state().dettid.as_raw(),
186            )
187        );
188    }
189}
190
191/// The record of a resource-limit read that a loader served before post-exec
192/// ([`crate::random::EarlyRequest::LimitRead`]), emitted by post-exec. It is
193/// the record [`Detcore::handle_prlimit64`] emits for the same read, from the
194/// same module; a `getrlimit` read records nothing, as in `handle_getrlimit`.
195pub(crate) fn record_early_limit_read(
196    call: crate::random::LimitReadCall,
197    resource: u32,
198    current: u64,
199    maximum: u64,
200) {
201    if let crate::random::LimitReadCall::Prlimit64 { pid } = call {
202        crate::detlog!(
203            "prlimit64: pid={pid}, resource={resource}, mutation=false, old={current}:{maximum}"
204        );
205    }
206}
207
208impl<T: RecordOrReplay> Detcore<T> {
209    // AUTONOMOUS-BOT-IMPLEMENTED
210    // TODO-HUMAN-REVIEW(#663)
211    /// Return one deterministic process resource limit through the legacy ABI.
212    pub async fn handle_getrlimit<G: Guest<Self>>(
213        &self,
214        guest: &mut G,
215        call: syscalls::Getrlimit,
216    ) -> Result<i64, Error> {
217        let resource = u32::try_from(call.resource()).map_err(|_| Errno::EINVAL)?;
218        let address = call.rlim().ok_or(Errno::EFAULT)?;
219        let limit = guest
220            .thread_state()
221            .resource_limits
222            .lock()
223            .expect("resource limits mutex poisoned")
224            .get(resource)
225            .ok_or(Errno::EINVAL)?;
226        let result = libc::rlimit {
227            rlim_cur: limit.current,
228            rlim_max: limit.maximum,
229        };
230        guest.memory().write_value(address, &result)?;
231        Ok(0)
232    }
233
234    // AUTONOMOUS-BOT-IMPLEMENTED
235    // TODO-HUMAN-REVIEW(#663)
236    /// Update one virtual process resource limit through the legacy ABI.
237    pub async fn handle_setrlimit<G: Guest<Self>>(
238        &self,
239        guest: &mut G,
240        call: syscalls::Setrlimit,
241    ) -> Result<i64, Error> {
242        let resource = u32::try_from(call.resource()).map_err(|_| Errno::EINVAL)?;
243        let address = call.rlim().ok_or(Errno::EFAULT)?;
244        let requested: libc::rlimit = guest.memory().read_value(address)?;
245        let requested = ResourceLimit {
246            current: requested.rlim_cur,
247            maximum: requested.rlim_max,
248        };
249        let resource_limits = guest.thread_state().resource_limits.clone();
250        let previous = {
251            let mut limits = resource_limits
252                .lock()
253                .expect("resource limits mutex poisoned");
254            let previous = limits.get(resource).ok_or(Errno::EINVAL)?;
255            validate_resource_limit_mutation(resource, previous, requested).inspect_err(
256                |errno| warn_refused_cpu_limit(guest, "setrlimit", resource, *errno),
257            )?;
258            if requested != previous {
259                limits.set(resource, requested);
260            }
261            previous
262        };
263        self.publish_rss_limit(guest, resource, previous, requested)
264            .await;
265        Ok(0)
266    }
267
268    /// Tell Detcore's process table a process's new soft `RLIMIT_RSS`
269    /// (`Scheduler::set_rss_limit`), which a rendered `/proc/<pid>/stat` shows
270    /// whoever reads it (field 25): after a `setrlimit` or `prlimit64` of the
271    /// caller's own process changed it from `previous` to `requested`.
272    // The syscall resource argument is unsigned; libc exposes RLIMIT_RSS as
273    // u32 on GNU and i32 on musl, with the same nonnegative Linux value.
274    #[allow(clippy::unnecessary_cast)]
275    async fn publish_rss_limit<G: Guest<Self>>(
276        &self,
277        guest: &mut G,
278        resource: u32,
279        previous: ResourceLimit,
280        requested: ResourceLimit,
281    ) {
282        if resource == libc::RLIMIT_RSS as u32 && requested.current != previous.current {
283            let process = self.own_detpid(guest);
284            crate::tool_global::set_rss_limit(guest, process, requested.current).await;
285        }
286    }
287
288    /// Virtualize `prlimit64(2)` for the current guest process.
289    ///
290    /// Queries return process-local deterministic values. Exact no-op updates
291    /// succeed for every valid resource, as on Linux. Changes are kept virtual
292    /// and restricted to limits that do not grant access to host resources or
293    /// affect host scheduling. Accepted changes update only guest-observable
294    /// compatibility state; they are not a sandbox boundary and do not ask the
295    /// host kernel to enforce the virtual limit.
296    // AUTONOMOUS-BOT-IMPLEMENTED
297    // TODO-HUMAN-REVIEW(#534)
298    pub async fn handle_prlimit64<G: Guest<Self>>(
299        &self,
300        guest: &mut G,
301        call: syscalls::Prlimit64,
302    ) -> Result<i64, Error> {
303        let resource = call.resource();
304        let resource_limits = guest.thread_state().resource_limits.clone();
305        if resource_limits
306            .lock()
307            .expect("resource limits mutex poisoned")
308            .get(resource)
309            .is_none()
310        {
311            return Err(Errno::EINVAL.into());
312        }
313
314        let requested = if let Some(address) = call.new_rlim() {
315            let limit: libc::rlimit64 = guest.memory().read_value(address)?;
316            Some(ResourceLimit {
317                current: limit.rlim_cur,
318                maximum: limit.rlim_max,
319            })
320        } else {
321            None
322        };
323
324        // A nested caller names its process by its own namespace's pid
325        // (https://github.com/rrnewton/hermit/issues/4155); a pid no task of
326        // that namespace has is ESRCH, as Linux's `find_task_by_vpid` answers.
327        let pid = if call.pid() > 0 && guest.thread_state().nested_pid_namespace {
328            match crate::tool_global::guest_pid(guest, call.pid()).await {
329                crate::scheduler::pid_namespaces::GuestPidAnswer::Task { task, .. } => {
330                    task.as_raw()
331                }
332                crate::scheduler::pid_namespaces::GuestPidAnswer::NoSuchPid => {
333                    return Err(Errno::ESRCH.into());
334                }
335                crate::scheduler::pid_namespaces::GuestPidAnswer::Untranslatable(reason) => {
336                    tracing::error!(
337                        "[detcore, dtid {}] prlimit64 from a nested PID namespace is not supported: {} (https://github.com/rrnewton/hermit/issues/4155)",
338                        guest.thread_state().dettid,
339                        reason,
340                    );
341                    return self
342                        .refuse_unserviceable_operation(guest, Sysno::prlimit64, Errno::ENOSYS)
343                        .await;
344                }
345            }
346        } else {
347            call.pid()
348        };
349        let deterministic_pid = guest.thread_state().detpid.map(|detpid| detpid.as_raw());
350        if !prlimit_targets_current_process(pid, deterministic_pid, guest.pid().as_raw()) {
351            return Err(Errno::EPERM.into());
352        }
353
354        let previous = {
355            let mut limits = resource_limits
356                .lock()
357                .expect("resource limits mutex poisoned");
358            let previous = limits
359                .get(resource)
360                .expect("resource validity changed while handling prlimit64");
361
362            if let Some(requested) = requested {
363                validate_resource_limit_mutation(resource, previous, requested).inspect_err(
364                    |errno| warn_refused_cpu_limit(guest, "prlimit64", resource, *errno),
365                )?;
366                if requested != previous {
367                    limits.set(resource, requested);
368                }
369            }
370
371            previous
372        };
373
374        if let Some(requested) = requested {
375            self.publish_rss_limit(guest, resource, previous, requested)
376                .await;
377        }
378
379        if let Some(address) = call.old_rlim() {
380            let previous = libc::rlimit64 {
381                rlim_cur: previous.current,
382                rlim_max: previous.maximum,
383            };
384            guest.memory().write_value(address, &previous)?;
385        }
386
387        crate::detlog!(
388            "prlimit64: pid={pid}, resource={resource}, mutation={}, old={}:{}",
389            requested.is_some(),
390            previous.current,
391            previous.maximum
392        );
393        Ok(0)
394    }
395    /// Return a deterministic resource-usage snapshot.
396    ///
397    /// `ru_utime`/`ru_stime` come from the SAME logical CPU accounting that backs `times(2)`
398    /// (see [`Self::handle_times`]), not from host scheduler counters. Reporting them as zero,
399    /// as this did previously, was both a fidelity bug and an internal contradiction: a guest
400    /// that called `times(2)` saw advancing CPU time while `getrusage(2)` insisted the same
401    /// process had consumed none. Deriving both from `ProcessCpuSnapshot` makes the two
402    /// syscalls agree by construction rather than by coincidence.
403    ///
404    /// The `who` values report different aggregates, matching Linux:
405    /// - `RUSAGE_SELF` — this process, summed across its threads.
406    /// - `RUSAGE_THREAD` — the calling thread alone. This reads the thread's own logical CPU
407    ///   counters rather than the process totals; substituting the process aggregate would
408    ///   over-report for every multithreaded guest.
409    /// - `RUSAGE_CHILDREN` — reaped children only, which is exactly what the `children_*`
410    ///   fields accumulate on `wait`.
411    ///
412    /// `ru_maxrss` is populated for the process/thread cases with the guest's peak resident set
413    /// size so that programs which require a positive maximum RSS (e.g. rr's `rusage` test)
414    /// behave like they do on Linux. This remains a best-effort host-procfs observation on
415    /// backends where [`Guest::pid`] names a host process; it is separate from the configured
416    /// system-wide memory reported by `sysinfo(2)` and virtual `/proc/meminfo`.
417    ///
418    /// Page-fault and context-switch counts remain zero: Detcore does not model them, and
419    /// synthesizing a plausible-looking number would be worse than reporting none.
420    pub async fn handle_getrusage<G: Guest<Self>>(
421        &self,
422        guest: &mut G,
423        call: syscalls::Getrusage,
424    ) -> Result<i64, Error> {
425        let who = call.who();
426        match who {
427            libc::RUSAGE_SELF | libc::RUSAGE_CHILDREN | libc::RUSAGE_THREAD => {}
428            _ => return Err(Errno::EINVAL.into()),
429        }
430
431        let usage_addr = call.usage().ok_or(Errno::EFAULT)?;
432
433        // SAFETY: `libc::rusage` is a plain-old-data C struct that is valid when zero-initialized.
434        let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
435
436        let (user, system) = match who {
437            libc::RUSAGE_THREAD => guest.thread_state_mut().thread_cpu_time(),
438            libc::RUSAGE_CHILDREN => {
439                let cpu = guest.thread_state_mut().process_cpu_time();
440                (cpu.children_user, cpu.children_system)
441            }
442            // RUSAGE_SELF
443            _ => {
444                let cpu = guest.thread_state_mut().process_cpu_time();
445                (cpu.user, cpu.system)
446            }
447        };
448        usage.ru_utime = timeval_from_logical(user);
449        usage.ru_stime = timeval_from_logical(system);
450
451        // RUSAGE_SELF/RUSAGE_THREAD report this process's peak RSS. RUSAGE_CHILDREN aggregates
452        // terminated children only; with no such accounting we leave it zero, matching Linux when
453        // no child has exited.
454        if matches!(who, libc::RUSAGE_SELF | libc::RUSAGE_THREAD) {
455            usage.ru_maxrss = self.guest_peak_rss_kb(guest) as libc::c_long;
456        }
457
458        guest.memory().write_value(usage_addr, &usage)?;
459        Ok(0)
460    }
461
462    // AUTONOMOUS-BOT-IMPLEMENTED
463    // TODO-HUMAN-REVIEW(#797): Review logical elapsed and process CPU accounting semantics.
464    /// Return deterministic elapsed ticks and process CPU accounting for `times(2)`.
465    ///
466    /// Linux's host boot epoch and scheduler CPU counters are nondeterministic. Detcore instead
467    /// derives the return value from its global logical clock. Per-process logical CPU accounting
468    /// aggregates user instruction and syscall-system time across threads; forked processes start
469    /// fresh counters and contribute their totals to the parent's child counters when reaped.
470    pub async fn handle_times<G: Guest<Self>>(
471        &self,
472        guest: &mut G,
473        call: syscalls::Times,
474    ) -> Result<i64, Error> {
475        let now = thread_observe_time(guest).await;
476        let boot = crate::types::DetTime::new(&self.cfg).as_nanos();
477        let ticks = logical_clock_ticks(now, boot, self.cfg.sysinfo_uptime_offset);
478        let cpu = guest.thread_state_mut().process_cpu_time();
479
480        if let Some(address) = call.buf() {
481            let usage = libc::tms {
482                tms_utime: clock_t_from_ticks(clock_ticks(cpu.user)),
483                tms_stime: clock_t_from_ticks(clock_ticks(cpu.system)),
484                tms_cutime: clock_t_from_ticks(clock_ticks(cpu.children_user)),
485                tms_cstime: clock_t_from_ticks(clock_ticks(cpu.children_system)),
486            };
487            guest.memory().write_value(address, &usage)?;
488        }
489
490        Ok(ticks as i64)
491    }
492
493    /// The guest's peak resident set size ("high water mark") in kibibytes, matching the units of
494    /// Linux `getrusage`'s `ru_maxrss`. This reads host procfs through [`Guest::pid`], which only
495    /// identifies the guest process on backends where it names a host process; always returns a
496    /// positive value so guests can rely on a nonzero maximum RSS even if the read fails.
497    fn guest_peak_rss_kb<G: Guest<Self>>(&self, guest: &G) -> u64 {
498        Process::new(guest.pid().as_raw())
499            .and_then(|process| process.status())
500            .ok()
501            .and_then(|status| status.vmhwm.or(status.vmrss))
502            .unwrap_or(0)
503            .max(1)
504    }
505
506    /// handle sysinfo syscall
507    pub async fn handle_sysinfo<G: Guest<Self>>(
508        &self,
509        guest: &mut G,
510        call: syscalls::Sysinfo,
511    ) -> Result<i64, Error> {
512        let info_addr = call.info().ok_or(Errno::EFAULT)?;
513        let sys_info = self.collect_sysinfo(guest).await?;
514        let mut memory = guest.memory();
515
516        // Typed moves do not guarantee initialized repr(C) padding. Serialize
517        // directly into initialized bytes for every target libc.
518        let bytes = sys_info.to_kernel_bytes();
519        memory.write_exact(info_addr.cast::<u8>(), &bytes)?;
520        Ok(0)
521    }
522
523    /// Whole-second `/proc/uptime` value (floor of elapsed logical time).
524    pub(super) async fn calculate_procfs_uptime<G: Guest<Self>>(
525        &self,
526        guest: &mut G,
527    ) -> Result<u64, Error> {
528        let global_time = thread_observe_time(guest).await;
529        Ok(procfs_uptime_seconds(
530            global_time,
531            crate::types::DetTime::new(&self.cfg).as_nanos(),
532            self.cfg.sysinfo_uptime_offset,
533        ))
534    }
535
536    /// Whole-second `/proc/stat` `btime`, fixed for the life of the run.
537    ///
538    /// `EOVERFLOW` only when the boot instant precedes `i64::MIN` seconds.
539    /// Only snapshots that render `btime` may ask, so that refusal stays
540    /// local to `/proc/stat`.
541    pub(super) fn calculate_procfs_boot_time(&self) -> Result<i64, Error> {
542        procfs_boot_time_seconds(
543            crate::types::DetTime::new(&self.cfg).as_nanos(),
544            self.cfg.sysinfo_uptime_offset,
545        )
546        .ok_or_else(|| Errno::EOVERFLOW.into())
547    }
548
549    async fn collect_sysinfo<G: Guest<Self>>(
550        &self,
551        guest: &mut G,
552    ) -> Result<syscalls::SysInfo, Error> {
553        let memory = configured_memory(crate::machine::MachineDescription::of(&self.cfg).memory);
554        let now = thread_observe_time(guest).await;
555        let epoch = crate::types::DetTime::new(&self.cfg).as_nanos();
556        Ok(syscalls::SysInfo {
557            uptime: sysinfo_uptime_seconds(now, epoch, self.cfg.sysinfo_uptime_offset)?,
558            loads_1: 1,
559            loads_5: 1,
560            loads_15: 1,
561            total_ram: memory.total_ram,
562            free_ram: memory.free_ram,
563            buffer_ram: memory.buffer_ram,
564            shared_ram: memory.shared_ram,
565            total_swap: memory.total_swap,
566            free_swap: memory.free_swap,
567            procs: 1,
568            total_high: memory.total_high,
569            free_high: memory.free_high,
570            mem_unit: memory.mem_unit,
571        })
572    }
573}
574
575// AUTONOMOUS-BOT-IMPLEMENTED
576// TODO-HUMAN-REVIEW(PR-2979): Deterministic free-memory accounting for sysinfo(2).
577#[derive(Debug, PartialEq, Eq)]
578struct ConfiguredMemory {
579    total_ram: u64,
580    free_ram: u64,
581    buffer_ram: u64,
582    shared_ram: u64,
583    total_swap: u64,
584    free_swap: u64,
585    total_high: u64,
586    free_high: u64,
587    mem_unit: u32,
588}
589
590/// Report the configured guest memory consistently with virtual `/proc/meminfo`.
591///
592/// Linux `sysinfo(2)` describes system-wide memory, not one process's virtual
593/// mappings. Detcore does not model allocation pressure within its configured
594/// memory limit, so all configured memory remains available and the other
595/// modeled memory categories remain empty.
596fn configured_memory(memory: u64) -> ConfiguredMemory {
597    ConfiguredMemory {
598        total_ram: memory,
599        free_ram: memory,
600        buffer_ram: 0,
601        shared_ram: 0,
602        total_swap: 0,
603        free_swap: 0,
604        total_high: 0,
605        free_high: 0,
606        mem_unit: 1,
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613    use crate::types::LogicalTime;
614
615    #[test]
616    fn logical_clock_ticks_include_boot_offset_and_fractional_seconds() {
617        let boot = LogicalTime::from_secs(1_000);
618        let now = boot + LogicalTime::from_millis(25);
619
620        assert_eq!(logical_clock_ticks(now, boot, 120), 12_002);
621    }
622
623    #[test]
624    fn sysinfo_uptime_rounds_positive_elapsed_up() {
625        let epoch = LogicalTime::from_nanos(1_000_000_000_000);
626        for (elapsed_ns, expected_zero, expected_offset) in [
627            (0, 0, 120),
628            (1, 1, 121),
629            (999_999_999, 1, 121),
630            (1_000_000_000, 1, 121),
631            (1_000_000_001, 2, 122),
632            (1_200_000_000, 2, 122),
633        ] {
634            let now = epoch + LogicalTime::from_nanos(elapsed_ns);
635            assert_eq!(
636                sysinfo_uptime_seconds(now, epoch, 0).unwrap(),
637                expected_zero,
638                "elapsed {elapsed_ns} ns without boot offset"
639            );
640            assert_eq!(
641                sysinfo_uptime_seconds(now, epoch, 120).unwrap(),
642                expected_offset,
643                "elapsed {elapsed_ns} ns with boot offset"
644            );
645        }
646    }
647
648    #[test]
649    fn sysinfo_uptime_ignores_epoch_fraction() {
650        for fraction_ns in [0, 1, 1_000, 999_999_000, 999_999_999] {
651            let epoch = LogicalTime::from_nanos(1_000_000_000_000 + fraction_ns);
652            for (elapsed_ns, expected) in [
653                (0, 120),
654                (1, 121),
655                (999_999_999, 121),
656                (1_000_000_000, 121),
657                (1_000_000_001, 122),
658                (1_200_000_000, 122),
659            ] {
660                let now = epoch + LogicalTime::from_nanos(elapsed_ns);
661                assert_eq!(
662                    sysinfo_uptime_seconds(now, epoch, 120).unwrap(),
663                    expected,
664                    "epoch fraction {fraction_ns} ns, elapsed {elapsed_ns} ns"
665                );
666            }
667        }
668    }
669
670    #[test]
671    fn sysinfo_uptime_rounds_maximum_duration_without_overflow() {
672        let epoch = LogicalTime::from_nanos(0);
673        assert_eq!(
674            sysinfo_uptime_seconds(LogicalTime::MAX, epoch, 0).unwrap(),
675            18_446_744_074
676        );
677        assert_eq!(
678            sysinfo_uptime_seconds(
679                LogicalTime::from_nanos(18_446_744_073_000_000_000),
680                epoch,
681                120,
682            )
683            .unwrap(),
684            18_446_744_193
685        );
686        // An absolute timestamp at the representation limit need not have a
687        // large elapsed duration; subtraction must precede the projection.
688        assert_eq!(
689            sysinfo_uptime_seconds(LogicalTime::MAX, LogicalTime::from_nanos(u64::MAX - 1), 120,)
690                .unwrap(),
691            121
692        );
693    }
694
695    #[test]
696    fn sysinfo_uptime_preserves_wrapping_offset_extension() {
697        let epoch = LogicalTime::from_nanos(0);
698        for (elapsed_ns, offset, expected) in [
699            (0, u64::MAX, u64::MAX),
700            (1, u64::MAX, 0),
701            (1_000_000_000, u64::MAX, 0),
702            (1_000_000_001, u64::MAX, 1),
703            (1, u64::MAX - 1, u64::MAX),
704            (1_000_000_001, u64::MAX - 1, 0),
705        ] {
706            assert_eq!(
707                sysinfo_uptime_seconds(LogicalTime::from_nanos(elapsed_ns), epoch, offset).unwrap(),
708                expected,
709                "elapsed {elapsed_ns} ns, offset {offset}"
710            );
711        }
712    }
713
714    #[test]
715    fn sysinfo_uptime_preserves_signed_abi_conversion() {
716        let epoch = LogicalTime::from_nanos(0);
717        for (elapsed_ns, offset, expected) in [
718            (0, i64::MAX as u64, i64::MAX),
719            (1, i64::MAX as u64, i64::MIN),
720            (0, u64::MAX, -1),
721            (1, u64::MAX, 0),
722        ] {
723            let uptime =
724                sysinfo_uptime_seconds(LogicalTime::from_nanos(elapsed_ns), epoch, offset).unwrap();
725            let info: reverie::syscalls::linux_abi::Sysinfo = syscalls::SysInfo {
726                uptime,
727                loads_1: 0,
728                loads_5: 0,
729                loads_15: 0,
730                total_ram: 0,
731                free_ram: 0,
732                shared_ram: 0,
733                buffer_ram: 0,
734                total_swap: 0,
735                free_swap: 0,
736                procs: 0,
737                total_high: 0,
738                free_high: 0,
739                mem_unit: 1,
740            }
741            .into();
742            assert_eq!(info.uptime, expected);
743        }
744    }
745
746    #[test]
747    fn sysinfo_uptime_rejects_time_before_epoch() {
748        let error = sysinfo_uptime_seconds(
749            LogicalTime::from_nanos(999),
750            LogicalTime::from_nanos(1_000),
751            120,
752        )
753        .unwrap_err();
754        let Error::Tool(error) = error else {
755            panic!("an impossible clock must be a tool failure, got {error:?}");
756        };
757        assert_eq!(
758            error.to_string(),
759            "sysinfo observed logical time 999 ns before epoch 1000 ns"
760        );
761    }
762
763    #[test]
764    fn procfs_uptime_subtracts_fractional_boot_before_truncating() {
765        let boot = LogicalTime::from_nanos(1_000_999_999_999);
766
767        assert_eq!(
768            procfs_uptime_seconds(boot + LogicalTime::from_nanos(1), boot, 120),
769            120
770        );
771        assert_eq!(
772            procfs_uptime_seconds(boot + LogicalTime::from_secs(1), boot, 120),
773            121
774        );
775    }
776
777    #[test]
778    fn procfs_boot_time_is_the_boot_instant_not_now_minus_uptime() {
779        // The reviewed reproducer: boot 1000.75s, offset 120s. Linux reports
780        // the seconds of the fixed boot instant, 880, for every sample.
781        let boot = LogicalTime::from_nanos(1_000_750_000_000);
782        assert_eq!(procfs_boot_time_seconds(boot, 120), Some(880));
783
784        // `floor(now) - uptime` is what the fix replaced: at these samples it
785        // yields 880, 881, 880. Pin that the old derivation really did move,
786        // so this test keeps meaning something if the helpers change.
787        let old_btime =
788            |now: LogicalTime| now.as_secs() as i64 - procfs_uptime_seconds(now, boot, 120) as i64;
789        let samples = [100, 400, 1_100].map(|millis| boot + LogicalTime::from_millis(millis));
790        assert_eq!(samples.map(old_btime), [880, 881, 880]);
791
792        // For an integral boot the two derivations agree, so whole-second
793        // epochs render exactly what they rendered before.
794        let integral_boot = LogicalTime::from_secs(1_000);
795        let integral_now = integral_boot + LogicalTime::from_millis(1_400);
796        assert_eq!(
797            procfs_boot_time_seconds(integral_boot, 120),
798            Some(
799                integral_now.as_secs() as i64
800                    - procfs_uptime_seconds(integral_now, integral_boot, 120) as i64
801            )
802        );
803
804        assert_eq!(procfs_boot_time_seconds(boot, u64::MAX), None);
805    }
806
807    #[test]
808    fn procfs_boot_time_is_exact_for_every_offset_whose_result_fits() {
809        // Config accepts every u64 offset. 2026-01-01T00:00:00Z minus 2^63 s
810        // is -9223372035087550208, which time64_t represents even though the
811        // offset alone exceeds i64::MAX.
812        let boot = LogicalTime::from_secs(1_767_225_600);
813        assert_eq!(procfs_boot_time_seconds(boot, 0), Some(1_767_225_600));
814        assert_eq!(
815            procfs_boot_time_seconds(boot, 1 << 63),
816            Some(-9_223_372_035_087_550_208)
817        );
818        // The result, not the offset, bounds the domain: exactly i64::MIN is
819        // representable and one second earlier is not.
820        assert_eq!(
821            procfs_boot_time_seconds(boot, 1_767_225_600 + (1 << 63)),
822            Some(i64::MIN)
823        );
824        assert_eq!(
825            procfs_boot_time_seconds(boot, 1_767_225_600 + (1 << 63) + 1),
826            None
827        );
828        assert_eq!(procfs_boot_time_seconds(boot, u64::MAX), None);
829    }
830
831    #[test]
832    fn sysinfo_uptime_rounds_fractional_elapsed_up_like_linux() {
833        // A boot instant with a fractional absolute second: rounding must see
834        // only the elapsed time, never the absolute boundary crossing.
835        let boot = LogicalTime::from_nanos(1_000_999_999_999);
836
837        assert_eq!(sysinfo_uptime_seconds(boot, boot, 120).unwrap(), 120);
838        assert_eq!(
839            sysinfo_uptime_seconds(boot + LogicalTime::from_nanos(1), boot, 120).unwrap(),
840            121
841        );
842        assert_eq!(
843            sysinfo_uptime_seconds(boot + LogicalTime::from_millis(999), boot, 120).unwrap(),
844            121
845        );
846        assert_eq!(
847            sysinfo_uptime_seconds(boot + LogicalTime::from_secs(1), boot, 120).unwrap(),
848            121
849        );
850        assert_eq!(
851            sysinfo_uptime_seconds(
852                boot + LogicalTime::from_secs(1) + LogicalTime::from_nanos(1),
853                boot,
854                120
855            )
856            .unwrap(),
857            122
858        );
859        // The floor-based procfs value differs exactly on fractional elapsed.
860        let fractional = boot + LogicalTime::from_millis(1_500);
861        assert_eq!(procfs_uptime_seconds(fractional, boot, 120), 121);
862        assert_eq!(sysinfo_uptime_seconds(fractional, boot, 120).unwrap(), 122);
863    }
864
865    #[test]
866    fn sysinfo_memory_matches_configured_memory() {
867        assert_eq!(
868            configured_memory(1_000_000_000),
869            ConfiguredMemory {
870                total_ram: 1_000_000_000,
871                free_ram: 1_000_000_000,
872                buffer_ram: 0,
873                shared_ram: 0,
874                total_swap: 0,
875                free_swap: 0,
876                total_high: 0,
877                free_high: 0,
878                mem_unit: 1,
879            },
880        );
881    }
882
883    #[test]
884    fn prlimit_self_target_prefers_deterministic_process_identity() {
885        assert!(prlimit_targets_current_process(3, Some(3), 10_003));
886        assert!(prlimit_targets_current_process(0, Some(3), 10_003));
887        assert!(!prlimit_targets_current_process(10_003, Some(3), 10_003));
888        assert!(!prlimit_targets_current_process(4, Some(3), 10_003));
889    }
890
891    #[test]
892    fn prlimit_self_target_falls_back_to_physical_identity_before_init() {
893        assert!(prlimit_targets_current_process(10_003, None, 10_003));
894        assert!(!prlimit_targets_current_process(3, None, 10_003));
895    }
896
897    #[test]
898    fn prlimit_accepts_exact_noop_for_restricted_resource() {
899        let limit = ResourceLimit {
900            current: 0,
901            maximum: 0,
902        };
903        assert_eq!(
904            validate_resource_limit_mutation(
905                reverie::syscalls::linux_abi::RLIMIT_CPU,
906                limit,
907                limit
908            ),
909            Ok(())
910        );
911    }
912
913    #[test]
914    fn prlimit_accepts_core_soft_limit_change() {
915        let previous = ResourceLimit {
916            current: 1,
917            maximum: 1,
918        };
919        let requested = ResourceLimit {
920            current: 0,
921            maximum: 1,
922        };
923        assert_eq!(
924            validate_resource_limit_mutation(
925                reverie::syscalls::linux_abi::RLIMIT_CORE,
926                previous,
927                requested
928            ),
929            Ok(())
930        );
931    }
932
933    #[test]
934    fn prlimit_rejects_actual_change_to_restricted_resource() {
935        let previous = ResourceLimit {
936            current: 1,
937            maximum: 1,
938        };
939        let requested = ResourceLimit {
940            current: 0,
941            maximum: 1,
942        };
943        assert_eq!(
944            validate_resource_limit_mutation(
945                reverie::syscalls::linux_abi::RLIMIT_CPU,
946                previous,
947                requested
948            ),
949            Err(Errno::EPERM)
950        );
951    }
952
953    #[test]
954    fn prlimit_rejects_invalid_soft_limit_before_noop_policy() {
955        let previous = ResourceLimit {
956            current: 1,
957            maximum: 1,
958        };
959        let requested = ResourceLimit {
960            current: 2,
961            maximum: 1,
962        };
963        assert_eq!(
964            validate_resource_limit_mutation(
965                reverie::syscalls::linux_abi::RLIMIT_CORE,
966                previous,
967                requested
968            ),
969            Err(Errno::EINVAL)
970        );
971    }
972
973    #[test]
974    fn prlimit_rejects_core_hard_limit_raise() {
975        let previous = ResourceLimit {
976            current: 1,
977            maximum: 1,
978        };
979        let requested = ResourceLimit {
980            current: 1,
981            maximum: 2,
982        };
983        assert_eq!(
984            validate_resource_limit_mutation(
985                reverie::syscalls::linux_abi::RLIMIT_CORE,
986                previous,
987                requested
988            ),
989            Err(Errno::EPERM)
990        );
991    }
992
993    #[test]
994    fn logical_cpu_ticks_exclude_boot_epoch() {
995        assert_eq!(clock_ticks(LogicalTime::from_millis(25)), 2);
996    }
997
998    #[test]
999    fn rusage_timeval_splits_seconds_and_microseconds() {
1000        let tv = timeval_from_logical(LogicalTime::from_millis(2_500));
1001        assert_eq!(tv.tv_sec, 2);
1002        assert_eq!(tv.tv_usec, 500_000);
1003    }
1004
1005    #[test]
1006    fn rusage_timeval_truncates_sub_microsecond_rather_than_rounding() {
1007        // 1_999 ns is a hair under 2us. Truncating yields 1us; rounding to nearest would
1008        // yield 2us and could make a later, larger duration report a SMALLER value once its
1009        // remainder shrank -- i.e. CPU time going backwards. Pin truncation explicitly.
1010        let tv = timeval_from_logical(LogicalTime::from_nanos(1_999));
1011        assert_eq!(tv.tv_sec, 0);
1012        assert_eq!(tv.tv_usec, 1);
1013    }
1014
1015    #[test]
1016    fn rusage_timeval_is_monotonic_in_the_logical_duration() {
1017        // The property that matters to a guest: CPU time never goes backwards. Walk a range
1018        // of nanosecond durations across microsecond and second boundaries and assert the
1019        // rendered timeval is non-decreasing at every step.
1020        let mut previous = (0_i64, 0_i64);
1021        for nanos in (0..3_000_000u64).step_by(997) {
1022            let tv = timeval_from_logical(LogicalTime::from_nanos(nanos));
1023            let current = (tv.tv_sec, tv.tv_usec);
1024            assert!(
1025                current >= previous,
1026                "rusage timeval went backwards at {nanos}ns: {previous:?} -> {current:?}"
1027            );
1028            previous = current;
1029        }
1030    }
1031
1032    #[test]
1033    fn rusage_zero_cpu_time_renders_as_zero() {
1034        let tv = timeval_from_logical(LogicalTime::ZERO);
1035        assert_eq!(tv.tv_sec, 0);
1036        assert_eq!(tv.tv_usec, 0);
1037    }
1038
1039    #[test]
1040    fn rusage_and_times_agree_within_one_clock_tick() {
1041        // Both syscalls project the same logical duration, but times(2) is
1042        // quantized to USER_HZ while getrusage(2) retains microseconds.
1043        for nanos in [0u64, 1_000_000, 300_484_000, 7_000_000_000, 12_345_678_901] {
1044            let duration = LogicalTime::from_nanos(nanos);
1045            let tv = timeval_from_logical(duration);
1046            let rusage_micros = tv.tv_sec as u64 * 1_000_000 + tv.tv_usec as u64;
1047            let times_micros = clock_ticks(duration) * (NANOS_PER_CLOCK_TICK / 1_000);
1048
1049            assert!(rusage_micros >= times_micros);
1050            assert!(rusage_micros - times_micros < NANOS_PER_CLOCK_TICK / 1_000);
1051
1052            // A tick-ALIGNED duration must agree EXACTLY, not merely to within
1053            // one tick. The bounds above are satisfied at every sample by an
1054            // implementation carrying a constant sub-tick offset, so without
1055            // this the suite cannot distinguish that from a correct one.
1056            if nanos % NANOS_PER_CLOCK_TICK == 0 {
1057                assert_eq!(rusage_micros, times_micros);
1058            }
1059        }
1060    }
1061
1062    #[test]
1063    fn logical_clock_ticks_wrap_configured_offset_like_linux_clock_t() {
1064        let boot = LogicalTime::from_secs(1_000);
1065        let before = logical_clock_ticks(boot, boot, u64::MAX);
1066        let after = logical_clock_ticks(boot + LogicalTime::from_millis(10), boot, u64::MAX);
1067
1068        assert_eq!(before, -100);
1069        assert_eq!(after, -99);
1070    }
1071}