Skip to main content

detcore/
util.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
9//! Widely useful small utilities.
10
11use std::sync::OnceLock;
12use std::sync::atomic::AtomicU64;
13use std::sync::atomic::Ordering;
14use std::time::Duration;
15
16use crate::types::NANOS_PER_RCB;
17
18/// Issues syscall `number` with `args` through the syscall instruction itself
19/// on x86-64, returning the kernel's result (a negative errno on failure).
20/// Under in-guest LiteInst Detcore runs inside the guest, where libc's
21/// wrappers (`open`, `fstat`, `syscall`, ...) are dynamic symbols the program
22/// or a preloaded library may define, and allocate in.
23///
24/// # Safety
25///
26/// As for the syscall itself: pointer arguments must be valid for it.
27pub unsafe fn raw_syscall(number: libc::c_long, args: [u64; 6]) -> i64 {
28    #[cfg(target_arch = "x86_64")]
29    {
30        let result: i64;
31        // SAFETY: the caller vouches for the arguments.
32        unsafe {
33            core::arch::asm!(
34                "syscall",
35                inlateout("rax") number => result,
36                in("rdi") args[0],
37                in("rsi") args[1],
38                in("rdx") args[2],
39                in("r10") args[3],
40                in("r8") args[4],
41                in("r9") args[5],
42                lateout("rcx") _,
43                lateout("r11") _,
44                options(nostack),
45            );
46        }
47        result
48    }
49    #[cfg(not(target_arch = "x86_64"))]
50    {
51        // SAFETY: the caller vouches for the arguments.
52        let result =
53            unsafe { libc::syscall(number, args[0], args[1], args[2], args[3], args[4], args[5]) };
54        if result < 0 {
55            -i64::from(
56                std::io::Error::last_os_error()
57                    .raw_os_error()
58                    .unwrap_or(libc::EIO),
59            )
60        } else {
61            result
62        }
63    }
64}
65
66/// [`raw_syscall`]'s result as an `io::Result`.
67pub fn raw_result(result: i64) -> std::io::Result<u64> {
68    if (-4095..0).contains(&result) {
69        Err(std::io::Error::from_raw_os_error(-result as i32))
70    } else {
71        Ok(result as u64)
72    }
73}
74
75/// [`raw_syscall`]'s result for a syscall whose only success value is 0
76/// (`fstat`, `pipe2`, `socketpair`, ...): any other value is refused, a
77/// negative one as the error it names. [`raw_result`] would take a positive
78/// return as success, which for such a call the kernel never gives.
79pub fn raw_zero_result(result: i64) -> std::io::Result<()> {
80    match raw_result(result)? {
81        0 => Ok(()),
82        other => Err(std::io::Error::other(format!(
83            "a syscall that returns 0 on success returned {other}"
84        ))),
85    }
86}
87
88/// Describes `error` without asking the C library for an errno message.
89///
90/// `std::io::Error`'s `Display` and `Debug` render an operating-system error
91/// through `strerror_r`, which can call `gettext` and allocate through the C
92/// library's malloc: under in-guest LiteInst, where Detcore runs inside the
93/// guest, the guest's own heap. An operating-system error is described by its
94/// kind and number instead; any other error, or an I/O error wrapping one, as
95/// before.
96pub fn describe_io_error(error: &std::io::Error) -> String {
97    if let Some(code) = error.raw_os_error() {
98        return format!("{:?} (errno {code})", error.kind());
99    }
100    match error
101        .get_ref()
102        .and_then(|inner| inner.downcast_ref::<std::io::Error>())
103    {
104        Some(inner) => describe_io_error(inner),
105        None => error.to_string(),
106    }
107}
108
109#[allow(dead_code)]
110/// A simple debugging helper function that makes it easy to printf-debug through
111/// layers of stdout/stderr caputure, such as when running under buck test/tpx.
112pub fn punch_out_print(msg: &str) {
113    use std::io::Write;
114    // TODO: if we want this to be more performant, we can have a lazy static
115    // global file handle for this. This, however, keeps it simple for occasional usage.œ
116    if let Ok(mut tty) = std::fs::OpenOptions::new()
117        .read(true)
118        .write(true)
119        .open("/dev/tty")
120    {
121        writeln!(tty, "{}", msg).unwrap();
122    } else {
123        // If devtty doesn't exist, we just use stderr.
124        eprintln!("{}", msg);
125    }
126}
127/// A helper function to convert a number of Retired Conditional Branches (RCBS) into
128/// a `std::time::Duration` via the `NANOS_PER_RCB` defined in ` types.rs`.
129pub fn rcbs_to_duration(rcbs: u64) -> Duration {
130    Duration::from_nanos((rcbs as f64 * NANOS_PER_RCB) as u64)
131}
132
133/// A little better than the builtin string truncation in format strings, because it includes ellipses.
134// TODO: There should be some advanced solution for printing potentially huge things that
135// doesn't actually render them all...
136pub fn truncated(width: usize, mut s: String) -> String {
137    if s.len() > width {
138        if width >= 3 {
139            s.truncate(width - 3);
140            s.push_str("...");
141            s
142        } else {
143            s.truncate(width);
144            s
145        }
146    } else {
147        s
148    }
149}
150
151/// A `Write` for the supervisor's own diagnostics on fd 2 that survives a
152/// NONBLOCKING description.
153///
154/// ⚠️ THE GUEST CAN SET `O_NONBLOCK` ON HERMIT'S STDERR AND SILENTLY CUT THE
155/// SUPERVISOR'S ERROR CHANNEL. fd 2 is an INHERITED open file description shared
156/// with the guest, so `fcntl(2, F_SETFL, O_NONBLOCK)` in the guest changes the
157/// behaviour of hermit's OWN later writes. Measured 2026-08-26 on a 4096-byte
158/// pipe under back-pressure, `hermit --log info run -- <guest>`:
159///
160/// ```text
161///   control guest (touches nothing)   rc=0    138 lines, ends on the run summary
162///   guest sets O_NONBLOCK on fd 2     rc=101  134 lines, ends mid-log
163/// ```
164///
165/// The summary was emitted by `eprint!`, which calls `write_all`; `write_all`
166/// does NOT retry `EAGAIN`, so it returns an error and the print macro PANICS.
167/// The panic message then went to the same full pipe and was lost as well, so
168/// the delivered output contained no panic text, no error, and no marker of any
169/// kind -- it simply stopped on a plausible-looking line. Exit 101 was the only
170/// surviving evidence, and a caller that reads output rather than status sees a
171/// short report, not a truncated one.
172///
173/// ⚠️ WAIT, DO NOT SPIN, AND DO NOT CLEAR THE FLAG. Clearing `O_NONBLOCK` would
174/// change what the guest observes on a descriptor it legitimately shares; this
175/// leaves the flag exactly as the guest set it and simply waits for the pipe to
176/// drain, which is what a blocking write would have done. A bare retry loop
177/// would busy-spin against a full pipe, so it blocks in `poll(POLLOUT)`.
178pub struct RetryingStderr;
179
180/// Total wall-clock ALL diagnostic writes in ONE PROCESS may spend waiting for a
181/// reader that is not draining. Hermit forks, so read the fork note below before
182/// treating this as the whole exit path's budget.
183///
184/// ⚠️ WITHOUT A CEILING THIS LOOP NEVER ENDS, AND IT RUNS WHILE HERMIT IS ALREADY
185/// FAILING. `EAGAIN` -> `poll(POLLOUT, 1s)` -> retry is correct for a reader that
186/// is SLOW and unbounded for a reader that is STOPPED. Measured 2026-08-26 on a
187/// 4096-byte pipe filled to 3900 with `O_NONBLOCK` set, where nothing ever reads:
188///
189/// ```text
190///   before RetryingStderr (eprintln!)   EXITED rc=101 immediately
191///   RetryingStderr, no ceiling          STILL RUNNING after 25s, no exit
192/// ```
193///
194/// The first is wrong loudly; the second does not return at all, on the path that
195/// reports why hermit is stopping. A supervisor that hangs while reporting an
196/// error is harder to diagnose than one that dies reporting it badly.
197///
198/// ⚠️ THIS IS A TOTAL FOR THE WHOLE PROCESS, NOT A BUDGET PER `write()` CALL, AND
199/// THE DIFFERENCE IS THE ENTIRE GUARANTEE. An earlier version started the clock
200/// inside `fn write`, so every call got the full allowance. Nothing writes to
201/// stderr exactly once on the exit path:
202///
203/// ```text
204///   hermit-cli/src/bin/hermit/main.rs      the failure class, the head of the
205///                                          error chain, then ONE PER CAUSE in
206///                                          `for cause in chain` -- 2 + chain length
207///   hermit-cli/src/bin/hermit/tracing.rs   .with_writer(|| RetryingStderr) is the
208///                                          writer for the WHOLE subscriber: one
209///                                          write per log event
210///   detcore/src/tool_global.rs             a multi-part `write!`, which `write_fmt`
211///                                          splits into several `write` calls
212/// ```
213///
214/// With a per-call budget the process-level cost was N times this number, N is
215/// bounded by the error chain rather than by anything here, and every caller's
216/// `let _ =` swallowed the overrun silently. A `--log info` run measured at 138
217/// lines would have been minutes, not seconds.
218///
219/// ⚠️ THE VALUE IS DERIVED FROM THE BOUND THAT ENCLOSES IT, NOT CHOSEN. Diagnostics
220/// on the exit path run inside `RUN_TIMEOUT_UNWIND_GRACE` (10s,
221/// `hermit-cli/src/lib.rs`): the window between a `--timeout` expiring and the
222/// SIGALRM fallback calling `_exit`. Overrun there does not merely delay the
223/// report, it loses it -- the fallback fires mid-sentence and additionally emits
224/// `HERMIT_RUN_TIMEOUT_FALLBACK`, which is supposed to mean the teardown wedged.
225/// So the arithmetic is:
226///
227/// ⚠️ IT USED TO APPLY ONCE PER PROCESS, BECAUSE HERMIT FORKS. `Container::run`
228/// forks the container init, which got its own COPY of the process-wide clock and
229/// spent its own full deadline; both write diagnostics to the same fd 2 on the way
230/// out. Measured 2026-08-26 against a stopped reader on a real guest, which is the
231/// case where both processes report:
232///
233/// ```text
234///   deadline 5000ms   ->  10.03s observed   two processes, one deadline each
235///   deadline 2500ms   ->   5.02s observed   the same two, halved
236///   deadline 2500ms   ->   2.52s observed   missing guest: only ONE process reports
237/// ```
238///
239/// ⚠️ AND THE MULTIPLIER WAS NOT ALWAYS TWO, WHICH IS WHY IT IS NO LONGER A
240/// CONSTANT. `hermit run --verify` runs the guest twice and forks once per run, so
241/// it is the outer hermit plus TWO container inits -- three writers, and siblings
242/// rather than a chain. From the call structure, so it needs no sampling:
243///
244/// ```text
245///   run.rs  fn verify()      Run1 -> self.run_verify(..)   Run2 -> self.run_verify(..)
246///           fn run_verify()  -> with_container(..) forks a container init
247/// ```
248///
249/// At three, `3 x 2500ms x 2 = 15s` against a 10s grace: the stated split does not
250/// hold. Correcting the constant to three would force the deadline to ~1666ms and
251/// leave a hardcoded count for the next differently-forking path to falsify.
252///
253/// So the ORIGIN is shared across the invocation instead (see
254/// `STDERR_ORIGIN_ADDR`): every hermit process measures from the same start, the
255/// invocation spends ONE deadline however many processes write, and the count
256/// stops being an input. The arithmetic no longer contains it:
257///
258/// ```text
259///   RUN_TIMEOUT_UNWIND_GRACE                    10s     the enclosing bound
260///   diagnostics may have half of it              5s     leaving 5s for the unwind
261///   this deadline, per INVOCATION             2500ms    comfortably inside the 5s
262/// ```
263///
264/// Half is a split, not a measurement, and is stated as one. What is ALSO measured is
265/// that the previous arrangement could not fit: at 5s per CALL, the same real-guest
266/// fixture took 15.04s -- three blocked writes -- against a 10s grace, so the inner
267/// bound exceeded its outer bound before any unwind work happened at all.
268/// `hermit-cli` asserts this relationship in a test, so raising either side without
269/// the other fails by name.
270///
271/// On expiry the write returns `WouldBlock`, `writeln!` gives up, the caller's
272/// `let _ =` drops that line, and hermit exits — the pre-existing contract for a
273/// diagnostic that cannot be delivered.
274pub const STDERR_DIAGNOSTIC_DEADLINE: std::time::Duration = std::time::Duration::from_millis(2500);
275
276/// When this process first found stderr unwritable, shared by every
277/// `RetryingStderr`, which is what makes the deadline above a process total.
278/// Set on the first `EAGAIN` and never reset: a run that recovers and later
279/// blocks again has still spent that earlier time on its exit path.
280///
281/// Fallback only: used when the invocation-wide origin below is unavailable.
282static STDERR_BLOCKED_SINCE: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
283
284/// The same instant, shared by every hermit process of ONE INVOCATION.
285///
286/// ⚠️ WITHOUT THIS THE DEADLINE IS MULTIPLIED BY THE NUMBER OF PROCESSES, AND
287/// THAT NUMBER IS NOT 2. A `static` lives per process, so each hermit sets its
288/// own origin and spends its own full deadline. The comment above states the
289/// multiplier as "2 -- hermit + the forked init", which is right for `hermit
290/// run` and wrong for `hermit run --verify`. From the call structure, not from
291/// sampling a process table:
292///
293/// ```text
294///   hermit-cli/src/bin/hermit/run.rs
295///     fn verify()                      -> Run1: self.run_verify(log1_file, global)
296///                                      -> Run2: self.run_verify(log2_file, global)
297///     fn run_verify()                  -> with_container(..) forks a container init
298/// ```
299///
300/// So `--verify` is the outer hermit plus TWO container inits, which share the
301/// outer as their parent. Three writing processes, and siblings rather than a
302/// chain. `record --verify` forks twice for the same reason, at
303/// `record_verify.record` and `record_verify.replay` in record_start.rs.
304///
305/// At three the stated derivation does not hold: 3 x 2500ms x 2 = 15s against a
306/// 10s grace. Correcting the constant to 3 would force the deadline to ~1666ms
307/// and would leave a hardcoded count that the next differently-forking path
308/// falsifies again. Sharing the ORIGIN makes the multiplier 1 by construction --
309/// every process measures from the same start, so the invocation spends one
310/// deadline however many processes write -- and the count stops being an input.
311///
312/// ⚠️ THE CARRIER IS AN INHERITED SHARED MAPPING, DELIBERATELY NOT AN ENVIRONMENT
313/// VARIABLE AND NOT A FILE. A variable set on hermit reaches the GUEST by default
314/// (`BaseEnv::Host` does not clear it), and a per-run-varying value visible to the
315/// guest, in a determinism tool, on a surface the argv/env hashing covers, would
316/// be a worse defect than the one being fixed. A file would put I/O on the path
317/// that runs while hermit is already failing. `exec` drops the mapping, so the
318/// guest never shares it.
319///
320/// `CLOCK_MONOTONIC` rather than `Instant`: an `Instant` is not meaningful in
321/// another process, while `CLOCK_MONOTONIC` is system-wide on Linux, so the same
322/// nanosecond count is directly comparable between parent and child. 0 means
323/// unset; the first blocked write in ANY process installs the origin.
324static STDERR_ORIGIN_ADDR: OnceLock<usize> = OnceLock::new();
325
326/// Create the origin cell every hermit process of this invocation shares.
327///
328/// ⚠️ MUST BE CALLED BEFORE THE FIRST FORK, and is called from the top of `main`.
329/// A mapping made after a fork is not shared with the child that already exists,
330/// so a late call would silently give each process its own origin -- the exact
331/// failure this removes. `main` dominates every `with_container` and
332/// `run_guarded_at` site in run.rs, replay.rs and record_start.rs.
333pub fn init_shared_stderr_deadline_origin() {
334    STDERR_ORIGIN_ADDR.get_or_init(|| {
335        // SAFETY: an anonymous shared mapping of one page, no fd, no fixed
336        // address. Never unmapped: it must outlive every child, and one page per
337        // invocation does not justify a teardown path on an exit route.
338        let addr = unsafe {
339            libc::mmap(
340                std::ptr::null_mut(),
341                std::mem::size_of::<AtomicU64>(),
342                libc::PROT_READ | libc::PROT_WRITE,
343                libc::MAP_SHARED | libc::MAP_ANONYMOUS,
344                -1,
345                0,
346            )
347        };
348        if addr == libc::MAP_FAILED {
349            // Not fatal and NOT silent: `stderr_deadline_is_shared` reports false
350            // and the per-process fallback still bounds each process.
351            return 0;
352        }
353        // SAFETY: freshly mapped, aligned for u64, sole owner at this point.
354        unsafe { (addr as *mut AtomicU64).write(AtomicU64::new(0)) };
355        addr as usize
356    });
357}
358
359fn shared_origin_cell() -> Option<&'static AtomicU64> {
360    match STDERR_ORIGIN_ADDR.get() {
361        None | Some(0) => None,
362        // SAFETY: the address came from `mmap` above, was initialised there, is
363        // never unmapped, and `AtomicU64` is safe to share across processes in a
364        // `MAP_SHARED` page.
365        Some(&addr) => Some(unsafe { &*(addr as *const AtomicU64) }),
366    }
367}
368
369fn monotonic_nanos() -> u64 {
370    let mut ts = libc::timespec {
371        tv_sec: 0,
372        tv_nsec: 0,
373    };
374    // SAFETY: one initialised `timespec`, a clock id the kernel always supports.
375    unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) };
376    (ts.tv_sec as u64) * 1_000_000_000 + (ts.tv_nsec as u64)
377}
378
379/// Whether the deadline is bounded per INVOCATION or only per process.
380///
381/// ⚠️ EXISTS SO THE WEAKER STATE CANNOT BE SILENT. If the mapping failed, or
382/// something forks before `init_shared_stderr_deadline_origin`, the bound
383/// degrades to per-process and is multiplied by however many processes write.
384#[doc(hidden)]
385pub fn stderr_deadline_is_shared() -> bool {
386    shared_origin_cell().is_some()
387}
388
389/// How long this invocation has been blocked on stderr, from the shared origin
390/// when there is one and from this process's own first block otherwise.
391fn stderr_blocked_for() -> std::time::Duration {
392    match shared_origin_cell() {
393        Some(cell) => {
394            let now = monotonic_nanos();
395            // The first blocked write in ANY process installs the origin; every
396            // later one, in any process, reads the value that won.
397            let origin = match cell.compare_exchange(0, now, Ordering::AcqRel, Ordering::Acquire) {
398                Ok(_) => now,
399                Err(existing) => existing,
400            };
401            std::time::Duration::from_nanos(now.saturating_sub(origin))
402        }
403        None => STDERR_BLOCKED_SINCE
404            .get_or_init(std::time::Instant::now)
405            .elapsed(),
406    }
407}
408
409/// Reset the shared origin. Test-only: brackets that measure the deadline need
410/// each case to start from zero, and nothing in a real run wants this.
411#[doc(hidden)]
412pub fn reset_stderr_deadline_origin_for_test() {
413    if let Some(cell) = shared_origin_cell() {
414        cell.store(0, Ordering::Release);
415    }
416}
417
418impl std::io::Write for RetryingStderr {
419    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
420        if buf.is_empty() {
421            return Ok(0);
422        }
423        loop {
424            // ⚠️ UNDER --max-log-bytes A BLOCKING DESCRIPTOR MUST NOT WAIT INSIDE
425            // write(2). The bounded wait below starts only at `EAGAIN`, which a
426            // blocking pipe never returns: its write simply waits for a reader,
427            // forever when the reader has stopped, and the run never reaches the
428            // cap. While `--max-log-bytes` is in force (review of
429            // https://github.com/rrnewton/hermit/pull/3686) each attempt is one
430            // that cannot wait for a reader, so a full pipe answers `EAGAIN` and
431            // takes the same bounded path as a guest-set `O_NONBLOCK`. Without the
432            // cap this is the plain `write(2)` it always was.
433            let must_not_wait = crate::nonwaiting_write::diagnostics_must_not_wait();
434            let attempt = if must_not_wait {
435                crate::nonwaiting_write::write_without_waiting_for_a_reader(
436                    libc::STDERR_FILENO,
437                    buf,
438                )
439            } else {
440                // SAFETY: `write(2)` on fd 2 reads `buf.len()` bytes from `buf`,
441                // which is valid for that length, and writes no memory.
442                let n = unsafe { libc::write(libc::STDERR_FILENO, buf.as_ptr().cast(), buf.len()) };
443                if n >= 0 {
444                    Ok(n as usize)
445                } else {
446                    Err(std::io::Error::last_os_error())
447                }
448            };
449            let err = match attempt {
450                Ok(n) => return Ok(n),
451                Err(err) => err,
452            };
453            match err.kind() {
454                // ⚠️ UNDER --max-log-bytes AN INTERRUPTED ATTEMPT IS RETRIED ONLY
455                // WITHIN THE SAME DEADLINE, AND GIVEN UP AS `TimedOut`. A seccomp
456                // policy can answer any call in the attempt with `EINTR` every
457                // time: answering `statx` and the file-type probes on fd 2 that
458                // way leaves the writer no type, and it returns `statx`'s
459                // `EINTR`. An unbounded retry then never returned to
460                // `DroppingStderr`, so the log was never charged and the cap
461                // never crossed. `Interrupted` itself must not escape either:
462                // `write_all`, behind `writeln!`, retries it forever. Without the
463                // cap an interrupted write is retried as it always was.
464                std::io::ErrorKind::Interrupted if must_not_wait => {
465                    if STDERR_DIAGNOSTIC_DEADLINE
466                        .checked_sub(stderr_blocked_for())
467                        .is_none()
468                    {
469                        return Err(std::io::ErrorKind::TimedOut.into());
470                    }
471                    continue;
472                }
473                std::io::ErrorKind::Interrupted => continue,
474                std::io::ErrorKind::WouldBlock => {
475                    // ⚠️ BOUNDED, AND THE CLOCK IS PROCESS-WIDE. Waiting for a slow
476                    // reader is the point; waiting for a stopped one is a hang on
477                    // hermit's exit path. Starting the clock here rather than at the
478                    // top of `write` is what makes the deadline a total: the first
479                    // blocked write sets it and every later one inherits it, so N
480                    // writes cost the deadline once instead of N times.
481                    // Measured from the origin shared by every hermit process
482                    // of this invocation when there is one, so N processes spend
483                    // ONE deadline rather than N. See STDERR_ORIGIN_ADDR.
484                    let spent = stderr_blocked_for();
485                    let Some(remaining) = STDERR_DIAGNOSTIC_DEADLINE.checked_sub(spent) else {
486                        return Err(err);
487                    };
488                    // Block until the reader makes room. A failed or timed-out
489                    // poll falls through to another write attempt rather than
490                    // dropping the bytes.
491                    let mut pfd = libc::pollfd {
492                        fd: libc::STDERR_FILENO,
493                        events: libc::POLLOUT,
494                        revents: 0,
495                    };
496                    // ⚠️ CLAMPED TO WHAT IS LEFT, NOT A FLAT SECOND. The check above
497                    // happens BEFORE the poll, so a flat 1000ms let a call that
498                    // started at 4.9s sleep a further second and overshoot to ~6s --
499                    // the deadline documented one number and delivered another.
500                    // Clamping makes the elapsed total the stated total.
501                    let timeout_ms = i32::try_from(remaining.as_millis())
502                        .unwrap_or(i32::MAX)
503                        .min(1000);
504                    // SAFETY: one initialised `pollfd`, count 1, timeout in ms.
505                    unsafe {
506                        libc::poll(&mut pfd, 1, timeout_ms);
507                    }
508                    continue;
509                }
510                _ => return Err(err),
511            }
512        }
513    }
514
515    fn flush(&mut self) -> std::io::Result<()> {
516        Ok(())
517    }
518}
519
520/// [`RetryingStderr`] for output whose loss must not change how a capped run
521/// ends: hermit's own log when it goes to stderr, and the stack traces that
522/// `--stacktrace-event` and `--preemption-stacktrace` print there.
523///
524/// While `--max-log-bytes` is in force
525/// ([`crate::nonwaiting_write::diagnostics_must_not_wait`]), bytes that
526/// [`RetryingStderr`] gives up on -- its bounded deadline for a reader that is
527/// not draining is spent, or the reader is gone -- are reported as written.
528/// So a log line that stderr could not take still counts against the cap,
529/// as `BoundedWriter` counts the bytes it truncates, and a run whose stderr
530/// reader stopped still reaches the cap and exits 123 instead of running on
531/// uncounted; and a caller that unwraps the write does not panic.
532/// Without the cap this is exactly [`RetryingStderr`], errors included.
533pub struct DroppingStderr;
534
535impl std::io::Write for DroppingStderr {
536    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
537        match RetryingStderr.write(buf) {
538            Err(_) if crate::nonwaiting_write::diagnostics_must_not_wait() => Ok(buf.len()),
539            result => result,
540        }
541    }
542
543    fn flush(&mut self) -> std::io::Result<()> {
544        Ok(())
545    }
546}
547
548/// `eprintln!` for detcore output that a capped run can print before the cap
549/// crosses: under `--max-log-bytes` the line goes through [`DroppingStderr`]
550/// and never waits beyond [`RetryingStderr`]'s deadline; otherwise this is the
551/// `eprintln!` it replaces, with the same arguments, so uncapped output is
552/// unchanged.
553macro_rules! capped_eprintln {
554    ($($arg:tt)*) => {
555        if crate::nonwaiting_write::diagnostics_must_not_wait() {
556            // One buffer, so the line is not split across several writes.
557            let mut line = format!($($arg)*);
558            line.push('\n');
559            let _ = std::io::Write::write_all(&mut crate::util::DroppingStderr, line.as_bytes());
560        } else {
561            eprintln!($($arg)*);
562        }
563    };
564}
565pub(crate) use capped_eprintln;
566
567#[cfg(test)]
568mod shared_stderr_origin_tests {
569    use super::*;
570
571    /// ⚠️ THESE TESTS SHARE ONE PROCESS-GLOBAL CELL, SO THEY MUST NOT RUN AT THE
572    /// SAME TIME. That is the point of the cell -- it is shared -- but it makes the
573    /// tests interfere: one resets the origin while another is timing from it.
574    /// Found the honest way: they passed under `--test-threads 1` and failed in the
575    /// real suite, so the single-threaded run was not evidence about the suite.
576    static SERIALISE: std::sync::Mutex<()> = std::sync::Mutex::new(());
577
578    fn exclusive() -> std::sync::MutexGuard<'static, ()> {
579        SERIALISE.lock().unwrap_or_else(|e| e.into_inner())
580    }
581
582    /// The origin must actually be SHARED across a fork, or nothing else holds.
583    ///
584    /// ⚠️ THIS IS THE ONE THING THAT CANNOT BE CHECKED BY READING THE CODE.
585    /// `MAP_SHARED` versus `MAP_PRIVATE` is a single token; get it wrong and every
586    /// single-process test still passes while each container init silently gets its
587    /// own deadline again -- the exact defect this removes. So this forks for real
588    /// and checks that a value written by the child is visible in the parent.
589    #[test]
590    fn the_origin_is_shared_across_a_fork() {
591        let _exclusive = exclusive();
592        init_shared_stderr_deadline_origin();
593        let cell = shared_origin_cell().expect("mapping must exist after init");
594        cell.store(0, Ordering::Release);
595
596        // SAFETY: the child does no allocation and no locking -- one atomic store
597        // and `_exit` -- so forking from a test harness thread is safe here.
598        let pid = unsafe { libc::fork() };
599        assert!(pid >= 0, "fork failed");
600        if pid == 0 {
601            shared_origin_cell()
602                .expect("child inherited no mapping")
603                .store(4242, Ordering::Release);
604            unsafe { libc::_exit(0) };
605        }
606        let mut status = 0;
607        assert_eq!(
608            unsafe { libc::waitpid(pid, &mut status, 0) },
609            pid,
610            "waitpid"
611        );
612        assert_eq!(status, 0, "child exited nonzero");
613
614        assert_eq!(
615            cell.load(Ordering::Acquire),
616            4242,
617            "the parent cannot see the child's write, so each container init would \
618             start its OWN deadline and the invocation would spend one per process -- \
619             check MAP_SHARED in init_shared_stderr_deadline_origin"
620        );
621        cell.store(0, Ordering::Release);
622    }
623
624    /// The first blocked write installs the origin; later ones inherit it.
625    ///
626    /// This is what makes the deadline a TOTAL rather than a fresh allowance per
627    /// caller, and it must hold across processes, not just across calls.
628    #[test]
629    fn the_first_writer_installs_the_origin_and_others_inherit_it() {
630        let _exclusive = exclusive();
631        init_shared_stderr_deadline_origin();
632        reset_stderr_deadline_origin_for_test();
633        let cell = shared_origin_cell().expect("mapping must exist after init");
634
635        let first = stderr_blocked_for();
636        let installed = cell.load(Ordering::Acquire);
637        assert_ne!(installed, 0, "the first call must install a nonzero origin");
638        // A fresh origin means almost no time has been spent yet.
639        assert!(
640            first < Duration::from_millis(50),
641            "first call saw {first:?}"
642        );
643
644        std::thread::sleep(Duration::from_millis(20));
645        let second = stderr_blocked_for();
646        assert_eq!(
647            cell.load(Ordering::Acquire),
648            installed,
649            "a later call moved the origin, so each caller would get a fresh \
650             allowance and the deadline would stop being a total"
651        );
652        assert!(
653            second >= Duration::from_millis(15),
654            "the second call reported {second:?}, so it is not measuring from the \
655             origin the first call installed"
656        );
657        reset_stderr_deadline_origin_for_test();
658    }
659
660    /// Which bound is in force must be answerable, not assumed.
661    #[test]
662    fn whether_the_deadline_is_invocation_wide_is_observable() {
663        let _exclusive = exclusive();
664        init_shared_stderr_deadline_origin();
665        // ⚠️ THE WEAKER STATE MUST NOT BE SILENT. If the mapping failed, or something
666        // forks before the init call, the deadline degrades to per-process and is
667        // multiplied by however many processes write.
668        assert!(
669            stderr_deadline_is_shared(),
670            "the origin is not shared after init, so the deadline is per-process and \
671             is multiplied by the number of writing processes"
672        );
673    }
674}
675
676/// A descriptor opened with a raw syscall, closed with one when dropped.
677pub struct RawFd(u64);
678
679impl RawFd {
680    /// `path` opened with `flags` (`O_CLOEXEC` added) by a raw `openat`, never
681    /// through libc's `open` wrapper (see `raw_syscall`).
682    pub fn open(path: &std::path::Path, flags: libc::c_int) -> std::io::Result<Self> {
683        let path = raw_path(path)?;
684        // SAFETY: a NUL-terminated path.
685        let fd = raw_result(unsafe {
686            raw_syscall(
687                libc::SYS_openat,
688                [
689                    libc::AT_FDCWD as u64,
690                    path.as_ptr() as u64,
691                    (flags | libc::O_CLOEXEC) as u64,
692                    0,
693                    0,
694                    0,
695                ],
696            )
697        })?;
698        Ok(Self(fd))
699    }
700
701    /// `name`, relative to this directory, opened with `flags` (`O_CLOEXEC`
702    /// added) by a raw `openat`.
703    pub fn open_at(&self, name: &std::ffi::OsStr, flags: libc::c_int) -> std::io::Result<Self> {
704        let name = raw_path(std::path::Path::new(name))?;
705        // SAFETY: a NUL-terminated name.
706        let fd = raw_result(unsafe {
707            raw_syscall(
708                libc::SYS_openat,
709                [
710                    self.0,
711                    name.as_ptr() as u64,
712                    (flags | libc::O_CLOEXEC) as u64,
713                    0,
714                    0,
715                    0,
716                ],
717            )
718        })?;
719        Ok(Self(fd))
720    }
721
722    /// The status of `name` in this directory, without following a final
723    /// link, by a raw `newfstatat`.
724    pub fn stat_at(&self, name: &std::ffi::OsStr) -> std::io::Result<libc::stat> {
725        let name = raw_path(std::path::Path::new(name))?;
726        // SAFETY: a NUL-terminated name; an all-zero `stat` is a valid value.
727        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
728        raw_zero_result(unsafe {
729            raw_syscall(
730                libc::SYS_newfstatat,
731                [
732                    self.0,
733                    name.as_ptr() as u64,
734                    &mut stat as *mut libc::stat as u64,
735                    libc::AT_SYMLINK_NOFOLLOW as u64,
736                    0,
737                    0,
738                ],
739            )
740        })?;
741        Ok(stat)
742    }
743
744    /// The descriptor's number.
745    pub fn raw(&self) -> u64 {
746        self.0
747    }
748
749    /// The open file's status, by a raw `fstat`.
750    pub fn stat(&self) -> std::io::Result<libc::stat> {
751        // SAFETY: an all-zero `stat` is a valid value, and the kernel writes
752        // at most its size.
753        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
754        raw_zero_result(unsafe {
755            raw_syscall(
756                libc::SYS_fstat,
757                [self.0, &mut stat as *mut libc::stat as u64, 0, 0, 0, 0],
758            )
759        })?;
760        Ok(stat)
761    }
762
763    /// The type of the file system the open file is on (`statfs::f_type`),
764    /// by a raw `fstatfs`.
765    pub fn fs_type(&self) -> std::io::Result<u64> {
766        // SAFETY: an all-zero `statfs` is a valid value, and the kernel
767        // writes at most its size.
768        let mut filesystem: libc::statfs = unsafe { std::mem::zeroed() };
769        raw_zero_result(unsafe {
770            raw_syscall(
771                libc::SYS_fstatfs,
772                [
773                    self.0,
774                    &mut filesystem as *mut libc::statfs as u64,
775                    0,
776                    0,
777                    0,
778                    0,
779                ],
780            )
781        })?;
782        Ok(filesystem.f_type as u64)
783    }
784
785    /// Up to `buffer.len()` bytes at `offset`, by a raw `pread64`.
786    pub fn read_at(&self, buffer: &mut [u8], offset: u64) -> std::io::Result<usize> {
787        // SAFETY: the buffer is writable for its length.
788        raw_result(unsafe {
789            raw_syscall(
790                libc::SYS_pread64,
791                [
792                    self.0,
793                    buffer.as_mut_ptr() as u64,
794                    buffer.len() as u64,
795                    offset,
796                    0,
797                    0,
798                ],
799            )
800        })
801        .map(|read| read as usize)
802    }
803}
804
805impl Drop for RawFd {
806    fn drop(&mut self) {
807        // SAFETY: a descriptor this value opened and owns.
808        unsafe { raw_syscall(libc::SYS_close, [self.0, 0, 0, 0, 0, 0]) };
809    }
810}
811
812fn raw_path(path: &std::path::Path) -> std::io::Result<std::ffi::CString> {
813    use std::os::unix::ffi::OsStrExt;
814    std::ffi::CString::new(path.as_os_str().as_bytes())
815        .map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidInput))
816}
817
818/// The status of the file at `path`, following links, by a raw `newfstatat`.
819pub fn raw_stat(path: &std::path::Path) -> std::io::Result<libc::stat> {
820    raw_stat_with(path, 0)
821}
822
823/// The status of the file at `path`, not following a link in its last
824/// component, by a raw `newfstatat`.
825pub fn raw_lstat(path: &std::path::Path) -> std::io::Result<libc::stat> {
826    raw_stat_with(path, libc::AT_SYMLINK_NOFOLLOW)
827}
828
829/// The status of the file at `path` by a raw `newfstatat` with `flags`.
830fn raw_stat_with(path: &std::path::Path, flags: libc::c_int) -> std::io::Result<libc::stat> {
831    let path = raw_path(path)?;
832    // SAFETY: a NUL-terminated path; an all-zero `stat` is a valid value.
833    let mut stat: libc::stat = unsafe { std::mem::zeroed() };
834    raw_zero_result(unsafe {
835        raw_syscall(
836            libc::SYS_newfstatat,
837            [
838                libc::AT_FDCWD as u64,
839                path.as_ptr() as u64,
840                &mut stat as *mut libc::stat as u64,
841                flags as u64,
842                0,
843                0,
844            ],
845        )
846    })?;
847    Ok(stat)
848}
849
850/// The target of the link at `path`, by a raw `readlinkat`.
851pub fn raw_read_link(path: &std::path::Path) -> std::io::Result<std::path::PathBuf> {
852    use std::os::unix::ffi::OsStrExt;
853    let path = raw_path(path)?;
854    let mut target = vec![0_u8; libc::PATH_MAX as usize];
855    // SAFETY: a NUL-terminated path and a buffer writable for its length.
856    let read = raw_result(unsafe {
857        raw_syscall(
858            libc::SYS_readlinkat,
859            [
860                libc::AT_FDCWD as u64,
861                path.as_ptr() as u64,
862                target.as_mut_ptr() as u64,
863                target.len() as u64,
864                0,
865                0,
866            ],
867        )
868    })? as usize;
869    target.truncate(read);
870    Ok(std::path::PathBuf::from(std::ffi::OsStr::from_bytes(
871        &target,
872    )))
873}
874
875/// The whole contents of the file at `path`, read with raw syscalls. Meant
876/// for small files such as procfs entries.
877pub fn raw_read_file(path: &std::path::Path) -> std::io::Result<Vec<u8>> {
878    let file = RawFd::open(path, libc::O_RDONLY)?;
879    let mut contents = Vec::new();
880    let mut chunk = vec![0_u8; 4096];
881    loop {
882        // SAFETY: the buffer is writable for its length.
883        let read = raw_result(unsafe {
884            raw_syscall(
885                libc::SYS_read,
886                [
887                    file.raw(),
888                    chunk.as_mut_ptr() as u64,
889                    chunk.len() as u64,
890                    0,
891                    0,
892                    0,
893                ],
894            )
895        })? as usize;
896        if read == 0 {
897            return Ok(contents);
898        }
899        contents.extend_from_slice(&chunk[..read]);
900    }
901}
902
903/// The whole contents of the procfs listing at `path`, read with raw
904/// syscalls and without allocating between reads, so that where the tool
905/// shares the guest's process its own allocator maps nothing while the
906/// kernel walks the address space: the buffer is allocated before the first
907/// read, and a listing that fills it is read again, from the start, into one
908/// twice the size.
909pub fn raw_read_listing(path: &std::path::Path) -> std::io::Result<Vec<u8>> {
910    let mut capacity = 64 * 1024;
911    loop {
912        let file = RawFd::open(path, libc::O_RDONLY)?;
913        let mut contents = vec![0_u8; capacity];
914        let mut filled = 0;
915        while filled < capacity {
916            // SAFETY: the buffer is writable from `filled` to its length.
917            let read = raw_result(unsafe {
918                raw_syscall(
919                    libc::SYS_read,
920                    [
921                        file.raw(),
922                        contents[filled..].as_mut_ptr() as u64,
923                        (capacity - filled) as u64,
924                        0,
925                        0,
926                        0,
927                    ],
928                )
929            })? as usize;
930            if read == 0 {
931                contents.truncate(filled);
932                return Ok(contents);
933            }
934            filled += read;
935        }
936        capacity *= 2;
937    }
938}
939
940/// The descriptor numbers the `linux_dirent64` records in `records` name: each
941/// entry whose name is a decimal number (a descriptor directory's), in
942/// order, leaving out `.` and `..`.
943pub fn descriptor_names(records: &[u8]) -> Vec<i32> {
944    let mut numbers = Vec::new();
945    let mut at = 0;
946    while at + 19 <= records.len() {
947        let length = usize::from(u16::from_ne_bytes([records[at + 16], records[at + 17]]));
948        if length < 19 || at + length > records.len() {
949            break;
950        }
951        let name = &records[at + 19..at + length];
952        let name = &name[..name
953            .iter()
954            .position(|&byte| byte == 0)
955            .unwrap_or(name.len())];
956        if !name.is_empty()
957            && name.iter().all(u8::is_ascii_digit)
958            && let Some(number) = std::str::from_utf8(name).ok().and_then(|n| n.parse().ok())
959        {
960            numbers.push(number);
961        }
962        at += length;
963    }
964    numbers
965}
966
967/// `path` with every symbolic link followed and every `.` and `..` resolved,
968/// as glibc's `realpath` (`std::fs::canonicalize`) gives it: relative paths
969/// from the working directory, and the same errors for an empty path
970/// (`ENOENT`), a missing component (`ENOENT`), a component that is not a
971/// directory but is followed by more, by `.`, `..` or a trailing `/`
972/// (`ENOTDIR`), and links nested too deep (`ELOOP` after 40, Linux's
973/// `MAXSYMLINKS`).
974///
975/// Walked with raw `getcwd`, `readlinkat` and `newfstatat` into buffers from
976/// Rust's allocator, never with `std::fs::canonicalize`: that calls glibc's
977/// `realpath`, whose result comes from the C library's `malloc`. Where
978/// Detcore runs in the guest's process (SaBRe, in-guest LiteInst), that heap
979/// is the guest's, and the allocation moves the guest's own `malloc` results
980/// and can grow its program break (see `find_in_directory`).
981pub fn canonicalize(path: &std::path::Path) -> std::io::Result<std::path::PathBuf> {
982    use std::os::unix::ffi::OsStrExt;
983    use std::path::PathBuf;
984    let error = std::io::Error::from_raw_os_error;
985    let bytes = path.as_os_str().as_bytes();
986    if bytes.is_empty() {
987        return Err(error(libc::ENOENT));
988    }
989    // What is left to walk, as `/`-separated pieces in reverse, so the next
990    // is at the end. A trailing `/` is kept as a final `.` piece: Linux then
991    // requires a directory, as it does before `.` and `..`.
992    let mut pending: Vec<Vec<u8>> = Vec::new();
993    let push = |pending: &mut Vec<Vec<u8>>, bytes: &[u8]| {
994        let mut pieces: Vec<Vec<u8>> = bytes
995            .split(|&byte| byte == b'/')
996            .filter(|piece| !piece.is_empty())
997            .map(<[u8]>::to_vec)
998            .collect();
999        if bytes.len() > 1 && bytes.ends_with(b"/") {
1000            pieces.push(b".".to_vec());
1001        }
1002        pieces.reverse();
1003        pending.extend(pieces);
1004    };
1005    let mut resolved = if bytes.starts_with(b"/") {
1006        PathBuf::from("/")
1007    } else {
1008        // The kernel answers within one PATH_MAX page and fails
1009        // ENAMETOOLONG past it, where glibc's getcwd walks `..` itself; so
1010        // does this (path_of_directory).
1011        let mut cwd = vec![0_u8; libc::PATH_MAX as usize];
1012        // SAFETY: the buffer is writable for its length. The kernel's length
1013        // counts the terminating NUL.
1014        let length = unsafe {
1015            raw_syscall(
1016                libc::SYS_getcwd,
1017                [cwd.as_mut_ptr() as u64, cwd.len() as u64, 0, 0, 0, 0],
1018            )
1019        };
1020        let length = if length == -(libc::ENAMETOOLONG as i64) {
1021            let here = RawFd::open(
1022                std::path::Path::new("."),
1023                libc::O_RDONLY | libc::O_DIRECTORY,
1024            )?;
1025            cwd = path_of_directory(here)?;
1026            cwd.push(0);
1027            cwd.len()
1028        } else {
1029            raw_result(length)? as usize
1030        };
1031        cwd.truncate(length.saturating_sub(1));
1032        // A working directory outside the process's root reads as
1033        // "(unreachable)..."; glibc's getcwd fails ENOENT there.
1034        if !cwd.starts_with(b"/") {
1035            return Err(error(libc::ENOENT));
1036        }
1037        PathBuf::from(std::ffi::OsStr::from_bytes(&cwd))
1038    };
1039    push(&mut pending, bytes);
1040    let is_directory = |path: &std::path::Path| {
1041        raw_stat(path).map(|stat| stat.st_mode & libc::S_IFMT == libc::S_IFDIR)
1042    };
1043    let mut links = 0;
1044    while let Some(piece) = pending.pop() {
1045        if piece == b"." || piece == b".." {
1046            if !is_directory(&resolved)? {
1047                return Err(error(libc::ENOTDIR));
1048            }
1049            if piece == b".." {
1050                resolved.pop();
1051            }
1052            continue;
1053        }
1054        let candidate = resolved.join(std::ffi::OsStr::from_bytes(&piece));
1055        let spelled = std::ffi::CString::new(candidate.as_os_str().as_bytes())
1056            .map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidInput))?;
1057        // A target that fills the buffer may have been cut short: read again
1058        // into a larger one.
1059        let mut target = vec![0_u8; libc::PATH_MAX as usize];
1060        let read = loop {
1061            // SAFETY: a NUL-terminated path and a buffer writable for its
1062            // length.
1063            let read = unsafe {
1064                raw_syscall(
1065                    libc::SYS_readlinkat,
1066                    [
1067                        libc::AT_FDCWD as u64,
1068                        spelled.as_ptr() as u64,
1069                        target.as_mut_ptr() as u64,
1070                        target.len() as u64,
1071                        0,
1072                        0,
1073                    ],
1074                )
1075            };
1076            if read != target.len() as i64 {
1077                break read;
1078            }
1079            target.resize(target.len() * 2, 0);
1080        };
1081        if read == -(libc::EINVAL as i64) {
1082            // There, and not a link.
1083            resolved = candidate;
1084            continue;
1085        }
1086        let read = raw_result(read)? as usize;
1087        links += 1;
1088        if links > 40 {
1089            return Err(error(libc::ELOOP));
1090        }
1091        if target[..read].starts_with(b"/") {
1092            resolved = PathBuf::from("/");
1093        }
1094        push(&mut pending, &target[..read]);
1095    }
1096    Ok(resolved)
1097}
1098
1099/// The absolute path of the directory open at `start`: each directory's
1100/// name is found among its parent's entries by device and inode, walking `..`
1101/// up to the process's root, as glibc's getcwd does where the kernel's
1102/// `getcwd` cannot answer (a path longer than a page). Raw syscalls
1103/// throughout. `ENOENT` where a directory is missing from its parent (it was
1104/// removed, or lies outside the root).
1105fn path_of_directory(start: RawFd) -> std::io::Result<Vec<u8>> {
1106    use std::os::unix::ffi::OsStrExt;
1107    let mut names: Vec<Vec<u8>> = Vec::new();
1108    let mut current = start;
1109    loop {
1110        let here = current.stat()?;
1111        let parent = current.open_at(
1112            std::ffi::OsStr::new(".."),
1113            libc::O_RDONLY | libc::O_DIRECTORY,
1114        )?;
1115        let above = parent.stat()?;
1116        if (above.st_dev, above.st_ino) == (here.st_dev, here.st_ino) {
1117            break;
1118        }
1119        let name = find_in_open_directory(&parent, |name| {
1120            parent
1121                .stat_at(name)
1122                .is_ok_and(|stat| (stat.st_dev, stat.st_ino) == (here.st_dev, here.st_ino))
1123                .then(|| name.as_bytes().to_vec())
1124        })?
1125        .ok_or_else(|| std::io::Error::from_raw_os_error(libc::ENOENT))?;
1126        names.push(name);
1127        current = parent;
1128    }
1129    let mut path = Vec::new();
1130    for name in names.iter().rev() {
1131        path.push(b'/');
1132        path.extend_from_slice(name);
1133    }
1134    if path.is_empty() {
1135        path.push(b'/');
1136    }
1137    Ok(path)
1138}
1139
1140/// Calls `visit` with each name in directory `path`, without `.` and `..`,
1141/// in the kernel's order, until it returns `Some`, and returns that value
1142/// (`None` when every name was visited).
1143///
1144/// Read with raw `getdents64` into one fixed buffer from Rust's allocator,
1145/// never with `std::fs::read_dir`: that calls glibc's `opendir`, whose
1146/// directory buffer comes from the C library's `malloc`. Under in-guest
1147/// LiteInst, Detcore runs inside the guest and that heap is the guest's: the
1148/// freed buffer would keep the kernel's bytes (raw `/proc` inode numbers, say)
1149/// where the guest's next allocation finds them, so the guest could observe
1150/// host-chosen data, for example in the padding of its own `getdents64`
1151/// records. Rust's allocator is private to the runtime there (inside a Tool
1152/// callback or an allocation scope). The names are streamed, not collected,
1153/// so a directory of any size costs the one buffer.
1154pub fn find_in_directory<T>(
1155    path: &std::path::Path,
1156    visit: impl FnMut(&std::ffi::OsStr) -> Option<T>,
1157) -> std::io::Result<Option<T>> {
1158    // Raw syscalls throughout (see raw_syscall); the descriptor closes when
1159    // dropped, a panicking visitor included.
1160    let directory = RawFd::open(path, libc::O_RDONLY | libc::O_DIRECTORY)?;
1161    find_in_open_directory(&directory, visit)
1162}
1163
1164/// [`find_in_directory`] of a directory already open at `directory`, from
1165/// its current offset.
1166pub fn find_in_open_directory<T>(
1167    directory: &RawFd,
1168    mut visit: impl FnMut(&std::ffi::OsStr) -> Option<T>,
1169) -> std::io::Result<Option<T>> {
1170    use std::os::unix::ffi::OsStrExt;
1171    let fd = directory.raw();
1172    let mut buffer = vec![0_u8; 32 * 1024];
1173    'listing: loop {
1174        // SAFETY: the buffer is writable for its length.
1175        let read = unsafe {
1176            raw_syscall(
1177                libc::SYS_getdents64,
1178                [fd, buffer.as_mut_ptr() as u64, buffer.len() as u64, 0, 0, 0],
1179            )
1180        };
1181        if read < 0 {
1182            break Err(std::io::Error::from_raw_os_error(-read as i32));
1183        }
1184        if read == 0 {
1185            break Ok(None);
1186        }
1187        let mut offset = 0_usize;
1188        let read = read as usize;
1189        // A record: d_ino (8), d_off (8), d_reclen (2), d_type (1), d_name.
1190        while offset + 19 <= read {
1191            let reclen = u16::from_ne_bytes([buffer[offset + 16], buffer[offset + 17]]) as usize;
1192            if reclen < 19 || offset + reclen > read {
1193                break;
1194            }
1195            let name = &buffer[offset + 19..offset + reclen];
1196            let name = &name[..name
1197                .iter()
1198                .position(|byte| *byte == 0)
1199                .unwrap_or(name.len())];
1200            if name != b"."
1201                && name != b".."
1202                && let Some(found) = visit(std::ffi::OsStr::from_bytes(name))
1203            {
1204                break 'listing Ok(Some(found));
1205            }
1206            offset += reclen;
1207        }
1208    }
1209}
1210
1211#[cfg(test)]
1212mod find_in_directory_tests {
1213    use super::*;
1214
1215    /// Every name `std::fs::read_dir` finds is visited once, the visit stops
1216    /// at the first `Some`, and a path that is not a directory is an error.
1217    #[test]
1218    #[allow(clippy::disallowed_methods)] // std::fs::read_dir is the reference
1219    fn find_in_directory_visits_every_name_and_stops_at_the_first_find() {
1220        let directory = tempfile::tempdir().unwrap();
1221        for index in 0..300 {
1222            std::fs::write(
1223                directory
1224                    .path()
1225                    .join(format!("entry-with-a-long-name-{index}")),
1226                b"",
1227            )
1228            .unwrap();
1229        }
1230        let mut visited = Vec::new();
1231        let none: Option<()> = find_in_directory(directory.path(), |name| {
1232            visited.push(name.to_owned());
1233            None
1234        })
1235        .unwrap();
1236        assert!(none.is_none());
1237        visited.sort();
1238        let mut expected: Vec<_> = std::fs::read_dir(directory.path())
1239            .unwrap()
1240            .map(|entry| entry.unwrap().file_name())
1241            .collect();
1242        expected.sort();
1243        assert_eq!(visited.len(), 300);
1244        assert_eq!(visited, expected);
1245        let mut calls = 0;
1246        let found = find_in_directory(directory.path(), |name| {
1247            calls += 1;
1248            (calls == 7).then(|| name.to_owned())
1249        })
1250        .unwrap();
1251        assert!(found.is_some());
1252        assert_eq!(calls, 7);
1253        let file = directory.path().join("entry-with-a-long-name-0");
1254        assert!(find_in_directory(&file, |_| Some(())).is_err());
1255    }
1256}
1257
1258#[cfg(test)]
1259mod canonicalize_tests {
1260    use std::os::unix::fs::symlink;
1261    use std::path::Path;
1262
1263    use super::*;
1264
1265    /// A directory's path is found by walking `..` where it is longer than
1266    /// the kernel's getcwd answers (a page), as glibc's getcwd does.
1267    #[test]
1268    fn a_long_directory_path_is_found_by_walking_up() {
1269        use std::os::unix::ffi::OsStrExt;
1270        let directory = tempfile::tempdir().unwrap();
1271        #[allow(clippy::disallowed_methods)] // std::fs::canonicalize is the independent oracle
1272        let root = std::fs::canonicalize(directory.path()).unwrap();
1273        let name = "d".repeat(200);
1274        let spelled = std::ffi::CString::new(name.as_str()).unwrap();
1275        let mut levels = vec![RawFd::open(&root, libc::O_RDONLY | libc::O_DIRECTORY).unwrap()];
1276        let mut expected = root.clone();
1277        // 25 levels of 201 bytes: past the 4096-byte page.
1278        for _ in 0..25 {
1279            let parent = levels.last().unwrap();
1280            // SAFETY: a NUL-terminated name relative to an open directory.
1281            assert_eq!(
1282                unsafe { libc::mkdirat(parent.raw() as i32, spelled.as_ptr(), 0o700) },
1283                0
1284            );
1285            let child = parent
1286                .open_at(
1287                    std::ffi::OsStr::new(&name),
1288                    libc::O_RDONLY | libc::O_DIRECTORY,
1289                )
1290                .unwrap();
1291            levels.push(child);
1292            expected.push(&name);
1293        }
1294        assert!(expected.as_os_str().len() > 4096);
1295        let deepest = levels
1296            .last()
1297            .unwrap()
1298            .open_at(
1299                std::ffi::OsStr::new("."),
1300                libc::O_RDONLY | libc::O_DIRECTORY,
1301            )
1302            .unwrap();
1303        let found = path_of_directory(deepest).unwrap();
1304        // A path short enough for the kernel is found the same way.
1305        let short = RawFd::open(&root, libc::O_RDONLY | libc::O_DIRECTORY).unwrap();
1306        let short = path_of_directory(short).unwrap();
1307        // The tree is too deep for a path-based removal: remove it level by
1308        // level before asserting.
1309        for parent in levels[..25].iter().rev() {
1310            // SAFETY: a NUL-terminated name relative to an open directory.
1311            unsafe { libc::unlinkat(parent.raw() as i32, spelled.as_ptr(), libc::AT_REMOVEDIR) };
1312        }
1313        assert_eq!(std::ffi::OsStr::from_bytes(&found), expected.as_os_str());
1314        assert_eq!(std::ffi::OsStr::from_bytes(&short), root.as_os_str());
1315    }
1316
1317    /// The walk gives what glibc's `realpath` gives (`std::fs::canonicalize`,
1318    /// allowed here as the reference) through absolute, relative and chained
1319    /// links, `..` across a link, and procfs's own links; and the same errors
1320    /// for a missing component, a file used as a directory, and a link loop.
1321    #[test]
1322    #[allow(clippy::disallowed_methods)] // std::fs::canonicalize is the reference
1323    fn canonicalize_matches_realpath() {
1324        let directory = tempfile::tempdir().unwrap();
1325        let root = directory.path();
1326        std::fs::create_dir_all(root.join("a/b/c")).unwrap();
1327        std::fs::write(root.join("a/b/c/file"), b"").unwrap();
1328        symlink(root.join("a/b"), root.join("absolute")).unwrap();
1329        symlink("a/b/c", root.join("relative")).unwrap();
1330        symlink("relative", root.join("chained")).unwrap();
1331        symlink("../../b", root.join("a/b/c/up")).unwrap();
1332        symlink("loop2", root.join("loop1")).unwrap();
1333        symlink("loop1", root.join("loop2")).unwrap();
1334        for spelled in [
1335            root.join("a/b/c/file"),
1336            root.join("absolute/c/file"),
1337            root.join("relative/file"),
1338            root.join("chained/./file"),
1339            root.join("chained/../c/file"),
1340            root.join("a/b/c/up/c/file"),
1341            root.join("absolute/c/up/../.."),
1342            Path::new("/proc/self").to_path_buf(),
1343            Path::new("/proc/self/exe").to_path_buf(),
1344            Path::new("/").to_path_buf(),
1345        ] {
1346            assert_eq!(
1347                canonicalize(&spelled).unwrap(),
1348                std::fs::canonicalize(&spelled).unwrap(),
1349                "{spelled:?}"
1350            );
1351        }
1352        // Trailing separators and `.` on directories, and a link to one.
1353        for spelled in [
1354            root.join("a/b/"),
1355            root.join("a/b/."),
1356            root.join("absolute/"),
1357            root.join("relative/."),
1358        ] {
1359            assert_eq!(
1360                canonicalize(&spelled).unwrap(),
1361                std::fs::canonicalize(&spelled).unwrap(),
1362                "{spelled:?}"
1363            );
1364        }
1365        // The same errors as realpath, including after a regular file.
1366        let error = |spelled: &Path| canonicalize(spelled).unwrap_err().raw_os_error();
1367        let reference = |spelled: &Path| std::fs::canonicalize(spelled).unwrap_err().raw_os_error();
1368        let file = root.join("a/b/c/file");
1369        let violating = [
1370            Path::new("").to_path_buf(),
1371            root.join("missing/file"),
1372            root.join("a/b/c/file/x"),
1373            root.join("loop1"),
1374            file.join(""),
1375            Path::new(&format!("{}/.", file.display())).to_path_buf(),
1376            Path::new(&format!("{}/..", file.display())).to_path_buf(),
1377            Path::new(&format!("{}/../file", file.display())).to_path_buf(),
1378        ];
1379        for spelled in &violating {
1380            assert_eq!(error(spelled), reference(spelled), "{spelled:?}");
1381        }
1382        assert_eq!(error(&violating[0]), Some(libc::ENOENT));
1383        assert_eq!(error(&violating[3]), Some(libc::ELOOP));
1384        assert_eq!(error(&violating[5]), Some(libc::ENOTDIR));
1385        // A relative path starts from the working directory.
1386        let here = std::env::current_dir().unwrap();
1387        assert_eq!(
1388            canonicalize(Path::new(".")).unwrap(),
1389            std::fs::canonicalize(&here).unwrap()
1390        );
1391    }
1392}