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}