Skip to main content

detcore/syscalls/
files.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//! System calls for dealing with the file system.
10
11use std::collections::BTreeMap;
12use std::collections::BTreeSet;
13use std::net::Ipv4Addr;
14use std::net::Ipv6Addr;
15use std::os::unix::fs::FileTypeExt;
16use std::os::unix::fs::MetadataExt;
17use std::path::Path;
18use std::path::PathBuf;
19use std::sync::atomic::AtomicBool;
20use std::sync::atomic::Ordering;
21
22use nix::fcntl::AtFlags;
23use nix::fcntl::OFlag;
24use rand::RngExt as _;
25use reverie::Error;
26use reverie::Guest;
27use reverie::Stack;
28use reverie::syscalls;
29use reverie::syscalls::Addr;
30use reverie::syscalls::AddrMut;
31use reverie::syscalls::AddrSlice;
32use reverie::syscalls::AddrSliceMut;
33use reverie::syscalls::Errno;
34use reverie::syscalls::FcntlCmd::*;
35use reverie::syscalls::MapFlags;
36use reverie::syscalls::MemoryAccess;
37use reverie::syscalls::PathPtr;
38use reverie::syscalls::ProtFlags;
39use reverie::syscalls::ReadAddr;
40use reverie::syscalls::SockFlag;
41use reverie::syscalls::StatPtr;
42use reverie::syscalls::StatxMask;
43use reverie::syscalls::Syscall;
44use reverie::syscalls::SyscallInfo;
45use reverie::syscalls::Sysno;
46use reverie::syscalls::Timespec;
47use reverie::syscalls::Whence;
48use reverie::syscalls::family::StatFamily;
49use tracing::error;
50use tracing::info;
51use tracing::trace;
52use tracing::warn;
53
54use super::deterministic_stdio_inode_for_resource;
55use crate::config::SchedHeuristic;
56use crate::dirents::*;
57use crate::fd::*;
58use crate::fifo_peer::FifoPeer;
59use crate::procfs::MountInfoSnapshot;
60use crate::procfs::ProcfsFile;
61use crate::procfs::ProcfsSnapshotContext;
62use crate::procfs::is_absent_cpu_entry;
63use crate::procfs::levels;
64use crate::procfs_inode;
65use crate::procfs_inode::DirectoryInodes;
66use crate::procfs_inode::ProcfsDirectory;
67use crate::procfs_inode::ProcfsName;
68use crate::record_or_replay::RecordOrReplay;
69use crate::resources::Device;
70use crate::resources::Permission;
71use crate::resources::ResourceID;
72use crate::resources::Resources;
73use crate::scheduler::HostTimedSignalScope;
74use crate::scheduler::runqueue::LAST_PRIORITY;
75use crate::scheduler::terminal_signals;
76use crate::stat::*;
77use crate::syscalls::guest_siginfo::GuestSiginfo;
78use crate::syscalls::signal::ProcessSignalTarget;
79use crate::syscalls::threads::kernel_sigset_bit;
80use crate::tool_global::*;
81use crate::tool_local::CapturedDetFdInstallError;
82use crate::tool_local::Detcore;
83use crate::tool_local::finish_partial_record_or_replay_write;
84use crate::types::*;
85
86pub(crate) mod descriptor_directory;
87#[cfg(test)]
88mod determinize_stat_tests;
89pub(crate) mod other_process_procfs;
90#[cfg(test)]
91mod prefilter_tests;
92pub(crate) mod procfs_levels;
93pub(crate) mod procfs_numbers;
94mod rendered_fdinfo;
95pub(crate) mod rendered_procfs;
96
97/// A conversion from SOCK_* flags to O_* flags which makes unsafe (but checked during testing) assumptions.
98fn oflag_from_sock_bits(s_bits: i32) -> OFlag {
99    // An otherwise unsafe "cast" which leans on the `linux_flags_assumptions` below.
100    OFlag::from_bits_truncate(s_bits & (libc::SOCK_CLOEXEC | libc::SOCK_NONBLOCK))
101}
102
103const UNIX_AUTOBIND_NAME_LEN: usize = 6;
104// AUTONOMOUS-BOT-IMPLEMENTED
105// TODO-HUMAN-REVIEW(PR-2150): Review timer-slack procfs parsing,
106// per-operation target checks, and scalar/vector I/O emulation.
107const TIMER_SLACK_PARSE_BYTES: usize = 66;
108#[derive(Clone, Copy)]
109struct TimerSlackIovec {
110    base: usize,
111    len: usize,
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115struct TimerSlackBinding {
116    target: i32,
117    device: u64,
118    inode: u64,
119}
120
121fn classify_timer_slack_binding(
122    binding: TimerSlackBinding,
123    observed_identity: Option<(u64, u64)>,
124    current_tid: i32,
125) -> Result<(), Errno> {
126    if observed_identity != Some((binding.device, binding.inode)) {
127        // The bound task exited. A missing path and a new task that recycled
128        // the same numeric TID are both ESRCH for the old open inode.
129        return Err(Errno::ESRCH);
130    }
131    if current_tid != binding.target {
132        // Cross-task CAP_SYS_NICE access is intentionally not exposed.
133        return Err(Errno::EPERM);
134    }
135    Ok(())
136}
137
138fn parse_timer_slack_write(bytes: &[u8]) -> Result<u64, Errno> {
139    // `kstrtoull_from_user` copies at most sign + 64 binary digits + newline,
140    // then accepts decimal digits with one optional leading '+' and one
141    // optional trailing newline. An embedded NUL terminates the C string.
142    let bytes = &bytes[..bytes.len().min(TIMER_SLACK_PARSE_BYTES)];
143    let end = bytes
144        .iter()
145        .position(|byte| *byte == 0)
146        .unwrap_or(bytes.len());
147    let mut value = &bytes[..end];
148    if value.first() == Some(&b'+') {
149        value = &value[1..];
150    }
151    if value.last() == Some(&b'\n') {
152        value = &value[..value.len() - 1];
153    }
154    if value.is_empty() || !value.iter().all(u8::is_ascii_digit) {
155        return Err(Errno::EINVAL);
156    }
157    value.iter().try_fold(0_u64, |parsed, digit| {
158        parsed
159            .checked_mul(10)
160            .and_then(|parsed| parsed.checked_add(u64::from(digit - b'0')))
161            .ok_or(Errno::ERANGE)
162    })
163}
164
165/// The file offset a `preadv2` or `pwritev2` passes in two registers: -1
166/// for the current one, at which either acts as `readv` or `writev` would.
167pub(crate) fn vectored_offset(low: u64, high: u64) -> i64 {
168    if std::mem::size_of::<usize>() == 8 {
169        low as i64
170    } else {
171        ((high << 32) | (low & u32::MAX as u64)) as i64
172    }
173}
174
175/// RNG vectors use either the shared stream or an independent explicit offset.
176struct RandomVectoredRead {
177    address: usize,
178    count: usize,
179    offset: Option<u64>,
180    flags: i32,
181}
182
183/// Apply Linux's checks after complete iovec import, before touching output.
184fn validate_random_vector_read(offset: Option<u64>, total: usize, flags: i32) -> Result<(), Errno> {
185    if total == 0 {
186        return Ok(());
187    }
188    if let Some(offset) = offset
189        && offset
190            .checked_add(total as u64)
191            .is_none_or(|end| end > i64::MAX as u64)
192    {
193        return Err(Errno::EINVAL);
194    }
195    // Linux 7.1's RWF_NOSIGNAL is newer than the pinned libc crate. Native
196    // random-device controls accept it, including with RWF_NOWAIT.
197    const RWF_NOSIGNAL: i32 = 0x100;
198    const KNOWN: i32 = libc::RWF_HIPRI
199        | libc::RWF_DSYNC
200        | libc::RWF_SYNC
201        | libc::RWF_NOWAIT
202        | libc::RWF_APPEND
203        | libc::RWF_NOAPPEND
204        | libc::RWF_ATOMIC
205        | libc::RWF_DONTCACHE
206        | RWF_NOSIGNAL;
207    // Unknown flags win over conflicting recognized flags, while recognized
208    // but unsupported ATOMIC/DONTCACHE are rejected after APPEND/NOAPPEND.
209    if flags & !KNOWN != 0 {
210        return Err(Errno::EOPNOTSUPP);
211    }
212    if flags & libc::RWF_APPEND != 0 && flags & libc::RWF_NOAPPEND != 0 {
213        return Err(Errno::EINVAL);
214    }
215    if flags & (libc::RWF_ATOMIC | libc::RWF_DONTCACHE) != 0 {
216        return Err(Errno::EOPNOTSUPP);
217    }
218    Ok(())
219}
220
221fn read_iovecs<M: MemoryAccess>(
222    memory: &M,
223    address: Option<Addr<libc::iovec>>,
224    count: usize,
225) -> Result<Vec<TimerSlackIovec>, Errno> {
226    if count > libc::UIO_MAXIOV as usize {
227        return Err(Errno::EINVAL);
228    }
229    if count == 0 {
230        return Ok(Vec::new());
231    }
232    let address = address.ok_or(Errno::EFAULT)?;
233    let mut iovecs = vec![
234        libc::iovec {
235            iov_base: std::ptr::null_mut(),
236            iov_len: 0,
237        };
238        count
239    ];
240    memory.read_values(address, &mut iovecs)?;
241    let total = iovecs.iter().try_fold(0_usize, |total, iovec| {
242        total.checked_add(iovec.iov_len).ok_or(Errno::EINVAL)
243    })?;
244    if total > isize::MAX as usize {
245        return Err(Errno::EINVAL);
246    }
247    Ok(iovecs
248        .into_iter()
249        .map(|iovec| TimerSlackIovec {
250            base: iovec.iov_base as usize,
251            len: iovec.iov_len,
252        })
253        .collect())
254}
255
256fn copy_timer_slack_output<M: MemoryAccess>(
257    memory: &mut M,
258    destination: Option<AddrMut<'_, u8>>,
259    bytes: &[u8],
260) -> Result<usize, Errno> {
261    if bytes.is_empty() {
262        return Ok(0);
263    }
264    let copied = memory.write(destination.ok_or(Errno::EFAULT)?, bytes)?;
265    if copied == 0 {
266        Err(Errno::EFAULT)
267    } else {
268        Ok(copied)
269    }
270}
271
272/// Copy sanitized procfs bytes to the caller's buffer as the kernel's
273/// `copy_to_user` does: through the guest's page protections, stopping at the
274/// first byte that cannot be written. Returns the bytes copied, or `EFAULT`
275/// when none could be. A debugger write is not equivalent: the ptrace
276/// backend writes exactly eight bytes with `PTRACE_POKEDATA`, which ignores
277/// `PROT_READ` and `PROT_NONE`.
278fn copy_procfs_output<M: MemoryAccess>(
279    memory: &mut M,
280    destination: Option<AddrMut<'_, u8>>,
281    bytes: &[u8],
282) -> Result<usize, Error> {
283    if bytes.is_empty() {
284        return Ok(0);
285    }
286    let destination = destination.ok_or(Errno::EFAULT)?;
287    let mut copied = 0;
288    while copied < bytes.len() {
289        let Some(address) = destination
290            .as_raw()
291            .checked_add(copied)
292            .and_then(AddrMut::<u8>::from_raw)
293        else {
294            break;
295        };
296        match memory.write_with_user_access(address, &bytes[copied..]) {
297            Ok(0) | Err(Errno::EFAULT) => break,
298            Ok(n) => copied += n,
299            // Any other failure is the backend's, not a guest fault: report it
300            // as a tool error and commit nothing.
301            Err(errno) => {
302                return Err(Error::Tool(anyhow::anyhow!(
303                    "procfs read publication failed after {copied} bytes: {errno}"
304                )));
305            }
306        }
307    }
308    if copied == 0 {
309        Err(Errno::EFAULT.into())
310    } else {
311        Ok(copied)
312    }
313}
314
315/// Whether a descriptor with status flags `status` can read: not `O_PATH`,
316/// and opened for reading (`O_RDONLY` or `O_RDWR`; not `O_WRONLY`, nor access
317/// mode 3, which grants neither).
318fn can_read(status: i32) -> bool {
319    status & libc::O_PATH == 0 && matches!(status & libc::O_ACCMODE, libc::O_RDONLY | libc::O_RDWR)
320}
321
322/// Copy the bytes of a read of a procfs file as its read handler does: a
323/// `seq_file` copies up to the first byte it cannot write, and a sysctl
324/// (`faults_on_a_short_copy`) copies all of them or fails with `EFAULT`, so
325/// that the caller commits nothing.
326fn copy_procfs_read<M: MemoryAccess>(
327    memory: &mut M,
328    destination: Option<AddrMut<'_, u8>>,
329    bytes: &[u8],
330    faults_on_a_short_copy: bool,
331) -> Result<usize, Error> {
332    let copied = copy_procfs_output(memory, destination, bytes)?;
333    if faults_on_a_short_copy && copied < bytes.len() {
334        return Err(Errno::EFAULT.into());
335    }
336    Ok(copied)
337}
338
339/// The `RWF_*` flags of a vectored read of a procfs file Detcore renders, as
340/// `kiocb_set_rw_flags` checks them for a file with `read_iter` and without
341/// `FMODE_NOWAIT` or `FOP_DONTCACHE`: an unknown flag, then `RWF_APPEND` with
342/// `RWF_NOAPPEND` (`EINVAL`), then `RWF_NOWAIT`, `RWF_ATOMIC` (a read) and
343/// `RWF_DONTCACHE` (`EOPNOTSUPP`). Measured on Linux 7.1 for `/proc/version`
344/// and the sysctls, which also accept `RWF_NOSIGNAL`. A file without
345/// `read_iter` (`/proc/modules`, `/proc/swaps`) is read by
346/// `do_loop_readv_writev`, which refuses every flag but `RWF_HIPRI`.
347fn validate_procfs_vector_flags(flags: i32, read_iter: bool) -> Result<(), Errno> {
348    if !read_iter {
349        return if flags & !libc::RWF_HIPRI != 0 {
350            Err(Errno::EOPNOTSUPP)
351        } else {
352            Ok(())
353        };
354    }
355    // Linux 7.1's RWF_NOSIGNAL is newer than the pinned libc crate.
356    const RWF_NOSIGNAL: i32 = 0x100;
357    const KNOWN: i32 = libc::RWF_HIPRI
358        | libc::RWF_DSYNC
359        | libc::RWF_SYNC
360        | libc::RWF_NOWAIT
361        | libc::RWF_APPEND
362        | libc::RWF_NOAPPEND
363        | libc::RWF_ATOMIC
364        | libc::RWF_DONTCACHE
365        | RWF_NOSIGNAL;
366    if flags & !KNOWN != 0 {
367        return Err(Errno::EOPNOTSUPP);
368    }
369    if flags & libc::RWF_APPEND != 0 && flags & libc::RWF_NOAPPEND != 0 {
370        return Err(Errno::EINVAL);
371    }
372    if flags & (libc::RWF_NOWAIT | libc::RWF_ATOMIC | libc::RWF_DONTCACHE) != 0 {
373        return Err(Errno::EOPNOTSUPP);
374    }
375    Ok(())
376}
377
378/// Check the caller's whole requested range as `vfs_read`'s `access_ok` does
379/// before the file is read, so an address range beyond the user address limit
380/// is `EFAULT` even at EOF, when nothing would be copied. A procfs snapshot read
381/// never hands that range to the kernel.
382///
383/// `limit` is asked for the guest's user address limit only when the answer
384/// can matter, so a count that no limit admits is `EFAULT` before any query
385/// can fail.
386fn validate_procfs_destination(
387    limit: impl FnOnce() -> Result<crate::iovecs::UserAddressLimit, Error>,
388    destination: Option<AddrMut<'_, u8>>,
389    len: usize,
390) -> Result<(), Error> {
391    // No count past isize::MAX fits below a user address limit, so `read`'s
392    // `access_ok` refuses it, whatever the limit.
393    if isize::try_from(len).is_err() {
394        return Err(Errno::EFAULT.into());
395    }
396    // Two descriptors keep the range uncapped, as `access_ok` sees it; a
397    // single one may be capped at MAX_RW_COUNT first (`import_ubuf`).
398    limit()?.validate(&[
399        crate::iovecs::ImportedIovec {
400            base: destination.map_or(0, |address| address.as_raw()),
401            len,
402        },
403        crate::iovecs::ImportedIovec { base: 0, len: 0 },
404    ])
405}
406
407/// Save the bytes at `scratch` into `original` if the guest's own protections
408/// let the capture read land there: the range is readable, and writing the
409/// same bytes back through the guest's protections copies all of them.
410fn save_procfs_scratch<M: MemoryAccess>(
411    memory: &mut M,
412    scratch: AddrMut<'_, u8>,
413    original: &mut [u8],
414) -> bool {
415    read_guest_exact(memory, scratch, original).is_ok()
416        && memory
417            .write_with_user_access(scratch, original)
418            .is_ok_and(|written| written == original.len())
419}
420
421/// Read exactly `buf.len()` bytes at `addr`, in one `read_vectored` of that
422/// length, or fail `EFAULT` if not all of them can be read. `read` and
423/// `read_exact` may widen a short read (ptrace turns up to eight bytes into one
424/// eight-byte `PTRACE_PEEKDATA`), which fails when a valid extent ends less
425/// than eight bytes before an unmapped page.
426pub(crate) fn read_guest_exact<M: MemoryAccess>(
427    memory: &M,
428    addr: AddrMut<'_, u8>,
429    buf: &mut [u8],
430) -> Result<(), Errno> {
431    let len = buf.len();
432    let remote = unsafe { AddrSlice::from_raw_parts(addr.into(), len) };
433    let remote = [unsafe { remote.as_ioslice() }];
434    let mut local = [std::io::IoSliceMut::new(buf)];
435    match memory.read_vectored(&remote, &mut local) {
436        Ok(copied) if copied == len => Ok(()),
437        Ok(_) => Err(Errno::EFAULT),
438        Err(errno) => Err(errno),
439    }
440}
441
442/// Capacity used for pipes that Detcore makes physically nonblocking: two of
443/// the host's pages.
444///
445/// Linux normally creates 16-page pipes (64 KiB on 4 KiB pages), but silently falls back to two
446/// pages once the creating UID crosses `pipe-user-pages-soft`. That host-global accounting can
447/// change between the two executions of `hermit run --verify`, changing whether the same write
448/// succeeds immediately or enters the scheduler's `InternalIOPolling` retry path. Two pages is the
449/// pressure-mode capacity (`PIPE_MIN_DEF_BUFFERS`, fs/pipe.c) and, unlike 16, never requires an
450/// unprivileged capacity increase while the soft limit is active.
451///
452/// It is counted in pages because Linux sizes every pipe in whole pages: `F_SETPIPE_SZ` rounds a
453/// request up to a power-of-two number of them (`round_pipe_size`). Two pages is 8 KiB on 4 KiB
454/// pages (every x86-64 host) and 128 KiB on a 64 KiB-page aarch64 kernel. A fixed 8 KiB pin could
455/// not be applied there: Linux gives the pipe one 64 KiB page, and the creation pin, which accepts
456/// only exactly the capacity it asked for, ends the run (`pipe_capacity_failure`).
457///
458/// Two pages is attainable and independent of host load on Linux 5.14 and later (whose quota
459/// fallback is two pages, not one) when `pipe-max-size` is at least two pages (the default is
460/// 1 MiB). A host that hardens it to one 64 KiB page gives a new pipe one page, or two under the
461/// quota, so there every pipe creation and capacity read is refused (`host_pipe_floor`).
462///
463/// This is also the ceiling the guest is allowed to raise a pipe to, and the
464/// value `/proc/sys/fs/pipe-max-size` reports. Those three must be ONE value:
465/// a pinned capacity, an advertised maximum and an enforced maximum that can
466/// drift apart are three copies of one rule, and a duplicated rule drifts in
467/// N-1 places while each copy looks right on its own.
468pub(crate) fn deterministic_pipe_capacity_bytes() -> i32 {
469    static CAPACITY: std::sync::OnceLock<i32> = std::sync::OnceLock::new();
470    *CAPACITY.get_or_init(|| {
471        // SAFETY: sysconf has no preconditions.
472        let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
473        i32::try_from(page)
474            .ok()
475            .filter(|page| *page > 0)
476            .and_then(|page| page.checked_mul(2))
477            .expect("the host's page size is a small positive number")
478    })
479}
480
481/// Whether a guest's `F_SETPIPE_SZ` request must be refused as a growth past
482/// the deterministic ceiling.
483///
484/// Separated from the handler so the BOUNDARY is testable without a guest. The
485/// boundary is the whole content of this rule: a request for exactly the pinned
486/// capacity must be allowed, or hermit's own pin value becomes unreachable to a
487/// guest that reads `/proc/sys/fs/pipe-max-size` and asks for precisely what it
488/// was told.
489pub(crate) fn pipe_capacity_request_exceeds_ceiling(requested: i32) -> bool {
490    requested > deterministic_pipe_capacity_bytes()
491}
492
493/// The capacity a guest's `F_GETPIPE_SZ` reports for a pipe or FIFO whose
494/// host capacity is `host`: never more than the deterministic ceiling.
495///
496/// A pipe Hermit did not create (an inherited stdout, a FIFO whose other end
497/// is on the host) keeps the capacity the host gave it: 16 pages (64 KiB)
498/// normally, but two pages (8192 on 4 KiB pages) once the user crosses
499/// `pipe-user-pages-soft`, which host load decides. Passed through, that made the same guest read 65536 in one
500/// run of `--verify` and 8192 in the other (`/bin/cat` on DBT, the validate of
501/// hermit 2cd56771). A pipe can never hold more than the ceiling a guest can
502/// raise it to, so above it the ceiling is reported, as `pipe-max-size` is. A
503/// capacity at or below it is the one a pipe Hermit created was pinned to, or
504/// one the guest set itself, and is reported as it is. Host load cannot make
505/// a pipe smaller than the ceiling on Linux 5.14 and later: the quota fallback
506/// is exactly two pages, and past the hard limit Linux refuses the pipe
507/// instead. So an inherited pipe below it is one its creator sized on purpose,
508/// the same in every run. Before 5.14 the fallback was one page; the creation
509/// pin already needs 5.14 there, since under the same quota its growth of a
510/// one-page pipe to the ceiling is refused and the run stops
511/// (`pipe_capacity_failure`).
512///
513/// Only the reported value is made deterministic. A write to a pipe drained on
514/// the host still completes as far as its real capacity and that reader allow.
515pub(crate) fn reported_pipe_capacity(host: i64) -> i64 {
516    host.min(i64::from(deterministic_pipe_capacity_bytes()))
517}
518
519/// Whether two pages is the capacity no host load can change on this host:
520/// whether its `pipe-max-size` is at least two pages, decided once per process.
521/// Linux caps a new unprivileged pipe at `pipe-max-size` and, under
522/// `pipe-user-pages-soft`, gives it two pages instead (fs/pipe.c
523/// `alloc_pipe_info`). With a `pipe-max-size` below two pages, host load
524/// therefore decides between one page and two, whatever capacity a given pipe
525/// shows, so the answer rests on the setting alone, never on a guest pipe's
526/// observed capacity. Where Detcore cannot read the setting, a pipe of its own
527/// can still prove it (`probe_two_page_pipe_max_size`).
528fn host_pipe_floor() -> bool {
529    static FLOOR: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
530    *FLOOR.get_or_init(|| {
531        host_pipe_floor_is_two_pages(host_pipe_max_size(), probe_two_page_pipe_max_size)
532    })
533}
534
535/// [`host_pipe_floor`] given the host's `pipe-max-size` (`None` when
536/// unreadable), with `probe` deciding only when it is unreadable.
537pub(crate) fn host_pipe_floor_is_two_pages(
538    host_pipe_max_size: Option<i64>,
539    probe: impl FnOnce() -> bool,
540) -> bool {
541    match host_pipe_max_size {
542        Some(max) => max >= i64::from(deterministic_pipe_capacity_bytes()),
543        None => probe(),
544    }
545}
546
547/// Whether a pipe of this process's own proves `pipe-max-size` at least two
548/// pages. For a caller without `CAP_SYS_RESOURCE`, Linux creates a pipe no
549/// larger than `pipe-max-size` (`alloc_pipe_info`) and refuses growth past it
550/// with `EPERM` (`pipe_set_size`, fs/pipe.c v6.9 :1356-1358): a new pipe of
551/// more than two pages, or a one-page pipe grown to two, proves it. A caller
552/// with `CAP_SYS_RESOURCE` passes that limit, so its probe proves nothing.
553/// Under `pipe-user-pages-soft` growth is refused too, so a probe can fail
554/// where the setting allows two pages; it then refuses, the safe side.
555fn probe_two_page_pipe_max_size() -> bool {
556    if holds_cap_sys_resource() {
557        return false;
558    }
559    let mut fds = [-1; 2];
560    // SAFETY: `fds` has room for both descriptors.
561    if unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) } != 0 {
562        return false;
563    }
564    let ceiling = deterministic_pipe_capacity_bytes();
565    let page = ceiling / 2;
566    // SAFETY: F_GETPIPE_SZ and F_SETPIPE_SZ only read and resize the pipe.
567    let proved = unsafe {
568        libc::fcntl(fds[0], libc::F_GETPIPE_SZ) > ceiling
569            || (libc::fcntl(fds[0], libc::F_SETPIPE_SZ, page) == page
570                && libc::fcntl(fds[0], libc::F_SETPIPE_SZ, ceiling) == ceiling)
571    };
572    // SAFETY: both descriptors were opened above and nothing else owns them.
573    unsafe {
574        libc::close(fds[0]);
575        libc::close(fds[1]);
576    }
577    proved
578}
579
580/// Whether this process may have `CAP_SYS_RESOURCE` in its effective set,
581/// read with `capget`, which needs no `/proc`. A set that cannot be read may
582/// hold it.
583fn holds_cap_sys_resource() -> bool {
584    #[repr(C)]
585    struct Header {
586        version: u32,
587        pid: i32,
588    }
589    #[repr(C)]
590    #[derive(Clone, Copy, Default)]
591    struct Data {
592        effective: u32,
593        permitted: u32,
594        inheritable: u32,
595    }
596    const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522;
597    const CAP_SYS_RESOURCE: u32 = 24;
598    let mut header = Header {
599        version: LINUX_CAPABILITY_VERSION_3,
600        pid: 0,
601    };
602    let mut data = [Data::default(); 2];
603    // SAFETY: capget writes only the header and the two data structs given.
604    let result = unsafe {
605        libc::syscall(
606            libc::SYS_capget,
607            &mut header as *mut Header,
608            data.as_mut_ptr(),
609        )
610    };
611    result != 0 || data[0].effective & (1 << CAP_SYS_RESOURCE) != 0
612}
613
614/// Ends the run because this host's `pipe-max-size` is below two pages, or
615/// cannot be shown to be at least two ([`host_pipe_floor`]), for `what`.
616fn below_two_page_pipe_floor(what: &str) -> String {
617    format!(
618        "[detcore] {what}: this host's pipe-max-size ({:?}; None when unreadable and not proved \
619         by a probe) is below two pages ({} bytes), so host load decides whether a pipe has one \
620         page or two. Determinism is unavailable for this run.",
621        host_pipe_max_size(),
622        deterministic_pipe_capacity_bytes(),
623    )
624}
625
626/// The host's `/proc/sys/fs/pipe-max-size`, read once.
627fn host_pipe_max_size() -> Option<i64> {
628    static MAX: std::sync::OnceLock<Option<i64>> = std::sync::OnceLock::new();
629    *MAX.get_or_init(|| {
630        std::fs::read_to_string("/proc/sys/fs/pipe-max-size")
631            .ok()?
632            .trim()
633            .parse()
634            .ok()
635    })
636}
637
638/// SIGIO and SIGURG, the signals Linux sends to a descriptor's owner when
639/// data, out-of-band data, a lease break, or a directory change arrives.
640fn async_io_signals() -> u64 {
641    kernel_sigset_bit(libc::SIGIO) | kernel_sigset_bit(libc::SIGURG)
642}
643
644/// The signals an `fcntl` command lets Linux send at a moment set by host
645/// timing, as a kernel sigset; 0 for every other command. `F_SETOWN` and
646/// `F_SETOWN_EX` name the owner, `F_SETLEASE` and `F_NOTIFY` make the caller
647/// the owner, `O_ASYNC` enables SIGIO, and `F_SETSIG` replaces SIGIO with
648/// another signal.
649fn fcntl_host_timed_signals(cmd: syscalls::FcntlCmd<'_>) -> u64 {
650    match cmd {
651        F_SETFL(flags) if flags & libc::O_ASYNC != 0 => async_io_signals(),
652        F_SETOWN | F_SETOWN_EX(_) | F_SETLEASE(_) | F_NOTIFY(_) => async_io_signals(),
653        F_SETSIG(signal) => async_io_signals() | kernel_sigset_bit(signal),
654        _ => 0,
655    }
656}
657
658/// The signals an `ioctl` request lets Linux send at a moment set by host
659/// timing, as a kernel sigset; 0 for every other request. `FIOASYNC` enables
660/// SIGIO, and `FIOSETOWN` and `SIOCSPGRP` name the owner. `TIOCSCTTY` makes
661/// the terminal the caller's controlling terminal, `TIOCGPTPEER` without
662/// `O_NOCTTY` opens a pseudoterminal's other end as `open` does and so can
663/// make it one (`open_can_acquire_controlling_terminal`), and `TIOCSPGRP`
664/// names the terminal's foreground process group, so the terminal can then
665/// signal the session and that group (`terminal_signals`). `TIOCNOTTY` and
666/// `TIOCSWINSZ` are not among them: the signals they send, they send inside
667/// the caller's call, in the caller's turn.
668fn ioctl_host_timed_signals(request: syscalls::ioctl::Request<'_>) -> u64 {
669    match request {
670        syscalls::ioctl::Request::FIOASYNC(_)
671        | syscalls::ioctl::Request::FIOSETOWN(_)
672        | syscalls::ioctl::Request::SIOCSPGRP(_) => async_io_signals(),
673        syscalls::ioctl::Request::TIOCSCTTY(_) | syscalls::ioctl::Request::TIOCSPGRP(_) => {
674            terminal_signals()
675        }
676        syscalls::ioctl::Request::TIOCGPTPEER(flags)
677            if open_can_acquire_controlling_terminal(OFlag::from_bits_truncate(flags)) =>
678        {
679            terminal_signals()
680        }
681        _ => 0,
682    }
683}
684
685/// Whether an `open` with these flags can make the terminal it opens the
686/// caller's controlling terminal. Linux's `tty_open` does that for a session
687/// leader that has none, unless `O_NOCTTY` is given; an `O_PATH` open never
688/// reaches `tty_open`.
689fn open_can_acquire_controlling_terminal(flags: OFlag) -> bool {
690    !flags.intersects(OFlag::O_NOCTTY | OFlag::O_PATH)
691}
692
693/// Whether the character device `rdev` is a terminal that an `open` can make
694/// its caller's controlling terminal. `tty_drivers` is the text of
695/// `/proc/tty/drivers`, one line per range of device numbers a terminal
696/// driver serves, ending in the major number, the minor number or range, and
697/// the driver's type. A device no line names is not a terminal. Linux's
698/// `tty_open` never makes `/dev/console` (5:1), `/dev/vc/0` (4:0) or a
699/// pseudoterminal master controlling, and `/dev/ptmx` (5:2) opens a master
700/// without reaching `tty_open`. `/dev/tty` (5:0) stays in: it reopens the
701/// caller's controlling terminal, and if a hangup clears that terminal during
702/// the `open`, the `open` can make it controlling again. A table that could
703/// not be read (`None`), or a line that does not parse when no other line
704/// names the device, answers true.
705fn terminal_can_become_controlling(rdev: libc::dev_t, tty_drivers: Option<&str>) -> bool {
706    let (major, minor) = (libc::major(rdev), libc::minor(rdev));
707    if matches!((major, minor), (5, 1) | (5, 2) | (4, 0)) {
708        return false;
709    }
710    let Some(table) = tty_drivers else {
711        return true;
712    };
713    let mut unparsed = false;
714    for line in table.lines().filter(|line| !line.trim().is_empty()) {
715        let fields: Vec<&str> = line.split_whitespace().collect();
716        let range = match fields[..] {
717            [.., line_major, minors, kind] => {
718                line_major.parse::<u32>().ok().and_then(|line_major| {
719                    let (first, last) = minors.split_once('-').unwrap_or((minors, minors));
720                    Some((
721                        line_major,
722                        first.parse::<u32>().ok()?,
723                        last.parse::<u32>().ok()?,
724                        kind,
725                    ))
726                })
727            }
728            _ => None,
729        };
730        match range {
731            Some((line_major, first, last, kind))
732                if line_major == major && (first..=last).contains(&minor) =>
733            {
734                return kind != "pty:master";
735            }
736            Some(_) => {}
737            None => unparsed = true,
738        }
739    }
740    unparsed
741}
742
743/// Whether an `open` in process `pid`, whose `/proc/<pid>/stat` lists
744/// `session`, of the character device `rdev` may have made that device
745/// `pid`'s controlling terminal: `pid` leads its session and the device is a
746/// terminal that can become controlling (`terminal_can_become_controlling`).
747/// Neither fact is the controlling terminal procfs lists, which a hangup of
748/// the terminal clears at any moment, so the answer does not depend on
749/// whether a hangup came before or after the read. A session leader that
750/// reopens the terminal it already controls, or opens a terminal while it
751/// controls another, also answers true.
752fn may_have_acquired_controlling_terminal(
753    pid: i32,
754    session: i32,
755    rdev: libc::dev_t,
756    tty_drivers: Option<&str>,
757) -> bool {
758    session == pid && terminal_can_become_controlling(rdev, tty_drivers)
759}
760
761/// Whether the `open` in process `pid` that returned `fd` may have made the
762/// opened terminal `pid`'s controlling terminal
763/// (`may_have_acquired_controlling_terminal`). `host_stat` is the
764/// descriptor's stat when the caller already read it. Every host read that
765/// fails answers true: a needless record only holds the terminal's signals
766/// (`terminal_signals`) to the end of the gated waits that follow, while a
767/// missed one lets the terminal interrupt one at a turn the host chose.
768fn opened_controlling_terminal(pid: i32, fd: RawFd, host_stat: Option<&libc::stat>) -> bool {
769    let rdev = match host_stat {
770        Some(stat) if stat.st_mode & libc::S_IFMT == libc::S_IFCHR => stat.st_rdev,
771        Some(_) => return false,
772        None => match std::fs::metadata(format!("/proc/{pid}/fd/{fd}")) {
773            Ok(metadata) if metadata.file_type().is_char_device() => metadata.rdev(),
774            Ok(_) => return false,
775            Err(_) => return true,
776        },
777    };
778    let session = match procfs::process::Process::new(pid).and_then(|process| process.stat()) {
779        Ok(stat) => stat.session,
780        Err(_) => return true,
781    };
782    let tty_drivers = std::fs::read_to_string("/proc/tty/drivers").ok();
783    may_have_acquired_controlling_terminal(pid, session, rdev, tty_drivers.as_deref())
784}
785
786/// Why the pin failed, and which descriptors Linux had already created when it did.
787///
788/// `pipe2` has already SUCCEEDED by the time the capacity is pinned, so the two descriptors
789/// exist in the guest whatever happens next. Carrying them alongside the errno is what makes
790/// releasing them possible; classifying separately from acting on it is what makes the
791/// classification unit-testable without a guest.
792#[derive(Debug, PartialEq, Eq)]
793struct PipeCapacityFailure {
794    created_fds: [i32; 2],
795    error: Errno,
796}
797
798impl PipeCapacityFailure {
799    fn close_syscalls(&self) -> [syscalls::Close; 2] {
800        self.created_fds
801            .map(|fd| syscalls::Close::new().with_fd(fd))
802    }
803}
804
805/// Classify the result of pinning a pipe's capacity.
806///
807/// The ONLY success shape is Linux returning exactly the requested capacity. `F_SETPIPE_SZ`
808/// returns the capacity it actually applied, and it may round; a rounded value is a pipe whose
809/// size we did not choose, which is the host-dependent capacity this path exists to remove. It
810/// is not a kernel errno, so it is reported as `EIO` rather than dressed up as one.
811fn pipe_capacity_failure(
812    created_fds: [i32; 2],
813    capacity_result: Result<i64, Errno>,
814) -> Option<PipeCapacityFailure> {
815    let error = match capacity_result {
816        Ok(applied) if applied == i64::from(deterministic_pipe_capacity_bytes()) => return None,
817        Ok(_) => Errno::EIO,
818        Err(error) => error,
819    };
820    Some(PipeCapacityFailure { created_fds, error })
821}
822
823fn random_device_lseek_result(status_flags: i32, whence: Whence) -> Result<i64, Errno> {
824    if status_flags & OFlag::O_PATH.bits() != 0 {
825        return Err(Errno::EBADF);
826    }
827    match whence {
828        Whence::SEEK_SET
829        | Whence::SEEK_CUR
830        | Whence::SEEK_END
831        | Whence::SEEK_DATA
832        | Whence::SEEK_HOLE => Ok(0),
833        _ => Err(Errno::EINVAL),
834    }
835}
836
837/// How a procfs file Detcore renders seeks (`Detcore::handle_lseek`), by its
838/// file operations' `llseek`.
839#[derive(Clone, Copy, Debug, PartialEq, Eq)]
840enum RenderedSeek {
841    /// `seq_lseek`: only from the start or the position.
842    SeqFile,
843    /// `generic_file_llseek` (a process's `auxv` and `cmdline`).
844    Generic,
845    /// `default_llseek` (a sysctl file).
846    Default,
847}
848
849/// The position a seek of a rendered procfs file, every one of which has
850/// size 0, moves to from `current`: Linux's answer for its `llseek` (`seek`).
851///
852/// `generic_file_llseek` (fs/read_write.c, v6.17) compares a `SEEK_DATA` or
853/// `SEEK_HOLE` offset with the size as an unsigned number (`must_set_pos`,
854/// read_write.c:120 and 129), so every offset, a negative one included, is
855/// at or past the end: ENXIO. Its `vfs_setpos` refuses a negative position or
856/// one past the superblock's `s_maxbytes` (EINVAL): procfs keeps
857/// `alloc_super`'s MAX_NON_LFS, 2^31 - 1. `default_llseek` (read_write.c:
858/// 349-370) compares signed and has no `s_maxbytes` bound: an offset at or
859/// past the size 0 is ENXIO; a negative `SEEK_DATA` offset stays and reaches
860/// its EINVAL for a negative position; a negative `SEEK_HOLE` offset is
861/// replaced by the size, so the seek moves to 0. `seq_lseek` refuses every
862/// whence but `SEEK_SET` and `SEEK_CUR` with EINVAL.
863/// `rw_verify_area`'s range check for a read of a procfs file that has no
864/// unsigned offsets (fs/read_write.c:456-470 in 6.17): a count above
865/// `SSIZE_MAX`, or a position plus count past `LLONG_MAX`, is EINVAL.
866fn verify_procfs_read_range(position: usize, count: usize) -> Result<(), Errno> {
867    let count = i64::try_from(count).map_err(|_| Errno::EINVAL)?;
868    i64::try_from(position)
869        .ok()
870        .and_then(|position| position.checked_add(count))
871        .map(drop)
872        .ok_or(Errno::EINVAL)
873}
874
875/// Whether the unsupported-operation policy's refusal would strand the run:
876/// where the tool shares the guest's process and descriptor table, and the
877/// fail-closed policy ends a refusal with `unrecoverable_shutdown`, the exit
878/// ends only the refusing guest process, from inside its tool, which Detcore's
879/// model does not see as that process's exit, so a process waiting on it
880/// waits on, and the run does not end (SaBRe: measured 2026-10-31, the
881/// receiving process of tests/c/rendered_io_and_diskstats.c's
882/// --sequence-received-no-alias refused, and the run stopped at its
883/// deadline with nothing printed). DBT's runtime ends a refusal through its
884/// own exit and report path (it clears `shutdown_on_unsupported_syscall`):
885/// a failed run, 101, or 137 where that path's kill reaches Hermit first,
886/// and the reason; LiteInst and in-guest-trap keep their own descriptor table,
887/// and their launcher ends the run with exit 122 and the reason; ptrace and
888/// KVM exit with 122 from Hermit's process. A run with
889/// `--allow-unsupported-syscalls` refuses with an errno, which strands
890/// nothing, and so does a refusal of a record sequence Detcore cannot know
891/// where this holds (`Detcore::refuse_unknown_record_sequence`).
892fn refusal_would_strand_the_run(config: &crate::Config) -> bool {
893    config.shutdown_on_unsupported_syscall && config.backend.tool_shares_guest_descriptor_table
894}
895
896/// Writes `line` to the guest's standard error with one raw `write` loop,
897/// keeping the calling thread's `errno`, as `detlog::forward_to_stderr`
898/// writes a record. Where the tool runs in the guest's process (SaBRe) that
899/// `errno` is the guest's, and no log subscriber prints `error!`, so this is
900/// how a refusal there is named (`Detcore::refuse_unknown_record_sequence`).
901/// A failed write drops the rest; the call's EOPNOTSUPP still says it was
902/// refused.
903fn write_to_guest_stderr(line: &str) {
904    // SAFETY: `__errno_location` returns the calling thread's errno slot.
905    let saved = unsafe { *libc::__errno_location() };
906    let mut rest = line.as_bytes();
907    while !rest.is_empty() {
908        // SAFETY: `rest` is a live, initialized byte slice of the given length.
909        let written = unsafe { libc::write(libc::STDERR_FILENO, rest.as_ptr().cast(), rest.len()) };
910        if written > 0 {
911            rest = &rest[written as usize..];
912        } else if written == 0
913            || std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR)
914        {
915            break;
916        }
917    }
918    // SAFETY: as above.
919    unsafe { *libc::__errno_location() = saved };
920}
921
922/// Why Detcore refuses a read or a seek of descriptor `fd`, a per-process
923/// file (`path`, where Detcore knows it) whose record sequence it cannot know
924/// here (`Detcore::refuse_unknown_record_sequence`).
925fn unknown_record_sequence_reason(fd: RawFd, path: Option<&Path>) -> String {
926    let file = path.map_or_else(
927        || "a per-process /proc file".to_owned(),
928        |path| format!("the per-process file {}", path.display()),
929    );
930    format!(
931        "descriptor {fd}, {file}, reached this process (over SCM_RIGHTS, or found in its \
932         descriptor table) without joining an open file of it this process's Detcore models; \
933         Linux keeps where the file's record sequence is, and the record it buffered, in the \
934         one open file every process holding it shares, and Detcore cannot follow that \
935         sequence here, so it neither reads nor seeks it rather than answer otherwise than Linux"
936    )
937}
938
939fn rendered_procfs_seek(
940    seek: RenderedSeek,
941    whence: Whence,
942    offset: i64,
943    current: usize,
944) -> Result<usize, Errno> {
945    const MAX_NON_LFS: usize = (1 << 31) - 1;
946    let requested = i128::from(offset);
947    let new_offset = match whence {
948        Whence::SEEK_SET => requested,
949        Whence::SEEK_CUR => current as i128 + requested,
950        _ if seek == RenderedSeek::SeqFile => return Err(Errno::EINVAL),
951        Whence::SEEK_END => requested,
952        // At or past the end, which is 0.
953        Whence::SEEK_DATA | Whence::SEEK_HOLE
954            if requested >= 0 || seek == RenderedSeek::Generic =>
955        {
956            return Err(Errno::ENXIO);
957        }
958        // default_llseek: the hole at the end, the size.
959        Whence::SEEK_HOLE => 0,
960        Whence::SEEK_DATA => requested,
961        _ => return Err(Errno::EINVAL),
962    };
963    // Each llseek adds in `loff_t`, so a sum past LLONG_MAX wraps negative,
964    // which it refuses as it refuses any negative position: EINVAL, the
965    // position unchanged (`seq_lseek`'s `offset += file->f_pos` and its
966    // `offset < 0` check, fs/seq_file.c:315-320 in 6.17). Never EOVERFLOW,
967    // which `ksys_lseek` returns only where `off_t` is narrower than `loff_t`
968    // (fs/read_write.c:393-408), not on x86_64.
969    let new_offset = i64::try_from(new_offset)
970        .ok()
971        .and_then(|offset| usize::try_from(offset).ok())
972        .ok_or(Errno::EINVAL)?;
973    if seek == RenderedSeek::Generic && new_offset > MAX_NON_LFS {
974        return Err(Errno::EINVAL);
975    }
976    Ok(new_offset)
977}
978
979/// Why a stat Detcore takes for its own bookkeeping failed.
980#[derive(Debug, Clone, Copy, PartialEq, Eq)]
981enum StatFailure {
982    /// The stat call failed: the kernel's answer, or under record and replay
983    /// the recorded one.
984    Call(Errno),
985    /// Its buffer could not be prepared (the stack scratch faulted, or the
986    /// transient page could not be mapped), and then nothing was recorded; or,
987    /// after a successful stat, the guest's borrowed stack bytes could not be
988    /// written back.
989    Scratch(Errno),
990}
991
992impl StatFailure {
993    fn errno(self) -> Errno {
994        match self {
995            Self::Call(errno) | Self::Scratch(errno) => errno,
996        }
997    }
998}
999
1000fn require_random_device_read_access(status_flags: i32) -> Result<(), Errno> {
1001    if status_flags & libc::O_PATH != 0
1002        || !matches!(
1003            status_flags & libc::O_ACCMODE,
1004            libc::O_RDONLY | libc::O_RDWR
1005        )
1006    {
1007        Err(Errno::EBADF)
1008    } else {
1009        Ok(())
1010    }
1011}
1012
1013/// Inherited container output is a stream even when an outer runner stores it
1014/// in a seekable file.  The backing file also carries Hermit's own diagnostics,
1015/// so exposing its live offset makes tool logging guest-visible.  Preserve real
1016/// file semantics after a guest replaces stdout/stderr: `dup2(file, 1)` copies
1017/// the file's resource rather than this container-output resource.
1018fn is_inherited_container_output(resource: Option<ResourceID>) -> bool {
1019    matches!(
1020        resource,
1021        Some(ResourceID::Device(
1022            Device::ContainerStdout | Device::ContainerStderr
1023        ))
1024    )
1025}
1026
1027/// Whether `resource` is one of the container's standard streams, which
1028/// Detcore models from the start with an identity each and one stat, the
1029/// supervisor's standard input's (`setup_stdio`), whatever descriptions the
1030/// launcher passed: a descriptor the guest put in their place carries its own
1031/// file's resource (`dup2(file, 1)` copies it), and a dup of one shares its
1032/// identity.
1033fn is_container_stream(resource: Option<ResourceID>) -> bool {
1034    matches!(
1035        resource,
1036        Some(ResourceID::Device(
1037            Device::ContainerStdin | Device::ContainerStdout | Device::ContainerStderr
1038        ))
1039    )
1040}
1041
1042/// The one-time notice for a guest's bind of the abstract `AF_UNIX` name
1043/// `name` (without its leading NUL) in a `--network=host` run
1044/// ([`Config::notice_host_network_abstract_binds`]): `in_use` when the bind
1045/// failed with `EADDRINUSE`.
1046fn host_network_abstract_bind_notice(name: &[u8], in_use: bool) -> String {
1047    let what = if in_use {
1048        "the guest's bind of the abstract unix socket"
1049    } else {
1050        "the guest bound the abstract unix socket"
1051    };
1052    let outcome = if in_use {
1053        " failed with EADDRINUSE: another program on the host, possibly a concurrent Hermit run, \
1054         holds that name"
1055    } else {
1056        ""
1057    };
1058    format!(
1059        "hermit: NOTICE: {what} \"@{name}\" under --network=host{outcome}. Abstract names \
1060         belong to the network namespace, here the host's, and every Hermit run reuses the same \
1061         pids and autobind names, so concurrent runs that bind the same name collide with \
1062         EADDRINUSE. --network=local (the default) gives each run its own names. (Shown once \
1063         per run.)",
1064        name = name.escape_ascii()
1065    )
1066}
1067
1068/// Print [`host_network_abstract_bind_notice`] for a bind of `name` that
1069/// returned `result`, on Hermit's own standard error, the first time this
1070/// process sees one that succeeded or failed with `EADDRINUSE`. Any other
1071/// failure is the guest's own mistake and prints nothing. Only set where
1072/// Detcore runs in Hermit's process, so the guest never sees the line;
1073/// Hermit's diagnostics share the container's output, whose offset the guest
1074/// is never shown ([`is_inherited_container_output`]).
1075fn notice_host_network_abstract_bind(name: &[u8], result: &Result<i64, Errno>) {
1076    static NOTICED: AtomicBool = AtomicBool::new(false);
1077    let in_use = match result {
1078        Ok(_) => false,
1079        Err(Errno::EADDRINUSE) => true,
1080        Err(_) => return,
1081    };
1082    if !NOTICED.swap(true, Ordering::Relaxed) {
1083        use std::io::Write as _;
1084        let _ = writeln!(
1085            crate::util::RetryingStderr,
1086            "{}",
1087            host_network_abstract_bind_notice(name, in_use)
1088        );
1089    }
1090}
1091
1092/// The name, without its leading NUL, of the abstract `AF_UNIX` address
1093/// `bytes` holds: the first `addrlen` bytes of a `sockaddr_un`. `None` for a
1094/// path name, a bare family (an autobind), or an address too short to hold a
1095/// name.
1096fn abstract_unix_name(bytes: &[u8]) -> Option<&[u8]> {
1097    let path = std::mem::offset_of!(libc::sockaddr_un, sun_path);
1098    match bytes.get(path..)? {
1099        [0, name @ ..] => Some(name),
1100        _ => None,
1101    }
1102}
1103
1104fn unix_autobind_addrlen() -> i32 {
1105    (std::mem::offset_of!(libc::sockaddr_un, sun_path) + UNIX_AUTOBIND_NAME_LEN) as i32
1106}
1107
1108fn unix_autobind_address(port: u16) -> libc::sockaddr_un {
1109    // Linux autobind names are a leading NUL followed by five lowercase hex digits.
1110    let mut address: libc::sockaddr_un = unsafe { std::mem::zeroed() };
1111    address.sun_family = libc::AF_UNIX as libc::sa_family_t;
1112    for (destination, source) in address.sun_path[1..UNIX_AUTOBIND_NAME_LEN]
1113        .iter_mut()
1114        .zip(format!("{port:05x}").bytes())
1115    {
1116        *destination = source as libc::c_char;
1117    }
1118    address
1119}
1120
1121// TODO-HUMAN-REVIEW(PR-904): Review the TCP_INFO compatibility boundary.
1122/// Retain the logical TCP state and negotiated option header while hiding all
1123/// host timing, rate, packet, and byte counters.
1124fn canonicalize_tcp_info(info: &mut [u8]) {
1125    for (offset, byte) in info.iter_mut().enumerate() {
1126        if !matches!(offset, 0 | 1 | 5 | 6) {
1127            *byte = 0;
1128        }
1129    }
1130}
1131
1132// Hermit exposes exactly one isolated guest network namespace.
1133const DETERMINISTIC_NETNS_COOKIE: u64 = 1;
1134
1135// Above Linux's PID range and below the high-bit IDs used by kernel autobind.
1136const DETERMINISTIC_NETLINK_PORT_ID_BASE: u32 = 0x4000_0000;
1137
1138/// Does the new guest descriptor `fd` reopen an anonymous pipe that the same
1139/// process already holds as a scheduler-managed pipe (`managed_pipe_fds`)?
1140///
1141/// Both `/proc/<pid>/fd/<fd>` links read `pipe:[<inode>]` for the same pipe.
1142/// A named FIFO links to its path, and on ptrace a host pipe is never in
1143/// `managed_pipe_fds` (see `scheduler_managed_pipe_fds` for SaBRe), so neither
1144/// matches: a host writer is outside the scheduler and must not be polled as if
1145/// it were a guest
1146/// (<https://github.com/rrnewton/hermit/pull/3534#issuecomment-5962369261>).
1147fn reopens_scheduler_managed_pipe(
1148    backend: &reverie::BackendCapabilities,
1149    pid: i32,
1150    fd: RawFd,
1151    managed_pipe_fds: &[RawFd],
1152) -> bool {
1153    // Under KVM `/proc/<pid>` is an unrelated host task, if any, and its
1154    // links say nothing about this guest's pipes.
1155    if !crate::procfs_inode::tool_can_name_guest_descriptors(backend) {
1156        return false;
1157    }
1158    let link = |fd: RawFd| std::fs::read_link(format!("/proc/{pid}/fd/{fd}")).ok();
1159    let Some(opened) = link(fd) else {
1160        return false;
1161    };
1162    if !opened
1163        .to_str()
1164        .is_some_and(|name| name.starts_with("pipe:["))
1165    {
1166        return false;
1167    }
1168    managed_pipe_fds
1169        .iter()
1170        .any(|&held| held != fd && link(held).as_ref() == Some(&opened))
1171}
1172
1173/// The path the kernel resolved an open descriptor to, read from the guest's
1174/// own `/proc/<pid>/fd/<fd>` link. This is the evidence authority for "which
1175/// object was opened": it is produced by the kernel from the descriptor itself,
1176/// so it is independent of the pathname spelling the guest used.
1177///
1178/// Used ONLY as a fallback for a pathname that does not classify on its own
1179/// (see the call site): a spelling that already classifies must keep its own
1180/// classification, because `/proc/self/...` and `/proc/thread-self/...` are
1181/// defined by the spelling and resolve to a different numeric path.
1182///
1183/// `None` when the link cannot be read (the descriptor is gone, or procfs is
1184/// unavailable), in which case the lexical result stands unchanged. Also
1185/// `None` under KVM, where `/proc/<pid>` does not name the guest (see
1186/// [`crate::procfs_inode::tool_can_name_guest_descriptors`]).
1187fn resolved_open_path(
1188    backend: &reverie::BackendCapabilities,
1189    pid: i32,
1190    fd: RawFd,
1191) -> Option<PathBuf> {
1192    if !crate::procfs_inode::tool_can_name_guest_descriptors(backend) {
1193        return None;
1194    }
1195    let link = std::fs::read_link(format!("/proc/{pid}/fd/{fd}")).ok()?;
1196    // A deleted or anonymous target is not a stable object name.
1197    link.is_absolute().then_some(link)
1198}
1199
1200/// What Detcore knows about a socket whose peer it classifies
1201/// (`Detcore::classify_socket_peer`).
1202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1203pub(crate) enum SocketRole {
1204    /// `connect` connected it or `accept` returned it, so it has a peer.
1205    Connected,
1206    /// Received over `SCM_RIGHTS`; it may be a listener, which has no peer.
1207    Received,
1208}
1209
1210/// The buffer `Detcore::probe_socket_option` lends the kernel: an option
1211/// value and its length, in one reservation so that the guest bytes under
1212/// both are saved and put back together.
1213#[repr(C)]
1214#[derive(Clone, Copy)]
1215struct SocketOptionProbe<V: Copy> {
1216    value: V,
1217    length: libc::socklen_t,
1218}
1219
1220/// The scope `pidfd_send_signal`'s flags select (Linux's `pidfd_send_signal`
1221/// since 6.9; <https://github.com/rrnewton/hermit/issues/4108>).
1222#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1223pub(crate) enum PidfdSignalScope {
1224    /// No flag, or `PIDFD_SIGNAL_THREAD_GROUP`: the process (Detcore's
1225    /// `pidfd_open` makes no thread pidfds).
1226    Process,
1227    /// `PIDFD_SIGNAL_THREAD`: the thread whose id is the pid.
1228    Thread,
1229    /// `PIDFD_SIGNAL_PROCESS_GROUP`: the process's process group.
1230    ProcessGroup,
1231}
1232
1233const PIDFD_SIGNAL_THREAD: u32 = 1 << 0;
1234const PIDFD_SIGNAL_THREAD_GROUP: u32 = 1 << 1;
1235const PIDFD_SIGNAL_PROCESS_GROUP: u32 = 1 << 2;
1236
1237/// `flags`' scope, or EINVAL as Linux answers: for a bit outside the three
1238/// scope flags, or for more than one of them.
1239pub(crate) fn pidfd_signal_scope(flags: u32) -> Result<PidfdSignalScope, Errno> {
1240    match flags {
1241        0 | PIDFD_SIGNAL_THREAD_GROUP => Ok(PidfdSignalScope::Process),
1242        PIDFD_SIGNAL_THREAD => Ok(PidfdSignalScope::Thread),
1243        PIDFD_SIGNAL_PROCESS_GROUP => Ok(PidfdSignalScope::ProcessGroup),
1244        _ => Err(Errno::EINVAL),
1245    }
1246}
1247
1248/// `pidfd_send_signal`'s sentinel for the calling thread (Linux 6.15 and later).
1249const PIDFD_SELF_THREAD: RawFd = -10000;
1250/// `pidfd_send_signal`'s sentinel for the calling thread's thread group.
1251const PIDFD_SELF_THREAD_GROUP: RawFd = -10001;
1252
1253/// The task a `PIDFD_SELF_*` sentinel names, and the scope Linux gives it when
1254/// no flag is set: the calling thread (thread-directed) or its thread group
1255/// (process-directed); `None` for any other descriptor
1256/// (https://github.com/rrnewton/hermit/issues/4147).
1257fn pidfd_self_target<T, G: Guest<Detcore<T>>>(
1258    guest: &mut G,
1259    pidfd: RawFd,
1260) -> Option<(DetPid, PidfdSignalScope)>
1261where
1262    T: RecordOrReplay,
1263{
1264    let state = guest.thread_state();
1265    match pidfd {
1266        PIDFD_SELF_THREAD => Some((
1267            DetPid::from_raw(state.dettid.as_raw()),
1268            PidfdSignalScope::Thread,
1269        )),
1270        PIDFD_SELF_THREAD_GROUP => Some((state.detpid?, PidfdSignalScope::Process)),
1271        _ => None,
1272    }
1273}
1274
1275/// What a descriptor that is not a modeled pidfd names for `pidfd_send_signal`.
1276#[derive(Clone, Debug, PartialEq, Eq)]
1277pub(crate) enum ProcessDirectoryDescriptor {
1278    /// A `/proc/<pid>` directory of the guest's procfs: Linux takes it as the
1279    /// process `<pid>`.
1280    Process(DetPid),
1281    /// Any other file, which Linux answers EBADF.
1282    NotAProcess,
1283    /// Detcore cannot tell; the reason names why.
1284    Unknown(&'static str),
1285}
1286
1287/// Facts the tracer reads about a guest descriptor (`process_directory_descriptor`).
1288#[derive(Clone, Debug, Default)]
1289pub(crate) struct DescriptorFacts {
1290    /// The descriptor's `/proc/<tid>/fd/<fd>` link, as the kernel spells it.
1291    pub(crate) link: PathBuf,
1292    /// Whether the file is on procfs.
1293    pub(crate) on_procfs: bool,
1294    /// Whether the file is a directory.
1295    pub(crate) is_dir: bool,
1296    /// The file's device, and its parent directory's device and inode.
1297    pub(crate) dev: u64,
1298    pub(crate) parent: Option<(u64, u64)>,
1299    /// The device of the procfs mounted at the guest's `/proc`.
1300    pub(crate) guest_proc_dev: Option<u64>,
1301}
1302
1303/// The inode of a procfs root directory (Linux's `PROC_ROOT_INO`).
1304const PROC_ROOT_INO: u64 = 1;
1305
1306/// Classify a descriptor from `facts` (`None`: unreadable). Linux's
1307/// `tgid_pidfd_to_pid` accepts exactly a procfs process directory, whose name
1308/// is its pid in the procfs instance's pid namespace and whose parent is the
1309/// procfs root. That instance must be the guest's own, at its `/proc`, so the
1310/// pid is a guest pid. A pidfd Detcore did not open cannot be classified.
1311pub(crate) fn classify_process_directory(
1312    facts: Option<&DescriptorFacts>,
1313) -> ProcessDirectoryDescriptor {
1314    use ProcessDirectoryDescriptor as D;
1315    let Some(facts) = facts else {
1316        return D::Unknown("Hermit cannot read what the descriptor names");
1317    };
1318    let link = facts.link.to_string_lossy();
1319    if link == "anon_inode:[pidfd]" || link.starts_with("pidfd:") {
1320        return D::Unknown("it is a pidfd that Detcore did not open with pidfd_open");
1321    }
1322    if !facts.on_procfs {
1323        return D::NotAProcess;
1324    }
1325    if !facts.is_dir {
1326        return D::NotAProcess;
1327    }
1328    let name = facts
1329        .link
1330        .file_name()
1331        .map(|name| name.to_string_lossy().into_owned())
1332        .unwrap_or_default();
1333    // A reaped task's directory is spelled with " (deleted)", and its ".."
1334    // cannot be read (ESRCH); Linux still takes it as the task, and answers
1335    // ESRCH for it. The spelling is then the only evidence, so it must be a
1336    // top-level entry of the guest's /proc.
1337    let name = match name.strip_suffix(" (deleted)") {
1338        Some(name) if facts.parent.is_none() => {
1339            if facts.link.parent() != Some(Path::new("/proc")) {
1340                return if facts.link.starts_with("/proc/") {
1341                    D::NotAProcess
1342                } else {
1343                    D::Unknown("it names a reaped task outside the guest's /proc")
1344                };
1345            }
1346            name.to_owned()
1347        }
1348        _ if facts.parent != Some((facts.dev, PROC_ROOT_INO)) => return D::NotAProcess,
1349        _ => name,
1350    };
1351    let Ok(pid) = name.parse::<i32>() else {
1352        return D::NotAProcess;
1353    };
1354    if pid <= 0 || name.starts_with(['+', '0']) {
1355        return D::NotAProcess;
1356    }
1357    if facts.guest_proc_dev != Some(facts.dev) {
1358        return D::Unknown("it is a procfs that is not the guest's /proc");
1359    }
1360    D::Process(DetPid::from_raw(pid))
1361}
1362
1363/// Read the facts for the guest descriptor `fd` of thread `tid` through
1364/// `/proc/<tid>/fd/<fd>` and classify it (`classify_process_directory`).
1365fn process_directory_descriptor(tid: i32, fd: RawFd) -> ProcessDirectoryDescriptor {
1366    let facts = (|| {
1367        let (link, on_procfs) = descriptor_identity(tid, fd)?;
1368        let target = std::fs::metadata(format!("/proc/{tid}/fd/{fd}")).ok()?;
1369        let parent = std::fs::metadata(format!("/proc/{tid}/fd/{fd}/.."))
1370            .ok()
1371            .map(|parent| (parent.dev(), parent.ino()));
1372        let guest_proc_dev = std::fs::metadata(format!("/proc/{tid}/root/proc"))
1373            .ok()
1374            .filter(|proc| proc.ino() == PROC_ROOT_INO)
1375            .map(|proc| proc.dev());
1376        Some(DescriptorFacts {
1377            link,
1378            on_procfs,
1379            is_dir: target.is_dir(),
1380            dev: target.dev(),
1381            parent: if target.is_dir() { parent } else { None },
1382            guest_proc_dev,
1383        })
1384    })();
1385    classify_process_directory(facts.as_ref())
1386}
1387
1388/// The host's default huge page size (`Hugepagesize` in /proc/meminfo), the
1389/// size of a MAP_HUGETLB mapping that names none, or 2 MiB, the x86_64
1390/// default, when it cannot be read (see [`may_change_untraced_code`]).
1391fn default_huge_page_size() -> u64 {
1392    static SIZE: std::sync::OnceLock<u64> = std::sync::OnceLock::new();
1393    *SIZE.get_or_init(|| {
1394        std::fs::read_to_string("/proc/meminfo")
1395            .ok()
1396            .and_then(|meminfo| {
1397                let line = meminfo
1398                    .lines()
1399                    .find(|line| line.starts_with("Hugepagesize:"))?;
1400                let kib: u64 = line.split_whitespace().nth(1)?.parse().ok()?;
1401                kib.checked_mul(1024)
1402            })
1403            .unwrap_or(2 << 20)
1404    })
1405}
1406
1407/// What a guest descriptor is, read through the opening thread's
1408/// `/proc/<tid>/fd/<fd>`: its link as the kernel spells it, and whether the
1409/// file is on procfs (see [`may_write_process_memory`]). Read through the
1410/// thread because its descriptor table may not be its leader's (a thread
1411/// cloned without CLONE_FILES). `None` when either cannot be read.
1412fn descriptor_identity(tid: i32, fd: RawFd) -> Option<(PathBuf, bool)> {
1413    let path = format!("/proc/{tid}/fd/{fd}");
1414    let link = std::fs::read_link(&path).ok()?;
1415    let path = std::ffi::CString::new(path).ok()?;
1416    let mut buf = std::mem::MaybeUninit::<libc::statfs>::uninit();
1417    // SAFETY: `path` is NUL-terminated and `buf` is large enough for statfs.
1418    if unsafe { libc::statfs(path.as_ptr(), buf.as_mut_ptr()) } != 0 {
1419        return None;
1420    }
1421    // SAFETY: statfs succeeded, so it filled `buf`.
1422    let on_procfs = unsafe { buf.assume_init() }.f_type as u64
1423        == reverie::syscalls::linux_abi::PROC_SUPER_MAGIC as u64;
1424    Some((link, on_procfs))
1425}
1426
1427/// Resolve an `AT_FDCWD`-relative spelling in the guest's filesystem view.
1428///
1429/// Replayer chroots the guest, so the tracer-visible cwd includes the replay
1430/// root. Stripping `/proc/<pid>/root` produces the same guest-absolute path in
1431/// record and replay without depending on the opened descriptor (which is an
1432/// eventfd placeholder during replay).
1433///
1434/// A working directory whose path is longer than the kernel renders into its
1435/// link has none here (`ENAMETOOLONG`); for that case see
1436/// [`climbed_out_of_unrendered_cwd`].
1437pub(crate) fn resolved_at_fdcwd_path(pid: i32, path: &Path) -> Option<PathBuf> {
1438    debug_assert!(!path.is_absolute());
1439    let root = std::fs::read_link(format!("/proc/{pid}/root")).ok()?;
1440    let cwd = match std::fs::read_link(format!("/proc/{pid}/cwd")) {
1441        Ok(cwd) => cwd,
1442        Err(error) if error.raw_os_error() == Some(libc::ENAMETOOLONG) => {
1443            return climbed_out_of_unrendered_cwd(pid, &root, path);
1444        }
1445        Err(_) => return None,
1446    };
1447    let guest_cwd = cwd.strip_prefix(root).ok()?;
1448    Some(Path::new("/").join(guest_cwd).join(path))
1449}
1450
1451/// [`resolved_at_fdcwd_path`] where the kernel cannot render the working
1452/// directory of process `pid` into its link because its path is longer than
1453/// `PATH_MAX`, which Linux still walks `..` out of. A spelling whose leading
1454/// `..` climb out of it is made absolute from the directory they reach, which
1455/// the kernel's own walk names: an `O_PATH` open of `/proc/<pid>/cwd/..` (as
1456/// many `..` as the spelling leads with), whose descriptor's link is that
1457/// directory's path; the rest of the spelling follows byte for byte
1458/// ([`split_leading_climb`]). `None`, resolving nothing, for any other
1459/// spelling, and where the open or the link fails: the call's own walk then
1460/// decides the spelling, with its own error
1461/// (<https://github.com/rrnewton/hermit/issues/4541>). Both reads are raw
1462/// syscalls ([`crate::util::RawFd`], [`crate::util::raw_read_link`]), as
1463/// Detcore may run inside the guest.
1464fn climbed_out_of_unrendered_cwd(pid: i32, root: &Path, path: &Path) -> Option<PathBuf> {
1465    let (climbs, rest) = split_leading_climb(path);
1466    if climbs == 0 {
1467        return None;
1468    }
1469    let climbed = crate::util::RawFd::open(
1470        Path::new(&format!("/proc/{pid}/cwd/{}", climb_spelling(climbs))),
1471        libc::O_PATH | libc::O_DIRECTORY,
1472    )
1473    .ok()?;
1474    let directory =
1475        crate::util::raw_read_link(Path::new(&format!("/proc/self/fd/{}", climbed.raw()))).ok()?;
1476    let guest_directory = directory.strip_prefix(root).ok()?;
1477    Some(joined_verbatim(&Path::new("/").join(guest_directory), rest))
1478}
1479
1480/// How many `..` the relative `path` climbs before its first other name,
1481/// past any `.`, and the rest of the spelling after them, byte for byte, so
1482/// that a trailing `/` or `/.` keeps its meaning for the walk of the rest:
1483/// `../../proc/fs` climbs 2, to `proc/fs`; `./..//proc/uptime/` climbs 1, to
1484/// `proc/uptime/`; `x/..` climbs none.
1485pub(crate) fn split_leading_climb(path: &Path) -> (usize, &std::ffi::OsStr) {
1486    use std::os::unix::ffi::OsStrExt;
1487    let mut rest = path.as_os_str().as_bytes();
1488    let mut climbs = 0;
1489    while !rest.is_empty() {
1490        let end = rest
1491            .iter()
1492            .position(|&byte| byte == b'/')
1493            .unwrap_or(rest.len());
1494        match &rest[..end] {
1495            b".." => climbs += 1,
1496            b"." => {}
1497            _ => break,
1498        }
1499        let slashes = rest[end..].iter().take_while(|&&byte| byte == b'/').count();
1500        rest = &rest[end + slashes..];
1501    }
1502    (climbs, std::ffi::OsStr::from_bytes(rest))
1503}
1504
1505/// Whether `path` is `PATH_MAX` bytes long or more, which no name may be:
1506/// Linux copies at most `PATH_MAX` bytes of a pathname, its NUL included,
1507/// and refuses one whose NUL is not among them with `ENAMETOOLONG`, before
1508/// any walk (`getname_flags`, fs/namei.c:206-210 at v6.17). Such a spelling
1509/// is never resolved, so that the call's own lookup refuses it.
1510pub(crate) fn names_past_path_max(path: &Path) -> bool {
1511    path.as_os_str().len() >= libc::PATH_MAX as usize
1512}
1513
1514/// `rest`, a relative spelling, below `directory`, byte for byte;
1515/// `directory` itself where `rest` is empty.
1516fn joined_verbatim(directory: &Path, rest: &std::ffi::OsStr) -> PathBuf {
1517    if rest.is_empty() {
1518        directory.to_path_buf()
1519    } else {
1520        directory.join(rest)
1521    }
1522}
1523
1524/// `..` `climbs` times: `..`, `../..`, and so on.
1525fn climb_spelling(climbs: usize) -> String {
1526    vec![".."; climbs].join("/")
1527}
1528
1529/// The calling thread's working directory, read for the syscall it is
1530/// running where the tool cannot read it from host procfs
1531/// (`procfs_inode::tool_can_read_guest_cwd`: KVM), at most once per syscall
1532/// (`ThreadState::syscall_cwd`, `Detcore::prepare_guest_cwd`).
1533#[derive(Clone, Debug, PartialEq, Eq)]
1534pub(crate) enum SyscallCwd {
1535    /// Its path, as the thread's `getcwd` answered it.
1536    Path(PathBuf),
1537    /// No path: `getcwd` failed with `ENOENT`, for a removed directory, or
1538    /// for one whose path is longer than `PATH_MAX` (`ENAMETOOLONG`,
1539    /// `ERANGE`), which Linux still walks `..` from. By how many `..` a
1540    /// spelling of the syscall climbs out of it, the path of the directory
1541    /// it climbs to, read through a descriptor for it
1542    /// (`Detcore::read_climbed_directory`), `None` where that failed.
1543    Unrendered(BTreeMap<usize, Option<PathBuf>>),
1544    /// No path otherwise (`getcwd` answered no path from the root, or failed
1545    /// another way): its spellings are decided as given.
1546    Unknown,
1547}
1548
1549/// The directory a listing of a descriptor reads, for
1550/// `Detcore::drop_absent_entries`. Where the tool's `/proc` entry for the
1551/// caller is the caller's (`tool_sees_caller`,
1552/// `procfs_inode::tool_can_name_guest_descriptors`): the kernel's path for
1553/// the descriptor (`linked`, its link in that entry), or, where that link
1554/// cannot be read (after a `chroot` into a root without `/proc`, where the
1555/// tool runs inside the guest), the path the guest opened it by (`recorded`).
1556/// Where the guest's threads are no host tasks (KVM), that entry names one of
1557/// the supervisor's own tasks, whose descriptor of that number is no guest's,
1558/// so its link is never read
1559/// (<https://github.com/rrnewton/hermit/issues/4541>): the directory is the
1560/// recorded path, with its `.` and `..` folded in by name, as the kernel's
1561/// path has neither (`/sys/devices/system/cpu/.` is the directory the
1562/// listing filters). `None` where the path is not absolute.
1563fn listed_directory(
1564    tool_sees_caller: bool,
1565    linked: impl FnOnce() -> Option<PathBuf>,
1566    recorded: impl FnOnce() -> Option<PathBuf>,
1567) -> Option<PathBuf> {
1568    let directory = if tool_sees_caller {
1569        linked().or_else(recorded)
1570    } else {
1571        recorded().map(|path| {
1572            let mut folded = PathBuf::new();
1573            for component in path.components() {
1574                match component {
1575                    std::path::Component::ParentDir => {
1576                        folded.pop();
1577                    }
1578                    std::path::Component::CurDir => {}
1579                    other => folded.push(other),
1580                }
1581            }
1582            folded
1583        })
1584    };
1585    directory.filter(|path| path.is_absolute())
1586}
1587
1588#[cfg(test)]
1589mod leading_climb_tests {
1590    use super::*;
1591
1592    /// A spelling climbs out of the working directory by the `..` before its
1593    /// first other name, past any `.`; the rest is kept byte for byte, a
1594    /// trailing `/` or `/.` included, for the walk below where they lead
1595    /// (<https://github.com/rrnewton/hermit/issues/4541>).
1596    #[test]
1597    fn a_spelling_climbs_by_its_leading_dotdots() {
1598        let climb = |path: &'static str| {
1599            let (climbs, rest) = split_leading_climb(Path::new(path));
1600            (climbs, rest.to_str().unwrap().to_owned())
1601        };
1602        let split = |climbs: usize, rest: &str| (climbs, rest.to_owned());
1603        assert_eq!(climb("../../proc/fs"), split(2, "proc/fs"));
1604        assert_eq!(climb("./../x"), split(1, "x"));
1605        assert_eq!(climb("..//./..//proc/uptime/"), split(2, "proc/uptime/"));
1606        assert_eq!(climb("../proc/."), split(1, "proc/."));
1607        assert_eq!(climb(".."), split(1, ""));
1608        assert_eq!(climb("../"), split(1, ""));
1609        assert_eq!(climb("x/.."), split(0, "x/.."));
1610        assert_eq!(climb("...x/../y"), split(0, "...x/../y"));
1611        assert_eq!(climb("proc/fs"), split(0, "proc/fs"));
1612        assert_eq!(climb(""), split(0, ""));
1613        assert_eq!(
1614            joined_verbatim(Path::new("/"), std::ffi::OsStr::new("proc/uptime/")),
1615            PathBuf::from("/proc/uptime/")
1616        );
1617        assert_eq!(
1618            joined_verbatim(Path::new("/tmp"), std::ffi::OsStr::new("")),
1619            PathBuf::from("/tmp")
1620        );
1621        assert_eq!(climb_spelling(1), "..");
1622        assert_eq!(climb_spelling(3), "../../..");
1623    }
1624}
1625
1626#[cfg(test)]
1627mod listed_directory_tests {
1628    use super::*;
1629
1630    /// Where the tool's `/proc` entry for the caller is not the caller's
1631    /// (KVM), a listing's directory is the path the guest opened it by, its
1632    /// `.` and `..` folded in, and the descriptor link of the entry is never
1633    /// read; elsewhere the link is the directory, and the recorded path, as
1634    /// it is, the fallback (<https://github.com/rrnewton/hermit/issues/4541>).
1635    #[test]
1636    fn a_listing_is_named_by_the_link_only_where_the_entry_is_the_callers() {
1637        // Compared byte for byte: `Path`'s own comparison ignores a `.`.
1638        let bytes = |path: Option<PathBuf>| path.map(PathBuf::into_os_string);
1639        let cpu = || Some(PathBuf::from("/sys/devices/system/cpu"));
1640        let unread = || -> Option<PathBuf> { panic!("the link of a host task was read") };
1641        for recorded in [
1642            "/sys/devices/system/cpu/.",
1643            "/sys/devices/system/cpu/",
1644            "/sys/devices/system/./cpu",
1645            "/sys/devices/system/cpu/../cpu",
1646        ] {
1647            assert_eq!(
1648                bytes(listed_directory(false, unread, || Some(PathBuf::from(
1649                    recorded
1650                )))),
1651                bytes(cpu()),
1652                "{recorded}"
1653            );
1654        }
1655        assert_eq!(
1656            listed_directory(false, unread, || Some(PathBuf::from("cpu"))),
1657            None
1658        );
1659        assert_eq!(
1660            bytes(listed_directory(true, cpu, || Some(PathBuf::from(
1661                "/tmp/x"
1662            )))),
1663            bytes(cpu())
1664        );
1665        // Where the link cannot be read, the recorded path as it is.
1666        assert_eq!(
1667            bytes(listed_directory(
1668                true,
1669                || None,
1670                || Some(PathBuf::from("/tmp/x/../y"))
1671            )),
1672            Some("/tmp/x/../y".into())
1673        );
1674    }
1675}
1676
1677/// A name `call` removes, as `(dirfd, path)`.
1678type RemovedName<'a> = (RawFd, Option<PathPtr<'a>>);
1679
1680/// The name `call` removes and, for a rename, its source, which replaces
1681/// nothing when it is the target's own file: the name an `unlink`,
1682/// `unlinkat` or (with `with_rmdir`) `rmdir` removes, or the target a
1683/// `rename`, `renameat` or `renameat2` replaces (not with
1684/// `RENAME_EXCHANGE`). `None` for every other call. What
1685/// [`Detcore::last_names_removed_by`] (with `rmdir`) and
1686/// [`Detcore::guest_made_fifos_removed_by`] (without) look up.
1687fn removed_names<'a>(
1688    call: &'a Syscall,
1689    #[cfg_attr(
1690        target_arch = "aarch64",
1691        allow(unused_variables, reason = "only x86_64 has rmdir")
1692    )]
1693    with_rmdir: bool,
1694) -> Option<(RemovedName<'a>, Option<RemovedName<'a>>)> {
1695    #[cfg(not(target_arch = "aarch64"))]
1696    let at = libc::AT_FDCWD;
1697    Some(match call {
1698        #[cfg(not(target_arch = "aarch64"))]
1699        Syscall::Unlink(c) => ((at, c.path()), None),
1700        Syscall::Unlinkat(c) => ((c.dirfd(), c.path()), None),
1701        #[cfg(not(target_arch = "aarch64"))]
1702        Syscall::Rmdir(c) if with_rmdir => ((at, c.path()), None),
1703        #[cfg(not(target_arch = "aarch64"))]
1704        Syscall::Rename(c) => ((at, c.newpath()), Some((at, c.oldpath()))),
1705        Syscall::Renameat(c) => (
1706            (c.newdirfd(), c.newpath()),
1707            Some((c.olddirfd(), c.oldpath())),
1708        ),
1709        Syscall::Renameat2(c) if c.flags() & libc::RENAME_EXCHANGE == 0 => (
1710            (c.newdirfd(), c.newpath()),
1711            Some((c.olddirfd(), c.oldpath())),
1712        ),
1713        _ => return None,
1714    })
1715}
1716
1717/// Whether a call numbered `sysno` may remove a name ([`removed_names`],
1718/// whatever its arguments): the syscalls [`Detcore::last_names_removed_by`]
1719/// and [`Detcore::guest_made_fifos_removed_by`] can act on. Part of
1720/// [`may_name_file_or_descriptor`]; `syscall_hot_path_tests` checks it
1721/// against [`removed_names`] for every syscall number.
1722pub(crate) fn removes_a_name(sysno: Sysno) -> bool {
1723    match sysno {
1724        #[cfg(not(target_arch = "aarch64"))]
1725        Sysno::unlink | Sysno::rmdir | Sysno::rename => true,
1726        Sysno::unlinkat | Sysno::renameat | Sysno::renameat2 => true,
1727        _ => false,
1728    }
1729}
1730
1731/// Whether a call numbered `sysno` can be acted on by any of the file and
1732/// descriptor checks `Detcore::handle_syscall_event` makes before it
1733/// dispatches the call: the names it removes ([`removes_a_name`]), the
1734/// rendered procfs descriptors it names
1735/// ([`rendered_procfs::names_a_descriptor`]), the `/proc` names it looks up
1736/// ([`procfs_levels::looks_up_names`]), and the rendered procfs calls
1737/// Detcore answers ([`rendered_procfs::may_answer_operation`],
1738/// [`rendered_procfs::may_answer_mapping`],
1739/// [`rendered_procfs::may_answer_path_call`]). Each check answers its
1740/// trivial value, with no effect, for any other call, so skipping them
1741/// for those calls changes nothing but the work. It reads the number only,
1742/// never the backend or the arguments.
1743pub(crate) fn may_name_file_or_descriptor(sysno: Sysno) -> bool {
1744    removes_a_name(sysno)
1745        || rendered_procfs::names_a_descriptor(sysno)
1746        || procfs_levels::looks_up_names(sysno)
1747        || rendered_procfs::may_answer_operation(sysno)
1748        || rendered_procfs::may_answer_mapping(sysno)
1749        || rendered_procfs::may_answer_path_call(sysno)
1750}
1751
1752/// Whether removing a name of the file `stat` describes leaves it with no
1753/// name: a directory has only one, and a non-directory with one link loses
1754/// its last. A host may then give its inode to another file
1755/// (<https://github.com/rrnewton/hermit/issues/3840>).
1756fn retires_on_removal(stat: &libc::stat) -> bool {
1757    stat.st_mode & libc::S_IFMT == libc::S_IFDIR || stat.st_nlink == 1
1758}
1759
1760/// The path to report for one operand of a successful namespace change (see
1761/// `Detcore::record_host_namespace_change`): the operand made absolute, or
1762/// `/` when it could not be read or made absolute. A tracer that may not read
1763/// a non-dumpable guest's memory, for one, cannot read it while the change
1764/// itself succeeds. `/` is an ancestor of every path, so no host input change
1765/// is named in that run, rather than one the guest may have made.
1766fn rebound_path(observed: Option<PathBuf>) -> String {
1767    match observed {
1768        Some(path) if path.is_absolute() => path.to_string_lossy().into_owned(),
1769        _ => String::from("/"),
1770    }
1771}
1772
1773/// Whether the raw call `number(args)` may change the guest memory in
1774/// `range`, `[start, end)` (see `Config::untraced_code_range`):
1775/// - a fixed mapping over any part of it: `mmap` with MAP_FIXED, `mremap`
1776///   with MREMAP_FIXED, `shmat` with SHM_REMAP;
1777/// - a protection, advice or unmapping over any part of it: `mprotect`,
1778///   `pkey_mprotect`, `madvise`, `munmap`, the old range of `mremap`.
1779///
1780/// Linux takes page-aligned addresses for these calls and rounds a length up
1781/// to whole pages, and `range` is whole pages, so the unrounded interval
1782/// overlaps it exactly when the rounded one does. A MAP_HUGETLB `mmap` rounds
1783/// to its own page size on both ends, so it is widened to the size its
1784/// MAP_HUGE_* bits name, or else to `default_huge_page` (the host's
1785/// `Hugepagesize`). A zero length counts as its first byte, and a `shmat` as
1786/// reaching the end of the address space, since its size is the segment's.
1787/// A mapping without a fixed address never lands on an existing one, and the
1788/// guest's `process_vm_writev`, `ptrace` and `remap_file_pages` are refused.
1789/// Scalar arguments are never read as addresses, so an ordinary call is
1790/// never counted.
1791pub(crate) fn may_change_untraced_code(
1792    number: Sysno,
1793    args: &syscalls::SyscallArgs,
1794    (start, end): (u64, u64),
1795    default_huge_page: u64,
1796) -> bool {
1797    let [arg0, arg1, arg2, arg3, arg4] =
1798        [args.arg0, args.arg1, args.arg2, args.arg3, args.arg4].map(|arg| arg as u64);
1799    let overlaps = |addr: u64, len: u64| addr < end && addr.saturating_add(len.max(1)) > start;
1800    let overlaps_huge = |addr: u64, len: u64, page: u64| {
1801        let page = page.max(1);
1802        let first = addr - addr % page;
1803        let last = addr
1804            .saturating_add(len.max(1))
1805            .div_ceil(page)
1806            .saturating_mul(page);
1807        overlaps(first, last.saturating_sub(first).max(1))
1808    };
1809    let huge_page = |flags: u64| match (flags >> libc::MAP_HUGE_SHIFT) & libc::MAP_HUGE_MASK as u64
1810    {
1811        0 => default_huge_page,
1812        shift => 1u64.checked_shl(shift as u32).unwrap_or(u64::MAX),
1813    };
1814    match number {
1815        Sysno::mmap if arg3 & libc::MAP_FIXED as u64 == 0 => false,
1816        Sysno::mmap if arg3 & libc::MAP_HUGETLB as u64 != 0 => {
1817            overlaps_huge(arg0, arg1, huge_page(arg3))
1818        }
1819        Sysno::mmap => overlaps(arg0, arg1),
1820        Sysno::mprotect | Sysno::pkey_mprotect | Sysno::madvise | Sysno::munmap => {
1821            overlaps(arg0, arg1)
1822        }
1823        Sysno::mremap => {
1824            overlaps(arg0, arg1) || (arg3 & libc::MREMAP_FIXED as u64 != 0 && overlaps(arg4, arg2))
1825        }
1826        Sysno::shmat => arg1 != 0 && arg2 & libc::SHM_REMAP as u64 != 0 && overlaps(arg1, u64::MAX),
1827        _ => false,
1828    }
1829}
1830
1831/// Whether an open with `flags` that returned a descriptor may write a
1832/// process's memory: `/proc/<pid>/mem` and `/proc/<pid>/task/<tid>/mem`
1833/// write through page protections. `descriptor` is what the descriptor is,
1834/// read through the opening thread's `/proc/<tid>/fd/<fd>` (see
1835/// [`descriptor_identity`]): its link, which names the file however the guest
1836/// reached it (a symlink, a `/proc/self/fd` reopen), and whether it is on
1837/// procfs. It counts only when it is a procfs file named `mem`, so an
1838/// ordinary file of that name, or a name that leads to a pipe, does not. A
1839/// descriptor that cannot be read at all counts, since Detcore cannot
1840/// establish what it is.
1841pub(crate) fn may_write_process_memory(flags: OFlag, descriptor: Option<(&Path, bool)>) -> bool {
1842    let writes = !flags.contains(OFlag::O_PATH) && flags & OFlag::O_ACCMODE != OFlag::O_RDONLY;
1843    writes
1844        && descriptor.is_none_or(|(link, on_procfs)| {
1845            on_procfs && link.file_name() == Some(std::ffi::OsStr::new("mem"))
1846        })
1847}
1848
1849/// [`Detcore::ledger_takes_fact`]'s rule: the backend's fact stands in for an
1850/// fdinfo only where the tool cannot read the guest's descriptors through the
1851/// kernel's procfs (`tool_can_name_guest_descriptors` false), and only where
1852/// Detcore renders procfs (`renders_procfs`), which it never does under
1853/// record and replay, whose recordings hold no fact.
1854fn ledger_takes_fact_where(tool_can_name_guest_descriptors: bool, renders_procfs: bool) -> bool {
1855    !tool_can_name_guest_descriptors && renders_procfs
1856}
1857
1858/// The status flags the guest's `F_GETFL` answers, from the kernel's
1859/// `physical` ones: `O_NONBLOCK` as the guest set it (`logical_nonblocking`),
1860/// which Detcore may have set physically for its own polling, and, for a
1861/// rendered procfs file's placeholder, which was reopened without the guest's
1862/// `O_NOFOLLOW` (`rendered_nofollow`), that flag, which Linux keeps. One rule
1863/// for `F_GETFL` and a rendered fdinfo's `flags:` line, so they cannot
1864/// disagree.
1865pub(crate) fn guest_status_flags(
1866    physical: i64,
1867    logical_nonblocking: bool,
1868    rendered_nofollow: bool,
1869) -> i64 {
1870    let physical = if rendered_nofollow {
1871        physical | i64::from(libc::O_NOFOLLOW)
1872    } else {
1873        physical
1874    };
1875    let nonblocking = i64::from(OFlag::O_NONBLOCK.bits());
1876    if logical_nonblocking {
1877        physical | nonblocking
1878    } else {
1879        physical & !nonblocking
1880    }
1881}
1882
1883/// The guest call after which [`Detcore::refresh_description_locks`] reads a
1884/// descriptor's locks.
1885#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1886pub(crate) enum LockCall {
1887    /// `F_OFD_SETLK` or `F_OFD_SETLKW`, and, where the ledger takes the
1888    /// backend's fact, `F_SETLK`, `F_SETLKW` or `F_SETLEASE`.
1889    Fcntl,
1890    /// `flock`.
1891    Flock,
1892    /// `pidfd_getfd`, which adds a slot.
1893    PidfdGetfd,
1894    /// `recvmsg` with `SCM_RIGHTS`, which adds a slot.
1895    Recvmsg,
1896    /// `recvmmsg` with `SCM_RIGHTS`, which adds slots.
1897    Recvmmsg,
1898    /// `ioctl(BINDER_WRITE_READ)` delivering a transaction that carries
1899    /// descriptors, which adds slots.
1900    BinderWriteRead,
1901    /// A descriptor received over `SCM_RIGHTS` that joined the description
1902    /// its sender's table modeled (`Detcore::record_received_facts`): it adds
1903    /// this table's slot to a description the ledger may know already, and
1904    /// brings no other identity, so it is not marked as received.
1905    Joined,
1906}
1907
1908impl LockCall {
1909    /// The call's name, as a refusal names it.
1910    fn name(self) -> &'static str {
1911        match self {
1912            Self::Fcntl => "fcntl",
1913            Self::Flock => "flock",
1914            Self::PidfdGetfd => "pidfd_getfd",
1915            Self::Recvmsg => "recvmsg",
1916            Self::Recvmmsg => "recvmmsg",
1917            Self::BinderWriteRead => "ioctl(BINDER_WRITE_READ)",
1918            Self::Joined => "an SCM_RIGHTS receipt",
1919        }
1920    }
1921
1922    /// Whether the call can take or release a lock.
1923    fn changes_locks(self) -> bool {
1924        matches!(self, Self::Fcntl | Self::Flock)
1925    }
1926}
1927
1928/// What a lock call's description held just before the call, where the
1929/// ledger takes the backend's fact (package K slice K8-h), and the class of
1930/// lock the call's request can take: one Linux records with the caller's
1931/// process as its taker (`flc_pid` is `current->tgid`: `flock_make_lock`,
1932/// `flock_to_posix_lock` and `lease_init`, fs/locks.c). An unlock takes
1933/// nothing, and an OFD lock's owner is its description, so neither has one.
1934#[derive(Clone, Debug, PartialEq, Eq)]
1935pub(crate) struct LockBaseline {
1936    /// `FLOCK`, `POSIX` or `LEASE`, as a lock line spells the class.
1937    pub(crate) class: &'static str,
1938    /// The description's locks before the call.
1939    pub(crate) held: Vec<crate::procfs::locks::HeldLock>,
1940}
1941
1942/// The locks of `after`, a description's locks after a call, that the call
1943/// proved it took ([`LockBaseline`]): those of the class its request can
1944/// take that the description did not hold before it. A lease the description
1945/// held already was changed in place (`lease_modify`), so none is taken then;
1946/// a flock of the same mode, or any lock it held before, is not the caller's.
1947/// Without a baseline (an unlock, an OFD request, or a fact the backend did
1948/// not give before the call) nothing is proved taken.
1949pub(crate) fn locks_taken(
1950    baseline: Option<&LockBaseline>,
1951    after: &[crate::procfs::locks::HeldLock],
1952) -> Vec<crate::procfs::locks::HeldLock> {
1953    let Some(baseline) = baseline else {
1954        return Vec::new();
1955    };
1956    if baseline.class == "LEASE" && baseline.held.iter().any(|lock| lock.class == "LEASE") {
1957        return Vec::new();
1958    }
1959    after
1960        .iter()
1961        .filter(|lock| lock.class == baseline.class && !baseline.held.contains(lock))
1962        .cloned()
1963        .collect()
1964}
1965
1966/// The lock class a `fcntl` record-lock or lease request (`l_type` or the
1967/// lease type) can take ([`LockBaseline`]), or `None` for an unlock.
1968fn requested_lock_class(lock_type: i32, class: &'static str) -> Option<&'static str> {
1969    (lock_type == libc::F_RDLCK || lock_type == libc::F_WRLCK).then_some(class)
1970}
1971
1972/// The files, as a lock line spells its object (`MAJOR:MINOR:INODE`), on which
1973/// the backend holds a guest's record lock as an OFD lock, from `locks`, the
1974/// description's locks just after a record-lock request through it succeeded
1975/// (`Detcore::note_record_lock_held_as_ofd`): none where the description holds
1976/// a `POSIX` lock (the backend took the record lock as Linux does), else the
1977/// files of its OFD locks. A successful request leaves a lock over its range,
1978/// so a backend that holds record locks as OFD locks shows one, merged with
1979/// any OFD lock the description held over the range already.
1980fn record_lock_files_held_as_ofd(locks: &[crate::procfs::locks::HeldLock]) -> Vec<String> {
1981    if locks.iter().any(|lock| lock.class == "POSIX") {
1982        return Vec::new();
1983    }
1984    let mut files = locks
1985        .iter()
1986        .filter(|lock| lock.class == "OFDLCK")
1987        .map(|lock| lock.object.clone())
1988        .collect::<Vec<_>>();
1989    files.sort();
1990    files.dedup();
1991    files
1992}
1993
1994#[cfg(test)]
1995mod lock_baseline_tests {
1996    use super::*;
1997    use crate::procfs::locks::HeldLock;
1998
1999    fn held(class: &str, access: &str, inode: u64) -> HeldLock {
2000        HeldLock {
2001            class: class.to_owned(),
2002            mode: if class == "LEASE" {
2003                "ACTIVE"
2004            } else {
2005                "ADVISORY"
2006            }
2007            .to_owned(),
2008            access: access.to_owned(),
2009            object: format!("00:18:{inode}"),
2010            start: "0".to_owned(),
2011            end: "EOF".to_owned(),
2012        }
2013    }
2014
2015    fn baseline(class: &'static str, held: Vec<HeldLock>) -> LockBaseline {
2016        LockBaseline { class, held }
2017    }
2018
2019    /// A call proves it took only a lock of the class its request takes that
2020    /// the description did not hold just before it: never a lock inherited or
2021    /// received already held (a host's flock on an inherited standard
2022    /// input), whatever the call; nothing without a baseline (an unlock, an
2023    /// OFD request, no fact before the call); a same-mode flock again takes
2024    /// nothing, a conversion takes the new mode; a lease changed in place
2025    /// takes nothing, a new lease is taken.
2026    #[test]
2027    fn a_call_takes_only_the_new_locks_of_its_class() {
2028        let inherited = held("FLOCK", "WRITE", 7);
2029        // An unlock, or an OFD request: no baseline, nothing taken, even on
2030        // the description's first observation.
2031        assert!(locks_taken(None, std::slice::from_ref(&inherited)).is_empty());
2032        // A record lock beside an inherited flock: only the record lock.
2033        let record = held("POSIX", "WRITE", 7);
2034        assert_eq!(
2035            locks_taken(
2036                Some(&baseline("POSIX", vec![inherited.clone()])),
2037                &[inherited.clone(), record.clone()]
2038            ),
2039            [record]
2040        );
2041        // The inherited flock asked for again in its own mode: nothing.
2042        assert!(
2043            locks_taken(
2044                Some(&baseline("FLOCK", vec![inherited.clone()])),
2045                std::slice::from_ref(&inherited)
2046            )
2047            .is_empty()
2048        );
2049        // Converted to the other mode: the new lock is the caller's.
2050        let shared = held("FLOCK", "READ", 7);
2051        assert_eq!(
2052            locks_taken(
2053                Some(&baseline("FLOCK", vec![inherited.clone()])),
2054                std::slice::from_ref(&shared)
2055            ),
2056            [shared]
2057        );
2058        // A lease of another type through a description that has one: the
2059        // existing lease changed in place, nothing taken.
2060        let read_lease = held("LEASE", "READ", 7);
2061        let write_lease = held("LEASE", "WRITE", 7);
2062        assert!(
2063            locks_taken(
2064                Some(&baseline("LEASE", vec![read_lease.clone()])),
2065                std::slice::from_ref(&write_lease)
2066            )
2067            .is_empty()
2068        );
2069        // A first lease: taken.
2070        assert_eq!(
2071            locks_taken(
2072                Some(&baseline("LEASE", Vec::new())),
2073                std::slice::from_ref(&read_lease)
2074            ),
2075            [read_lease]
2076        );
2077        // Only an F_RDLCK or F_WRLCK request takes a lock.
2078        assert_eq!(requested_lock_class(libc::F_WRLCK, "POSIX"), Some("POSIX"));
2079        assert_eq!(requested_lock_class(libc::F_RDLCK, "LEASE"), Some("LEASE"));
2080        assert_eq!(requested_lock_class(libc::F_UNLCK, "POSIX"), None);
2081    }
2082
2083    /// After a successful record-lock request, the description's OFD locks
2084    /// name the files on which the backend holds the record lock as an OFD
2085    /// lock, each once, beside any flock; a `POSIX` lock says the backend took
2086    /// it as Linux does, so none.
2087    #[test]
2088    fn a_record_lock_held_as_an_ofd_lock_names_its_file() {
2089        let flock = held("FLOCK", "READ", 7);
2090        let ofd = held("OFDLCK", "WRITE", 7);
2091        let mut second = held("OFDLCK", "READ", 7);
2092        second.start = "100".to_owned();
2093        assert_eq!(
2094            record_lock_files_held_as_ofd(&[flock.clone(), ofd.clone(), second]),
2095            ["00:18:7"]
2096        );
2097        assert!(record_lock_files_held_as_ofd(std::slice::from_ref(&flock)).is_empty());
2098        assert!(record_lock_files_held_as_ofd(&[held("POSIX", "WRITE", 7), ofd]).is_empty());
2099    }
2100}
2101
2102/// The inode number an fdinfo names (its `ino:` line), as `/proc/locks`
2103/// prints the last field of a lock's object.
2104fn fdinfo_inode(fdinfo: &[u8]) -> Option<String> {
2105    String::from_utf8_lossy(fdinfo)
2106        .lines()
2107        .find_map(|line| line.strip_prefix("ino:"))
2108        .map(|inode| inode.trim().to_string())
2109        .filter(|inode| !inode.is_empty() && inode.bytes().all(|byte| byte.is_ascii_digit()))
2110}
2111
2112/// The fdinfo page [`Detcore::read_own_fdinfo`] stages its calls in, and its
2113/// layout: the path, `openat2`'s `struct open_how`, the `fstatfs`/`fstat`
2114/// buffer, then the read buffer.
2115const FDINFO_PAGE: usize = 4096;
2116const FDINFO_PATH: usize = 0;
2117const FDINFO_HOW: usize = 64;
2118const FDINFO_HOW_SIZE: usize = 24;
2119const FDINFO_STAT: usize = 128;
2120const FDINFO_READ: usize = 512;
2121const _: () = assert!(FDINFO_HOW + FDINFO_HOW_SIZE <= FDINFO_STAT);
2122const _: () = assert!(FDINFO_STAT + std::mem::size_of::<libc::stat>() <= FDINFO_READ);
2123const _: () = assert!(FDINFO_STAT + std::mem::size_of::<libc::statfs>() <= FDINFO_READ);
2124/// `openat2`'s resolution for the fdinfo entry: no step out of the anchor
2125/// and no mount crossed.
2126const FDINFO_RESOLVE: u64 = libc::RESOLVE_NO_XDEV | libc::RESOLVE_BENEATH;
2127
2128/// What [`Detcore::read_own_fdinfo`] found.
2129pub(crate) enum FdinfoRead {
2130    /// The fdinfo's text, from an entry proved to be on the guest's procfs.
2131    Contents(Vec<u8>),
2132    /// A step failed for a reason that says nothing about the description's
2133    /// locks: they are unknown (`procfs::locks::LockLedger::note_unknown`).
2134    Unknown(String),
2135    /// The guest's `/proc` could not be proved to be procfs, or a step failed
2136    /// for a cause the guest's own view or the backend decides
2137    /// ([`fdinfo_failure_hides`]): the description contributes no lock, which
2138    /// can hide the guest's row but never admits a host's.
2139    Untrackable(String),
2140    /// Where the guest's procfs is not the kernel's, what the backend's
2141    /// `descriptor_status` fact reports (`Detcore::ledger_descriptor_status`).
2142    /// Boxed, so that the futures that hold an `FdinfoRead` stay their size.
2143    Facts(Box<FactLocks>),
2144}
2145
2146/// The locks the backend's `descriptor_status` fact reports a description
2147/// holds, every class, spelled as an fdinfo spells them
2148/// ([`held_locks_of_status`]), and the inode number of its file, if known.
2149pub(crate) struct FactLocks {
2150    locks: Vec<crate::procfs::locks::HeldLock>,
2151    inode: Option<String>,
2152}
2153
2154impl FdinfoRead {
2155    /// The inode number the fdinfo names, if it was read.
2156    fn inode(&self) -> Option<String> {
2157        match self {
2158            Self::Contents(contents) => fdinfo_inode(contents),
2159            Self::Facts(facts) => facts.inode.clone(),
2160            Self::Unknown(_) | Self::Untrackable(_) => None,
2161        }
2162    }
2163}
2164
2165/// The locks of a backend's `descriptor_status` fact, spelled as a
2166/// `/proc/locks` row and a fdinfo `lock:` line spell them
2167/// (`procfs::locks::HeldLock`): class, mode (`ADVISORY`, `MANDATORY`, or a
2168/// lease's `ACTIVE`), access, the object as `%02x:%02x:%llu` of the
2169/// superblock device and the inode, and the range. Every class: where the
2170/// guest's procfs is not the kernel's, the ledger is the only record of the
2171/// guest's leases and record locks too.
2172pub(crate) fn held_locks_of_status(
2173    status: &reverie::DescriptorStatus,
2174) -> Vec<crate::procfs::locks::HeldLock> {
2175    status
2176        .locks
2177        .iter()
2178        .map(|lock| {
2179            let (class, mode) = match lock.class {
2180                reverie::LockClass::Posix => ("POSIX", None),
2181                reverie::LockClass::OpenFileDescription => ("OFDLCK", None),
2182                reverie::LockClass::Flock => ("FLOCK", None),
2183                reverie::LockClass::Lease => ("LEASE", Some("ACTIVE")),
2184                _ => ("UNKNOWN", Some("UNKNOWN")),
2185            };
2186            let mode = mode.unwrap_or(if lock.mandatory {
2187                "MANDATORY"
2188            } else {
2189                "ADVISORY"
2190            });
2191            let record = matches!(
2192                lock.class,
2193                reverie::LockClass::Posix | reverie::LockClass::OpenFileDescription
2194            );
2195            crate::procfs::locks::HeldLock {
2196                class: class.to_owned(),
2197                mode: mode.to_owned(),
2198                access: if lock.write { "WRITE" } else { "READ" }.to_owned(),
2199                object: format!(
2200                    "{:02x}:{:02x}:{}",
2201                    libc::major(lock.raw_device),
2202                    libc::minor(lock.raw_device),
2203                    lock.raw_inode
2204                ),
2205                start: if record { lock.start } else { 0 }.to_string(),
2206                end: match lock.end {
2207                    Some(end) if record => end.to_string(),
2208                    _ => "EOF".to_owned(),
2209                },
2210            }
2211        })
2212        .collect()
2213}
2214
2215/// Why [`Detcore::read_staged_fdinfo`] did not read an fdinfo.
2216enum FdinfoFailure {
2217    /// A step (`open`, `fstatfs`, `fstat`, `read`) failed with an errno.
2218    Failed(&'static str, Errno),
2219    /// The guest's `/proc` was shown not to be procfs, for this reason.
2220    NotProcfs(&'static str),
2221}
2222
2223/// Whether an fdinfo step that failed with `errno` shows the description
2224/// is untrackable: a cause the guest's own filesystem view or the backend
2225/// decides, the same in every run of the same program (no `/proc` or an
2226/// entry missing, ENOENT or ENOTDIR; a symlink loop, ELOOP; an entry outside
2227/// the anchor or across a mount, EXDEV; a backend that refuses the call,
2228/// EINVAL, as KVM refuses an `O_PATH` open of its `/proc`). Every other
2229/// errno, ambient or resource state that can differ between runs, leaves the
2230/// locks unknown, so a later `/proc/locks` read refuses rather than shows a
2231/// row that depends on host state: a full descriptor table, EMFILE; a full
2232/// host file table, ENFILE; no memory, ENOMEM; an interruption, EINTR; an I/O
2233/// error, EIO; EAGAIN; a kernel without `openat2`, ENOSYS; a permission
2234/// refusal, EACCES or EPERM, which a host security module keyed by the host
2235/// process can give as well as the guest's own seccomp filter; anything
2236/// unlisted.
2237fn fdinfo_failure_hides(errno: Errno) -> bool {
2238    matches!(
2239        errno,
2240        Errno::ENOENT | Errno::ENOTDIR | Errno::ELOOP | Errno::EXDEV | Errno::EINVAL
2241    )
2242}
2243
2244/// Why Detcore does not know which locks the description descriptor `fd`
2245/// names holds after the guest's `call` through it: `step` (the open or a
2246/// read of its fdinfo) failed with `errno`. A failure says nothing about
2247/// which locks it holds (`Detcore::read_own_fdinfo`); a later `/proc/locks`
2248/// read refuses with this reason (`procfs::locks::LockLedger::unknown`).
2249fn fdinfo_refusal(call: &str, fd: RawFd, step: &str, errno: Errno) -> String {
2250    format!(
2251        "after {call} on fd {fd}, Detcore could not {step} that descriptor's fdinfo \
2252         ({errno}), so it does not know which locks the description holds"
2253    )
2254}
2255
2256/// What a status snapshot reports for the signal mask and D37's held masks
2257/// of the thread it describes (`status_published_sigblk`).
2258#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2259pub(crate) struct StatusMaskFacts {
2260    /// The `SigBlk` reported in place of the kernel's, or `None` to keep the
2261    /// kernel's.
2262    pub(crate) sigblk: Option<u64>,
2263    /// Under the in-guest delivery gate, while the thread holds a
2264    /// temporary-mask publication (A5 step 3c.1e A, IR-23(a)), the ignored
2265    /// signals it keeps pending from its masked call's entry in its private
2266    /// and shared queues (D37's held masks), which the status shows although
2267    /// the published temporary mask does not block them; zero otherwise.
2268    pub(crate) held_private: u64,
2269    pub(crate) held_shared: u64,
2270}
2271
2272/// The `SigBlk` a status snapshot reports for the thread it describes
2273/// (`mask_thread`, from `ProcfsFile::status_mask_thread`) in place of the
2274/// kernel's, or `None` to keep the kernel's: the guest mask that thread
2275/// published to the scheduler (`Scheduler::published_guest_sigmask`), where
2276/// guest masks are published at all (`publishes_guest_masks`): under
2277/// signal-target modelling, where the kernel's mask is Detcore's private one
2278/// while it holds the thread under it (Codex review of R2b-2 stage 2, P2;
2279/// <https://github.com/rrnewton/hermit/issues/4273>), and under the in-guest
2280/// delivery gate, where each request publishes the guest's mask and the
2281/// kernel's may be the in-guest runtime's (A5 step 3c.1b). With nothing
2282/// published for the thread the scheduler answers `None`, and the kernel's
2283/// `SigBlk` is reported, as before.
2284///
2285/// Under the gate (A5 step 3c.1e A) the answer is tri-state (IR-18) and
2286/// carries D37's held masks of a temporary-mask publication in force
2287/// (`Scheduler::status_guest_mask`). A mask the scheduler holds as unknown is
2288/// shown neither as the call's earlier mask nor as the kernel's `SigBlk`
2289/// (the in-guest runtime's mask once it has one): the read is refused by name
2290/// through the run-wide stopping refusal HERMIT_GUEST_MASK_UNKNOWN (D49),
2291/// never an errno to the reader; were the run to go on, Hermit's own failure
2292/// (`Error::Tool`) ends the read. `reader` is the reading thread.
2293pub(crate) async fn status_published_sigblk<G, T>(
2294    guest: &mut G,
2295    mask_thread: Option<i32>,
2296) -> Result<StatusMaskFacts, Error>
2297where
2298    G: Guest<Detcore<T>>,
2299    T: RecordOrReplay,
2300{
2301    use crate::scheduler::guest_mask_publication::PublishedGuestMask;
2302    let Some(thread) = mask_thread else {
2303        return Ok(StatusMaskFacts::default());
2304    };
2305    if !crate::syscalls::helpers::publishes_guest_masks(guest.config()) {
2306        return Ok(StatusMaskFacts::default());
2307    }
2308    if !guest.config().in_guest_signal_delivery {
2309        return Ok(StatusMaskFacts {
2310            sigblk: crate::tool_global::published_guest_sigmask(guest, DetTid::from_raw(thread))
2311                .await,
2312            ..Default::default()
2313        });
2314    }
2315    let status = crate::tool_global::status_guest_mask(guest, DetTid::from_raw(thread)).await;
2316    let sigblk = match status.mask {
2317        Some(PublishedGuestMask::Known(mask)) => Some(mask),
2318        None => None,
2319        Some(PublishedGuestMask::Unknown) => {
2320            let reason = crate::refused_signal_controls::guest_mask_unknown_refusal(
2321                guest.thread_state().dettid.as_raw(),
2322                thread,
2323            );
2324            tracing::error!("{}", reason);
2325            crate::tool_global::stopping_refusal(guest, reason.clone()).await;
2326            return Err(Error::Tool(anyhow::anyhow!(reason)));
2327        }
2328    };
2329    Ok(StatusMaskFacts {
2330        sigblk,
2331        held_private: status.held_private,
2332        held_shared: status.held_shared,
2333    })
2334}
2335
2336#[cfg(test)]
2337mod fdinfo_refusal_tests {
2338    use super::*;
2339
2340    /// A guest at its descriptor limit gets EMFILE from Detcore's fdinfo
2341    /// open after a lock call that needed no new descriptor; a later
2342    /// `/proc/locks` read stops the run with that named, naming the call,
2343    /// rather than taking the description to hold nothing. hermit-cli's
2344    /// `a_full_descriptor_table_refuses_the_next_proc_locks_read` runs that
2345    /// end to end; this pins the reason's wording.
2346    #[test]
2347    fn a_failed_fdinfo_read_is_a_named_refusal_not_an_empty_one() {
2348        let reason = fdinfo_refusal("recvmsg", 7, "open", Errno::EMFILE);
2349        assert!(reason.contains("after recvmsg on fd 7"), "{reason}");
2350        assert!(
2351            reason.contains("could not open that descriptor's fdinfo"),
2352            "{reason}"
2353        );
2354        assert!(reason.contains("EMFILE"), "{reason}");
2355        assert!(fdinfo_refusal("flock", 7, "read", Errno::EIO).contains("could not read that"));
2356    }
2357
2358    /// A failure the guest's filesystem view or the backend decides hides
2359    /// the description; ambient or resource state, and anything unlisted,
2360    /// leaves its locks unknown (and a later `/proc/locks` read refused).
2361    #[test]
2362    fn only_a_guest_or_backend_decided_failure_hides_the_description() {
2363        for errno in [
2364            Errno::ENOENT,
2365            Errno::ENOTDIR,
2366            Errno::ELOOP,
2367            Errno::EXDEV,
2368            Errno::EINVAL,
2369        ] {
2370            assert!(fdinfo_failure_hides(errno), "{errno}");
2371        }
2372        for errno in [
2373            Errno::EACCES,
2374            Errno::EPERM,
2375            Errno::EMFILE,
2376            Errno::ENFILE,
2377            Errno::ENOMEM,
2378            Errno::EINTR,
2379            Errno::EIO,
2380            Errno::EAGAIN,
2381            Errno::ENOSYS,
2382            Errno::EBADF,
2383        ] {
2384            assert!(!fdinfo_failure_hides(errno), "{errno}");
2385        }
2386    }
2387
2388    /// The lock ledger takes the backend's fact only where no kernel fdinfo
2389    /// of the guest's descriptors exists and Detcore renders procfs: never
2390    /// under record and replay (`renders_procfs` is false there), where the
2391    /// fact would decide the ledger from the replaying host's state, and
2392    /// never where the kernel's procfs is the guest's (ptrace and the
2393    /// in-guest backends keep their recorded fdinfo reads).
2394    #[test]
2395    fn the_ledger_takes_the_fact_only_where_procfs_is_rendered_and_not_the_kernels() {
2396        assert!(ledger_takes_fact_where(false, true));
2397        // KVM under record and replay, or after a chroot.
2398        assert!(!ledger_takes_fact_where(false, false));
2399        // ptrace, DBT, SaBRe, LiteInst, in-guest, rendering or not.
2400        assert!(!ledger_takes_fact_where(true, true));
2401        assert!(!ledger_takes_fact_where(true, false));
2402    }
2403
2404    /// The locks of a `descriptor_status` fact are spelled as the fdinfo
2405    /// text the ledger reads elsewhere spells them: for the OFD and flock
2406    /// locks the text path keeps (`held_locks_in_fdinfo`), field for field
2407    /// the same; the record locks and leases it leaves out are kept too,
2408    /// with a lease's `ACTIVE` state as its mode and a record lock's range.
2409    #[test]
2410    fn a_facts_locks_are_spelled_as_an_fdinfo_spells_them() {
2411        let fdinfo = b"pos:\t0\nflags:\t0100002\nmnt_id:\t31\nino:\t1234\n\
2412            lock:\t1: FLOCK  ADVISORY  READ 7 00:2a:1234 0 EOF\n\
2413            lock:\t2: POSIX  ADVISORY  WRITE 7 00:2a:1234 0 9\n\
2414            lock:\t3: OFDLCK ADVISORY  READ -1 00:2a:1234 100 EOF\n\
2415            lock:\t4: LEASE  ACTIVE    WRITE 7 103:fffff:1234 0 EOF\n";
2416        let status = reverie::DescriptorStatus::from_fdinfo(fdinfo).expect("Linux's format");
2417        let from_fact = held_locks_of_status(&status);
2418        let lock = |class: &str, mode: &str, access: &str, object: &str, start: &str, end: &str| {
2419            crate::procfs::locks::HeldLock {
2420                class: class.to_owned(),
2421                mode: mode.to_owned(),
2422                access: access.to_owned(),
2423                object: object.to_owned(),
2424                start: start.to_owned(),
2425                end: end.to_owned(),
2426            }
2427        };
2428        assert_eq!(
2429            from_fact,
2430            [
2431                lock("FLOCK", "ADVISORY", "READ", "00:2a:1234", "0", "EOF"),
2432                lock("POSIX", "ADVISORY", "WRITE", "00:2a:1234", "0", "9"),
2433                lock("OFDLCK", "ADVISORY", "READ", "00:2a:1234", "100", "EOF"),
2434                lock("LEASE", "ACTIVE", "WRITE", "103:fffff:1234", "0", "EOF"),
2435            ]
2436        );
2437        let from_text = crate::procfs::locks::held_locks_in_fdinfo(fdinfo);
2438        assert_eq!(
2439            from_text,
2440            [from_fact[0].clone(), from_fact[2].clone()],
2441            "the classes the text path keeps"
2442        );
2443    }
2444
2445    /// `F_GETFL`'s answer, which a rendered fdinfo's `flags:` line shares:
2446    /// the guest's own `O_NONBLOCK` whatever Detcore set physically, and a
2447    /// rendered procfs file's `O_NOFOLLOW`, which its placeholder lost.
2448    #[test]
2449    fn the_guests_status_flags_follow_its_own_nonblocking_and_nofollow() {
2450        let nonblock = i64::from(libc::O_NONBLOCK);
2451        let rdwr = i64::from(libc::O_RDWR);
2452        assert_eq!(guest_status_flags(rdwr | nonblock, false, false), rdwr);
2453        assert_eq!(guest_status_flags(rdwr, true, false), rdwr | nonblock);
2454        assert_eq!(
2455            guest_status_flags(0, false, true),
2456            i64::from(libc::O_NOFOLLOW)
2457        );
2458    }
2459
2460    #[test]
2461    fn an_fdinfo_names_its_inode_as_proc_locks_prints_it() {
2462        let fdinfo = b"pos:\t0\nflags:\t0100002\nmnt_id:\t31\nino:\t1234\n\
2463            lock:\t1: OFDLCK ADVISORY READ -1 00:18:1234 0 0\n";
2464        assert_eq!(fdinfo_inode(fdinfo).as_deref(), Some("1234"));
2465        assert_eq!(fdinfo_inode(b"pos:\t0\n"), None);
2466        assert_eq!(fdinfo_inode(b"ino:\tx\n"), None);
2467    }
2468}
2469
2470/// Puts back the guest bytes under a `Detcore::probe_socket_option` buffer.
2471fn restore_socket_option_probe<M: MemoryAccess>(memory: &mut M, buffer: AddrMut<u8>, saved: &[u8]) {
2472    if memory.write_exact(buffer, saved).is_err() {
2473        info!("Could not restore the guest bytes under a socket option probe.");
2474    }
2475}
2476
2477/// Writes back the guest bytes that a pre-call lookup buffer (utimensat's, or
2478/// the removal lookup's) covered.
2479fn restore_lookup_buffer<M: MemoryAccess>(memory: &mut M, buffer: StatPtr, saved: &[u8]) {
2480    if memory.write_exact(buffer.0.cast(), saved).is_err() {
2481        info!("Could not restore the guest bytes under a pre-call lookup buffer.");
2482    }
2483}
2484
2485/// Whether a lookup `buffer` overlaps `path` through its NUL, which a guest
2486/// call still has to read. A path that cannot be read counts as overlapping,
2487/// so that the kernel, not a lookup, reports the fault.
2488fn path_overlaps_buffer<M: MemoryAccess>(
2489    memory: &M,
2490    path: Option<syscalls::PathPtr<'_>>,
2491    buffer: StatPtr,
2492) -> bool {
2493    path_overlaps_bytes(
2494        memory,
2495        path,
2496        buffer.0.as_raw(),
2497        std::mem::size_of::<libc::stat>(),
2498    )
2499}
2500
2501/// Whether the `len` bytes at `start` overlap `path` through its NUL. A path
2502/// that cannot be read counts as overlapping.
2503fn path_overlaps_bytes<M: MemoryAccess>(
2504    memory: &M,
2505    path: Option<syscalls::PathPtr<'_>>,
2506    start: usize,
2507    len: usize,
2508) -> bool {
2509    use reverie::syscalls::FromToRaw;
2510
2511    let end = start.saturating_add(len);
2512    path.is_some_and(|ptr| match ptr.read(memory) {
2513        Ok(read) => {
2514            let addr = Some(ptr).into_raw();
2515            let read: PathBuf = read;
2516            addr < end && start < addr.saturating_add(read.as_os_str().len() + 1)
2517        }
2518        Err(_) => true,
2519    })
2520}
2521
2522/// Whether the utimensat lookup buffer overlaps the guest memory Linux reads
2523/// for `call`: the path through its NUL and, with `guest_times`, the two
2524/// timespecs. A path that cannot be read counts as overlapping, so that the
2525/// kernel, not a lookup, reports the fault.
2526fn utimensat_input_overlaps<M: MemoryAccess>(
2527    memory: &M,
2528    call: &syscalls::Utimensat,
2529    guest_times: bool,
2530    buffer: StatPtr,
2531) -> bool {
2532    let start = buffer.0.as_raw();
2533    let end = start + std::mem::size_of::<libc::stat>();
2534    let overlaps = |addr: usize, len: usize| addr < end && start < addr.saturating_add(len);
2535    let path = path_overlaps_buffer(memory, call.path(), buffer);
2536    let times = guest_times
2537        && call
2538            .times()
2539            .is_some_and(|times| overlaps(times.as_raw(), std::mem::size_of::<[Timespec; 2]>()));
2540    path || times
2541}
2542
2543/// Whether a `*at` stat call names no path: it passes `AT_EMPTY_PATH` with an
2544/// empty or NULL path (Linux 6.11 accepts NULL), and so describes `dirfd`
2545/// itself, or the working directory for `AT_FDCWD`. A path that cannot be
2546/// read counts as nonempty, so the call keeps the path-based numbering and
2547/// the kernel reports the fault. The path is read only after the flag check,
2548/// so an ordinary `stat()` reads no guest memory here.
2549fn names_no_path<M: MemoryAccess>(memory: &M, path: Option<PathPtr>, flags: AtFlags) -> bool {
2550    flags.contains(AtFlags::AT_EMPTY_PATH)
2551        && match path {
2552            None => true,
2553            Some(path) => path
2554                .read(memory)
2555                .is_ok_and(|path| path.as_os_str().is_empty()),
2556        }
2557}
2558
2559/// The fixed inode that `fd` reports when it is still the container's
2560/// inherited stdin, stdout or stderr, as fdinfo and `/proc/self/fd/N` report
2561/// it. Every stat call that describes the descriptor itself must use it, or
2562/// the guest's libc decides which number `fstat()` sees.
2563fn stdio_inode_override<T: RecordOrReplay, G: Guest<Detcore<T>>>(
2564    guest: &G,
2565    fd: i32,
2566) -> Option<DetInode> {
2567    guest
2568        .thread_state()
2569        .with_detfd(fd, |detfd| {
2570            deterministic_stdio_inode_for_resource(fd, detfd.resource())
2571        })
2572        .ok()
2573        .flatten()
2574}
2575
2576/// The file a stat call describes, as Detcore finds it again to learn the
2577/// canonical path of a procfs file (see [`procfs_inode`]).
2578#[derive(Clone, Copy)]
2579enum StatTarget<'a> {
2580    /// The file open as this descriptor.
2581    Descriptor(RawFd),
2582    /// The calling thread's working directory.
2583    Cwd,
2584    /// The file `path` names relative to `dirfd`, or the symbolic link itself
2585    /// when `follow` is false.
2586    Path {
2587        dirfd: i32,
2588        path: Option<PathPtr<'a>>,
2589        follow: bool,
2590    },
2591    /// A file named by a path that the call's result overwrote, which Detcore
2592    /// cannot look up again.
2593    Overwritten,
2594}
2595
2596impl<'a> StatTarget<'a> {
2597    /// The target of an `fstatat` or `statx` call with these operands.
2598    fn at<M: MemoryAccess>(
2599        memory: &M,
2600        dirfd: i32,
2601        path: Option<PathPtr<'a>>,
2602        flags: AtFlags,
2603    ) -> Self {
2604        if names_no_path(memory, path, flags) {
2605            if dirfd == libc::AT_FDCWD {
2606                return StatTarget::Cwd;
2607            }
2608            if dirfd >= 0 {
2609                return StatTarget::Descriptor(dirfd);
2610            }
2611        }
2612        StatTarget::Path {
2613            dirfd,
2614            path,
2615            follow: !flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW),
2616        }
2617    }
2618
2619    /// This target, or [`StatTarget::Overwritten`] if it is a path that the
2620    /// `len` bytes of the call's result at `result` overlap. Linux reads the
2621    /// path before it writes the result, so the call succeeds, but the path
2622    /// the guest named is gone once it returns. Found before the call.
2623    fn unless_overwritten<M: MemoryAccess>(self, memory: &M, result: usize, len: usize) -> Self {
2624        match self {
2625            StatTarget::Path { path, .. } if path_overlaps_bytes(memory, path, result, len) => {
2626                StatTarget::Overwritten
2627            }
2628            target => target,
2629        }
2630    }
2631
2632    /// The descriptor the call describes itself, if it names no path.
2633    fn descriptor(&self) -> Option<RawFd> {
2634        match self {
2635            StatTarget::Descriptor(fd) => Some(*fd),
2636            StatTarget::Cwd | StatTarget::Path { .. } | StatTarget::Overwritten => None,
2637        }
2638    }
2639}
2640
2641/// The rendered procfs file that a stat result describes, if it is one: by
2642/// the name Detcore gave it, or by its kernel identity where Detcore names no
2643/// procfs files. Never a per-process file, whose kernel-reached instances
2644/// keep the kernel's identity. `files` are the files the session renders
2645/// (`Detcore::rendered_files`); `recordreplay_modes` is the run's
2646/// `Config::recordreplay_modes`, under which Detcore renders none.
2647fn rendered_entry(
2648    procfs: &ProcfsEntry,
2649    stat: &DetStat,
2650    recordreplay_modes: bool,
2651    files: crate::procfs::RenderedFiles,
2652) -> Option<&'static Path> {
2653    if recordreplay_modes {
2654        return None;
2655    }
2656    let found = match procfs {
2657        ProcfsEntry::At(within) => rendered_procfs::rendered_path_within(within, files),
2658        ProcfsEntry::NotProcfs | ProcfsEntry::Unknown => {
2659            rendered_procfs::rendered_path_of_raw(stat, files)
2660        }
2661        ProcfsEntry::Unnamed => None,
2662    };
2663    // A per-process file the kernel reached (through `..`, after `chroot`,
2664    // in a nested PID namespace) is served from the kernel's bytes
2665    // (`ProcfsFile::served_rendered_path`), so it keeps the kernel's metadata
2666    // too: its identity is the kernel's file's, not the rendered file's.
2667    found.filter(|path| !crate::procfs::rendered_path_is_per_process(path))
2668}
2669
2670/// `STATX_MNT_ID_UNIQUE` (include/uapi/linux/stat.h, since Linux 6.8). Reverie's
2671/// `StatxMask` does not name it and drops it from a mask read through it, so
2672/// Detcore reads a call's raw mask ([`statx_requested_mask`]).
2673const STATX_MNT_ID_UNIQUE: u32 = 0x4000;
2674
2675/// What Linux adds to every unique mount ID so that none equals a classic
2676/// 32-bit one: `MNT_UNIQUE_ID_OFFSET` (v6.12 fs/namespace.c:73). Linux counts
2677/// up from it and never reuses an ID, so the first mount's is the offset plus
2678/// one (fs/namespace.c:74 and 244).
2679const MNT_UNIQUE_ID_OFFSET: u64 = 1 << 31;
2680
2681/// The mask a `statx` call asks for, every bit kept.
2682fn statx_requested_mask(call: syscalls::Statx) -> u32 {
2683    syscalls::SyscallArgs::from(call).arg3 as u32
2684}
2685
2686/// `call` asking the kernel for the classic mount ID (`STATX_MNT_ID`) where
2687/// it asked for the unique one, and otherwise the same call.
2688fn statx_asking_classic_mount_id(call: syscalls::Statx) -> syscalls::Statx {
2689    let mut args = syscalls::SyscallArgs::from(call);
2690    let mask = args.arg3 as u32;
2691    if mask & STATX_MNT_ID_UNIQUE != 0 {
2692        args.arg3 =
2693            ((mask & !STATX_MNT_ID_UNIQUE) | reverie::syscalls::linux_abi::STATX_MNT_ID) as usize;
2694    }
2695    syscalls::Statx::from(args)
2696}
2697
2698/// The `stx_mask` bits of the fields a recording keeps: every field up to
2699/// `stx_mnt_id` (`STATX_BASIC_STATS`, `STATX_BTIME`, `STATX_MNT_ID`) and the
2700/// unique mount ID, which is also `stx_mnt_id`.
2701const STATX_MODELED_MASK: u32 = 0x1fff | STATX_MNT_ID_UNIQUE;
2702
2703/// Where a `struct statx` stops holding fields Detcore models: the direct-I/O
2704/// alignments (0x98), `stx_subvol` (0xa0), the atomic-write limits and the
2705/// spare words up to its end (0x100) follow `stx_mnt_id`.
2706const STATX_UNMODELED_TAIL: usize = 0x98;
2707
2708/// Make the `statx` result at `addr` report only the fields Detcore models:
2709/// clear the other `stx_mask` bits and zero the rest of the structure, as a
2710/// kernel without those fields would. A recording keeps only the modeled
2711/// fields (Reverie's `StatxBuf` skips the rest), so without this a recorded
2712/// run saw the host's `stx_subvol` (a btrfs subvolume ID) and its replay saw
2713/// zero; and outside recording Detcore already writes back only the modeled
2714/// fields (`determinize_stat`). Applied after the call in recording and in
2715/// replay alike, so both show the same bytes.
2716fn canonicalize_statx_tail<M: MemoryAccess>(memory: &mut M, addr: usize) -> Result<(), Errno> {
2717    const SIZE: usize = std::mem::size_of::<reverie::syscalls::linux_abi::Statx>();
2718    let start = AddrMut::<u8>::from_raw(addr).ok_or(Errno::EFAULT)?;
2719    let mut bytes = [0u8; SIZE];
2720    memory.read_exact(start, &mut bytes)?;
2721    let mut canonical = bytes;
2722    let mask = u32::from_ne_bytes(canonical[..4].try_into().unwrap()) & STATX_MODELED_MASK;
2723    canonical[..4].copy_from_slice(&mask.to_ne_bytes());
2724    canonical[STATX_UNMODELED_TAIL..].fill(0);
2725    if canonical != bytes {
2726        memory.write_exact(start, &canonical)?;
2727    }
2728    Ok(())
2729}
2730
2731#[cfg(test)]
2732mod statx_tail_tests {
2733    use reverie::syscalls::LocalMemory;
2734
2735    use super::*;
2736
2737    /// What a recording keeps of a `statx` result is what the guest sees: the
2738    /// modeled fields and mask bits stay, `stx_subvol` and the rest of the
2739    /// tail become zero and their mask bits are cleared.
2740    #[test]
2741    fn a_statx_result_keeps_only_the_fields_a_recording_keeps() {
2742        let mut buf = [0xa5u8; 256];
2743        // STATX_BASIC_STATS | STATX_BTIME | STATX_MNT_ID | STATX_DIOALIGN |
2744        // STATX_MNT_ID_UNIQUE | STATX_SUBVOL | STATX_WRITE_ATOMIC.
2745        let mask: u32 = 0x7ff | 0x800 | 0x1000 | 0x2000 | 0x4000 | 0x8000 | 0x10000;
2746        buf[..4].copy_from_slice(&mask.to_ne_bytes());
2747        let modeled = buf[4..STATX_UNMODELED_TAIL].to_vec();
2748        let mut memory = LocalMemory::new();
2749        canonicalize_statx_tail(&mut memory, buf.as_mut_ptr() as usize).unwrap();
2750        assert_eq!(
2751            u32::from_ne_bytes(buf[..4].try_into().unwrap()),
2752            0x7ff | 0x800 | 0x1000 | 0x4000
2753        );
2754        assert_eq!(buf[4..STATX_UNMODELED_TAIL], modeled[..]);
2755        assert!(buf[STATX_UNMODELED_TAIL..].iter().all(|byte| *byte == 0));
2756        // Idempotent: replay applies it to what recording already produced.
2757        let once = buf;
2758        canonicalize_statx_tail(&mut memory, buf.as_mut_ptr() as usize).unwrap();
2759        assert_eq!(buf, once);
2760    }
2761}
2762
2763/// Whether `statx` accepts these `flags` and `mask` (`vfs_statx`'s and the
2764/// syscall's own checks): no reserved mask bit, known flags only (unless the
2765/// call names no path), and not both synchronization types at once.
2766fn statx_arguments_valid(flags: i32, mask: u32, names_no_path: bool) -> bool {
2767    let known = libc::AT_SYMLINK_NOFOLLOW
2768        | libc::AT_EMPTY_PATH
2769        | libc::AT_NO_AUTOMOUNT
2770        | reverie::syscalls::linux_abi::AT_STATX_SYNC_TYPE;
2771    // A call on the descriptor itself (`AT_EMPTY_PATH` with an empty or
2772    // NULL path) does not look at the other flags (measured on Linux 7.1).
2773    mask & 0x8000_0000 == 0
2774        && (names_no_path || flags & !known == 0)
2775        && flags & reverie::syscalls::linux_abi::AT_STATX_SYNC_TYPE
2776            != reverie::syscalls::linux_abi::AT_STATX_SYNC_TYPE
2777}
2778
2779/// Whether `path` spells a descriptor link: `/dev/fd/N`, `/proc/*/fd/N` or
2780/// `/proc/*/task/*/fd/N`.
2781fn names_descriptor_link(path: &Path) -> bool {
2782    let parts = path
2783        .components()
2784        .filter_map(|component| match component {
2785            std::path::Component::Normal(part) => {
2786                Some(std::os::unix::ffi::OsStrExt::as_bytes(part))
2787            }
2788            _ => None,
2789        })
2790        .collect::<Vec<_>>();
2791    let number = |part: &[u8]| !part.is_empty() && part.iter().all(u8::is_ascii_digit);
2792    match parts.as_slice() {
2793        [b"dev", b"fd", fd] | [b"proc", _, b"fd", fd] | [b"proc", _, b"task", _, b"fd", fd] => {
2794            number(fd)
2795        }
2796        _ => false,
2797    }
2798}
2799
2800/// Who opens a path that may be one of its descriptor links, for
2801/// [`path_to_record_for_open`].
2802#[derive(Debug, Clone, Copy)]
2803struct DescriptorLinkOpener {
2804    /// The caller's pid and tid as the guest's `/proc/<pid>` and
2805    /// `.../task/<tid>` spellings name them on every backend, or `None`
2806    /// where Detcore does not know those names (a nested PID namespace).
2807    /// See [`Detcore::procfs_caller_ids`].
2808    own: Option<(i32, i32)>,
2809    /// Whether the descriptor table procfs reads for a process-level link
2810    /// (`/proc/self/fd/N`, `/dev/fd/N`), the thread-group leader's, is
2811    /// provably the caller's own: the caller leads its group, or holds the
2812    /// table the leader holds now (`ThreadState::process_files`). A thread
2813    /// cloned without `CLONE_FILES` holds a table of its own.
2814    holds_process_table: bool,
2815}
2816
2817/// The descriptor `N` that a descriptor link in an open's spelling names,
2818/// as it stood when the open looked the link up: in the open's own turn,
2819/// before the call, which may block and leave the turn (a FIFO waiting for
2820/// its other end) while a thread sharing the table replaces `N`. The kernel
2821/// follows the link at that lookup, so the file it opens is the one `N` was
2822/// then, whatever `N` is when the open completes.
2823#[derive(Debug, Clone, PartialEq, Eq)]
2824struct LinkSource {
2825    fd: RawFd,
2826    /// The path Detcore had recorded for `N`.
2827    path: Option<PathBuf>,
2828    /// `N`'s open file description.
2829    open_file: OpenFileId,
2830    /// Where `N`'s `/proc/self` or `/proc/thread-self` spelling led, and the
2831    /// identity of the task it led to (`DetFd::procfs_reached`).
2832    reached: Option<(PathBuf, Option<crate::procfs::TaskIncarnation>)>,
2833}
2834
2835impl LinkSource {
2836    /// The path to record, for [`path_to_record_for_open`], for descriptor
2837    /// `fd` of the caller's table, whose open file description is `current`
2838    /// when the open completes: `N`'s path as the lookup saw it, only if
2839    /// `fd` is `N` and `N` still holds that description. Where `N` was
2840    /// replaced meanwhile, nothing, and the open keeps its spelling, as
2841    /// before this change.
2842    fn path_if_unchanged(&self, fd: RawFd, current: Option<OpenFileId>) -> Option<PathBuf> {
2843        (fd == self.fd && current == Some(self.open_file))
2844            .then(|| self.path.clone())
2845            .flatten()
2846    }
2847
2848    /// Where `N`'s selector spelling led, for a new descriptor opened through
2849    /// the link to `N` (`fd`), only if `fd` is `N` and `N` still holds its
2850    /// description: the new open file is that task's directory too.
2851    fn reached_if_unchanged(
2852        &self,
2853        fd: RawFd,
2854        current: Option<OpenFileId>,
2855    ) -> Option<(PathBuf, Option<crate::procfs::TaskIncarnation>)> {
2856        (fd == self.fd && current == Some(self.open_file))
2857            .then(|| self.reached.clone())
2858            .flatten()
2859    }
2860}
2861
2862/// Where the spelling `recorded` led, if it names a task's directory: through
2863/// a `/proc` selector, `/proc/self`, `/proc/self/task`,
2864/// `/proc/self/task/<tid>` or `/proc/thread-self`, with the selector replaced
2865/// by the opener's numbers (`own`, its pid and tid as `/proc` spells them), as
2866/// Linux resolved it at the open; or by number, `/proc/<pid>`,
2867/// `/proc/<pid>/task` or `/proc/<pid>/task/<tid>`, itself
2868/// (`DetFd::procfs_reached`). Linux binds the directory to the task it named
2869/// at the open, not to the number, so either is that task while it lives.
2870/// Nothing for any other path, such as a descriptor link (`fd/N`), which
2871/// leads to another file, and nothing where Detcore does not know the
2872/// caller's numbers (`own` is `None` in a nested PID namespace).
2873fn procfs_selector_reached(recorded: &Path, own: Option<(i32, i32)>) -> Option<PathBuf> {
2874    let (pid, tid) = own?;
2875    let below = recorded.strip_prefix("/proc").ok()?;
2876    let names = below
2877        .components()
2878        .map(|component| match component {
2879            std::path::Component::Normal(name) => name.to_str(),
2880            _ => None,
2881        })
2882        .collect::<Option<Vec<_>>>()?;
2883    let number = |name: &str| {
2884        (!name.starts_with('0') && !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()))
2885            .then(|| name.parse::<i32>().ok())
2886            .flatten()
2887    };
2888    Some(PathBuf::from(match names.as_slice() {
2889        ["self"] => format!("/proc/{pid}"),
2890        ["self", "task"] => format!("/proc/{pid}/task"),
2891        ["self", "task", thread] => format!("/proc/{pid}/task/{}", number(thread)?),
2892        ["thread-self"] => format!("/proc/{pid}/task/{tid}"),
2893        [process] => format!("/proc/{}", number(process)?),
2894        [process, "task"] => format!("/proc/{}/task", number(process)?),
2895        [process, "task", thread] => {
2896            format!("/proc/{}/task/{}", number(process)?, number(thread)?)
2897        }
2898        _ => return None,
2899    }))
2900}
2901
2902/// The task whose directory `reached` (where a `/proc` selector led,
2903/// `procfs_selector_reached`) is or is below: `/proc/<pid>`, and the thread of
2904/// `/proc/<pid>/task/<tid>`.
2905fn procfs_reached_task(reached: &Path) -> Option<crate::procfs::BoundTask> {
2906    let names = reached
2907        .strip_prefix("/proc")
2908        .ok()?
2909        .components()
2910        .map(|component| component.as_os_str().to_str())
2911        .collect::<Option<Vec<_>>>()?;
2912    let number = |name: &str| name.parse::<i32>().ok().filter(|number| *number > 0);
2913    let process = number(names.first()?)?;
2914    let thread = match names.as_slice() {
2915        [_, "task", thread, ..] => Some(number(thread)?),
2916        _ => None,
2917    };
2918    Some(crate::procfs::BoundTask { process, thread })
2919}
2920
2921/// Whether `spelling` names a `/proc` selector link itself: `/proc/self` or
2922/// `/proc/thread-self`, which are symbolic links to a task's directory.
2923fn names_a_selector_link(spelling: &Path) -> bool {
2924    spelling.parent() == Some(Path::new("/proc"))
2925        && spelling
2926            .file_name()
2927            .is_some_and(|name| name == "self" || name == "thread-self")
2928}
2929
2930/// Whether an open of the absolute spelling `observed` with `flags` opens a
2931/// descriptor link itself rather than the file it reaches: `O_NOFOLLOW`
2932/// does not follow the last component (with `O_PATH` the link is opened,
2933/// without it the open fails with `ELOOP`), unless the spelling follows it
2934/// anyway with a trailing slash or `/.`.
2935fn opens_the_link_itself(observed: &Path, flags: OFlag) -> bool {
2936    let bytes = std::os::unix::ffi::OsStrExt::as_bytes(observed.as_os_str());
2937    flags.contains(OFlag::O_NOFOLLOW) && !(bytes.ends_with(b"/") || bytes.ends_with(b"/."))
2938}
2939
2940/// The path Detcore records for a descriptor the guest opened at the absolute
2941/// spelling `observed` with `flags`. Opened through one of the caller's own
2942/// descriptor links (`/proc/self/fd/N`, `/dev/fd/N`, ... see
2943/// [`crate::syscalls::namespace::own_descriptor_link`]), the new descriptor
2944/// is the file that descriptor `N` is, so it takes the path recorded for `N`
2945/// (by `recorded`): a name relative to the new descriptor is then looked up
2946/// below that file, as Linux looks it up, and not below the link's spelling,
2947/// which names no procfs file of its own.
2948///
2949/// It keeps the spelling, as every other open does, wherever the recorded
2950/// path could name another file than the one the kernel opened:
2951/// - the open does not follow the link (`O_NOFOLLOW` without a trailing
2952///   slash or `/.`), so it opened the link itself;
2953/// - the link names the process's table, which is the thread-group
2954///   leader's, and the caller is not proven to hold that table
2955///   (`opener.holds_process_table`); `recorded` reads the caller's;
2956/// - descriptor `N` has no recorded path, or a relative one;
2957/// - `N`'s recorded path is resolved per caller (`/proc/self`,
2958///   `/proc/thread-self`, `/dev/fd`, ... see
2959///   [`crate::syscalls::namespace::names_through_caller_selector`]): `N` is
2960///   the file its opener reached, which another process or thread that
2961///   inherited or shares it would not reach by that path.
2962///
2963/// Where the link names the caller's own table and `N` has no recorded path
2964/// at all, as for a descriptor received through `SCM_RIGHTS`, `identity` is
2965/// recorded instead if given: the procfs directory that the kernel names for
2966/// the opened descriptor (see `Detcore::procfs_directory_named_for`). Not
2967/// where `N`'s recorded path is refused above, nor where the link names a
2968/// table the caller is not proven to hold, whose `N` is not the one
2969/// `recorded` describes.
2970fn path_to_record_for_open(
2971    observed: &Path,
2972    flags: OFlag,
2973    opener: DescriptorLinkOpener,
2974    recorded: impl FnOnce(RawFd) -> Option<PathBuf>,
2975    identity: Option<PathBuf>,
2976) -> PathBuf {
2977    use crate::syscalls::namespace::DescriptorTable;
2978    if opens_the_link_itself(observed, flags) {
2979        return observed.to_path_buf();
2980    }
2981    let Some(link) = crate::syscalls::namespace::own_descriptor_link(observed, opener.own) else {
2982        return observed.to_path_buf();
2983    };
2984    if !(link.table == DescriptorTable::Thread || opener.holds_process_table) {
2985        return observed.to_path_buf();
2986    }
2987    match recorded(link.fd) {
2988        Some(path) => {
2989            (!crate::syscalls::namespace::names_through_caller_selector(&path)).then_some(path)
2990        }
2991        None => identity,
2992    }
2993    .unwrap_or_else(|| observed.to_path_buf())
2994}
2995
2996/// The components below `/proc` of the kernel's name for a procfs directory
2997/// (a descriptor link), and the positions among them of the task numbers it
2998/// names: `/proc/<pid>` and its `task/<tid>`. `None` for a name outside
2999/// `/proc`, a deleted or anonymous object, a relative name, or a task number
3000/// procfs would not have (a leading zero).
3001fn procfs_directory_task_positions(link: &str) -> Option<(Vec<String>, Vec<usize>)> {
3002    if link.ends_with(" (deleted)") {
3003        return None;
3004    }
3005    let below = link.strip_prefix("/proc")?;
3006    if !(below.is_empty() || below.starts_with('/')) {
3007        return None;
3008    }
3009    let names = below
3010        .split('/')
3011        .filter(|name| !name.is_empty())
3012        .map(str::to_owned)
3013        .collect::<Vec<_>>();
3014    if names.iter().any(|name| name == "." || name == "..") {
3015        return None;
3016    }
3017    let number = |name: &str| {
3018        !name.is_empty() && !name.starts_with('0') && name.bytes().all(|byte| byte.is_ascii_digit())
3019    };
3020    let mut positions = Vec::new();
3021    if let Some(first) = names.first()
3022        && first.bytes().all(|byte| byte.is_ascii_digit())
3023    {
3024        if !number(first) {
3025            return None;
3026        }
3027        positions.push(0);
3028        if names.get(1).map(String::as_str) == Some("task")
3029            && let Some(thread) = names.get(2)
3030        {
3031            if !number(thread) {
3032                return None;
3033            }
3034            positions.push(2);
3035        }
3036    }
3037    Some((names, positions))
3038}
3039
3040/// Whether `link`, the kernel's name for a descriptor (its
3041/// `/proc/thread-self/fd/N` link), names a procfs directory path Detcore
3042/// may record for it: `/proc`, or a path below it that names no process or
3043/// thread (`procfs::ids::named_ids`, and no numeric component at all),
3044/// whose numbers are the kernel's and need not be the guest's, nor a
3045/// selector procfs resolves per caller. Not a deleted directory
3046/// (`" (deleted)"`), an anonymous object or a relative target, nor any other
3047/// directory.
3048fn procfs_directory_link(link: &Path) -> bool {
3049    use std::os::unix::ffi::OsStrExt;
3050    let bytes = link.as_os_str().as_bytes();
3051    link.starts_with("/proc")
3052        && !bytes.ends_with(b" (deleted)")
3053        && link.components().all(|component| match component {
3054            std::path::Component::RootDir => true,
3055            std::path::Component::Normal(name) => !name.as_bytes().iter().all(u8::is_ascii_digit),
3056            _ => false,
3057        })
3058        && crate::procfs::ids::named_ids(link).is_empty()
3059        && !crate::syscalls::namespace::names_through_caller_selector(link)
3060}
3061
3062/// Whether a stat call's own arguments are ones Linux accepts before it looks
3063/// up any file: `newfstatat` refuses unknown flags (`vfs_fstatat`), except on
3064/// the descriptor itself (`AT_EMPTY_PATH` with an empty path), which it
3065/// stats without looking at them (measured on Linux 7.1).
3066fn stat_family_arguments_valid<M: MemoryAccess>(memory: &M, call: &StatFamily) -> bool {
3067    match call {
3068        StatFamily::Fstatat(call) => {
3069            let known = libc::AT_SYMLINK_NOFOLLOW
3070                | libc::AT_NO_AUTOMOUNT
3071                | libc::AT_EMPTY_PATH
3072                | reverie::syscalls::linux_abi::AT_STATX_SYNC_TYPE;
3073            names_no_path(memory, call.path(), call.flags()) || call.flags().bits() & !known == 0
3074        }
3075        _ => true,
3076    }
3077}
3078
3079/// The file a call of the stat family describes.
3080fn stat_family_target<'a, M: MemoryAccess>(memory: &M, call: &'a StatFamily) -> StatTarget<'a> {
3081    match call {
3082        StatFamily::Fstat(call) => StatTarget::Descriptor(call.fd()),
3083        StatFamily::Fstatat(call) => {
3084            StatTarget::at(memory, call.dirfd(), call.path(), call.flags())
3085        }
3086        #[cfg(not(target_arch = "aarch64"))]
3087        StatFamily::Stat(call) => StatTarget::Path {
3088            dirfd: libc::AT_FDCWD,
3089            path: call.path(),
3090            follow: true,
3091        },
3092        #[cfg(not(target_arch = "aarch64"))]
3093        StatFamily::Lstat(call) => StatTarget::Path {
3094            dirfd: libc::AT_FDCWD,
3095            path: call.path(),
3096            follow: false,
3097        },
3098    }
3099}
3100
3101/// How many times Detcore injects the `openat` or the `close` of the
3102/// `O_PATH` descriptor it opens in the guest to name a procfs entry when the
3103/// call had no effect, before it records a determinism loss instead.
3104const OBJECT_DESCRIPTOR_ATTEMPTS: usize = 8;
3105
3106/// What Detcore learned of a stat result that may describe a procfs file
3107/// (see [`procfs_inode`]).
3108#[derive(Debug, Clone, PartialEq, Eq)]
3109enum ProcfsEntry {
3110    /// Not a file on a procfs mount, or a mode in which Detcore does not name
3111    /// procfs files.
3112    NotProcfs,
3113    /// A procfs file at this path within procfs.
3114    At(PathBuf),
3115    /// A procfs file that Detcore could not name. It recorded a determinism
3116    /// loss.
3117    Unnamed,
3118    /// A file Detcore could not tell from a procfs file, as it could not read
3119    /// the guest's mount table or the file's filesystem type, or did not look,
3120    /// as the guest's threads are not sequentialized. It recorded a
3121    /// determinism loss.
3122    Unknown,
3123}
3124
3125impl ProcfsEntry {
3126    /// Whether the file is known to be on a procfs mount.
3127    fn is_procfs(&self) -> bool {
3128        matches!(self, ProcfsEntry::At(_) | ProcfsEntry::Unnamed)
3129    }
3130
3131    /// The name of the file, which is on `device`.
3132    fn name(&self, device: u64) -> Option<ProcfsName> {
3133        match self {
3134            ProcfsEntry::At(within) => Some(ProcfsName::new(device, within)),
3135            ProcfsEntry::NotProcfs | ProcfsEntry::Unnamed | ProcfsEntry::Unknown => None,
3136        }
3137    }
3138}
3139
3140impl<T: RecordOrReplay> Detcore<T> {
3141    async fn observe_timer_slack_identity<G: Guest<Self>>(
3142        &self,
3143        guest: &mut G,
3144        target: i32,
3145    ) -> Result<Option<(u64, u64)>, Error> {
3146        // Re-resolve the numeric proc path on every operation. Linux gives a
3147        // recycled TID a different proc inode, while the original open file
3148        // description remains bound to the exited task's inode.
3149        // The kernel's number for the guest's (DBT; `procfs_numbers`): one
3150        // `/proc` does not list has no file to bind.
3151        let Some(target) = Self::kernel_procfs_number(guest, target).await else {
3152            return Ok(None);
3153        };
3154        let path = format!("/proc/{target}/timerslack_ns");
3155        let path_bytes = path.as_bytes();
3156        let mut path_buffer = [0_u8; 64];
3157        assert!(path_bytes.len() < path_buffer.len());
3158        path_buffer[..path_bytes.len()].copy_from_slice(path_bytes);
3159
3160        let mut stack = guest.stack().await;
3161        let path_address = stack.push(path_buffer).cast::<libc::c_char>();
3162        let statptr = StatPtr(stack.reserve());
3163        let stack_guard = stack.commit()?;
3164        let call = syscalls::Fstatat::new()
3165            .with_dirfd(libc::AT_FDCWD)
3166            .with_path(PathPtr::from_ptr(
3167                path_address.as_raw() as *const libc::c_char
3168            ))
3169            .with_stat(Some(statptr))
3170            .with_flags(AtFlags::empty());
3171        let mut identity = match guest.inject_with_retry(call).await {
3172            Ok(_) => {
3173                let stat = statptr.read(&guest.memory())?;
3174                Some((stat.st_dev, stat.st_ino))
3175            }
3176            Err(Errno::ENOENT) | Err(Errno::ESRCH) => None,
3177            Err(error) => return Err(error.into()),
3178        };
3179        drop(stack_guard);
3180        // Replayer runs the guest in a filesystem chroot whose `/proc` path is
3181        // intentionally absent, while the tracing process remains in the same
3182        // PID namespace and can resolve the task through its own proc mount.
3183        // Use that equivalent view only for record/replay; other backends keep
3184        // the guest-path result above as their sole authority.
3185        if identity.is_none()
3186            && guest.config().recordreplay_modes
3187            && let Ok(metadata) = std::fs::metadata(path)
3188        {
3189            identity = Some((metadata.dev(), metadata.ino()));
3190        }
3191        Ok(identity)
3192    }
3193
3194    async fn require_current_timer_slack_target<G: Guest<Self>>(
3195        &self,
3196        guest: &mut G,
3197        binding: TimerSlackBinding,
3198    ) -> Result<(), Error> {
3199        let observed_identity = self
3200            .observe_timer_slack_identity(guest, binding.target)
3201            .await?;
3202        let current = guest.inject(syscalls::Gettid::new()).await? as i32;
3203        classify_timer_slack_binding(binding, observed_identity, current).map_err(Into::into)
3204    }
3205
3206    fn timer_slack_binding<G: Guest<Self>>(
3207        &self,
3208        guest: &G,
3209        fd: RawFd,
3210    ) -> Result<Option<TimerSlackBinding>, Errno> {
3211        guest.thread_state().with_detfd(fd, |detfd| {
3212            detfd
3213                .procfs_timer_slack_binding()
3214                .map(|(target, device, inode)| TimerSlackBinding {
3215                    target,
3216                    device,
3217                    inode,
3218                })
3219        })
3220    }
3221
3222    fn require_timer_slack_access<G: Guest<Self>>(
3223        &self,
3224        guest: &G,
3225        fd: RawFd,
3226        write: bool,
3227    ) -> Result<(), Errno> {
3228        guest.thread_state().with_detfd(fd, |detfd| {
3229            let flags = detfd.status_flags();
3230            let mode = flags & libc::O_ACCMODE;
3231            let denied = flags & libc::O_PATH != 0
3232                || if write {
3233                    mode == libc::O_RDONLY
3234                } else {
3235                    mode == libc::O_WRONLY
3236                };
3237            (!denied).then_some(()).ok_or(Errno::EBADF)
3238        })?
3239    }
3240
3241    fn read_timer_slack_input<G: Guest<Self>>(
3242        &self,
3243        guest: &G,
3244        buffer: Option<Addr<u8>>,
3245        count: usize,
3246    ) -> Result<u64, Errno> {
3247        let mut bytes = vec![0_u8; count.min(TIMER_SLACK_PARSE_BYTES)];
3248        if !bytes.is_empty() {
3249            guest
3250                .memory()
3251                .read_exact(buffer.ok_or(Errno::EFAULT)?, &mut bytes)?;
3252        }
3253        parse_timer_slack_write(&bytes)
3254    }
3255
3256    async fn read_timer_slack<G: Guest<Self>>(
3257        &self,
3258        guest: &mut G,
3259        fd: RawFd,
3260        buffer: Option<AddrMut<'_, u8>>,
3261        maximum: usize,
3262    ) -> Result<i64, Error> {
3263        self.require_timer_slack_access(guest, fd, false)?;
3264        if maximum == 0 {
3265            return Ok(0);
3266        }
3267        let binding = self
3268            .timer_slack_binding(guest, fd)?
3269            .expect("timer-slack read lost its procfs classification");
3270        self.require_current_timer_slack_target(guest, binding)
3271            .await?;
3272        let value = guest.thread_state().timer_slack_ns;
3273        let preview = guest
3274            .thread_state()
3275            .with_detfd(fd, |detfd| detfd.preview_procfs_timer_slack(value, maximum))?
3276            .expect("timer-slack procfs state disappeared");
3277        let copied = copy_timer_slack_output(&mut guest.memory(), buffer, &preview.bytes)?;
3278        if copied != 0 {
3279            guest.thread_state().with_detfd(fd, |detfd| {
3280                detfd.commit_procfs_timer_slack_read(&preview, copied);
3281            })?;
3282        }
3283        Ok(copied as i64)
3284    }
3285
3286    async fn pread_timer_slack<G: Guest<Self>>(
3287        &self,
3288        guest: &mut G,
3289        fd: RawFd,
3290        buffer: Option<AddrMut<'_, u8>>,
3291        maximum: usize,
3292        offset: i64,
3293    ) -> Result<i64, Error> {
3294        if offset < 0 {
3295            return Err(Errno::EINVAL.into());
3296        }
3297        self.require_timer_slack_access(guest, fd, false)?;
3298        if maximum == 0 {
3299            return Ok(0);
3300        }
3301        let binding = self
3302            .timer_slack_binding(guest, fd)?
3303            .expect("timer-slack pread lost its procfs classification");
3304        self.require_current_timer_slack_target(guest, binding)
3305            .await?;
3306        let value = guest.thread_state().timer_slack_ns;
3307        let bytes = guest
3308            .thread_state()
3309            .with_detfd(fd, |detfd| {
3310                detfd.take_procfs_timer_slack_at(value, offset as usize, maximum)
3311            })?
3312            .expect("timer-slack procfs state disappeared");
3313        Ok(copy_timer_slack_output(&mut guest.memory(), buffer, &bytes)? as i64)
3314    }
3315
3316    /// A vectored read of a procfs file Detcore renders, or of a per-process
3317    /// seq file the kernel opened that the session does not render
3318    /// (`ProcfsFile::follows_record_sequence`: `arch_status` where the guest's
3319    /// CPUID instruction is not intercepted), whose reads Detcore already
3320    /// serves from a sanitized capture (before, its vectored reads were
3321    /// ENOSYS): one read of its snapshot, as Linux's `read_iter` serves the
3322    /// whole vector at once,
3323    /// scattered over the `count` vectors at `address`, at `offset` or at
3324    /// (and advancing) the shared cursor. The order is `vfs_readv`'s: the
3325    /// descriptor's access mode, then the vectors' import (`EFAULT` for any
3326    /// destination outside the user address range, even at end of file), an
3327    /// empty vector (0), the range (`rw_verify_area`), and the flags. A
3328    /// record sequence a fork shares comes from Detcore's global state and
3329    /// goes back to it (`Self::readv_with_shared_sequence`).
3330    async fn readv_rendered_procfs<G: Guest<Self>>(
3331        &self,
3332        guest: &mut G,
3333        fd: RawFd,
3334        address: usize,
3335        count: usize,
3336        offset: Option<i64>,
3337        flags: i32,
3338    ) -> Result<i64, Error> {
3339        if let Some(id) = Self::procfs_sequence_to_take(guest, fd) {
3340            return self
3341                .readv_with_shared_sequence(guest, fd, address, count, offset, flags, id)
3342                .await;
3343        }
3344        let status = guest
3345            .thread_state()
3346            .with_detfd(fd, |detfd| detfd.status_flags())?;
3347        if !can_read(status) {
3348            return Err(Errno::EBADF.into());
3349        }
3350        let iovecs = crate::iovecs::import_read_iovecs(&guest.memory(), address, count, || {
3351            crate::iovecs::UserAddressLimit::from_query(guest.user_address_limit())
3352        })?;
3353        let maximum = iovecs.iter().map(|iovec| iovec.len).sum::<usize>();
3354        if maximum == 0 {
3355            return Ok(0);
3356        }
3357        // `rw_verify_area` checks the position the read starts at, explicit
3358        // or the shared cursor (`readv`, `preadv2` at -1), plus the whole
3359        // count, before the flags are looked at (fs/read_write.c
3360        // `vfs_readv`): an end past LLONG_MAX is EINVAL.
3361        let position = match offset {
3362            Some(offset) => offset,
3363            None => {
3364                self.adopt_rendered_procfs_position(guest, fd).await?;
3365                let cursor = guest
3366                    .thread_state()
3367                    .with_detfd(fd, |detfd| detfd.procfs_position())?
3368                    .map_or(0, |(position, _)| position);
3369                i64::try_from(cursor).map_err(|_| Errno::EINVAL)?
3370            }
3371        };
3372        if i64::try_from(maximum)
3373            .ok()
3374            .and_then(|maximum| position.checked_add(maximum))
3375            .is_none()
3376        {
3377            return Err(Errno::EINVAL.into());
3378        }
3379        let read_iter = guest
3380            .thread_state()
3381            .with_detfd(fd, |detfd| detfd.procfs_has_read_iter())?;
3382        validate_procfs_vector_flags(flags, read_iter)?;
3383        // A rendered `cmdline` has a plain `read`, which Linux calls once per
3384        // vector, in order, at the advancing position, stopping at the first
3385        // short or failed one (`do_loop_readv_writev`, fs/read_write.c), each
3386        // call reading memory again.
3387        if guest
3388            .thread_state()
3389            .with_detfd(fd, |detfd| detfd.procfs_reads_memory_at_every_read())?
3390        {
3391            let start = position as u64;
3392            let mut total = 0_usize;
3393            for iovec in iovecs {
3394                match self
3395                    .read_rendered_cmdline(guest, fd, iovec.base, iovec.len, start + total as u64)
3396                    .await
3397                {
3398                    Ok(copied) => {
3399                        total += copied;
3400                        if copied != iovec.len {
3401                            break;
3402                        }
3403                    }
3404                    Err(error) if total == 0 => return Err(error),
3405                    Err(_) => break,
3406                }
3407            }
3408            if offset.is_none() {
3409                guest
3410                    .thread_state()
3411                    .with_detfd(fd, |detfd| detfd.commit_procfs_read(start as usize, total))?;
3412                self.sync_rendered_procfs_position(guest, fd).await?;
3413            }
3414            return Ok(total as i64);
3415        }
3416        // A rendered per-process file runs its record's `show` where the read
3417        // does not continue the record's sequence
3418        // (`ProcfsFile::drop_stale_task_snapshot`, `Self::check_record_show`).
3419        let follows_record = guest
3420            .thread_state()
3421            .with_detfd(fd, |detfd| detfd.procfs_follows_record_sequence())?;
3422        let task_record_start = if follows_record {
3423            let start = usize::try_from(position).map_err(|_| Errno::EINVAL)?;
3424            if guest
3425                .thread_state()
3426                .with_detfd(fd, |detfd| detfd.procfs_drop_stale_task_snapshot(start))?
3427            {
3428                self.check_record_show(guest, fd, start).await?;
3429            }
3430            Some(start)
3431        } else {
3432            None
3433        };
3434        // A task's `schedstat` shows the runtime anew at a positioned read at
3435        // 0 (`ProcfsFile::drop_schedstat_snapshot_for_read_at`).
3436        if offset.is_some() {
3437            guest.thread_state().with_detfd(fd, |detfd| {
3438                detfd.procfs_drop_schedstat_snapshot_for_read_at(position as u64)
3439            })?;
3440        }
3441        // A file the kernel opened takes its snapshot at its first read.
3442        if guest
3443            .thread_state()
3444            .with_detfd(fd, |detfd| detfd.procfs_needs_snapshot())?
3445        {
3446            let rendered = self
3447                .initialize_procfs_snapshot(guest, syscalls::Read::new().with_fd(fd))
3448                .await;
3449            if let Some(start) = task_record_start {
3450                guest.thread_state().with_detfd(fd, |detfd| {
3451                    detfd.procfs_note_task_record_rendered(start, rendered.is_ok())
3452                })?;
3453            }
3454            rendered?;
3455        }
3456        let start = match offset {
3457            Some(offset) => offset as usize,
3458            None => {
3459                self.adopt_rendered_procfs_position(guest, fd).await?;
3460                guest
3461                    .thread_state()
3462                    .with_detfd(fd, |detfd| detfd.procfs_position())?
3463                    .map_or(0, |(offset, _)| offset)
3464            }
3465        };
3466        let (bytes, faults_on_a_short_copy) = guest.thread_state().with_detfd(fd, |detfd| {
3467            (
3468                detfd.take_procfs_at(start, maximum).unwrap_or_default(),
3469                detfd.procfs_faults_on_a_short_copy(),
3470            )
3471        })?;
3472        // Like `copy_to_iter`, stop at the first byte that cannot be written,
3473        // leaving later vectors untouched.
3474        let mut copied = 0;
3475        for iovec in iovecs {
3476            if copied == bytes.len() {
3477                break;
3478            }
3479            if iovec.len == 0 {
3480                continue;
3481            }
3482            let end = (copied + iovec.len).min(bytes.len());
3483            let written = match copy_procfs_output(
3484                &mut guest.memory(),
3485                AddrMut::from_raw(iovec.base),
3486                &bytes[copied..end],
3487            ) {
3488                Ok(written) => written,
3489                Err(Error::Errno(Errno::EFAULT)) => 0,
3490                Err(error) => return Err(error),
3491            };
3492            copied += written;
3493            if copied < end {
3494                break;
3495            }
3496        }
3497        if copied < bytes.len() && (faults_on_a_short_copy || copied == 0) {
3498            return Err(Errno::EFAULT.into());
3499        }
3500        if offset.is_none() {
3501            guest
3502                .thread_state()
3503                .with_detfd(fd, |detfd| detfd.commit_procfs_read(start, copied))?;
3504            self.sync_rendered_procfs_position(guest, fd).await?;
3505        } else {
3506            guest
3507                .thread_state()
3508                .with_detfd(fd, |detfd| detfd.procfs_note_read_end(start + copied))?;
3509        }
3510        Ok(copied as i64)
3511    }
3512
3513    async fn readv_timer_slack<G: Guest<Self>>(
3514        &self,
3515        guest: &mut G,
3516        fd: RawFd,
3517        iovecs: Vec<TimerSlackIovec>,
3518        offset: Option<i64>,
3519        flags: i32,
3520    ) -> Result<i64, Error> {
3521        self.require_timer_slack_access(guest, fd, false)?;
3522        let maximum = iovecs.iter().map(|iovec| iovec.len).sum::<usize>();
3523        if maximum == 0 {
3524            return Ok(0);
3525        }
3526        if flags & !libc::RWF_HIPRI != 0 {
3527            return Err(Errno::EOPNOTSUPP.into());
3528        }
3529        let binding = self
3530            .timer_slack_binding(guest, fd)?
3531            .expect("timer-slack readv lost its procfs classification");
3532        self.require_current_timer_slack_target(guest, binding)
3533            .await?;
3534        let value = guest.thread_state().timer_slack_ns;
3535        let mut positioned_offset = offset.map(|offset| offset as usize);
3536        let mut total = 0_usize;
3537        for iovec in iovecs {
3538            if iovec.len == 0 {
3539                continue;
3540            }
3541            let sequential_preview = if positioned_offset.is_none() {
3542                guest.thread_state().with_detfd(fd, |detfd| {
3543                    detfd.preview_procfs_timer_slack(value, iovec.len)
3544                })?
3545            } else {
3546                None
3547            };
3548            let bytes = match (&sequential_preview, positioned_offset) {
3549                (Some(preview), None) => preview.bytes.clone(),
3550                (None, Some(offset)) => guest
3551                    .thread_state()
3552                    .with_detfd(fd, |detfd| {
3553                        detfd.take_procfs_timer_slack_at(value, offset, iovec.len)
3554                    })?
3555                    .expect("timer-slack procfs state disappeared"),
3556                _ => unreachable!("timer-slack read mode changed while reading"),
3557            };
3558            if bytes.is_empty() {
3559                break;
3560            }
3561            let copied = match copy_timer_slack_output(
3562                &mut guest.memory(),
3563                AddrMut::from_raw(iovec.base),
3564                &bytes,
3565            ) {
3566                Ok(copied) => copied,
3567                Err(_) if total > 0 => return Ok(total as i64),
3568                Err(error) => return Err(error.into()),
3569            };
3570            if let Some(preview) = &sequential_preview {
3571                guest.thread_state().with_detfd(fd, |detfd| {
3572                    detfd.commit_procfs_timer_slack_read(preview, copied);
3573                })?;
3574            }
3575            total += copied;
3576            if let Some(offset) = positioned_offset.as_mut() {
3577                *offset += copied;
3578            }
3579            if copied != bytes.len() {
3580                return Ok(total as i64);
3581            }
3582            if bytes.len() != iovec.len {
3583                break;
3584            }
3585        }
3586        Ok(total as i64)
3587    }
3588
3589    async fn write_timer_slack<G: Guest<Self>>(
3590        &self,
3591        guest: &mut G,
3592        fd: RawFd,
3593        buffer: Option<Addr<'_, u8>>,
3594        count: usize,
3595    ) -> Result<i64, Error> {
3596        self.require_timer_slack_access(guest, fd, true)?;
3597        let requested = self.read_timer_slack_input(guest, buffer, count)?;
3598        let binding = self
3599            .timer_slack_binding(guest, fd)?
3600            .expect("timer-slack write lost its procfs classification");
3601        self.require_current_timer_slack_target(guest, binding)
3602            .await?;
3603        let state = guest.thread_state_mut();
3604        state.timer_slack_ns = if requested == 0 {
3605            state.default_timer_slack_ns
3606        } else {
3607            requested
3608        };
3609        i64::try_from(count).map_err(|_| Errno::EINVAL.into())
3610    }
3611
3612    async fn writev_timer_slack<G: Guest<Self>>(
3613        &self,
3614        guest: &mut G,
3615        fd: RawFd,
3616        iovecs: Vec<TimerSlackIovec>,
3617        flags: i32,
3618    ) -> Result<i64, Error> {
3619        self.require_timer_slack_access(guest, fd, true)?;
3620        if iovecs.iter().all(|iovec| iovec.len == 0) {
3621            return Ok(0);
3622        }
3623        if flags & !libc::RWF_HIPRI != 0 {
3624            return Err(Errno::EOPNOTSUPP.into());
3625        }
3626        // Procfs supplies only `.write`, so Linux's writev fallback invokes it
3627        // once per nonempty iovec. Preserve its partial-success behavior and
3628        // let the last successful segment determine the current slack.
3629        let mut total = 0_i64;
3630        for iovec in iovecs {
3631            if iovec.len == 0 {
3632                continue;
3633            }
3634            let buffer = Addr::from_raw(iovec.base).ok_or(Errno::EFAULT);
3635            let requested = match buffer
3636                .and_then(|buffer| self.read_timer_slack_input(guest, Some(buffer), iovec.len))
3637            {
3638                Ok(requested) => requested,
3639                Err(_error) if total > 0 => return Ok(total),
3640                Err(error) => return Err(error.into()),
3641            };
3642            let binding = self
3643                .timer_slack_binding(guest, fd)?
3644                .expect("timer-slack writev lost its procfs classification");
3645            if let Err(error) = self
3646                .require_current_timer_slack_target(guest, binding)
3647                .await
3648            {
3649                return if total > 0 { Ok(total) } else { Err(error) };
3650            }
3651            let state = guest.thread_state_mut();
3652            state.timer_slack_ns = if requested == 0 {
3653                state.default_timer_slack_ns
3654            } else {
3655                requested
3656            };
3657            total = total
3658                .checked_add(i64::try_from(iovec.len).map_err(|_| Errno::EINVAL)?)
3659                .ok_or(Errno::EINVAL)?;
3660        }
3661        Ok(total)
3662    }
3663    /// Set the kernel's O_NONBLOCK on `fd`'s open file description, keeping its
3664    /// other status flags. The caller records the change with
3665    /// `maybe_set_nonblocking_fd`; the guest-visible flags are unchanged.
3666    async fn inject_physical_nonblocking<G: Guest<Self>>(
3667        &self,
3668        guest: &mut G,
3669        fd: RawFd,
3670    ) -> Result<(), Errno> {
3671        let flags = guest
3672            .inject(syscalls::Fcntl::new().with_fd(fd).with_cmd(F_GETFL))
3673            .await?;
3674        guest
3675            .inject(
3676                syscalls::Fcntl::new()
3677                    .with_fd(fd)
3678                    .with_cmd(F_SETFL(flags as i32 | OFlag::O_NONBLOCK.bits())),
3679            )
3680            .await?;
3681        Ok(())
3682    }
3683
3684    /// A blocking `openat` of the FIFO `fifo` (found by the tracer at `path`),
3685    /// which a guest made in this run (`crate::fifo_peer::is_guest_made`), for
3686    /// reading or for writing. On Linux it waits in the kernel until the other
3687    /// end is open; injected as is, it would wait while it holds the scheduler
3688    /// turn, and a guest that opens the other end could never run
3689    /// (<https://github.com/rrnewton/hermit/issues/2203>).
3690    ///
3691    /// First the open is tried nonblocking. Any error other than "no reader
3692    /// yet" (`ENXIO`, for a writer) is the open's own, returned at once, as
3693    /// Linux returns it before waiting (`EEXIST`, `ENOENT`, `EACCES`, ...), and
3694    /// nothing else is opened. A writer that finds a reader completes then, as
3695    /// does a reader whose writer a guest holds or waits to open. Otherwise the
3696    /// open waits in polling turns ([`crate::fifo_peer::PendingOpen`]), and the
3697    /// guest's try is closed in the same turn, so no sibling sees a descriptor
3698    /// for it. It waits until a guest holds the other end, is waiting to open
3699    /// it, or has opened it since, then opens nonblocking and is made
3700    /// blocking, as the guest asked.
3701    ///
3702    /// A signal ends the wait as Linux's: `ERESTARTSYS`, which the kernel turns
3703    /// into `EINTR` or a restart by the guest's disposition.
3704    ///
3705    /// When the scheduler answers that nothing else can run
3706    /// (`ResumeStatus::NothingElseCanRun`), no guest can open the other end,
3707    /// so the open is made as the guest asked, waiting in the kernel while it
3708    /// holds the turn, as without the poll: only a host could complete it.
3709    ///
3710    /// Either way the open completes on the FIFO it waited on
3711    /// ([`Self::open_fifo_waited_on`]).
3712    async fn open_fifo_once_a_guest_holds_its_peer<G: Guest<Self>>(
3713        &self,
3714        guest: &mut G,
3715        call: syscalls::Openat,
3716        fifo: (u64, u64),
3717        path: &std::ffi::OsStr,
3718    ) -> Result<i64, Error> {
3719        let reading = call.flags().bits() & libc::O_ACCMODE == libc::O_RDONLY;
3720        let peer = if reading {
3721            FifoPeer::Writer
3722        } else {
3723            FifoPeer::Reader
3724        };
3725        let nonblocking = call.with_flags(call.flags() | OFlag::O_NONBLOCK);
3726        let opens_before = crate::fifo_peer::guest_opens(fifo);
3727        // Guest state only, which changes in guest turns: the answer at each
3728        // polling turn is deterministic.
3729        let guest_peer_ready = || {
3730            crate::fifo_peer::guest_holds_peer_of(fifo, peer)
3731                || crate::fifo_peer::pending_opens(fifo).of(peer) > 0
3732                || crate::fifo_peer::guest_opens(fifo).of(peer) > opens_before.of(peer)
3733        };
3734        // A nonblocking FIFO open never waits, so an injection a signal
3735        // interrupts did not run, and is made again; the signal is delivered
3736        // as the guest resumes.
3737        // Each try is one injection, its path respelled for that injection
3738        // alone where it leads below a descriptor directory
3739        // (`Self::inject_translated`): nothing staged outlives a try, or the
3740        // polling turns between them.
3741        let pending =
3742            match Self::inject_translated_retrying(guest, Syscall::from(nonblocking), true).await {
3743                // A writer found a reader: Linux's open completes at once.
3744                Ok(fd) if !reading => return Ok(self.make_blocking(guest, fd as RawFd).await?),
3745                Ok(fd) if guest_peer_ready() => {
3746                    crate::fifo_peer::stand_in_for_waiting_writers(fifo, path);
3747                    return Ok(self.make_blocking(guest, fd as RawFd).await?);
3748                }
3749                Ok(fd) => {
3750                    // The tracer's reader opens before the guest's try closes, so
3751                    // the FIFO keeps a reader throughout.
3752                    let pending =
3753                        crate::fifo_peer::PendingOpen::begin(fifo, path, call.flags().bits());
3754                    guest
3755                        .inject_with_retry(syscalls::Close::new().with_fd(fd as RawFd))
3756                        .await?;
3757                    pending
3758                }
3759                Err(Errno::ENXIO) if !reading => {
3760                    crate::fifo_peer::PendingOpen::begin(fifo, path, call.flags().bits())
3761                }
3762                Err(errno) => return Err(errno.into()),
3763            };
3764        info!("[detcore] the FIFO open polls for a guest peer");
3765        // A waiting open is an end on Linux: it readies the opens waiting for
3766        // this end (`--pipe-wake-latency`).
3767        let in_effect = self.cfg.pipe_wake_latency_in_effect().is_some();
3768        let channel = crate::pipe_wake::ChannelId::Fifo(if in_effect {
3769            crate::fifo_peer::fifo_channel(fifo)
3770        } else {
3771            0
3772        });
3773        let (readies, waits_for) = if reading {
3774            (
3775                crate::pipe_wake::Wants::PeerReader,
3776                crate::pipe_wake::Wants::PeerWriter,
3777            )
3778        } else {
3779            (
3780                crate::pipe_wake::Wants::PeerWriter,
3781                crate::pipe_wake::Wants::PeerReader,
3782            )
3783        };
3784        self.note_pipe_readied(guest, channel, readies).await;
3785
3786        let mut resources = Resources::new(guest.thread_state().dettid);
3787        resources.insert(ResourceID::InternalIOPolling, Permission::W);
3788        resources.fyi(call.name());
3789        resources.polls_guest_state_only = true;
3790        resources.waits_until_nothing_else_can_run = true;
3791        if in_effect {
3792            resources.polls_channel = Some(Box::new((channel, waits_for)));
3793        }
3794        // As a polled pipe read: a signal another task sends ends the wait.
3795        resources.set_signal_interrupt_errno(Errno::ERESTARTSYS);
3796        resources.signal_state_task = crate::syscalls::helpers::guest_signal_state_task(guest);
3797        // The peer is checked again after every answer, the last one included:
3798        // a guest may have opened (and closed) the other end in the turns
3799        // before it.
3800        loop {
3801            let status = resource_request(guest, resources.clone()).await;
3802            resources.poll_attempt += 1;
3803            if guest_peer_ready() {
3804                break;
3805            }
3806            match status {
3807                ResumeStatus::Signaled(_) => return Err(Errno::ERESTARTSYS.into()),
3808                ResumeStatus::NothingElseCanRun => {
3809                    info!(
3810                        "[detcore] nothing else can run; the FIFO open waits in the kernel, as without the poll"
3811                    );
3812                    let waited_on = pending.guest_path();
3813                    return self
3814                        .open_fifo_waited_on(
3815                            guest,
3816                            call,
3817                            fifo,
3818                            waited_on.as_deref(),
3819                            call.flags(),
3820                            false,
3821                        )
3822                        .await;
3823                }
3824                _ => {}
3825            }
3826        }
3827        let waited_on = pending.guest_path();
3828        let open = async |guest: &mut G| {
3829            self.open_fifo_waited_on(
3830                guest,
3831                call,
3832                fifo,
3833                waited_on.as_deref(),
3834                nonblocking.flags(),
3835                true,
3836            )
3837            .await
3838        };
3839        let mut opened = open(guest).await;
3840        if matches!(opened, Err(Error::Errno(Errno::ENXIO))) {
3841            // A writer whose only reader has come and gone opens as Linux's did
3842            // when the reader opened; the tracer stands in for that reader while
3843            // the guest opens, so the guest's writes then fail with `EPIPE`, as
3844            // on Linux.
3845            let _stand_in = pending.open_in_tracer(libc::O_RDONLY);
3846            opened = open(guest).await;
3847        }
3848        if reading && opened.is_ok() {
3849            pending.stand_in_for_waiting_writers();
3850        }
3851        drop(pending);
3852        Ok(self.make_blocking(guest, opened? as RawFd).await?)
3853    }
3854
3855    /// Open, in the guest, the FIFO `fifo` a blocking open waited on, with
3856    /// the guest's `flags`, whatever its name is now: through the tracer's
3857    /// descriptor for it, `waited_on` ([`Self::open_waited_on_fifo`]).
3858    ///
3859    /// The kernel lets a guest into the tracer's descriptors only if it holds
3860    /// `CAP_SYS_PTRACE` or every capability the tracer has
3861    /// (`proc_fd_access_allowed`), and only if the tracer's `/proc` is under
3862    /// its root; and a root whose `/proc` is not the tracer's may name another
3863    /// file at that path, so what it opened is checked to be the FIFO, and
3864    /// closed if not. A guest that cannot reach the descriptor (`EACCES`,
3865    /// `ENOENT`, another file), or whose open waited without one (`waited_on`
3866    /// is `None`),
3867    /// opens the FIFO by the name it gave, as its first try did, provided the
3868    /// name still reaches that FIFO: looked up first, so that no other file
3869    /// is opened, and checked again once open. If the name reaches another
3870    /// file or none, the open cannot complete on its FIFO, as Linux's does,
3871    /// and is refused (Claude re-check of
3872    /// <https://github.com/rrnewton/hermit/pull/4329> at revision 4,
3873    /// MEDIUM-A).
3874    ///
3875    /// Either way this is a new open, so the FIFO's mode is checked again
3876    /// against the guest's credentials when it completes: a FIFO whose mode
3877    /// the guest may no longer open (a `chmod` during the wait) gives
3878    /// `EACCES`, where Linux checked only at the start.
3879    ///
3880    /// `retry`: an open that cannot wait (nonblocking), which a signal stops
3881    /// only before it runs. `ENXIO` (a writer whose reader is gone) and
3882    /// `ERESTARTSYS` (a signal ended a blocking open) are the caller's.
3883    async fn open_fifo_waited_on<G: Guest<Self>>(
3884        &self,
3885        guest: &mut G,
3886        call: syscalls::Openat,
3887        fifo: (u64, u64),
3888        waited_on: Option<&str>,
3889        flags: OFlag,
3890        retry: bool,
3891    ) -> Result<i64, Error> {
3892        let is_the_fifo = |stat: &libc::stat| (stat.st_dev, stat.st_ino) == fifo;
3893        let unreachable = match waited_on {
3894            Some(waited_on) => {
3895                match Self::open_waited_on_fifo(guest, fifo, waited_on, flags, retry).await {
3896                    Err(errno) if !matches!(errno, Errno::ENXIO | Errno::ERESTARTSYS) => {
3897                        Some(errno)
3898                    }
3899                    // A root whose `/proc` is not the tracer's can name
3900                    // another file at that path (Claude re-check at
3901                    // revision 5, LOW-C).
3902                    Ok(fd)
3903                        if !self
3904                            .inject_fstat(guest, fd as RawFd)
3905                            .await
3906                            .is_ok_and(|stat| is_the_fifo(&stat)) =>
3907                    {
3908                        info!(
3909                            "[detcore] the tracer's descriptor path reached another file in the guest's root"
3910                        );
3911                        guest
3912                            .inject_with_retry(syscalls::Close::new().with_fd(fd as RawFd))
3913                            .await?;
3914                        Some(Errno::ENOENT)
3915                    }
3916                    opened => return Ok(opened?),
3917                }
3918            }
3919            None => None,
3920        };
3921        if let Some(errno) = unreachable {
3922            info!(
3923                "[detcore] the guest cannot reach the tracer's descriptor for the FIFO ({}); it opens the FIFO by its name",
3924                errno
3925            );
3926        }
3927        let path = call.path().ok_or(Errno::EFAULT)?;
3928        let lookup = if flags.contains(OFlag::O_NOFOLLOW) {
3929            AtFlags::AT_SYMLINK_NOFOLLOW
3930        } else {
3931            AtFlags::empty()
3932        };
3933        if Self::inject_statat(guest, call.dirfd(), path, [Some(path), None], lookup)
3934            .await
3935            .is_ok_and(|stat| is_the_fifo(&stat))
3936        {
3937            // The name was checked by the first try, and reaches the FIFO:
3938            // nothing is created.
3939            let open = call.with_flags(flags - (OFlag::O_CREAT | OFlag::O_EXCL));
3940            let fd = Self::inject_translated_retrying(guest, Syscall::from(open), retry).await?;
3941            if self
3942                .inject_fstat(guest, fd as RawFd)
3943                .await
3944                .is_ok_and(|stat| is_the_fifo(&stat))
3945            {
3946                return Ok(fd);
3947            }
3948            guest
3949                .inject_with_retry(syscalls::Close::new().with_fd(fd as RawFd))
3950                .await?;
3951        }
3952        error!(
3953            "[detcore] a blocking open of a FIFO a guest made waited for its peer, and cannot complete \
3954             on that FIFO: the guest cannot reach the tracer's descriptor for it, and its name no \
3955             longer reaches it (a guest without CAP_SYS_PTRACE, or without the tracer's /proc, whose \
3956             FIFO was renamed or unlinked during the wait); refused \
3957             (https://github.com/rrnewton/hermit/pull/4329)"
3958        );
3959        self.refuse_unserviceable_operation(
3960            guest,
3961            Sysno::openat,
3962            unreachable.unwrap_or(Errno::ENOENT),
3963        )
3964        .await
3965    }
3966
3967    /// Open, in the guest, the FIFO a waiting open waited on, at `waited_on`
3968    /// (`fifo_peer::PendingOpen::guest_path`), with the guest's `flags`: the
3969    /// checks of its name were made by the first try, so `O_CREAT`, `O_EXCL`
3970    /// and `O_NOFOLLOW` go (the path is the tracer's descriptor link).
3971    /// `retry`: an open that cannot wait (nonblocking), which a signal stops
3972    /// only before it runs.
3973    ///
3974    /// The path is staged in a private page mapped for this open only, not
3975    /// in the guest stack scratch, so that an open needs no spare stack
3976    /// below the guest's stack pointer, which Linux's does not (Codex
3977    /// re-check of <https://github.com/rrnewton/hermit/pull/4329> at revision
3978    /// 4, P2). The page is unmapped before the guest resumes, so the guest's
3979    /// memory keeps no tracer descriptor number (Claude's LOW-B there). An
3980    /// error mapping or writing the page is returned, and the caller opens
3981    /// the FIFO by its name instead.
3982    ///
3983    /// A task killed while the open waits in the kernel drops this call
3984    /// before its unmap; the page is then unmapped by the next task of the
3985    /// same address space to reach Detcore
3986    /// ([`crate::fifo_peer::StagedPage`], [`Self::unmap_orphaned_staging_pages`]).
3987    ///
3988    /// What the path names is looked up first, and must be the FIFO `fifo`:
3989    /// a root whose `/proc` is not the tracer's can name another file there,
3990    /// which is not opened (`ESTALE`; Claude re-check at revision 6, LOW-H).
3991    async fn open_waited_on_fifo<G: Guest<Self>>(
3992        guest: &mut G,
3993        fifo: (u64, u64),
3994        waited_on: &str,
3995        flags: OFlag,
3996        retry: bool,
3997    ) -> Result<i64, Errno> {
3998        let mut path_buffer = [0_u8; crate::fifo_peer::STAGED_PAGE_LENGTH];
3999        let bytes = waited_on.as_bytes();
4000        if bytes.len() >= path_buffer.len() {
4001            return Err(Errno::ENAMETOOLONG);
4002        }
4003        path_buffer[..bytes.len()].copy_from_slice(bytes);
4004        // The page's owner exists before its `mmap` runs, at an address
4005        // chosen first: a task killed during the `mmap` leaves its address
4006        // space's next task an address to check.
4007        let address =
4008            crate::fifo_peer::free_staging_address(guest.tid().as_raw()).ok_or(Errno::ENOMEM)?;
4009        let page = AddrMut::<u8>::from_raw(address).ok_or(Errno::ENOMEM)?;
4010        let staged =
4011            crate::fifo_peer::StagedPage::new(guest.thread_state().mm_id, address, path_buffer);
4012        match guest
4013            .inject_with_retry(Syscall::Mmap(
4014                syscalls::Mmap::new()
4015                    .with_addr(Some(page.cast::<libc::c_void>().into()))
4016                    .with_len(path_buffer.len())
4017                    .with_prot(ProtFlags::PROT_READ | ProtFlags::PROT_WRITE)
4018                    .with_flags(
4019                        MapFlags::MAP_PRIVATE
4020                            | MapFlags::MAP_ANONYMOUS
4021                            | MapFlags::MAP_FIXED_NOREPLACE,
4022                    )
4023                    .with_fd(-1)
4024                    .with_offset(0),
4025            ))
4026            .await
4027        {
4028            Ok(mapped) if mapped as usize == address => {
4029                crate::race_probe::delay(crate::race_probe::FIFO_STAGING_MAPPED).await;
4030            }
4031            // Nothing was mapped at the address (`EEXIST`: something is
4032            // there): this route is not taken.
4033            Err(errno) => {
4034                staged.unmapped();
4035                return Err(errno);
4036            }
4037            // A kernel without MAP_FIXED_NOREPLACE takes the address as a
4038            // hint and maps elsewhere.
4039            Ok(mapped) => {
4040                staged.unmapped();
4041                if let Some(elsewhere) = Addr::<libc::c_void>::from_raw(mapped as usize) {
4042                    let _ = guest
4043                        .inject_with_retry(Syscall::Munmap(
4044                            syscalls::Munmap::new()
4045                                .with_addr(Some(elsewhere))
4046                                .with_len(path_buffer.len()),
4047                        ))
4048                        .await;
4049                }
4050                return Err(Errno::ENOMEM);
4051            }
4052        }
4053        let path = PathPtr::from_ptr(page.as_raw() as *const libc::c_char)
4054            .expect("a mapped page is not null");
4055        let opened = match guest.memory().write_exact(page, &path_buffer) {
4056            Ok(()) => match Self::inject_statat(
4057                guest,
4058                libc::AT_FDCWD,
4059                path,
4060                [Some(path), None],
4061                AtFlags::empty(),
4062            )
4063            .await
4064            {
4065                Ok(stat) if (stat.st_dev, stat.st_ino) == fifo => {
4066                    let open = syscalls::Openat::new()
4067                        .with_dirfd(libc::AT_FDCWD)
4068                        .with_path(Some(path))
4069                        .with_flags(flags - (OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_NOFOLLOW))
4070                        .with_mode(None);
4071                    if retry {
4072                        guest.inject_with_retry(open).await
4073                    } else {
4074                        guest.inject(open).await
4075                    }
4076                }
4077                Ok(_) => {
4078                    info!(
4079                        "[detcore] the tracer's descriptor path names another file in the guest's root"
4080                    );
4081                    Err(Errno::ESTALE)
4082                }
4083                Err(errno) => Err(errno),
4084            },
4085            Err(error) => Err(error),
4086        };
4087        match guest
4088            .inject_with_retry(Syscall::Munmap(
4089                syscalls::Munmap::new()
4090                    .with_addr(Some(page.cast::<libc::c_void>().into()))
4091                    .with_len(path_buffer.len()),
4092            ))
4093            .await
4094        {
4095            Ok(_) => staged.unmapped(),
4096            // An unmap that did not run (stopped before it ran) is left, with
4097            // the guard, to this address space's next task.
4098            Err(errno) => info!(
4099                "[detcore] the transient FIFO path page is left to the next task: {}",
4100                errno
4101            ),
4102        }
4103        opened
4104    }
4105
4106    /// Unmap the pages a completing FIFO open staged in this task's address
4107    /// space and could not unmap, because its task went away while the open
4108    /// waited ([`crate::fifo_peer::StagedPage`]). Called as a task reaches
4109    /// Detcore, before it runs guest code again; costs one load while no page
4110    /// is left.
4111    pub(crate) async fn unmap_orphaned_staging_pages<G: Guest<Self>>(guest: &mut G) {
4112        // Each taken page is owned by its guard until it is unmapped: one this
4113        // task does not get to (its unmap does not run, or the task exits
4114        // while it waits) is left again.
4115        for page in crate::fifo_peer::take_orphaned_pages(guest.thread_state().mm_id) {
4116            // Only a page that still holds what was staged: the guest may have
4117            // unmapped it and mapped something else there.
4118            let mut contents = [0_u8; crate::fifo_peer::STAGED_PAGE_LENGTH];
4119            let Some(address) = Addr::<u8>::from_raw(page.address()) else {
4120                page.unmapped();
4121                continue;
4122            };
4123            let readable = guest.memory().read_exact(address, &mut contents).is_ok();
4124            // A task killed during the `mmap` may have left the page before
4125            // anything was written to it: a page of zeros is the staging page
4126            // only where it is a lone one-page anonymous mapping.
4127            if !readable
4128                || (contents != *page.contents()
4129                    && !(contents == [0; crate::fifo_peer::STAGED_PAGE_LENGTH]
4130                        && crate::fifo_peer::is_lone_staging_mapping(
4131                            guest.tid().as_raw(),
4132                            page.address(),
4133                        )))
4134            {
4135                info!("[detcore] a FIFO open's path page that a killed task left is gone");
4136                page.unmapped();
4137                continue;
4138            }
4139            info!("[detcore] unmapping a FIFO open's path page that a killed task left");
4140            crate::race_probe::delay(crate::race_probe::FIFO_STAGING_RECLAIM).await;
4141            let unmapped = guest
4142                .inject_with_retry(Syscall::Munmap(
4143                    syscalls::Munmap::new()
4144                        .with_addr(Addr::<libc::c_void>::from_raw(page.address()))
4145                        .with_len(contents.len()),
4146                ))
4147                .await;
4148            match unmapped {
4149                Ok(_) => page.unmapped(),
4150                Err(Errno::EINVAL) => {
4151                    warn!(
4152                        "[detcore] could not unmap the transient FIFO path page: {}",
4153                        Errno::EINVAL
4154                    );
4155                    page.unmapped();
4156                }
4157                // Stopped before it ran: the guard leaves it to the next task
4158                // (Claude re-check at revision 6, MEDIUM-G).
4159                Err(errno) => info!(
4160                    "[detcore] the transient FIFO path page is left to the next task: {}",
4161                    errno
4162                ),
4163            }
4164        }
4165    }
4166
4167    /// Clear `O_NONBLOCK` on the guest's FIFO descriptor `fd`, which was opened
4168    /// nonblocking in place of a blocking open, and return it.
4169    async fn make_blocking<G: Guest<Self>>(&self, guest: &mut G, fd: RawFd) -> Result<i64, Errno> {
4170        let flags = guest
4171            .inject_with_retry(syscalls::Fcntl::new().with_fd(fd).with_cmd(F_GETFL))
4172            .await?;
4173        guest
4174            .inject_with_retry(
4175                syscalls::Fcntl::new()
4176                    .with_fd(fd)
4177                    .with_cmd(F_SETFL(flags as i32 & !OFlag::O_NONBLOCK.bits())),
4178            )
4179            .await?;
4180        Ok(fd as i64)
4181    }
4182
4183    /// Before a logically blocking read (`peer` is `Writer`) or write
4184    /// (`Reader`) on a FIFO that `handle_openat` marked: if a guest holds the
4185    /// other end (`crate::fifo_peer`), shrink the FIFO's capacity to the one
4186    /// `handle_pipe2` pins a pipe to (never growing one the guest shrank), make
4187    /// it physically nonblocking, and treat it from then on as a
4188    /// scheduler-managed pipe. The call then polls in
4189    /// turns instead of blocking in the kernel while it holds the turn, where
4190    /// the guest at the other end could never run
4191    /// (<https://github.com/rrnewton/hermit/issues/4200>). A FIFO whose other
4192    /// ends are all on the host is left blocking, as before.
4193    ///
4194    /// The open file description may be shared with another process that
4195    /// already made this change: the kernel then reports it nonblocking while
4196    /// this process still sees it blocking, and this process follows. On
4197    /// ptrace a forked child shares the parent's model (`fork_for` clones the
4198    /// `Arc`), so it sees the change at once; this branch is for a backend
4199    /// whose fork copies the model into the child's address space (DBT), and
4200    /// the ptrace tests do not reach it.
4201    /// Shrink a FIFO that is about to be polled as a pipe to the capacity
4202    /// `handle_pipe2` pins a pipe to, if it is larger, so that its partial
4203    /// writes do not depend on the host's pipe defaults. A capacity the guest
4204    /// set smaller is kept, as Linux keeps it. The kernel refuses (`EBUSY`)
4205    /// while more is buffered than the new capacity holds; the FIFO is then
4206    /// polled at its current capacity, which only that buffered amount
4207    /// decides.
4208    async fn shrink_fifo_capacity<G: Guest<Self>>(&self, guest: &mut G, fd: RawFd) {
4209        let Ok(capacity) = guest
4210            .inject(syscalls::Fcntl::new().with_fd(fd).with_cmd(F_GETPIPE_SZ))
4211            .await
4212        else {
4213            return;
4214        };
4215        if capacity > i64::from(deterministic_pipe_capacity_bytes())
4216            && let Err(errno) = guest
4217                .inject(
4218                    syscalls::Fcntl::new()
4219                        .with_fd(fd)
4220                        .with_cmd(F_SETPIPE_SZ(deterministic_pipe_capacity_bytes())),
4221                )
4222                .await
4223        {
4224            info!(
4225                "[detcore] FIFO fd {} keeps its {}-byte capacity: shrinking it failed with {}",
4226                fd, capacity, errno
4227            );
4228        }
4229    }
4230
4231    async fn fifo_polls_if_a_guest_holds_its_peer<G: Guest<Self>>(
4232        &self,
4233        guest: &mut G,
4234        fd: RawFd,
4235        peer: FifoPeer,
4236    ) {
4237        let Ok((awaiting, logically_nonblocking)) = guest.thread_state().with_detfd(fd, |detfd| {
4238            (detfd.fifo_awaiting_guest_peer(), detfd.is_nonblocking())
4239        }) else {
4240            return;
4241        };
4242        if !awaiting || logically_nonblocking {
4243            return;
4244        }
4245        // The calling thread's own table: it may not be its leader's (a thread
4246        // cloned without CLONE_FILES), and `fd` is changed in this one.
4247        let tid = guest.tid().as_raw();
4248        let kernel_nonblocking = crate::fifo_peer::kernel_nonblocking(tid, fd);
4249        let joins = match kernel_nonblocking {
4250            Some(true) => true,
4251            Some(false) => crate::fifo_peer::guest_holds_peer(tid, fd, peer),
4252            None => false,
4253        };
4254        if !joins {
4255            return;
4256        }
4257        // A FIFO the scheduler polls is polled at its capacity, which host
4258        // load chooses where `pipe-max-size` is below two pages.
4259        if !host_pipe_floor() {
4260            self.refuse_below_two_page_pipe_floor(
4261                guest,
4262                &format!("FIFO fd {fd} has a guest at its other end and would be polled as a pipe"),
4263            )
4264            .await;
4265        }
4266        if kernel_nonblocking == Some(false) {
4267            self.shrink_fifo_capacity(guest, fd).await;
4268            if self.inject_physical_nonblocking(guest, fd).await.is_err() {
4269                return;
4270            }
4271        }
4272        info!(
4273            "[detcore] fd {} is a FIFO with a guest at its other end; polling it as a pipe",
4274            fd
4275        );
4276        let _ = guest
4277            .thread_state()
4278            .with_detfd(fd, |detfd| detfd.fifo_joins_internal_polling());
4279    }
4280
4281    /// Ends the run where a pipe's capacity is one host load chose
4282    /// ([`host_pipe_floor`]), for `what`.
4283    async fn refuse_below_two_page_pipe_floor<G: Guest<Self>>(
4284        &self,
4285        guest: &mut G,
4286        what: &str,
4287    ) -> ! {
4288        error!("{}", below_two_page_pipe_floor(what));
4289        unrecoverable_shutdown(guest, detcore_model::HERMIT_POLICY_REFUSAL_EXIT).await
4290    }
4291
4292    /// Inject an extra fstat to retrieve file metadata.
4293    ///
4294    /// The kernel needs a writable `struct stat` in the guest. It is staged
4295    /// first in the guest stack scratch, which on the ptrace backend lies just
4296    /// below the red zone under the guest's stack pointer and costs nothing
4297    /// when the stack has room. That memory is not guaranteed to be writable:
4298    /// the guest may run with its stack pointer just above a guard page (a
4299    /// thread, fiber or alternate signal stack), or within a few hundred bytes
4300    /// of the lowest page of the main-thread stack, which a tracer's write does
4301    /// not grow. The fault then belongs to Detcore's bookkeeping, not to the
4302    /// guest, so the same fstat is repeated with its buffer in a transient
4303    /// private page that is unmapped before the guest resumes
4304    /// (<https://github.com/rrnewton/hermit/issues/3328>).
4305    pub(crate) async fn inject_fstat<G: Guest<Self>>(
4306        &self,
4307        guest: &mut G,
4308        raw_fd: RawFd,
4309    ) -> Result<libc::stat, Errno> {
4310        crate::ordered_info!(
4311            "Injecting additional fstat to retrieve file metadata on fd {}.",
4312            raw_fd
4313        );
4314        let fstat = move |statptr: StatPtr<'_>| {
4315            Syscall::Fstat(
4316                syscalls::Fstat::new()
4317                    .with_fd(raw_fd)
4318                    .with_stat(Some(statptr)),
4319            )
4320        };
4321        let copied = Self::stat_with_fallback(guest, raw_fd, fstat, None)
4322            .await
4323            .map_err(StatFailure::errno)?;
4324        trace!("extra fstat returned inode {}", copied.st_ino);
4325        Ok(copied)
4326    }
4327
4328    /// `stat_call` for descriptor `raw_fd` with its buffer in the guest stack
4329    /// scratch or, where the scratch cannot hold it (the commit or the call
4330    /// faults), in a transient private page; see [`Self::inject_fstat`].
4331    /// `recorded` routes the call through that Detcore's recording
4332    /// ([`Self::recorded_fstat`]); `None` injects it.
4333    async fn stat_with_fallback<G: Guest<Self>>(
4334        guest: &mut G,
4335        raw_fd: RawFd,
4336        stat_call: impl Fn(StatPtr<'_>) -> Syscall + Copy,
4337        recorded: Option<&Self>,
4338    ) -> Result<libc::stat, StatFailure> {
4339        match Self::inject_stat_on_stack(guest, stat_call, recorded).await {
4340            Err(StatFailure::Scratch(Errno::EFAULT) | StatFailure::Call(Errno::EFAULT)) => {
4341                info!(
4342                    "Guest stack scratch cannot hold the fstat buffer for fd {}; \
4343                     using a transient page instead.",
4344                    raw_fd
4345                );
4346                Self::inject_stat_in_transient_page(guest, stat_call, recorded).await
4347            }
4348            result => result,
4349        }
4350    }
4351
4352    /// `fstatat(dirfd, path, AT_SYMLINK_NOFOLLOW)` for a path the guest named
4353    /// in a call that has not run yet and must still read `inputs`, its paths
4354    /// ([`Detcore::inject_statat`]).
4355    async fn inject_lstatat<G: Guest<Self>>(
4356        guest: &mut G,
4357        dirfd: RawFd,
4358        path: syscalls::PathPtr<'_>,
4359        inputs: [Option<syscalls::PathPtr<'_>>; 2],
4360    ) -> Result<libc::stat, Errno> {
4361        Self::inject_statat(guest, dirfd, path, inputs, AtFlags::AT_SYMLINK_NOFOLLOW).await
4362    }
4363
4364    /// `fstatat(dirfd, path, flags)` for a path the guest named in a call that
4365    /// has not run yet and must still read `inputs`, its paths.
4366    ///
4367    /// The buffer is staged as the utimensat target lookup stages its own: in
4368    /// the guest stack scratch, below the red zone, where a raw syscall may
4369    /// keep its own path, so only when it overlaps none of `inputs`, and with
4370    /// the bytes under it saved and put back after the commit and after the
4371    /// lookup, so that the call reads its inputs unchanged (claude review of
4372    /// <https://github.com/rrnewton/hermit/pull/3849>, P3-2). Otherwise it is
4373    /// a transient private page, unmapped before the guest resumes.
4374    pub(crate) async fn inject_statat<G: Guest<Self>>(
4375        guest: &mut G,
4376        dirfd: RawFd,
4377        path: syscalls::PathPtr<'_>,
4378        inputs: [Option<syscalls::PathPtr<'_>>; 2],
4379        flags: AtFlags,
4380    ) -> Result<libc::stat, Errno> {
4381        // A `/proc` path naming a process or thread by the guest's number
4382        // is looked up by the kernel's (DBT), as the guest's own call is
4383        // (`procfs_numbers`).
4384        let probe = crate::arch::fstatat_syscall(
4385            syscalls::Fstatat::new()
4386                .with_dirfd(dirfd)
4387                .with_path(Some(path))
4388                .with_flags(flags),
4389        );
4390        let Some(translated) = Self::procfs_translated_call(guest, &probe).await? else {
4391            return Self::inject_statat_as_spelled(guest, dirfd, path, inputs, flags).await;
4392        };
4393        // The one path, where the translation staged it, looked up from the
4394        // directory it gives (`AT_FDCWD` for a path respelled below a
4395        // descriptor directory).
4396        let (_, staged) = translated.call.into_parts();
4397        let respelled = PathPtr::from_ptr(staged.arg1 as *const libc::c_char);
4398        let dirfd = staged.arg0 as RawFd;
4399        let looked_up = match respelled {
4400            Some(respelled) => {
4401                Self::inject_statat_as_spelled(
4402                    guest,
4403                    dirfd,
4404                    respelled,
4405                    [Some(respelled), None],
4406                    flags,
4407                )
4408                .await
4409            }
4410            None => Err(Errno::ENOMEM),
4411        };
4412        Self::finish_translated_call(guest, translated).await;
4413        looked_up
4414    }
4415
4416    /// [`Self::inject_statat`] of `path` as it is spelled.
4417    async fn inject_statat_as_spelled<G: Guest<Self>>(
4418        guest: &mut G,
4419        dirfd: RawFd,
4420        path: syscalls::PathPtr<'_>,
4421        inputs: [Option<syscalls::PathPtr<'_>>; 2],
4422        flags: AtFlags,
4423    ) -> Result<libc::stat, Errno> {
4424        let lstatat = move |statptr: StatPtr<'_>| {
4425            crate::arch::fstatat_syscall(
4426                syscalls::Fstatat::new()
4427                    .with_dirfd(dirfd)
4428                    .with_path(Some(path))
4429                    .with_stat(Some(statptr))
4430                    .with_flags(flags),
4431            )
4432        };
4433        // The scratch starts 128 bytes below the stack pointer, at addresses
4434        // computed by subtraction.
4435        let scratch = 128 + std::mem::size_of::<libc::stat>();
4436        if usize::try_from(crate::arch::stack_pointer(&guest.regs().await))
4437            .map_or(true, |rsp| rsp <= scratch)
4438        {
4439            return Self::inject_stat_in_transient_page(guest, lstatat, None)
4440                .await
4441                .map_err(StatFailure::errno);
4442        }
4443        let mut stack = guest.stack().await;
4444        let statptr: StatPtr = StatPtr(stack.reserve());
4445        let mut saved = [0u8; std::mem::size_of::<libc::stat>()];
4446        let staged_on_stack = !inputs
4447            .iter()
4448            .any(|input| path_overlaps_buffer(&guest.memory(), *input, statptr))
4449            && guest
4450                .memory()
4451                .read_exact(statptr.0.cast(), &mut saved)
4452                .is_ok();
4453        if !staged_on_stack {
4454            drop(stack);
4455            return Self::inject_stat_in_transient_page(guest, lstatat, None)
4456                .await
4457                .map_err(StatFailure::errno);
4458        }
4459        let guard = match stack.commit() {
4460            Ok(guard) => guard,
4461            Err(_) => {
4462                restore_lookup_buffer(&mut guest.memory(), statptr, &saved);
4463                return Self::inject_stat_in_transient_page(guest, lstatat, None)
4464                    .await
4465                    .map_err(StatFailure::errno);
4466            }
4467        };
4468        restore_lookup_buffer(&mut guest.memory(), statptr, &saved);
4469        let looked_up = Self::inject_stat_into(guest, lstatat(statptr), statptr, None)
4470            .await
4471            .map_err(StatFailure::errno);
4472        restore_lookup_buffer(&mut guest.memory(), statptr, &saved);
4473        drop(guard);
4474        looked_up
4475    }
4476
4477    /// The fast path of [`Self::inject_fstat`]: the buffer lives in the guest
4478    /// stack scratch. Returns `Scratch(EFAULT)` when that scratch is not
4479    /// writable.
4480    async fn inject_stat_on_stack<G: Guest<Self>>(
4481        guest: &mut G,
4482        stat_call: impl Fn(StatPtr<'_>) -> Syscall,
4483        recorded: Option<&Self>,
4484    ) -> Result<libc::stat, StatFailure> {
4485        let mut stack = guest.stack().await;
4486        let statptr: StatPtr = StatPtr(stack.reserve());
4487        // Ptrace lends bytes below the guest red zone. Save them before
4488        // commit can zero or partially write the reserved buffer, as the
4489        // adjacent pre-call lookup path does. Tool-owned scratch is likewise
4490        // held by stack/guard throughout every access.
4491        let mut saved = [0u8; std::mem::size_of::<libc::stat>()];
4492        guest
4493            .memory()
4494            .read_exact(statptr.0.cast(), &mut saved)
4495            .map_err(StatFailure::Scratch)?;
4496        let guard = match stack.commit() {
4497            Ok(guard) => guard,
4498            Err(errno) => {
4499                restore_lookup_buffer(&mut guest.memory(), statptr, &saved);
4500                return Err(StatFailure::Scratch(errno));
4501            }
4502        };
4503        restore_lookup_buffer(&mut guest.memory(), statptr, &saved);
4504        // No early `?`: even kernel/readback errors must put back the borrowed
4505        // bytes before the guard releases a backend's Tool-owned arena.
4506        let copied = Self::inject_stat_into(guest, stat_call(statptr), statptr, recorded).await;
4507        let restored = guest.memory().write_exact(statptr.0.cast(), &saved);
4508        drop(guard);
4509        // Preserve the actual injection/readback errno. On success, preserve
4510        // the old contract that a failed final scratch write is an error.
4511        match copied {
4512            Ok(copied) => restored.map(|()| copied).map_err(StatFailure::Scratch),
4513            Err(failure) => {
4514                if restored.is_err() {
4515                    info!("Could not restore the guest bytes under a pre-call lookup buffer.");
4516                }
4517                Err(failure)
4518            }
4519        }
4520    }
4521
4522    /// The fallback of [`Self::inject_fstat`] and [`Self::inject_lstatat`]:
4523    /// the buffer lives in a private
4524    /// anonymous page mapped for this call only. The guest never learns its
4525    /// address, and it is unmapped before the guest resumes, so the guest's
4526    /// address space is the same as before the call.
4527    async fn inject_stat_in_transient_page<G: Guest<Self>>(
4528        guest: &mut G,
4529        stat_call: impl Fn(StatPtr<'_>) -> Syscall,
4530        recorded: Option<&Self>,
4531    ) -> Result<libc::stat, StatFailure> {
4532        let len = std::mem::size_of::<libc::stat>();
4533        let mapped = guest
4534            .inject_with_retry(Syscall::Mmap(
4535                syscalls::Mmap::new()
4536                    .with_addr(None)
4537                    .with_len(len)
4538                    .with_prot(ProtFlags::PROT_READ | ProtFlags::PROT_WRITE)
4539                    .with_flags(MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS)
4540                    .with_fd(-1)
4541                    .with_offset(0),
4542            ))
4543            .await
4544            .map_err(StatFailure::Scratch)?;
4545        let page = usize::try_from(mapped)
4546            .ok()
4547            .and_then(AddrMut::<libc::stat>::from_raw)
4548            .unwrap_or_else(|| panic!("transient fstat page mmap returned {mapped}"));
4549        let copied =
4550            Self::inject_stat_into(guest, stat_call(StatPtr(page)), StatPtr(page), recorded).await;
4551        if let Err(errno) = guest
4552            .inject_with_retry(Syscall::Munmap(
4553                syscalls::Munmap::new()
4554                    .with_addr(Some(page.cast::<libc::c_void>().into()))
4555                    .with_len(len),
4556            ))
4557            .await
4558        {
4559            // Not expected: the page was mapped by this call and its address
4560            // never reached the guest. The metadata is still valid, so a
4561            // leftover page is no reason to fail the guest's syscall.
4562            warn!(
4563                "[detcore] could not unmap the transient stat page: {}",
4564                errno
4565            );
4566        }
4567        copied
4568    }
4569
4570    /// Inject `stat_call`, which writes a `struct stat` at `statptr`, and read
4571    /// back what the kernel wrote. With `recorded`, the call goes through that
4572    /// Detcore's recording ([`Detcore::record_or_replay`]): recorded under
4573    /// record, served from the recording under replay, injected otherwise.
4574    async fn inject_stat_into<G: Guest<Self>>(
4575        guest: &mut G,
4576        stat_call: Syscall,
4577        statptr: StatPtr<'_>,
4578        recorded: Option<&Self>,
4579    ) -> Result<libc::stat, StatFailure> {
4580        // NOTE: Must retry the injection here. This could get interrupted and
4581        // we don't want to rerun the entire syscall handler twice. Through the
4582        // recording, every attempt is a recorded event, so replay consumes the
4583        // same attempts, in order, and retries exactly as the record did.
4584        match recorded {
4585            None => {
4586                guest
4587                    .inject_with_retry(stat_call)
4588                    .await
4589                    .map_err(StatFailure::Call)?;
4590            }
4591            Some(detcore) => loop {
4592                match detcore.record_or_replay(guest, stat_call).await {
4593                    Ok(_) => break,
4594                    Err(Errno::EINTR | Errno::ERESTARTSYS) => continue,
4595                    Err(errno) => return Err(StatFailure::Call(errno)),
4596                }
4597            },
4598        }
4599        statptr.read(&guest.memory()).map_err(StatFailure::Call)
4600    }
4601
4602    // helper function to track a new file descriptor.
4603    pub(crate) async fn add_fd<G: Guest<Self>>(
4604        &self,
4605        guest: &mut G,
4606        fd: RawFd,
4607        flags: OFlag,
4608        ty: FdType,
4609    ) -> Result<(), Errno> {
4610        self.add_fd_with_stat(guest, fd, flags, ty).await.map(drop)
4611    }
4612
4613    /// Track `fd`, which the guest already holds from a call that has
4614    /// succeeded whatever happens here (a clone's pidfd), as [`Self::add_fd`]
4615    /// does, except that a failure leaves the descriptor open and untracked
4616    /// rather than closing it: the guest keeps what Linux gave it.
4617    pub(crate) async fn track_held_fd<G: Guest<Self>>(
4618        &self,
4619        guest: &mut G,
4620        fd: RawFd,
4621        flags: OFlag,
4622        ty: FdType,
4623    ) -> Result<(), Errno> {
4624        let host_stat = if guest.config().virtualize_metadata {
4625            Some(self.inject_fstat(guest, fd).await?)
4626        } else {
4627            None
4628        };
4629        guest
4630            .thread_state()
4631            .add_fd(fd, flags, ty, host_stat.map(Into::into))
4632    }
4633
4634    /// [`Self::add_fd`], also returning the host `fstat` it took of `fd`, which
4635    /// it takes only when metadata is virtualized.
4636    async fn add_fd_with_stat<G: Guest<Self>>(
4637        &self,
4638        guest: &mut G,
4639        fd: RawFd,
4640        flags: OFlag,
4641        ty: FdType,
4642    ) -> Result<Option<libc::stat>, Errno> {
4643        let host_stat = if guest.config().virtualize_metadata {
4644            match self.inject_fstat(guest, fd).await {
4645                Ok(stat) => Some(stat),
4646                Err(errno) => {
4647                    // `fd` is already open in the guest, but Detcore cannot
4648                    // model it: with metadata virtualization on, a descriptor
4649                    // without its stat is an invariant violation. The caller
4650                    // reports this error as the syscall's result, so close
4651                    // the descriptor rather than leave an untracked one open
4652                    // behind that error. Not retried on EINTR: Linux releases
4653                    // the descriptor even then, and a retry could close a
4654                    // reused number.
4655                    if let Err(close_errno) = guest.inject(syscalls::Close::new().with_fd(fd)).await
4656                    {
4657                        warn!(
4658                            "[detcore] could not close fd {} after failing to record its \
4659                             metadata ({}): {}",
4660                            fd, errno, close_errno
4661                        );
4662                    }
4663                    return Err(errno);
4664                }
4665            }
4666        } else {
4667            None
4668        };
4669        guest
4670            .thread_state()
4671            .add_fd(fd, flags, ty, host_stat.map(Into::into))?;
4672        Ok(host_stat)
4673    }
4674
4675    /// Under replay, makes descriptor `fd`, which an `SCM_RIGHTS` receive
4676    /// installed and which the recording found to name the open file of `other`
4677    /// in this table, name that file in the kernel too.
4678    ///
4679    /// Replay serves the receive from the log and holds a placeholder eventfd
4680    /// in `fd`, but the guest keeps using both descriptors: a write to a
4681    /// received eventfd must reach the reader of the original, as it does in
4682    /// the recording, and replay applies eventfd reads and writes in the kernel
4683    /// (`ReplayFdKind::Eventfd`). `dup3` replaces the placeholder with `other`'s
4684    /// file, which is the live object the recording's file corresponds to: the
4685    /// open file the descriptor table names at `other` in both runs.
4686    pub(crate) async fn install_live_alias<G: Guest<Self>>(
4687        &self,
4688        guest: &mut G,
4689        other: RawFd,
4690        fd: RawFd,
4691        flags: OFlag,
4692    ) -> Result<(), Errno> {
4693        let installed = guest
4694            .inject_with_retry(
4695                syscalls::Dup3::new()
4696                    .with_oldfd(other)
4697                    .with_newfd(fd)
4698                    .with_flags(flags & OFlag::O_CLOEXEC),
4699            )
4700            .await?;
4701        if installed != i64::from(fd) {
4702            warn!("[detcore] replay's alias of {other} landed in {installed}, not {fd}");
4703            return Err(Errno::EBADF);
4704        }
4705        Ok(())
4706    }
4707
4708    /// Marks a socket whose peer is a guest of this container as internal
4709    /// (`DetFd::set_guest_peer`), so record and replay poll a blocking call on
4710    /// it in the caller's turn, as they do on a pipe or a socketpair endpoint,
4711    /// instead of running it outside the turn as external I/O.
4712    ///
4713    /// Outside the turn, the kernel completed such a call at a host-timed
4714    /// moment relative to the other threads' calls. A `recvmsg` that receives
4715    /// descriptors over `SCM_RIGHTS` then took descriptor slots at that moment,
4716    /// while replay, which serves the call from the log, reserves them when the
4717    /// call rejoins the schedule; another thread's open in between took a
4718    /// different slot in each, and replay refused with "replay FD namespace
4719    /// diverged". In the turn, no other guest thread runs between the attempt
4720    /// that receives and the reservation that replays it.
4721    ///
4722    /// The peer's credentials decide. Only `AF_UNIX` stream and seqpacket
4723    /// sockets carry a peer's; Linux reports any other socket's, and a peer
4724    /// outside the recording's PID namespace, as pid 0, which leaves the socket
4725    /// external. So a pid above 0 names a guest only because `hermit record`
4726    /// runs its guests in a PID namespace of their own (`cred_to_ucred` maps
4727    /// the peer's pid into the caller's namespace, `pid_nr_ns`); a recording
4728    /// that shared the host's PID namespace would count a host process's
4729    /// socket as internal, and poll a wait on it in the caller's turn. Every
4730    /// probe goes through the recording, so replay, which holds
4731    /// placeholders, classifies the socket exactly as the recording did. A run
4732    /// without sequentialized threads, or with `--debug-externalize-sockets`,
4733    /// keeps every socket external; a plain run polls every socket already.
4734    pub(crate) async fn classify_socket_peer<G: Guest<Self>>(
4735        &self,
4736        guest: &mut G,
4737        fd: RawFd,
4738        role: SocketRole,
4739    ) {
4740        if !self.cfg.recordreplay_modes || !self.cfg.use_nonblocking_sockets() {
4741            return;
4742        }
4743        match self.mark_guest_peer(guest, fd, role).await {
4744            Ok(true) => trace!("[detcore] socket {fd} has a guest peer: polled as internal"),
4745            Ok(false) => trace!("[detcore] socket {fd} has no guest peer: stays external"),
4746            Err(errno) => {
4747                trace!("[detcore] socket {fd} stays external: peer probe failed with {errno}")
4748            }
4749        }
4750    }
4751
4752    /// [`Self::classify_socket_peer`]'s probes. Returns whether it marked the
4753    /// socket.
4754    async fn mark_guest_peer<G: Guest<Self>>(
4755        &self,
4756        guest: &mut G,
4757        fd: RawFd,
4758        role: SocketRole,
4759    ) -> Result<bool, Errno> {
4760        // A listener reports its own credentials as SO_PEERCRED, not a peer's
4761        // (sock_getsockopt in net/core/sock.c).
4762        if role == SocketRole::Received
4763            && self
4764                .probe_socket_option::<_, libc::c_int>(guest, fd, libc::SO_ACCEPTCONN)
4765                .await?
4766                != 0
4767        {
4768            return Ok(false);
4769        }
4770        let peer: libc::ucred = self
4771            .probe_socket_option(guest, fd, libc::SO_PEERCRED)
4772            .await?;
4773        if peer.pid <= 0 {
4774            return Ok(false);
4775        }
4776        let (physically_nonblocking, status_flags) =
4777            guest.thread_state().with_detfd(fd, |detfd| {
4778                (detfd.physically_nonblocking(), detfd.status_flags())
4779            })?;
4780        if !physically_nonblocking {
4781            // The guest's flags are unchanged: F_GETFL reports the logical ones.
4782            self.record_or_replay(
4783                guest,
4784                syscalls::Fcntl::new()
4785                    .with_fd(fd)
4786                    .with_cmd(F_SETFL(status_flags | OFlag::O_NONBLOCK.bits())),
4787            )
4788            .await?;
4789            guest
4790                .thread_state()
4791                .with_detfd(fd, |detfd| detfd.set_physically_nonblocking())?;
4792        }
4793        guest
4794            .thread_state()
4795            .with_detfd(fd, |detfd| detfd.set_guest_peer())?;
4796        Ok(true)
4797    }
4798
4799    /// Reads the `SOL_SOCKET` option `optname` of `fd` through the recording
4800    /// into a buffer borrowed from the guest stack scratch, whose bytes are put
4801    /// back afterwards, as `inject_stat_on_stack` borrows its buffer.
4802    async fn probe_socket_option<G: Guest<Self>, V: Copy>(
4803        &self,
4804        guest: &mut G,
4805        fd: RawFd,
4806        optname: libc::c_int,
4807    ) -> Result<V, Errno> {
4808        let mut stack = guest.stack().await;
4809        let probe: AddrMut<SocketOptionProbe<V>> = stack.reserve();
4810        let value: AddrMut<V> = probe.cast();
4811        let length = AddrMut::<libc::socklen_t>::from_raw(
4812            probe.as_raw() + std::mem::offset_of!(SocketOptionProbe<V>, length),
4813        )
4814        .ok_or(Errno::EFAULT)?;
4815        // Ptrace lends bytes below the guest's red zone; save them before
4816        // commit can write the reservation.
4817        let mut saved = vec![0u8; std::mem::size_of::<SocketOptionProbe<V>>()];
4818        guest.memory().read_exact(probe.cast(), &mut saved)?;
4819        let guard = match stack.commit() {
4820            Ok(guard) => guard,
4821            Err(errno) => {
4822                restore_socket_option_probe(&mut guest.memory(), probe.cast(), &saved);
4823                return Err(errno);
4824            }
4825        };
4826        let probed = match guest
4827            .memory()
4828            .write_value(length, &(std::mem::size_of::<V>() as libc::socklen_t))
4829        {
4830            Ok(()) => self
4831                .record_or_replay(
4832                    guest,
4833                    syscalls::Getsockopt::new()
4834                        .with_fd(fd)
4835                        .with_level(libc::SOL_SOCKET)
4836                        .with_optname(optname)
4837                        .with_optval(Some(value.cast()))
4838                        .with_optlen(Some(length)),
4839                )
4840                .await
4841                .and_then(|_| guest.memory().read_value(value)),
4842            Err(errno) => Err(errno),
4843        };
4844        restore_socket_option_probe(&mut guest.memory(), probe.cast(), &saved);
4845        drop(guard);
4846        probed
4847    }
4848
4849    pub(crate) async fn release_port_for_open_file<G: Guest<Self>>(
4850        &self,
4851        guest: &mut G,
4852        open_file_id: OpenFileId,
4853    ) -> Option<u16> {
4854        let response = send_and_update_time(guest, GlobalRequest::ReleasePort(open_file_id)).await;
4855        match response.1 {
4856            GlobalResponse::ReleasePort(port) => port,
4857            other => panic!("unexpected release-port response: {other:?}"),
4858        }
4859    }
4860
4861    pub(crate) async fn restore_port_for_open_file<G: Guest<Self>>(
4862        &self,
4863        guest: &mut G,
4864        open_file_id: OpenFileId,
4865        port: u16,
4866    ) {
4867        let response =
4868            send_and_update_time(guest, GlobalRequest::AddUsedPort(port, open_file_id)).await;
4869        match response.1 {
4870            GlobalResponse::AddUsedPort => {}
4871            other => panic!("unexpected restore-port response: {other:?}"),
4872        }
4873    }
4874
4875    /// [`Self::observed_path`], with a `/proc` directory that `dirfd` names
4876    /// taken as the directory it is open on, as Linux looks a relative name up
4877    /// below that directory whoever holds the descriptor: a `/proc/self` or
4878    /// `/proc/thread-self` directory is the task the selector reached when it
4879    /// was opened (`DetFd::procfs_reached`), and a descriptor Detcore has no
4880    /// record of (one received over `SCM_RIGHTS`, which its record or a fresh
4881    /// discovery leaves without one) is named as the kernel names the directory it is
4882    /// open on, where that is on procfs ([`Self::unrecorded_procfs_directory`]).
4883    /// Detcore's own decisions about procfs names (the files it renders, the
4884    /// `--proc` level, the links it canonicalizes) use this spelling. A name
4885    /// below the recorded directory of a task that is gone is ESRCH, and one
4886    /// below a selector link opened as the link itself ENOTDIR, as Linux
4887    /// answers them.
4888    pub(crate) async fn procfs_spelling_at<G: Guest<Self>>(
4889        &self,
4890        guest: &mut G,
4891        dirfd: i32,
4892        path: &Path,
4893    ) -> Result<PathBuf, Error> {
4894        if !path.is_absolute() && !path.as_os_str().is_empty() && dirfd != libc::AT_FDCWD {
4895            let (reached, link_itself) = guest
4896                .thread_state()
4897                .with_detfd(dirfd, |detfd| {
4898                    (detfd.procfs_reached(), detfd.procfs_link_itself())
4899                })
4900                .unwrap_or((None, false));
4901            // A selector link itself is no directory.
4902            if link_itself {
4903                return Err(Errno::ENOTDIR.into());
4904            }
4905            if let Some((reached, incarnation)) = reached {
4906                // The directory's task, while it lives: Linux checks the
4907                // task a pid directory was opened on at every lookup below it
4908                // (`proc_pid_permission`), and a task that is gone, whose
4909                // number another task may hold by now, fails every name with
4910                // ESRCH (measured on Linux 7.1: `cgroup`, `status`, `exe` and
4911                // a name no task has alike).
4912                // TODO-HUMAN-REVIEW(#4213): Review the stale task-directory
4913                // decision: a directory bound to a task identity at its open
4914                // answers ESRCH for every name once that identity no longer
4915                // holds its numbers (`ProcfsTaskAnswer::still_names`).
4916                if let Some(incarnation) = incarnation
4917                    && let Some(task) = procfs_reached_task(&reached)
4918                {
4919                    let answer = crate::tool_global::procfs_task(guest, task).await;
4920                    if !answer.still_names(incarnation) {
4921                        return Err(Errno::ESRCH.into());
4922                    }
4923                }
4924                return Ok(reached.join(path));
4925            }
4926            if let Some(directory) = self.unrecorded_procfs_directory(guest, dirfd).await {
4927                return Ok(directory.join(path));
4928            }
4929        }
4930        Self::prepare_guest_cwd(guest, dirfd, path).await;
4931        Self::observed_path(guest, dirfd, path)
4932    }
4933
4934    /// The procfs directory descriptor `dirfd` is open on, where Detcore has
4935    /// no record of the descriptor, as the guest names it: the kernel's name
4936    /// for it (its `/proc/thread-self/fd/N` link, one injected `readlinkat`),
4937    /// if that is `/proc` or a path below it, with the kernel's numbers of
4938    /// the task it names taken back to the guest's where the two differ (DBT,
4939    /// `Self::translates_procfs_numbers`). Every backend's kernel beneath
4940    /// answers that link for the calling thread's own table: the host kernel
4941    /// on ptrace and DBT, KVM's provider (`guest_fd_path`), which has one
4942    /// table per process. `None` for a descriptor Detcore has a record of, any
4943    /// other object, a task number no task of the guest's holds, and where
4944    /// Detcore does not render procfs files (record and replay, after
4945    /// `chroot`).
4946    ///
4947    /// In every threading mode, `--no-sequentialize-threads` included. For a
4948    /// file Detcore renders, the link is the only observation of the
4949    /// descriptor, read once inside the guest's own call, and the answer comes
4950    /// from that name alone: another thread that replaces the descriptor
4951    /// concurrently is ordered before that read (the call looks below the new
4952    /// directory) or after it (below the old one), and Linux takes the
4953    /// descriptor at one point inside the call too (`path_init`), so each
4954    /// answer is one Linux can give. Without this a relaxed run's received
4955    /// `/proc` would leave its mount tables to the backend, which keeps no copy
4956    /// (KVM).
4957    ///
4958    /// Where the name only guides a call the kernel then resolves through the
4959    /// same descriptor, there are two observations, and without
4960    /// sequentialized threads another thread can replace the descriptor
4961    /// between them. A directory opened below it gets procfs provenance only
4962    /// if the descriptor the kernel returned holds a directory below `/proc`
4963    /// (`handle_openat`). The `--proc` level's refusal of an omitted name below
4964    /// it is best-effort there for `stat`, `access` and `readlink`, which have
4965    /// no check after the kernel's lookup: a residual of the relaxed mode,
4966    /// narrower than before, when no level applied below a received descriptor
4967    /// in that mode at all.
4968    pub(crate) async fn unrecorded_procfs_directory<G: Guest<Self>>(
4969        &self,
4970        guest: &mut G,
4971        dirfd: i32,
4972    ) -> Option<PathBuf> {
4973        if dirfd < 0 || !self.renders_procfs(guest) {
4974            return None;
4975        }
4976        let recorded = guest
4977            .thread_state()
4978            .with_detfd(dirfd, |detfd| detfd.path())
4979            .unwrap_or(None);
4980        if recorded.is_some() {
4981            return None;
4982        }
4983        const LINK_CAPACITY: usize = 40;
4984        const TARGET_CAPACITY: usize = 256;
4985        let mut stack = guest.stack().await;
4986        let link = stack.push(rendered_procfs::c_string::<LINK_CAPACITY>(
4987            format!("/proc/thread-self/fd/{dirfd}").as_bytes(),
4988        ));
4989        let target = stack.reserve::<[u8; TARGET_CAPACITY]>();
4990        let stack_guard = stack.commit().ok()?;
4991        let length = guest
4992            .inject(
4993                syscalls::Readlinkat::new()
4994                    .with_dirfd(libc::AT_FDCWD)
4995                    .with_path(PathPtr::from_ptr(link.as_raw() as *const libc::c_char))
4996                    .with_buf(Some(target.cast::<libc::c_char>()))
4997                    .with_buf_len(TARGET_CAPACITY),
4998            )
4999            .await;
5000        let bytes = match length.map(|length| length as usize) {
5001            Ok(length) if length < TARGET_CAPACITY => {
5002                let mut bytes = vec![0; length];
5003                guest
5004                    .memory()
5005                    .read_exact(target.cast::<u8>(), &mut bytes)
5006                    .ok()
5007                    .map(|()| bytes)
5008            }
5009            _ => None,
5010        };
5011        drop(stack_guard);
5012        let text = String::from_utf8(bytes?).ok()?;
5013        // The placeholder directory Detcore serves `/proc` and descriptor
5014        // directories on, which no guest path reaches: the directory the
5015        // descriptor stands for (`descriptor_directory`).
5016        if guest
5017            .config()
5018            .directory_placeholder
5019            .as_deref()
5020            .is_some_and(|placeholder| placeholder.as_os_str() == std::ffi::OsStr::new(&text))
5021        {
5022            return Self::unrecorded_placeholder_base(guest, dirfd).await;
5023        }
5024        let (mut names, positions) = procfs_directory_task_positions(&text)?;
5025        if Self::translates_procfs_numbers(guest) && !positions.is_empty() {
5026            let kernel = positions
5027                .iter()
5028                .map(|position| names[*position].parse::<i32>().ok())
5029                .collect::<Option<Vec<_>>>()?;
5030            let translated = crate::tool_global::procfs_ids(guest, false, kernel).await;
5031            for (position, number) in positions.iter().zip(translated) {
5032                names[*position] = number?.to_string();
5033            }
5034        }
5035        let mut directory = PathBuf::from("/proc");
5036        directory.extend(names);
5037        Some(directory)
5038    }
5039
5040    /// Whether a decision about `path`, looked up relative to `dirfd`, reads
5041    /// the guest's working directory from the guest
5042    /// ([`Self::prepare_guest_cwd`]): a nonempty spelling relative to the
5043    /// working directory (`AT_FDCWD`), where the tool cannot read the working
5044    /// directory from host procfs
5045    /// ([`crate::procfs_inode::tool_can_read_guest_cwd`]: KVM). An absolute
5046    /// spelling is bound already, and a directory descriptor supplies its own
5047    /// recorded path. The empty spelling (`AT_EMPTY_PATH`) is the working
5048    /// directory itself, which no decision classifies (`Detcore::decide`
5049    /// takes it for no name), so it is kept as given
5050    /// ([`Self::observed_path`]); so is a spelling `PATH_MAX` bytes long or
5051    /// more, which Linux refuses before any walk ([`names_past_path_max`]).
5052    fn reads_guest_cwd<G: Guest<Self>>(guest: &G, dirfd: i32, path: &Path) -> bool {
5053        dirfd == libc::AT_FDCWD
5054            && !path.is_absolute()
5055            && !path.as_os_str().is_empty()
5056            && !names_past_path_max(path)
5057            && !crate::procfs_inode::tool_can_read_guest_cwd(&guest.config().backend)
5058    }
5059
5060    /// Read what a decision about `path` needs of the calling thread's
5061    /// working directory for the syscall it is running, where
5062    /// [`Self::reads_guest_cwd`] says it reads it from the guest, before the
5063    /// decision, which does not await ([`Self::observed_path`]): the
5064    /// directory's path, once per syscall
5065    /// ([`Self::read_guest_cwd_for_syscall`]); and where that has none (a
5066    /// removed directory, or one whose path is longer than `PATH_MAX`), the
5067    /// directory the spelling climbs to out of it with `..`, once per number
5068    /// of `..` ([`Self::read_climbed_directory`]). Nothing happens elsewhere,
5069    /// so no other spelling, and no backend whose tool reads the working
5070    /// directory from host procfs, makes a call for it.
5071    ///
5072    /// The calls it injects stage their buffers in the guest stack scratch,
5073    /// which KVM gives out once at a time and refuses with a panic while
5074    /// taken (Reverie's `KvmStack`); so it must run before its caller takes
5075    /// that scratch, never while the caller holds it. A caller that holds the
5076    /// scratch across a call whose translation may reach it prepares the
5077    /// spelling first (`Detcore::whole_link`), which leaves nothing for it to
5078    /// read again.
5079    pub(crate) async fn prepare_guest_cwd<G: Guest<Self>>(guest: &mut G, dirfd: i32, path: &Path) {
5080        if !Self::reads_guest_cwd(guest, dirfd, path) {
5081            return;
5082        }
5083        if guest.thread_state().syscall_cwd.is_none() {
5084            Box::pin(Self::read_guest_cwd_for_syscall(guest)).await;
5085        }
5086        let (climbs, _) = split_leading_climb(path);
5087        let unread = climbs > 0
5088            && matches!(
5089                &guest.thread_state().syscall_cwd,
5090                Some(SyscallCwd::Unrendered(climbed)) if !climbed.contains_key(&climbs)
5091            );
5092        if unread {
5093            let climbed = Box::pin(Self::read_climbed_directory(guest, climbs)).await;
5094            if let Some(SyscallCwd::Unrendered(paths)) = &mut guest.thread_state_mut().syscall_cwd {
5095                paths.insert(climbs, climbed);
5096            }
5097        }
5098    }
5099
5100    /// The calling thread's working directory as its own `getcwd` answers
5101    /// it, read once for the syscall it is running
5102    /// ([`ThreadState::syscall_cwd`]): one `getcwd` injected in the thread,
5103    /// its buffer staged by [`crate::syscalls::scratch::inject_with_scratch`]
5104    /// (the guest stack scratch, whose bytes are put back after the call, or
5105    /// a transient private page where that scratch cannot hold the buffer).
5106    /// On KVM that scratch is not the guest's stack but the Tool's own page in
5107    /// the boot-reserved layout, open to user access only while a Tool
5108    /// callback runs (Reverie's `KvmStack`), and `PATH_MAX` bytes fill it
5109    /// exactly, so no guest memory holds the buffer and no page is mapped
5110    /// for it.
5111    ///
5112    /// A `getcwd` that fails with `ENOENT` (the directory was removed) or
5113    /// with `ENAMETOOLONG` or `ERANGE` (its path is longer than `PATH_MAX`)
5114    /// leaves the directory unrendered, as Linux leaves it walkable: a
5115    /// spelling that climbs out of it with `..` is then decided from the
5116    /// directory it climbs to ([`Self::prepare_guest_cwd`]). One that fails
5117    /// otherwise, or that answers no path from the root (its "(unreachable)"
5118    /// prefix), leaves every spelling to be decided as given, as a spelling
5119    /// Detcore could not resolve always was. Called through
5120    /// [`Self::prepare_guest_cwd`], boxed, so that its buffers do not grow
5121    /// every system call's future.
5122    async fn read_guest_cwd_for_syscall<G: Guest<Self>>(guest: &mut G) {
5123        const PATH_MAX: usize = libc::PATH_MAX as usize;
5124        let answered =
5125            crate::syscalls::scratch::inject_with_scratch(guest, [0_u8; PATH_MAX], |buffer| {
5126                Syscall::from(
5127                    syscalls::Getcwd::new()
5128                        .with_buf(Some(buffer.cast::<libc::c_char>()))
5129                        .with_size(PATH_MAX),
5130                )
5131            })
5132            .await;
5133        let cwd = match answered {
5134            // The length counts the NUL.
5135            Ok((length, bytes)) => usize::try_from(length)
5136                .ok()
5137                .filter(|length| (1..=PATH_MAX).contains(length))
5138                .map(|length| &bytes[..length - 1])
5139                .filter(|path| path.starts_with(b"/"))
5140                .map_or(SyscallCwd::Unknown, |path| {
5141                    SyscallCwd::Path(PathBuf::from(
5142                        <std::ffi::OsStr as std::os::unix::ffi::OsStrExt>::from_bytes(path),
5143                    ))
5144                }),
5145            Err(Errno::ENOENT | Errno::ENAMETOOLONG | Errno::ERANGE) => {
5146                SyscallCwd::Unrendered(BTreeMap::new())
5147            }
5148            Err(_) => SyscallCwd::Unknown,
5149        };
5150        guest.thread_state_mut().syscall_cwd = Some(cwd);
5151    }
5152
5153    /// The path of the directory `climbs` `..` lead to from the calling
5154    /// thread's working directory, where `getcwd` renders none for it (a
5155    /// removed directory, or one whose path is longer than `PATH_MAX`), read
5156    /// through a descriptor for that directory, three calls injected in the
5157    /// thread: an `O_PATH` open of `..` (`climbs` of them), which the
5158    /// executor walks from the working directory itself, as Linux's walk
5159    /// leaves a removed directory for its parent; a `readlinkat` of that
5160    /// descriptor's link (`/proc/thread-self/fd/<it>`, which the executor
5161    /// answers from its own descriptor, as for
5162    /// `Detcore::received_descriptor_link`); and the descriptor's `close`,
5163    /// before the guest runs again, so the guest's next descriptor number
5164    /// does not move. Each call's path and buffer are staged as
5165    /// [`Self::read_guest_cwd_for_syscall`] stages `getcwd`'s. `None` where a
5166    /// call fails, or where the link fills its buffer and may be cut.
5167    async fn read_climbed_directory<G: Guest<Self>>(
5168        guest: &mut G,
5169        climbs: usize,
5170    ) -> Option<PathBuf> {
5171        const PATH_MAX: usize = libc::PATH_MAX as usize;
5172        const LINK_PATH: usize = 64;
5173        const LINK_TARGET: usize = PATH_MAX - LINK_PATH;
5174        let climbed = climb_spelling(climbs);
5175        if climbed.len() >= PATH_MAX {
5176            return None;
5177        }
5178        let mut spelled = [0_u8; PATH_MAX];
5179        spelled[..climbed.len()].copy_from_slice(climbed.as_bytes());
5180        let flags = OFlag::O_PATH | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC;
5181        let (opened, _) = crate::syscalls::scratch::inject_with_scratch(guest, spelled, |base| {
5182            syscalls::Openat::new()
5183                .with_dirfd(libc::AT_FDCWD)
5184                .with_path(PathPtr::from_ptr(base.as_raw() as *const libc::c_char))
5185                .with_flags(flags)
5186                .with_mode(None)
5187                .into()
5188        })
5189        .await
5190        .ok()?;
5191        let fd = RawFd::try_from(opened).ok()?;
5192        let mut buffer = [0_u8; LINK_PATH + LINK_TARGET];
5193        let link = format!("/proc/thread-self/fd/{fd}");
5194        buffer[..link.len()].copy_from_slice(link.as_bytes());
5195        let linked = crate::syscalls::scratch::inject_with_scratch(guest, buffer, |base| {
5196            // SAFETY: the buffer holds LINK_PATH + LINK_TARGET bytes.
5197            let target = unsafe { base.add(LINK_PATH) };
5198            syscalls::Readlinkat::new()
5199                .with_dirfd(libc::AT_FDCWD)
5200                .with_path(PathPtr::from_ptr(base.as_raw() as *const libc::c_char))
5201                .with_buf(Some(target.cast::<libc::c_char>()))
5202                .with_buf_len(LINK_TARGET)
5203                .into()
5204        })
5205        .await;
5206        let _ = guest.inject(syscalls::Close::new().with_fd(fd)).await;
5207        let (length, bytes) = linked.ok()?;
5208        let length = usize::try_from(length)
5209            .ok()
5210            .filter(|length| *length < LINK_TARGET)?;
5211        let path = &bytes[LINK_PATH..LINK_PATH + length];
5212        path.starts_with(b"/").then(|| {
5213            PathBuf::from(<std::ffi::OsStr as std::os::unix::ffi::OsStrExt>::from_bytes(path))
5214        })
5215    }
5216
5217    /// `path`, as a guest syscall named it relative to `dirfd`, made
5218    /// absolute in the guest's view. A relative spelling is not the object:
5219    /// absolute paths are already bound, a dirfd supplies its own prefix, and
5220    /// AT_FDCWD-relative spellings are resolved through the guest's root and
5221    /// cwd, so the result does not depend on Replayer's placeholder
5222    /// descriptor. A spelling that cannot be resolved is kept as given.
5223    ///
5224    /// Where the tool cannot read the guest's working directory from host
5225    /// procfs ([`crate::procfs_inode::tool_can_read_guest_cwd`]: KVM, whose
5226    /// guest process IDs name the supervisor's own threads), the working
5227    /// directory is the one the guest's `getcwd` answered for this syscall,
5228    /// which an async caller reads first ([`Self::prepare_guest_cwd`]). Where
5229    /// `getcwd` answered none (a removed directory, or one whose path is
5230    /// longer than `PATH_MAX`), a spelling whose leading `..` climb out of it
5231    /// is made absolute from the directory they reach, as the executor's own
5232    /// walk named it ([`Self::read_climbed_directory`]), the rest of the
5233    /// spelling following byte for byte ([`split_leading_climb`]). Any other
5234    /// spelling, and one whose climb failed, is kept as given, so that the
5235    /// call's own walk decides it, with its own error.
5236    ///
5237    /// A spelling relative to the working directory that is `PATH_MAX` bytes
5238    /// long or more is kept as given before anything is read or reused for
5239    /// it, on every backend: Linux refuses it with `ENAMETOOLONG` before any
5240    /// walk ([`names_past_path_max`]), and a climb would shorten it into a
5241    /// name Detcore serves.
5242    pub(crate) fn observed_path<G: Guest<Self>>(
5243        guest: &G,
5244        dirfd: i32,
5245        path: &Path,
5246    ) -> Result<PathBuf, Error> {
5247        // An absolute spelling is bound already; a relative one of PATH_MAX
5248        // bytes or more names nothing, and the call refuses it.
5249        let as_given = path.is_absolute() || (dirfd == libc::AT_FDCWD && names_past_path_max(path));
5250        Ok(if as_given {
5251            path.to_path_buf()
5252        } else if dirfd == libc::AT_FDCWD {
5253            if crate::procfs_inode::tool_can_read_guest_cwd(&guest.config().backend) {
5254                resolved_at_fdcwd_path(guest.pid().as_raw(), path)
5255                    .unwrap_or_else(|| path.to_path_buf())
5256            } else {
5257                match &guest.thread_state().syscall_cwd {
5258                    _ if path.as_os_str().is_empty() => path.to_path_buf(),
5259                    Some(SyscallCwd::Path(cwd)) => cwd.join(path),
5260                    Some(SyscallCwd::Unrendered(climbed)) => {
5261                        let (climbs, rest) = split_leading_climb(path);
5262                        match climbed.get(&climbs) {
5263                            Some(Some(directory)) if climbs > 0 => joined_verbatim(directory, rest),
5264                            _ => path.to_path_buf(),
5265                        }
5266                    }
5267                    _ => path.to_path_buf(),
5268                }
5269            }
5270        } else {
5271            // A descriptor Detcore has no state for (one received over
5272            // `SCM_RIGHTS` and not recorded,
5273            // `Detcore::record_received_descriptors`) keeps the relative spelling, which
5274            // the kernel looks up from it; the kernel, not this lookup,
5275            // decides whether the descriptor is open. Before, such an `openat`
5276            // failed with EBADF though the descriptor was open.
5277            guest
5278                .thread_state()
5279                .with_detfd(dirfd, |detfd| detfd.path())
5280                .unwrap_or(None)
5281                .map_or_else(|| path.to_path_buf(), |directory| directory.join(path))
5282        })
5283    }
5284
5285    /// For `hermit run --verify`, report each path `call` may rebind in the
5286    /// guest's file namespace: both names of a rename, the new name of a link
5287    /// or symlink, an unlinked name, and a node, directory or file it makes
5288    /// or removes (see `detcore_model::host_input::HostMutationRecord`). A
5289    /// host file whose path the guest rebinds may have been replaced by the
5290    /// guest itself. Called before the call runs, whatever its outcome.
5291    pub(crate) async fn record_host_namespace_change<G: Guest<Self>>(
5292        &self,
5293        guest: &mut G,
5294        call: &Syscall,
5295    ) {
5296        #[cfg(not(target_arch = "aarch64"))]
5297        let at = libc::AT_FDCWD;
5298        let changed: Vec<(i32, Option<syscalls::PathPtr>)> = match call {
5299            #[cfg(not(target_arch = "aarch64"))]
5300            Syscall::Rename(c) => vec![(at, c.oldpath()), (at, c.newpath())],
5301            Syscall::Renameat(c) => vec![(c.olddirfd(), c.oldpath()), (c.newdirfd(), c.newpath())],
5302            Syscall::Renameat2(c) => {
5303                vec![(c.olddirfd(), c.oldpath()), (c.newdirfd(), c.newpath())]
5304            }
5305            #[cfg(not(target_arch = "aarch64"))]
5306            Syscall::Link(c) => vec![(at, c.newpath())],
5307            Syscall::Linkat(c) => vec![(c.newdirfd(), c.newpath())],
5308            #[cfg(not(target_arch = "aarch64"))]
5309            Syscall::Unlink(c) => vec![(at, c.path())],
5310            Syscall::Unlinkat(c) => vec![(c.dirfd(), c.path())],
5311            #[cfg(not(target_arch = "aarch64"))]
5312            Syscall::Symlink(c) => vec![(at, c.linkpath())],
5313            Syscall::Symlinkat(c) => vec![(c.newdirfd(), c.linkpath())],
5314            #[cfg(not(target_arch = "aarch64"))]
5315            Syscall::Mknod(c) => vec![(at, c.path())],
5316            Syscall::Mknodat(c) => vec![(c.dirfd(), c.path())],
5317            #[cfg(not(target_arch = "aarch64"))]
5318            Syscall::Mkdir(c) => vec![(at, c.path())],
5319            Syscall::Mkdirat(c) => vec![(c.dirfd(), c.path())],
5320            #[cfg(not(target_arch = "aarch64"))]
5321            Syscall::Rmdir(c) => vec![(at, c.path())],
5322            #[cfg(not(target_arch = "aarch64"))]
5323            Syscall::Creat(c) => vec![(at, c.path())],
5324            _ => return,
5325        };
5326        for (dirfd, path) in changed {
5327            let observed = match path.map(|path| path.read(&guest.memory())) {
5328                Some(Ok(path)) => {
5329                    let path: PathBuf = path;
5330                    Self::prepare_guest_cwd(guest, dirfd, &path).await;
5331                    Self::observed_path(guest, dirfd, &path).ok()
5332                }
5333                _ => None,
5334            };
5335            crate::tool_global::record_host_mutation(guest, rebound_path(observed)).await;
5336        }
5337    }
5338
5339    /// The host inodes whose last name `call` would remove: the name an
5340    /// `unlink`, `unlinkat` or `rmdir` removes, or the target a `rename`,
5341    /// `renameat` or `renameat2` replaces (not with `RENAME_EXCHANGE`), when
5342    /// it is a directory or a file with one link (see [`retires_on_removal`]).
5343    /// Each name is stat'ed with an injected `fstatat(AT_SYMLINK_NOFOLLOW)`
5344    /// before the call runs; a name that does not resolve removes nothing.
5345    /// Pass the result to [`Self::retire_removed_inodes`] if the call
5346    /// succeeds. Only with virtualized metadata, the only mode that numbers
5347    /// inodes.
5348    ///
5349    /// Nothing is retired without sequentialized threads. Only then do the
5350    /// lookups, the call and the retirement run in one turn; otherwise another
5351    /// thread can link, unlink or rename the file in between, and a file that
5352    /// keeps a name could be retired and renumbered (codex review of
5353    /// <https://github.com/rrnewton/hermit/pull/3849>, P2). Without them inode
5354    /// numbers behave as before retirement existed.
5355    pub(crate) async fn last_names_removed_by<G: Guest<Self>>(
5356        &self,
5357        guest: &mut G,
5358        call: &Syscall,
5359    ) -> Vec<RawInode> {
5360        if !guest.config().virtualize_metadata || !guest.config().sequentialize_threads {
5361            return Vec::new();
5362        }
5363        // The name removed, and for a rename the source, which replaces
5364        // nothing when it is the target's own file (`rename(a, a)`, or two
5365        // links of one file).
5366        let Some((removed, source)) = removed_names(call, true) else {
5367            return Vec::new();
5368        };
5369        // Every path the call reads, which the lookups must leave unchanged.
5370        let inputs = [removed.1, source.and_then(|source| source.1)];
5371        let Some(path) = removed.1 else {
5372            return Vec::new();
5373        };
5374        let Ok(stat) = Self::inject_lstatat(guest, removed.0, path, inputs).await else {
5375            return Vec::new();
5376        };
5377        let inode = RawInode::new(stat.st_dev, stat.st_ino);
5378        if let Some((dirfd, Some(path))) = source
5379            && Self::inject_lstatat(guest, dirfd, path, inputs)
5380                .await
5381                .is_ok_and(|source| RawInode::new(source.st_dev, source.st_ino) == inode)
5382        {
5383            return Vec::new();
5384        }
5385        if retires_on_removal(&stat) {
5386            vec![inode]
5387        } else {
5388            Vec::new()
5389        }
5390    }
5391
5392    /// The guest-made FIFO (`crate::fifo_peer::is_guest_made`) whose last
5393    /// name `call` removes: the name an `unlink` or `unlinkat` removes, or the
5394    /// target a `rename`, `renameat` or `renameat2` replaces (not with
5395    /// `RENAME_EXCHANGE`), when it is that FIFO's only link
5396    /// ([`retires_on_removal`]) and not the rename's own source (another link
5397    /// of the same FIFO, or `rename(a, a)`). Stat'ed before the call runs; pass
5398    /// the result to `crate::fifo_peer::forget_guest_made` if the call
5399    /// succeeds, so that a FIFO that later reuses the number is not taken for
5400    /// a guest's, while a FIFO that keeps a name stays a guest's. Only where
5401    /// the set is shared by every guest (`fifo_peer::guest_descriptors_are_visible`).
5402    pub(crate) fn guest_made_fifos_removed_by<'a, G: Guest<Self>>(
5403        &'a self,
5404        guest: &'a mut G,
5405        call: &'a Syscall,
5406    ) -> impl std::future::Future<Output = Vec<(u64, u64)>> + 'a {
5407        // The run's set, through no wrapper future of its own.
5408        self.guest_made_fifos_removed_from(guest, call, &crate::fifo_peer::GuestMadeRegistry)
5409    }
5410
5411    /// [`Self::guest_made_fifos_removed_by`] against the guest-made FIFOs
5412    /// `made`: the run's registry in production, a test's own set in tests,
5413    /// which so leave the process-wide registry untouched.
5414    pub(crate) async fn guest_made_fifos_removed_from<G, M>(
5415        &self,
5416        guest: &mut G,
5417        call: &Syscall,
5418        made: &M,
5419    ) -> Vec<(u64, u64)>
5420    where
5421        G: Guest<Self>,
5422        M: crate::fifo_peer::GuestMadeFifos + Sync,
5423    {
5424        // The call's number, then the backend capability, both lock-free,
5425        // before the registry's lock: most calls remove no name, and where
5426        // the capability is false (KVM, DBT) the set is never consulted. The
5427        // three conjuncts are pure, so their order changes no answer.
5428        if !removes_a_name(call.number())
5429            || !crate::fifo_peer::guest_descriptors_are_visible(
5430                &self.cfg.backend,
5431                guest.thread_state().physical_tid,
5432            )
5433            || !made.any()
5434        {
5435            return Vec::new();
5436        }
5437        let Some((removed, source)) = removed_names(call, false) else {
5438            return Vec::new();
5439        };
5440        let inputs = [removed.1, source.and_then(|source| source.1)];
5441        let Some(path) = removed.1 else {
5442            return Vec::new();
5443        };
5444        let Ok(stat) = Self::inject_lstatat(guest, removed.0, path, inputs).await else {
5445            return Vec::new();
5446        };
5447        let fifo = (stat.st_dev, stat.st_ino);
5448        if stat.st_mode & libc::S_IFMT != libc::S_IFIFO
5449            || !made.contains(fifo)
5450            || !retires_on_removal(&stat)
5451        {
5452            return Vec::new();
5453        }
5454        if let Some((dirfd, Some(path))) = source
5455            && Self::inject_lstatat(guest, dirfd, path, inputs)
5456                .await
5457                .is_ok_and(|source| (source.st_dev, source.st_ino) == fifo)
5458        {
5459            return Vec::new();
5460        }
5461        vec![fifo]
5462    }
5463
5464    /// After a successful `mknod` or `mknodat` that made a FIFO, record it as
5465    /// a guest's (`crate::fifo_peer::note_guest_made`), stat'ed at the name the
5466    /// call made, in the same turn. Only where a blocking open of it can wait
5467    /// in polling turns (sequentialized `hermit run` under a separate tracer
5468    /// whose guest thread IDs are host tasks).
5469    pub(crate) async fn note_guest_made_fifo<G: Guest<Self>>(&self, guest: &mut G, call: &Syscall) {
5470        // Only where one set serves every guest, as `fifo_at` requires.
5471        if !self.cfg.use_nonblocking_sockets()
5472            || self.cfg.recordreplay_modes
5473            || !crate::fifo_peer::guest_descriptors_are_visible(
5474                &self.cfg.backend,
5475                guest.thread_state().physical_tid,
5476            )
5477        {
5478            return;
5479        }
5480        let (dirfd, path, mode) = match call {
5481            #[cfg(not(target_arch = "aarch64"))]
5482            Syscall::Mknod(c) => (libc::AT_FDCWD, c.path(), c.mode().bits()),
5483            Syscall::Mknodat(c) => (c.dirfd(), c.path(), c.mode().bits()),
5484            _ => return,
5485        };
5486        if mode & libc::S_IFMT != libc::S_IFIFO {
5487            return;
5488        }
5489        let Some(path) = path else {
5490            return;
5491        };
5492        if let Ok(stat) = Self::inject_lstatat(guest, dirfd, path, [Some(path), None]).await
5493            && stat.st_mode & libc::S_IFMT == libc::S_IFIFO
5494        {
5495            crate::fifo_peer::note_guest_made((stat.st_dev, stat.st_ino));
5496        }
5497    }
5498
5499    /// Retire the mappings of `inodes`, whose last names a successful call
5500    /// removed (see [`Self::last_names_removed_by`] and `InodePool::retire`).
5501    pub(crate) async fn retire_removed_inodes<G: Guest<Self>>(
5502        &self,
5503        guest: &mut G,
5504        inodes: Vec<RawInode>,
5505    ) {
5506        for inode in inodes {
5507            retire_inode(guest, inode).await;
5508        }
5509    }
5510
5511    /// For `hermit run --verify` on a backend whose guest holds code that
5512    /// makes untraced syscalls (`Config::untraced_code_range`): report `/`
5513    /// when `call` may change the memory holding it (see
5514    /// [`may_change_untraced_code`]). Intact, that code faults right after
5515    /// its syscall, and the fault is reported (`handle_signal_event`); a
5516    /// guest that rewrote or replaced it could use it without faulting, so
5517    /// Detcore could not establish what the guest changed, and `/`, the
5518    /// ancestor of every path, keeps a host input change from being named
5519    /// in that run. Called before the call runs, whatever its outcome.
5520    pub(crate) async fn record_untraced_code_change<G: Guest<Self>>(
5521        &self,
5522        guest: &mut G,
5523        call: &Syscall,
5524    ) {
5525        let Some(range) = guest.config().untraced_code_range else {
5526            return;
5527        };
5528        let (number, args) = call.into_parts();
5529        if may_change_untraced_code(number, &args, range, default_huge_page_size()) {
5530            crate::tool_global::record_host_mutation(guest, String::from("/")).await;
5531        }
5532    }
5533
5534    /// Open an `exe` link, by `call`, as the image Detcore recorded for the
5535    /// process it reaches, `exe` (`Detcore::launched_exe`): Linux opens the
5536    /// image the link names, which the kernel's link here does not (it names
5537    /// the launcher). The kernel first opens the guest's spelling itself as
5538    /// a path (`O_PATH`), so whether the link is there and every error stay
5539    /// its own (an exited thread-group leader's `ENOENT`). Without a record,
5540    /// or for an image no longer at its path, which only the kernel's link to
5541    /// the image could still open, the run is refused by name.
5542    async fn open_launched_exe<G: Guest<Self>>(
5543        &self,
5544        guest: &mut G,
5545        call: syscalls::Openat,
5546        exe: Option<&crate::exec_record::ExecRecord>,
5547    ) -> Result<i64, Error> {
5548        // The longest path Linux opens, NUL included.
5549        const CAPACITY: usize = libc::PATH_MAX as usize;
5550        let probe = syscalls::Openat::new()
5551            .with_dirfd(call.dirfd())
5552            .with_path(call.path())
5553            .with_flags(OFlag::O_PATH | OFlag::O_CLOEXEC);
5554        let probed = Self::inject_translated(guest, Syscall::from(probe)).await?;
5555        let _ = guest
5556            .inject(syscalls::Close::new().with_fd(probed as RawFd))
5557            .await;
5558        let present = exe.filter(|exe| {
5559            self.guest_root(guest)
5560                .is_some_and(|root| exe.is_present(&root))
5561        });
5562        let Some(exe) = present else {
5563            return self.refuse_launched_exe(guest, Sysno::openat, exe).await;
5564        };
5565        let bytes = std::os::unix::ffi::OsStrExt::as_bytes(exe.exe.as_os_str());
5566        let mut stack = guest.stack().await;
5567        if bytes.len() >= CAPACITY || stack.capacity() < CAPACITY {
5568            drop(stack);
5569            let reason = format!(
5570                "unsupported: opening the /proc/<pid>/exe link of {:?}, whose path does not \
5571                 fit the guest stack, where the kernel's link names the launcher",
5572                exe.exe
5573            );
5574            return self
5575                .refuse_launched_exe_because(guest, Sysno::openat, reason)
5576                .await;
5577        }
5578        let mut buffer = [0_u8; CAPACITY];
5579        buffer[..bytes.len()].copy_from_slice(bytes);
5580        let address = stack.push(buffer).cast::<libc::c_char>();
5581        let stack_guard = stack.commit()?;
5582        let call = call.with_path(PathPtr::from_ptr(address.as_raw() as *const libc::c_char));
5583        let result = self.record_or_replay(guest, Syscall::Openat(call)).await;
5584        drop(stack_guard);
5585        Ok(result?)
5586    }
5587
5588    /// Whether guest descriptor `fd` is open on the launcher a launched run
5589    /// declared (`Detcore::launched_exe`), read where the tool shares the
5590    /// guest's descriptor table, as a launcher's tool does. Asked only for an
5591    /// open of a possible `exe` link
5592    /// (`crate::syscalls::namespace::exe_link_candidate`).
5593    fn opened_the_launcher(&self, fd: RawFd) -> bool {
5594        use std::os::unix::fs::MetadataExt;
5595        let Some(launcher) = self.declared_launcher() else {
5596            return false;
5597        };
5598        if !self.cfg.backend.tool_shares_guest_descriptor_table {
5599            return false;
5600        }
5601        let mut opened: libc::stat = unsafe { std::mem::zeroed() };
5602        // Runtime-owned read-only inspection of the shared descriptor table.
5603        let result = unsafe {
5604            crate::util::raw_syscall(
5605                libc::SYS_fstat,
5606                [fd as u64, (&raw mut opened) as u64, 0, 0, 0, 0],
5607            )
5608        };
5609        result == 0
5610            && std::fs::metadata(launcher)
5611                .is_ok_and(|meta| (meta.dev(), meta.ino()) == (opened.st_dev, opened.st_ino))
5612    }
5613
5614    /// Openat system call.
5615    pub async fn handle_openat<G: Guest<Self>>(
5616        &self,
5617        guest: &mut G,
5618        call: syscalls::Openat,
5619    ) -> Result<i64, Error> {
5620        let path = call.path().ok_or(Errno::EFAULT)?;
5621        let path: PathBuf = path.read(&guest.memory())?;
5622        // A relative spelling is not the object. `chdir("/sys/module/kvm");
5623        // open("refcnt")` and an absolute open name the SAME kernel object, so
5624        // classifying the unresolved lexical pathname lets one spelling bypass
5625        // normalization and expose the host value. Absolute paths are already
5626        // bound; a dirfd supplies its own prefix; AT_FDCWD-relative spellings
5627        // are resolved through the guest's root and cwd before the open so the
5628        // result does not depend on Replayer's placeholder descriptor.
5629        Self::prepare_guest_cwd(guest, call.dirfd(), &path).await;
5630        let observed_path = Self::observed_path(guest, call.dirfd(), &path)?;
5631        // The spelling Detcore's own lookups of the files it renders see.
5632        let procfs_spelling = self.procfs_spelling_at(guest, call.dirfd(), &path).await?;
5633        // A path below a descriptor directory Detcore serves is decided about
5634        // as the path Linux resolves it to, which its injection respells it to
5635        // (`descriptor_directory`).
5636        let below_descriptor_directory =
5637            Self::path_below_descriptor_directory(guest, call.dirfd(), &path, &observed_path);
5638        // The guest's own spelling is kept for the open of a descriptor
5639        // directory, which counts the links the spelling leads through.
5640        let (observed_path, procfs_spelling, guest_spelled) = match &below_descriptor_directory {
5641            Some(below) => (
5642                below.clone(),
5643                below.clone(),
5644                Some((observed_path, procfs_spelling)),
5645            ),
5646            None => (observed_path, procfs_spelling, None),
5647        };
5648        crate::race_probe::delay(crate::race_probe::PROCFS_SPELLING_RESOLVED).await;
5649        if guest.config().record_host_inputs && call.flags().contains(OFlag::O_CREAT) {
5650            // It may create the file it opens. Reported before the open runs,
5651            // as `record_host_namespace_change` reports the other changes.
5652            crate::tool_global::record_host_mutation(
5653                guest,
5654                rebound_path(Some(observed_path.clone())),
5655            )
5656            .await;
5657        }
5658
5659        let resource = ResourceID::Path(path.clone());
5660        // Ask for permission to resolve this path into a file:
5661        let request = guest.thread_state().mk_request(resource, Permission::R);
5662        resource_request(guest, request).await;
5663        // An empty path names nothing to `openat`, which has no
5664        // `AT_EMPTY_PATH`: the kernel's ENOENT.
5665        // A writable open of a `..` spelling that may name a `comm` Detcore
5666        // renders, which it cannot resolve, is refused by name rather than
5667        // forwarded (`Detcore::refuse_undetermined_comm_write`).
5668        let rendered = match if path.as_os_str().is_empty() {
5669            Ok(None)
5670        } else {
5671            match self
5672                .refuse_undetermined_comm_write(guest, &procfs_spelling, call.flags())
5673                .await
5674            {
5675                Ok(()) => self.rendered_procfs_at(guest, &procfs_spelling).await,
5676                Err(error) => Err(error),
5677            }
5678        } {
5679            Ok(rendered) => rendered,
5680            Err(error) => {
5681                resource_release_all(guest).await;
5682                // Flags `build_open_flags` refuses fail before the lookup.
5683                return Err(
5684                    rendered_procfs::open_flags_refused(call.flags()).map_or(error, Into::into)
5685                );
5686            }
5687        };
5688        // `O_PATH|O_NOFOLLOW` opens a descriptor link itself, which the
5689        // kernel has. Every other open of a rendered file is Detcore's, an
5690        // open of a rendered `comm` for writing included, which it refuses
5691        // with no injected call (`open_rendered_procfs`,
5692        // `rendered_procfs::asks_to_write_comm`).
5693        if let Some(target) = rendered.filter(|target| {
5694            !(target.via_link && call.flags().contains(OFlag::O_PATH | OFlag::O_NOFOLLOW))
5695        }) {
5696            let result = self.open_rendered_procfs(guest, call.flags(), target).await;
5697            resource_release_all(guest).await;
5698            return result;
5699        }
5700        // `/proc` itself, or the caller's own descriptor directory, which
5701        // Detcore serves on a placeholder descriptor, listing them from its
5702        // model on ptrace and KVM (`descriptor_directory`).
5703        if !path.as_os_str().is_empty()
5704            && !Self::starts_at_a_recorded_link(guest, call.dirfd(), &path)
5705            && let Some(opened) = self
5706                .descriptor_directory_opened(
5707                    guest,
5708                    guest_spelled
5709                        .as_ref()
5710                        .map_or(&observed_path, |(observed, _)| observed),
5711                    guest_spelled
5712                        .as_ref()
5713                        .map_or(&procfs_spelling, |(_, spelling)| spelling),
5714                    call.flags(),
5715                )
5716                .await
5717        {
5718            let result = match opened {
5719                Ok(opened) => self.open_placeholder(guest, call.flags(), opened).await,
5720                Err(errno) => Err(errno.into()),
5721            };
5722            resource_release_all(guest).await;
5723            return result;
5724        }
5725        // `/proc` itself, or a descriptor directory, where the guest's procfs
5726        // is the backend's own and Detcore serves no placeholder: refused
5727        // before the open can reach the backend's `/proc`
5728        // (`descriptor_directory`).
5729        if !path.as_os_str().is_empty()
5730            && !Self::starts_at_a_recorded_link(guest, call.dirfd(), &path)
5731            && Self::refuses_backend_procfs_directory(
5732                guest,
5733                guest_spelled
5734                    .as_ref()
5735                    .map_or(&observed_path, |(observed, _)| observed),
5736                guest_spelled
5737                    .as_ref()
5738                    .map_or(&procfs_spelling, |(_, spelling)| spelling),
5739            )
5740            .await
5741        {
5742            resource_release_all(guest).await;
5743            return Err(Errno::EACCES.into());
5744        }
5745        // An `exe` link Detcore answers from its record (`Detcore::launched_exe`)
5746        // opens that image, for reading only: a write open of a running image
5747        // is the kernel's `ETXTBSY`, `O_NOFOLLOW` its `ELOOP`, and `O_PATH`
5748        // with it the link itself.
5749        let launched_exe = if call.flags().bits() & libc::O_ACCMODE == libc::O_RDONLY
5750            && !call
5751                .flags()
5752                .intersects(OFlag::O_NOFOLLOW | OFlag::O_TRUNC | OFlag::O_CREAT)
5753        {
5754            self.launched_exe(guest, (call.dirfd(), &path), true).await
5755        } else {
5756            None
5757        };
5758        // Signal phase 1 (`sigalrm_phase1`): in a process that handles SIGALRM,
5759        // an open that may wait (a FIFO, a character or block device) is
5760        // refused. Checked after the grant: no other guest runs before the open.
5761        if guest.thread_state().sigalrm_handled
5762            && !call.flags().intersects(OFlag::O_DIRECTORY | OFlag::O_PATH)
5763            && let Some(path) = call.path()
5764            && crate::sigalrm_phase1::open_refused(
5765                Self::inject_statat(
5766                    guest,
5767                    call.dirfd(),
5768                    path,
5769                    [Some(path), None],
5770                    AtFlags::empty(),
5771                )
5772                .await,
5773                call.flags().contains(OFlag::O_NONBLOCK),
5774            )
5775        {
5776            return Err(Errno::EOPNOTSUPP.into());
5777        }
5778        // The descriptor a descriptor link in the spelling names, as the open
5779        // finds it now, in its own turn, where the kernel looks the link up
5780        // (`LinkSource`). The open below may block and leave the turn.
5781        let link_source = crate::syscalls::namespace::own_descriptor_link(
5782            &observed_path,
5783            self.procfs_caller_ids(guest),
5784        )
5785        .and_then(|link| {
5786            guest
5787                .thread_state()
5788                .with_detfd(link.fd, |detfd| LinkSource {
5789                    fd: link.fd,
5790                    path: detfd.path(),
5791                    open_file: detfd.open_file_id(),
5792                    reached: detfd.procfs_reached(),
5793                })
5794                .ok()
5795        });
5796        // A blocking open, for reading or for writing, of a FIFO a guest made
5797        // waits in polling turns for a guest at the other end
5798        // (https://github.com/rrnewton/hermit/issues/2203).
5799        let own_ids = self.procfs_caller_ids(guest);
5800        let fifo = (self.cfg.use_nonblocking_sockets()
5801            && !self.cfg.recordreplay_modes
5802            && !call.flags().intersects(OFlag::O_NONBLOCK | OFlag::O_PATH)
5803            && call.flags().bits() & libc::O_ACCMODE != libc::O_RDWR)
5804            .then(|| {
5805                // The tracer looks a path below a descriptor directory up as
5806                // the guest's injection is respelled.
5807                let (dirfd, path) = match &below_descriptor_directory {
5808                    Some(below) => (libc::AT_FDCWD, below.as_path()),
5809                    None => (call.dirfd(), path.as_path()),
5810                };
5811                crate::fifo_peer::fifo_at(
5812                    &self.cfg.backend,
5813                    guest.thread_state().physical_tid,
5814                    guest.tid().as_raw(),
5815                    own_ids,
5816                    dirfd,
5817                    path,
5818                )
5819            })
5820            .flatten()
5821            // Only a FIFO a guest made; every other one is opened as before.
5822            .filter(|(fifo, _)| crate::fifo_peer::is_guest_made(*fifo));
5823        // A later descriptor lookup proves the object, while this observation
5824        // brackets its kernel open against target exec/PID replacement.
5825        let maps_before = if guest.runtime_private_memory_regions().is_some() {
5826            if let Some((pid, tid)) = self.procfs_caller_ids(guest)
5827                && let Some(task) = crate::procfs::maps_task_at(&procfs_spelling, pid, tid)
5828            {
5829                Some((task, crate::tool_global::procfs_task(guest, task).await))
5830            } else {
5831                None
5832            }
5833        } else {
5834            None
5835        };
5836        let res: Result<i64, Error> = match fifo {
5837            Some((fifo, fifo_path)) => {
5838                self.open_fifo_once_a_guest_holds_its_peer(guest, call, fifo, &fifo_path)
5839                    .await
5840            }
5841            None => match launched_exe {
5842                Some(exe) => self.open_launched_exe(guest, call, exe.as_deref()).await,
5843                None => self
5844                    .record_or_replay(guest, Syscall::Openat(call))
5845                    .await
5846                    .map_err(Error::from),
5847            },
5848        };
5849
5850        match res {
5851            Ok(fd) => {
5852                let fd = fd as RawFd;
5853                // A link Detcore could not resolve whose kernel open reached
5854                // the launcher, in the guest's image's place.
5855                if crate::syscalls::namespace::exe_link_candidate(&path)
5856                    && self.opened_the_launcher(fd)
5857                {
5858                    let _ = guest.inject(syscalls::Close::new().with_fd(fd)).await;
5859                    resource_release_all(guest).await;
5860                    return self.refuse_launched_exe(guest, Sysno::openat, None).await;
5861                }
5862                // Another process's descriptor link, or another spelling of
5863                // this one's, to a placeholder: the guest opened the rendered
5864                // file again, as Linux opens the procfs file again.
5865                // (Where one tool process serves every guest, it knows every
5866                // placeholder, and a run that made none pays nothing.)
5867                if names_descriptor_link(&observed_path)
5868                    && !call.flags().contains(OFlag::O_PATH | OFlag::O_NOFOLLOW)
5869                    && (rendered_procfs::placeholders_exist()
5870                        || guest.config().backend.tool_shares_guest_descriptor_table)
5871                {
5872                    let behind = self.rendered_procfs_behind(guest, fd).await;
5873                    if !matches!(behind, Ok(None)) {
5874                        let _ = guest.inject(syscalls::Close::new().with_fd(fd)).await;
5875                    }
5876                    let result = match behind {
5877                        Ok(None) => None,
5878                        Ok(Some(path)) => {
5879                            let target = rendered_procfs::RenderedTarget {
5880                                procfs: ProcfsFile::from_rendered_path(path)
5881                                    .expect("a rendered path is a procfs file"),
5882                                via_link: true,
5883                            };
5884                            Some(self.open_rendered_procfs(guest, call.flags(), target).await)
5885                        }
5886                        Err(error) => Some(Err(error)),
5887                    };
5888                    if let Some(result) = result {
5889                        resource_release_all(guest).await;
5890                        return result;
5891                    }
5892                }
5893                // Under a network trace mode only; the run ends before the
5894                // guest sees the descriptor.
5895                let pid = guest.pid().as_raw();
5896                let backend = self.cfg.backend;
5897                self.network_check_open(guest, &observed_path, || {
5898                    resolved_open_path(&backend, pid, fd)
5899                })
5900                .await;
5901                // Linux's random devices are classified below by the device
5902                // the open reached. An `O_PATH` descriptor, which reads
5903                // nothing, keeps the type its literal name gave it before.
5904                let fd_type = path.to_str().map_or(FdType::Regular, |fname| {
5905                    if call.flags().contains(OFlag::O_PATH)
5906                        && (fname == "/dev/random" || fname == "/dev/urandom")
5907                    {
5908                        FdType::Rng
5909                    } else {
5910                        FdType::Regular
5911                    }
5912                });
5913                // A guest-created pipe reopened by path (`/dev/stdin`,
5914                // `/proc/self/fd/N`, bash's `< <(cmd)` as `/dev/fd/63`) is a NEW open
5915                // file description, so it carries neither the Pipe type nor the
5916                // physical O_NONBLOCK that `handle_pipe2` gave the original. Left as a
5917                // physically blocking Regular fd, a read that waits for a writer blocks
5918                // in the kernel while holding the scheduler turn, and the writer never
5919                // runs (https://github.com/rrnewton/hermit/issues/1850). Give it the
5920                // same treatment as `handle_pipe2`: Pipe type plus a Detcore-internal
5921                // physical O_NONBLOCK, which F_GETFL hides from the guest. Only a pipe
5922                // this process already holds as scheduler-managed qualifies; a host
5923                // pipe or a named FIFO keeps `deterministic_read`'s Regular handling.
5924                // Gated like the forced O_NONBLOCK in F_SETFL: Replayer's descriptor
5925                // is an eventfd placeholder, so record and replay would not classify
5926                // alike.
5927                let fd_type = if fd_type == FdType::Regular
5928                    && self.cfg.use_nonblocking_sockets()
5929                    && !self.cfg.recordreplay_modes
5930                    && !call.flags().contains(OFlag::O_PATH)
5931                    && reopens_scheduler_managed_pipe(
5932                        &self.cfg.backend,
5933                        guest.pid().as_raw(),
5934                        fd,
5935                        &guest.thread_state().scheduler_managed_pipe_fds(),
5936                    )
5937                    && (call.flags().contains(OFlag::O_NONBLOCK)
5938                        || self.inject_physical_nonblocking(guest, fd).await.is_ok())
5939                {
5940                    FdType::Pipe
5941                } else {
5942                    fd_type
5943                };
5944                let host_stat = self
5945                    .add_fd_with_stat(guest, fd, call.flags(), fd_type)
5946                    .await?;
5947                // Linux names a random device by its device number, whatever
5948                // path reached it (a literal or relative name, `.` or `..`, a
5949                // symbolic link, `/proc/self/fd/N`, a bind mount, a copy of
5950                // the node), so every such open reads Detcore's stream, not
5951                // the host's entropy (https://github.com/rrnewton/hermit/issues/4544).
5952                // The device number comes from the `fstat` taken above when
5953                // metadata is virtualized, so the default run makes no extra
5954                // call; otherwise from one `fstat`, which under record and
5955                // replay goes through the recording, so replay classifies by
5956                // the recorded device, not by its placeholder's. If that
5957                // `fstat` fails, the open fails with its error: a descriptor
5958                // Detcore could not classify must not read the host.
5959                let fd_type = if fd_type == FdType::Regular && !call.flags().contains(OFlag::O_PATH)
5960                {
5961                    match self
5962                        .opened_random_device(guest, fd, host_stat.as_ref())
5963                        .await
5964                    {
5965                        Ok(true) => {
5966                            guest.thread_state().classify_random_device(fd)?;
5967                            FdType::Rng
5968                        }
5969                        Ok(false) => fd_type,
5970                        // Under record and replay, a buffer the guest's
5971                        // address space could not provide was never
5972                        // recorded; continuing would consume a different
5973                        // event than the other run did, so the run stops.
5974                        Err(StatFailure::Scratch(errno)) if self.cfg.recordreplay_modes => {
5975                            resource_release_all(guest).await;
5976                            return Err(Error::Tool(anyhow::anyhow!(
5977                                "cannot classify fd {fd} under record and replay: no buffer \
5978                                 for its fstat ({errno})"
5979                            )));
5980                        }
5981                        Err(failure) => {
5982                            let cleanup = self.close_unclassified(guest, fd).await;
5983                            resource_release_all(guest).await;
5984                            cleanup?;
5985                            return Err(failure.errno().into());
5986                        }
5987                    }
5988                } else {
5989                    fd_type
5990                };
5991                // A placeholder reached through any other spelling (a symbolic
5992                // link to a descriptor link, `/dev/stdin`): the kernel opened
5993                // the placeholder, which is the rendered file again, also for
5994                // `O_PATH`. The kernel already followed every link, so none
5995                // remains for `O_NOFOLLOW` to refuse (an `O_PATH|O_NOFOLLOW`
5996                // descriptor for a link is the link, no placeholder).
5997                // Without virtualized metadata no stat was taken; take one
5998                // while a placeholder may be about.
5999                let identity = match &host_stat {
6000                    Some(stat) => Some(*stat),
6001                    None if (self.renders_procfs(guest)
6002                        && (guest.thread_state().holds_a_rendered_procfs_file()
6003                            || rendered_procfs::placeholders_exist()))
6004                        || guest.config().directory_placeholder.is_some() =>
6005                    {
6006                        self.inject_fstat(guest, fd).await.ok()
6007                    }
6008                    None => None,
6009                };
6010                // The placeholder directory, reached through a spelling the
6011                // `--proc` level table could not resolve: hidden, as
6012                // `/proc/fs` is (`descriptor_directory`).
6013                if let Some(stat) = &identity
6014                    && Self::reaches_hidden_placeholder(
6015                        guest,
6016                        stat.st_mode,
6017                        stat.st_dev,
6018                        stat.st_ino,
6019                    )
6020                    .await
6021                {
6022                    guest.thread_state().remove_fd(fd);
6023                    let _ = guest.inject(syscalls::Close::new().with_fd(fd)).await;
6024                    resource_release_all(guest).await;
6025                    return Err(Errno::ENOENT.into());
6026                }
6027                let reached = match &identity {
6028                    Some(stat) if self.renders_procfs(guest) => {
6029                        self.recognize_placeholder(
6030                            guest,
6031                            &DetStat::from(*stat),
6032                            StatTarget::Descriptor(fd),
6033                        )
6034                        .await
6035                    }
6036                    _ => None,
6037                };
6038                if let Some(path) = reached {
6039                    guest.thread_state().remove_fd(fd);
6040                    let _ = guest.inject(syscalls::Close::new().with_fd(fd)).await;
6041                    let target = rendered_procfs::RenderedTarget {
6042                        procfs: ProcfsFile::from_rendered_path(path)
6043                            .expect("a rendered path is a procfs file"),
6044                        via_link: false,
6045                    };
6046                    let result = self.open_rendered_procfs(guest, call.flags(), target).await;
6047                    resource_release_all(guest).await;
6048                    return result;
6049                }
6050                // A `/proc` name outside the run's level that the kernel
6051                // reached through a spelling the lookup check does not
6052                // resolve (a symbolic link, `..`), named by the kernel's path
6053                // for the descriptor (see `refuse_procfs_name_outside_level`).
6054                if self
6055                    .opened_procfs_name_outside_level(
6056                        guest,
6057                        fd,
6058                        identity.as_ref().map(|stat| (stat.st_dev, stat.st_ino)),
6059                    )
6060                    .await
6061                {
6062                    guest.thread_state().remove_fd(fd);
6063                    let _ = guest.inject(syscalls::Close::new().with_fd(fd)).await;
6064                    resource_release_all(guest).await;
6065                    return Err(Errno::ENOENT.into());
6066                }
6067                // A FIFO (a named one, or a pipe reached by path that the
6068                // check above left `Regular`) stays `Regular` and physically
6069                // blocking, so a FIFO whose other end is on the host blocks as
6070                // before. Its first blocking read or write that finds a guest
6071                // holding the other end makes it an internally polled pipe
6072                // (`fifo_polls_if_a_guest_holds_its_peer`). Run mode only, as
6073                // for a reopened pipe: Replayer's descriptor is a placeholder.
6074                // `fifo_of` finds a FIFO only where it can read the guest's
6075                // descriptors through `/proc/<tid>`: under a separate tracer
6076                // (not DBT, in its PID container) whose guest thread IDs are
6077                // host tasks (not KVM). Elsewhere a FIFO blocks as before.
6078                if fd_type == FdType::Regular
6079                    && self.cfg.use_nonblocking_sockets()
6080                    && !self.cfg.recordreplay_modes
6081                    && !call.flags().contains(OFlag::O_PATH)
6082                    && let Some(fifo) = crate::fifo_peer::fifo_of(
6083                        &self.cfg.backend,
6084                        guest.thread_state().physical_tid,
6085                        guest.tid().as_raw(),
6086                        fd,
6087                    )
6088                {
6089                    guest
6090                        .thread_state()
6091                        .with_detfd(fd, |detfd| detfd.mark_fifo_awaiting_guest_peer())?;
6092                    crate::fifo_peer::note_guest_open(fifo, call.flags().bits());
6093                    // Name the FIFO for `--pipe-wake-latency`, and ready the
6094                    // opens waiting for the end this one opened.
6095                    if self.cfg.pipe_wake_latency_in_effect().is_some() {
6096                        let accmode = call.flags().bits() & libc::O_ACCMODE;
6097                        let end = crate::pipe_wake::ChannelEnd {
6098                            channel: crate::pipe_wake::ChannelId::Fifo(
6099                                crate::fifo_peer::fifo_channel(fifo),
6100                            ),
6101                            reads: accmode != libc::O_WRONLY,
6102                            writes: accmode != libc::O_RDONLY,
6103                        };
6104                        guest
6105                            .thread_state()
6106                            .with_detfd(fd, |detfd| detfd.set_channel(end))?;
6107                        for (applies, readies) in [
6108                            (end.reads, crate::pipe_wake::Wants::PeerReader),
6109                            (end.writes, crate::pipe_wake::Wants::PeerWriter),
6110                        ] {
6111                            if applies {
6112                                self.note_pipe_readied(guest, end.channel, readies).await;
6113                            }
6114                        }
6115                    }
6116                }
6117                if let (true, Some(stat)) = (guest.config().record_host_inputs, &host_stat) {
6118                    let identity = detcore_model::host_input::HostFileIdentity::from_stat(stat);
6119                    let file_type = stat.st_mode & libc::S_IFMT;
6120                    // Where the tool can name the guest's descriptor, the
6121                    // kernel's path for it (a readlink, which raises no
6122                    // inotify event the guest could see): whether it reached
6123                    // a kernel object whatever path the guest spelled, and
6124                    // otherwise, for a regular file or a directory, where the
6125                    // run's end reads its bytes or names, which the guest
6126                    // sees where it does not see the host's inode or times
6127                    // (`crate::host_inputs::content_at_end`). Nothing is read
6128                    // now, while the guest runs.
6129                    let target = crate::fifo_peer::guest_descriptors_are_visible(
6130                        &self.cfg.backend,
6131                        guest.thread_state().physical_tid,
6132                    )
6133                    .then(|| {
6134                        crate::util::raw_read_link(Path::new(&format!(
6135                            "/proc/{}/fd/{fd}",
6136                            guest.tid().as_raw()
6137                        )))
6138                        .ok()
6139                    })
6140                    .flatten();
6141                    // A FIFO, a socket or a device holds no bytes the run's
6142                    // end could read and no content a replay could compare:
6143                    // what the guest reads from it comes from whatever
6144                    // writes to it, and its times move with each write
6145                    // (the recorded run's own writes to a FIFO among them).
6146                    // It counts as a kernel object, as one under /dev does.
6147                    let kernel_object = matches!(
6148                        file_type,
6149                        libc::S_IFIFO | libc::S_IFSOCK | libc::S_IFCHR | libc::S_IFBLK
6150                    ) || target.as_deref().is_some_and(|target| {
6151                        detcore_model::host_input::is_kernel_object(&target.to_string_lossy())
6152                    });
6153                    let source = target
6154                        .filter(|_| {
6155                            !kernel_object && matches!(file_type, libc::S_IFREG | libc::S_IFDIR)
6156                        })
6157                        .and_then(|target| target.to_str().map(str::to_owned));
6158                    crate::tool_global::record_host_input(
6159                        guest,
6160                        observed_path.to_string_lossy().into_owned(),
6161                        identity,
6162                        source,
6163                        file_type == libc::S_IFREG,
6164                        false,
6165                        kernel_object,
6166                    )
6167                    .await;
6168                }
6169                // An open that finds its file linked, or creates it
6170                // (`O_TMPFILE`), reaches a file that has a name or never had
6171                // one, so a retired mapping of its inode described a file the
6172                // host has freed (https://github.com/rrnewton/hermit/issues/3840).
6173                // Reopening an unlinked file through `/proc/self/fd/N` finds no
6174                // link and keeps it.
6175                if let Some(stat) = &host_stat
6176                    && (stat.st_nlink > 0 || call.flags().contains(OFlag::O_TMPFILE))
6177                {
6178                    // A procfs entry the host rebuilt keeps its number when it is
6179                    // next written or read through this descriptor.
6180                    let procfs =
6181                        Self::procfs_entry(guest, &DetStat::from(stat), StatTarget::Descriptor(fd))
6182                            .await;
6183                    forget_retired_inode(
6184                        guest,
6185                        RawInode::new(stat.st_dev, stat.st_ino),
6186                        procfs.name(stat.st_dev),
6187                    )
6188                    .await;
6189                }
6190                // A descriptor that writes process memory can rewrite the
6191                // untraced code (see `record_untraced_code_change`). Reported
6192                // once the open returns, before the guest has the descriptor;
6193                // the record has no position in the run, so it covers every
6194                // write through it.
6195                if guest.config().record_host_inputs
6196                    && guest.config().untraced_code_range.is_some()
6197                    && may_write_process_memory(
6198                        call.flags(),
6199                        descriptor_identity(guest.tid().as_raw(), fd)
6200                            .as_ref()
6201                            .map(|(link, on_procfs)| (link.as_path(), *on_procfs)),
6202                    )
6203                {
6204                    crate::tool_global::record_host_mutation(guest, String::from("/")).await;
6205                }
6206                // A session leader with no controlling terminal that opens a
6207                // terminal without O_NOCTTY gains it as one, and the terminal
6208                // can then signal the session and its foreground process group
6209                // at moments set by host timing (`terminal_signals`). Hold
6210                // those signals in every process, as `handle_ioctl` does for
6211                // TIOCSCTTY and TIOCGPTPEER. Only the kernel knows whether this
6212                // open made the terminal controlling, so the record is made
6213                // after the call; it is still this thread's turn, and no other
6214                // thread's gated wait reads the record before the turn ends.
6215                // Skipped under record and replay: Replayer's descriptor is a
6216                // placeholder that never becomes a terminal, so the two runs
6217                // would not record alike.
6218                if guest.config().backend.host_timed_signals_are_physical
6219                    && !self.cfg.recordreplay_modes
6220                    && open_can_acquire_controlling_terminal(call.flags())
6221                    && opened_controlling_terminal(guest.pid().as_raw(), fd, host_stat.as_ref())
6222                {
6223                    record_host_timed_signals(
6224                        guest,
6225                        HostTimedSignalScope::Container,
6226                        terminal_signals(),
6227                    )
6228                    .await;
6229                }
6230                if fd_type == FdType::Pipe {
6231                    self.maybe_set_nonblocking_fd(guest, fd);
6232                }
6233                // Classify the spelling the guest used FIRST. Several kinds are
6234                // defined by that spelling and MUST keep it: `/proc/self/...`,
6235                // `/proc/thread-self/...` and the mountinfo aliases all resolve
6236                // through `/proc/<pid>/fd/<fd>` to a numeric `/proc/<pid>/...`
6237                // path, which is a DIFFERENT (or absent) classification. So
6238                // resolution must never overwrite a spelling that already
6239                // classifies.
6240                //
6241                // Only when the spelling yields nothing do we ask the kernel
6242                // what the descriptor actually names. That is exactly the
6243                // AT_FDCWD/alias gap -- `chdir("/sys/module/kvm"); open("refcnt")`
6244                // classifies as nothing lexically -- and scoping it this way
6245                // makes the fallback MONOTONE: it can only add a classification
6246                // where there was none, never change one that already existed.
6247                //
6248                // Under record and replay the descriptor's link is read through
6249                // the recording: replay holds the descriptor's number with an
6250                // eventfd, which resolves to no procfs file, so both runs must
6251                // classify the open by the link the recording read
6252                // (`recorded_open_path`).
6253                // A process's `auxv` the kernel opened keeps the kernel's
6254                // bytes, unclassified, as before Detcore rendered the file,
6255                // and so does a file of a CPU the machine does not have
6256                // (`ProcfsFile::of_kernel_open`): by its spelling here, and
6257                // by its link below.
6258                let cpus = self.cfg.virtual_cpu_count();
6259                let mut procfs = ProcfsFile::of_kernel_open(&observed_path, cpus);
6260                // Whether the guest's own spelling classified the open, not the
6261                // descriptor's link, whose numbers are the kernel's.
6262                let classified_by_spelling = procfs.is_some();
6263                // Under record and replay the link is read for every open,
6264                // also one whose spelling classifies: a spelling through `..`
6265                // (`/proc/self/../self/io`) classifies as the file it reaches,
6266                // which may be outside the level.
6267                if procfs.is_none() || self.cfg.recordreplay_modes {
6268                    let resolved = if self.cfg.recordreplay_modes {
6269                        self.recorded_open_path(guest, fd).await?
6270                    } else if crate::procfs_inode::tool_can_name_guest_descriptors(
6271                        &self.cfg.backend,
6272                    ) {
6273                        resolved_open_path(&self.cfg.backend, guest.pid().as_raw(), fd)
6274                    } else if call.dirfd() != libc::AT_FDCWD && !path.is_absolute() {
6275                        // Where the tool cannot read the guest's descriptor
6276                        // links itself, it asks the kernel beneath the backend
6277                        // for the same link (`descriptor_kernel_link`, one
6278                        // injected `readlinkat`), but only for a lookup
6279                        // relative to a directory descriptor: its prefix is
6280                        // that directory's recorded spelling, which need not
6281                        // name the directory (a symbolic link, a `..`, a `.`
6282                        // after `chdir`, a descriptor received over a
6283                        // socket), so `openat(dirfd, "cpu0/cpufreq/boost")`
6284                        // must classify by the object as it does where the
6285                        // tool reads the link. Absolute and AT_FDCWD-relative
6286                        // spellings stay lexical there, so that an
6287                        // unclassified open costs no injected call. Without
6288                        // sequentialized threads, a sibling's dup2 over the
6289                        // descriptor before the install below can misattach
6290                        // the class, as for a spelling-classified open on
6291                        // every backend
6292                        // (https://github.com/rrnewton/hermit/issues/4543).
6293                        Self::descriptor_kernel_link(guest, fd)
6294                            .await
6295                            .filter(|link| link.is_absolute())
6296                    } else {
6297                        None
6298                    };
6299                    // Under record and replay, a `/proc` name outside the
6300                    // run's level that a spelling the lookup check does not
6301                    // resolve reached, named by the link the recording read
6302                    // (in the guest's view, also after a `chroot`), so both
6303                    // runs refuse it alike (outside record and replay the
6304                    // check after the open above does it). Only where the
6305                    // recording saw that the descriptor is procfs: after a
6306                    // `chroot`, or under a user bind, `/proc` need not be.
6307                    if self.cfg.recordreplay_modes
6308                        && resolved.as_deref().is_some_and(|resolved| {
6309                            levels::procfs_name(resolved)
6310                                .is_some_and(|name| name.absent_at(self.cfg.proc_level))
6311                        })
6312                        // And the descriptor is procfs, not a user bind over
6313                        // part of `/proc`, as the recording saw it.
6314                        && self.recorded_descriptor_is_on_procfs(guest, fd).await
6315                    {
6316                        guest.thread_state().remove_fd(fd);
6317                        let _ = self
6318                            .record_or_replay(guest, syscalls::Close::new().with_fd(fd))
6319                            .await;
6320                        resource_release_all(guest).await;
6321                        return Err(Errno::ENOENT.into());
6322                    }
6323                    if procfs.is_none() {
6324                        procfs = resolved
6325                            .filter(|resolved| resolved != &observed_path)
6326                            .and_then(|resolved| ProcfsFile::of_kernel_open(&resolved, cpus));
6327                    }
6328                }
6329                // A process's cgroup file bind-mounted at another path, or
6330                // reached through another procfs mount, classifies by neither
6331                // its spelling nor its link, which names the bind's mount
6332                // point, and would read as the host's bytes. Name the procfs
6333                // entry the descriptor reaches, as a stat does, and classify
6334                // that name, so the alias is the file it is (Codex review of
6335                // https://github.com/rrnewton/hermit/pull/4386). Only that
6336                // kind (`ProcfsFile::may_be_named_by_provenance`): a mountinfo
6337                // file of another procfs mount failed its identity check and
6338                // stopped the run (Claude re-check). Like the link above, this
6339                // only adds a classification. Record and replay name no
6340                // entries, and without sequentialized threads naming records a
6341                // determinism loss for any file that may be procfs, so neither
6342                // looks.
6343                if procfs.is_none()
6344                    && guest.config().sequentialize_threads
6345                    && let Some(stat) = host_stat
6346                    && let ProcfsEntry::At(within) =
6347                        Self::procfs_entry(guest, &DetStat::from(stat), StatTarget::Descriptor(fd))
6348                            .await
6349                {
6350                    procfs = ProcfsFile::from_path(
6351                        &Path::new("/proc").join(within.strip_prefix("/").unwrap_or(&within)),
6352                    )
6353                    .filter(ProcfsFile::may_be_named_by_provenance);
6354                }
6355                if guest.runtime_private_memory_regions().is_some()
6356                    && procfs
6357                        .as_ref()
6358                        .and_then(ProcfsFile::private_mapping_format)
6359                        .is_some()
6360                    && let Some(stat) = host_stat
6361                {
6362                    match self
6363                        .maps_descriptor_entry(guest, &DetStat::from(stat), fd)
6364                        .await
6365                    {
6366                        ProcfsEntry::At(within) => {
6367                            let actual = Path::new("/proc")
6368                                .join(within.strip_prefix("/").unwrap_or(&within));
6369                            if let Some(task) = self.maps_guest_target(guest, &actual).await
6370                                && let Some((before_task, before)) = maps_before.as_ref()
6371                                && *before_task == task
6372                            {
6373                                let after = crate::tool_global::procfs_task(guest, task).await;
6374                                if let Some(target) =
6375                                    crate::procfs::MapsTarget::from_open(task, before, &after)
6376                                    && guest.runtime_private_memory_is_current(&target.private)
6377                                {
6378                                    procfs.as_mut().unwrap().bind_maps_target(target);
6379                                }
6380                            }
6381                        }
6382                        // A shadowed proc spelling is an ordinary file. Its
6383                        // contents must not receive procfs projection at all.
6384                        ProcfsEntry::NotProcfs => procfs = None,
6385                        ProcfsEntry::Unnamed | ProcfsEntry::Unknown => {}
6386                    }
6387                }
6388                // A process's `mountinfo` or `mounts` the kernel opened reads
6389                // the view its task's root gives it, decided now, at the open,
6390                // as Linux takes the root at the open.
6391                if let Some(file) = procfs.as_ref()
6392                    && file.mount_table_rendering().is_some()
6393                {
6394                    let kernel = self.mount_view_is_kernel(guest, file).await;
6395                    procfs
6396                        .as_mut()
6397                        .expect("a classified procfs file disappeared")
6398                        .pin_mount_view(kernel);
6399                }
6400                if procfs
6401                    .as_ref()
6402                    .is_some_and(ProcfsFile::needs_bound_thread_identity)
6403                {
6404                    // TODO-HUMAN-REVIEW(PR-964): Bind thread-self at open time,
6405                    // matching procfs inode resolution even if another thread or
6406                    // a forked process later reads the shared descriptor.
6407                    let tgid = guest.inject(syscalls::Getpid::new()).await? as i32;
6408                    let tid = guest.inject(syscalls::Gettid::new()).await? as i32;
6409                    let ppid = guest.inject(syscalls::Getppid::new()).await? as i32;
6410                    procfs
6411                        .as_mut()
6412                        .expect("thread identity request lost its procfs file")
6413                        .bind_thread_identity(tgid, tid, ppid);
6414                }
6415                // A task's `schedstat` describes the task its path named at
6416                // the open, `self` and `thread-self` resolved then, as Linux
6417                // resolves them at lookup, and a directory descriptor's task as
6418                // its open found it (`procfs_spelling`). Only by a spelling,
6419                // whose numbers are Detcore's, and outside a nested PID
6420                // namespace (`Detcore::procfs_caller_ids`), where Linux reads
6421                // the numbers in the namespace of the procfs mount
6422                // (`proc_pid_lookup`), not the caller's: no task otherwise, and
6423                // the file shows no runtime.
6424                if procfs.as_ref().is_some_and(ProcfsFile::is_task_schedstat) {
6425                    let thread = self
6426                        .procfs_caller_ids(guest)
6427                        .filter(|_| classified_by_spelling)
6428                        .and_then(|(pid, tid)| {
6429                            crate::procfs::schedstat_task_at(&procfs_spelling, pid, tid)
6430                        })
6431                        .map(crate::types::DetTid::from_raw);
6432                    procfs
6433                        .as_mut()
6434                        .expect("a classified procfs file disappeared")
6435                        .bind_schedstat_thread(thread);
6436                }
6437                // A process's `maps` lists the address space its task held at
6438                // the open: Linux takes the task's memory map then
6439                // (`proc_maps_open`), or none for a task that has released
6440                // it, whose file then reads empty. Detcore may render the
6441                // file from the backend's memory map
6442                // (`Detcore::maps_from_memory_map`) where that task is of the
6443                // opener's own process, whose address space it shares, and
6444                // still holds it at the open, as Detcore's process table says
6445                // (`procfs::memory_map::Binding`), which also tells which task
6446                // holding those numbers it is. Named by a spelling, as
6447                // `schedstat` above is.
6448                if procfs.as_ref().is_some_and(ProcfsFile::is_maps)
6449                    && self.renders_procfs(guest)
6450                    && let Some((pid, tid)) = self
6451                        .procfs_caller_ids(guest)
6452                        .filter(|_| classified_by_spelling)
6453                    && let Some(task) = crate::procfs::maps_task_at(&procfs_spelling, pid, tid)
6454                    && task.process == pid
6455                {
6456                    let answer = crate::tool_global::procfs_task(guest, task).await;
6457                    let opener = guest.thread_state().mm_id;
6458                    if let Some(binding) =
6459                        crate::procfs::memory_map::Binding::at_open(task, &answer, opener)
6460                    {
6461                        procfs
6462                            .as_mut()
6463                            .expect("a classified procfs file disappeared")
6464                            .bind_memory_map(binding);
6465                    }
6466                }
6467                if procfs
6468                    .as_ref()
6469                    .and_then(ProcfsFile::timer_slack_target)
6470                    .is_some()
6471                {
6472                    let target = procfs
6473                        .as_ref()
6474                        .and_then(ProcfsFile::timer_slack_target)
6475                        .expect("timer-slack target disappeared");
6476                    let stat = match guest.thread_state().with_detfd(fd, |detfd| detfd.stat())? {
6477                        Some(stat) => libc::stat::from(&stat),
6478                        None => self.inject_fstat(guest, fd).await?,
6479                    };
6480                    // Recorder opens a real proc inode, while Replayer reserves
6481                    // the recorded descriptor number with an anonymous eventfd.
6482                    // A real proc descriptor is the strongest open-time task
6483                    // incarnation witness. For a virtual replay descriptor,
6484                    // bind the live numeric proc path instead; every operation
6485                    // re-resolves that same path, so exit or TID reuse still
6486                    // changes the inode and returns ESRCH.
6487                    let identity = if stat.st_mode & libc::S_IFMT == libc::S_IFREG {
6488                        (stat.st_dev, stat.st_ino)
6489                    } else {
6490                        self.observe_timer_slack_identity(guest, target)
6491                            .await?
6492                            .unwrap_or((stat.st_dev, stat.st_ino))
6493                    };
6494                    procfs
6495                        .as_mut()
6496                        .expect("timer-slack classification disappeared")
6497                        .bind_timer_slack_identity(identity.0, identity.1);
6498                }
6499                // Signal phase 1: what the kernel says this file is, for the
6500                // table's descriptor rules (`sigalrm_phase1`).
6501                let provenance = guest
6502                    .thread_state()
6503                    .sigalrm_handled
6504                    .then(|| crate::sigalrm_phase1::kernel_provenance(guest.pid().as_raw(), fd));
6505                // An open through one of the caller's descriptor links
6506                // records the path of the descriptor it reopens, so that a
6507                // name relative to it (`openat(fd, "stat")` with `fd` from
6508                // `open("/proc/self/fd/<a /proc descriptor>")`) is the file
6509                // Linux finds there (see `path_to_record_for_open`, which
6510                // also keeps the spelling wherever the link may reach
6511                // another file). Not after a `chroot`, where `/proc` and
6512                // `/dev/fd` need not be what they are outside it.
6513                //
6514                // The leader holds the table procfs reads for
6515                // `/proc/self/fd`, and so does every thread that holds the
6516                // same table: the process's, which the group shares
6517                // (`process_files`). Not known only for a state restored
6518                // from before that was kept, where only the leader is shown
6519                // to hold it.
6520                let opener = {
6521                    let detpid = self.own_detpid(guest);
6522                    let state = guest.thread_state();
6523                    DescriptorLinkOpener {
6524                        own: self.procfs_caller_ids(guest),
6525                        holds_process_table: state.dettid == detpid || state.holds_process_files(),
6526                    }
6527                };
6528                // Where `N`, in the caller's own table, has no recorded path
6529                // at all (a descriptor received through SCM_RIGHTS, recorded
6530                // without one or not recorded, or one discovered without one), a
6531                // procfs directory is named as the kernel beneath the backend
6532                // names the opened descriptor (`procfs_directory_named_for`,
6533                // one injected `readlinkat`). Only where Detcore renders
6534                // procfs files (not under record and replay, which does not
6535                // record that call, nor after a `chroot`), and with
6536                // sequentialized threads, so that no other guest thread can
6537                // replace the descriptor before the link is read.
6538                let unrecorded_link_identity = if self.renders_procfs(guest)
6539                    && guest.config().sequentialize_threads
6540                    && !opens_the_link_itself(&observed_path, call.flags())
6541                    && host_stat.is_none_or(|stat| stat.st_mode & libc::S_IFMT == libc::S_IFDIR)
6542                    && link_source
6543                        .as_ref()
6544                        .is_none_or(|source| source.path.is_none())
6545                    && crate::syscalls::namespace::own_descriptor_link(&observed_path, opener.own)
6546                        .is_some_and(|link| {
6547                            link.table == crate::syscalls::namespace::DescriptorTable::Thread
6548                                || opener.holds_process_table
6549                        }) {
6550                    Self::procfs_directory_named_for(guest, fd).await
6551                } else {
6552                    None
6553                };
6554                let recorded_path = {
6555                    let state = guest.thread_state();
6556                    if state.changed_root.load(Ordering::SeqCst) {
6557                        observed_path.clone()
6558                    } else {
6559                        // `N` as the lookup saw it, and only if `N` still holds
6560                        // the same open file description: a thread sharing
6561                        // the table may have replaced it while the open
6562                        // waited (`LinkSource`).
6563                        path_to_record_for_open(
6564                            &observed_path,
6565                            call.flags(),
6566                            opener,
6567                            |target| {
6568                                link_source.as_ref().and_then(|source| {
6569                                    source.path_if_unchanged(
6570                                        target,
6571                                        state.with_detfd(target, |detfd| detfd.open_file_id()).ok(),
6572                                    )
6573                                })
6574                            },
6575                            unrecorded_link_identity,
6576                        )
6577                    }
6578                };
6579                // A `/proc/self` or `/proc/thread-self` directory, opened by
6580                // that spelling or through a descriptor link to one, is the
6581                // task the selector reached at its first open; a name below it
6582                // is that task's file whoever looks it up (a fork's child, a
6583                // thread). Recorded only where Detcore renders per-process
6584                // files, which read it.
6585                let link_itself = self.renders_procfs(guest)
6586                    && opens_the_link_itself(&procfs_spelling, call.flags())
6587                    && names_a_selector_link(&procfs_spelling);
6588                let reached = if self.renders_procfs(guest) {
6589                    let linked = {
6590                        let state = guest.thread_state();
6591                        crate::syscalls::namespace::own_descriptor_link(&observed_path, opener.own)
6592                            .filter(|link| {
6593                                !opens_the_link_itself(&observed_path, call.flags())
6594                                    && (link.table
6595                                        == crate::syscalls::namespace::DescriptorTable::Thread
6596                                        || opener.holds_process_table)
6597                            })
6598                            .and_then(|link| {
6599                                link_source.as_ref().and_then(|source| {
6600                                    source.reached_if_unchanged(
6601                                        link.fd,
6602                                        state
6603                                            .with_detfd(link.fd, |detfd| detfd.open_file_id())
6604                                            .ok(),
6605                                    )
6606                                })
6607                            })
6608                    };
6609                    match linked {
6610                        Some(reached) => Some(reached),
6611                        // An `O_PATH | O_NOFOLLOW` open of `/proc/self` or
6612                        // `/proc/thread-self` is the link itself, no
6613                        // directory: nothing is below it (ENOTDIR,
6614                        // `procfs_link_itself` below).
6615                        None if link_itself => None,
6616                        None => match procfs_selector_reached(&procfs_spelling, opener.own) {
6617                            // The caller's own task, as the process table
6618                            // identifies it now.
6619                            Some(selected) => {
6620                                let incarnation = match procfs_reached_task(&selected) {
6621                                    Some(task) => {
6622                                        let answer =
6623                                            crate::tool_global::procfs_task(guest, task).await;
6624                                        answer.exists.then(|| answer.incarnation())
6625                                    }
6626                                    None => None,
6627                                };
6628                                Some((selected, incarnation))
6629                            }
6630                            // Below a directory Detcore named: a selector's
6631                            // keeps its task's identity, and one the kernel
6632                            // named (received over SCM_RIGHTS) is named again
6633                            // at each use.
6634                            None if procfs_spelling != observed_path => {
6635                                let inherited = (!path.is_absolute()
6636                                    && call.dirfd() != libc::AT_FDCWD)
6637                                    .then(|| {
6638                                        guest
6639                                            .thread_state()
6640                                            .with_detfd(call.dirfd(), |detfd| {
6641                                                detfd.procfs_reached()
6642                                            })
6643                                            .ok()
6644                                            .flatten()
6645                                    })
6646                                    .flatten()
6647                                    .and_then(|(_, incarnation)| incarnation);
6648                                Some((procfs_spelling.clone(), inherited))
6649                            }
6650                            None => None,
6651                        },
6652                    }
6653                } else {
6654                    None
6655                };
6656                // TODO-HUMAN-REVIEW(https://github.com/rrnewton/hermit/issues/4213):
6657                // Review binding procfs directory provenance to the object the
6658                // kernel opened, without sequentialized threads.
6659                // Without sequentialized threads another guest thread can
6660                // replace the descriptor (or move the working directory) that
6661                // a relative lookup starts from, between Detcore naming it and
6662                // the kernel's open. Provenance taken from the spelling then
6663                // describes an object the new descriptor may not hold: a
6664                // `/proc/self` mark on an ordinary directory would make a
6665                // later relative `mountinfo` render a file Linux has no way to
6666                // open there. So it is installed only if the descriptor the
6667                // kernel returned holds a directory below `/proc`, as its own
6668                // link names it (`descriptor_kernel_link`, one more injected
6669                // `readlinkat`); for `O_PATH | O_NOFOLLOW` of a selector, only
6670                // if it holds that selector link. With sequentialized threads
6671                // no other thread runs in between, and nothing is checked.
6672                let (reached, link_itself) = if (reached.is_some() || link_itself)
6673                    && !path.is_absolute()
6674                    && !guest.config().sequentialize_threads
6675                {
6676                    let held = Self::descriptor_kernel_link(guest, fd).await;
6677                    let below_proc = |name: &Path| {
6678                        use std::os::unix::ffi::OsStrExt;
6679                        name.starts_with("/proc")
6680                            && !name.as_os_str().as_bytes().ends_with(b" (deleted)")
6681                    };
6682                    let reached = reached.filter(|_| held.as_deref().is_some_and(below_proc));
6683                    let link_itself = link_itself
6684                        && held.as_deref().is_some_and(|name| {
6685                            name == Path::new("/proc/self")
6686                                || name == Path::new("/proc/thread-self")
6687                        });
6688                    (reached, link_itself)
6689                } else {
6690                    (reached, link_itself)
6691                };
6692                guest.thread_state().with_detfd(fd, |detfd| {
6693                    detfd.set_path(&recorded_path);
6694                    if let Some((reached, incarnation)) = reached.clone() {
6695                        detfd.set_procfs_reached(reached, incarnation);
6696                    }
6697                    if link_itself {
6698                        detfd.set_procfs_link_itself();
6699                    }
6700                    if let Some(procfs) = procfs.clone() {
6701                        detfd.set_procfs(procfs);
6702                    }
6703                    if let Some(provenance) = provenance {
6704                        detfd.set_sigalrm_phase1_provenance(provenance);
6705                    }
6706                })?;
6707                resource_release_all(guest).await;
6708                Ok(fd as i64)
6709            }
6710            // TODO: audit for error-nondeterminism:
6711            Err(e) => {
6712                // A root-only `/proc` name outside the run's level, reached
6713                // through a spelling the lookup check does not resolve,
6714                // refuses the open before its descriptor can be named; and
6715                // the KVM executor refuses whatever reaches procfs.
6716                let e = if matches!(e, Error::Errno(Errno::EACCES | Errno::EPERM))
6717                    && self
6718                        .refused_open_reaches_procfs_name_outside_level(guest, &call)
6719                        .await
6720                {
6721                    Errno::ENOENT.into()
6722                } else {
6723                    e
6724                };
6725                resource_release_all(guest).await;
6726                Err(e)
6727            }
6728        }
6729    }
6730
6731    /// SYS_close system call.
6732    pub async fn handle_close<G: Guest<Self>>(
6733        &self,
6734        guest: &mut G,
6735        call: syscalls::Close,
6736    ) -> Result<i64, Error> {
6737        let fd = call.fd();
6738        if let Ok(waited) = u32::try_from(fd)
6739            && let Some(refusal) = self
6740                .refuse_close_of_waited_listener(guest, Sysno::close, waited..=waited)
6741                .await
6742        {
6743            return refusal;
6744        }
6745        let channel_end = self.pipe_channel_end(guest, fd);
6746        let lock_tracked = self.lock_tracked_descriptions(guest, fd..=fd);
6747        let res = self.record_or_replay(guest, call).await;
6748        let fd_was_released = !matches!(res, Err(Errno::EBADF) | Err(Errno::ERESTARTSYS));
6749        if fd_was_released {
6750            if let Some(open_file_id) = guest.thread_state_mut().remove_fd(fd) {
6751                self.release_port_for_open_file(guest, open_file_id).await;
6752            }
6753            self.report_lock_references(guest, lock_tracked).await;
6754            if let Ok(fd) = u32::try_from(fd) {
6755                crate::tool_global::descriptors_replaced(guest, fd, fd).await;
6756            }
6757            trace!("Closed {}", fd);
6758            self.note_channel_end_closed(guest, channel_end).await;
6759        }
6760        res.map_err(Error::from)
6761    }
6762
6763    // AUTONOMOUS-BOT-IMPLEMENTED
6764    // TODO-HUMAN-REVIEW(PR-838): Review close_range descriptor-table synchronization.
6765    /// Close a contiguous descriptor range and mirror successful closes in Detcore.
6766    ///
6767    /// The pinned Reverie revision exposes close_range as `Syscall::Other`. The
6768    /// common flags=0 operation cannot block and is deterministic for the
6769    /// process-local descriptor table. CLOSE_RANGE_UNSHARE and
6770    /// CLOSE_RANGE_CLOEXEC need separate shared-table modeling, so return ENOSYS
6771    /// for nonzero flags rather than letting strict execution silently diverge.
6772    pub async fn handle_close_range<G: Guest<Self>>(
6773        &self,
6774        guest: &mut G,
6775        call: Syscall,
6776    ) -> Result<i64, Error> {
6777        let Syscall::Other(_, args) = call else {
6778            unreachable!("close_range unexpectedly gained a typed variant")
6779        };
6780        let first = args.arg0 as u32;
6781        let last = args.arg1 as u32;
6782        let flags = args.arg2 as u32;
6783        if flags != 0 {
6784            return Err(Errno::ENOSYS.into());
6785        }
6786        if first <= last
6787            && let Some(refusal) = self
6788                .refuse_close_of_waited_listener(guest, Sysno::close_range, first..=last)
6789                .await
6790        {
6791            return refusal;
6792        }
6793
6794        let channel_ends = self.pipe_channel_ends_in(guest, first, last);
6795        let lock_tracked = match (i32::try_from(first), i32::try_from(last)) {
6796            (Ok(first), Ok(last)) => self.lock_tracked_descriptions(guest, first..=last),
6797            (Ok(first), Err(_)) => self.lock_tracked_descriptions(guest, first..=RawFd::MAX),
6798            _ => Vec::new(),
6799        };
6800        let result = self.record_or_replay(guest, call).await;
6801        if result.is_ok() {
6802            let released = guest.thread_state_mut().remove_fd_range(first, last);
6803            for open_file_id in released {
6804                self.release_port_for_open_file(guest, open_file_id).await;
6805            }
6806            self.report_lock_references(guest, lock_tracked).await;
6807            crate::tool_global::descriptors_replaced(guest, first, last).await;
6808            for end in channel_ends {
6809                self.note_channel_end_closed(guest, Some(end)).await;
6810            }
6811        }
6812        result.map_err(Error::from)
6813    }
6814
6815    // AUTONOMOUS-BOT-IMPLEMENTED
6816    // TODO-HUMAN-REVIEW(#2373)
6817    /// Advisory whole-file locks, forwarded to the kernel.
6818    ///
6819    /// This was previously an unconditional no-op success, justified by the
6820    /// claim that "an advisory whole-file lock is never contended within the
6821    /// serialized container". That is false: serializing guest threads stops
6822    /// them EXECUTING simultaneously, it does not stop their lock HOLD
6823    /// INTERVALS from overlapping. A holder that is descheduled -- because it
6824    /// blocked, forked, or simply used up its timeslice -- keeps holding while
6825    /// another process runs and observes the lock. Measured before this change,
6826    /// on both ptrace and DBI, two processes held the same `LOCK_EX`
6827    /// simultaneously while native correctly returned `EWOULDBLOCK`.
6828    ///
6829    /// A no-op is the wrong failure direction for a determinism tool. It is
6830    /// deterministically wrong, so double-run verification cannot see it, and it
6831    /// silently removes mutual exclusion from every guest that uses a lockfile.
6832    ///
6833    /// Forwarding is what `fcntl` already does for POSIX record locks, which is
6834    /// why those work. The guest's descriptor is a real host descriptor, so the
6835    /// kernel supplies the whole contract for free and consistently with itself:
6836    /// shared vs exclusive, `LOCK_NB`, upgrade/downgrade (which Linux performs
6837    /// non-atomically -- see below, this handler compensates), release on
6838    /// `LOCK_UN`, release when the last descriptor for the open file
6839    /// description is closed, and release on process exit.
6840    ///
6841    /// Determinism, scoped to what is actually true. When every contender is
6842    /// inside the container the outcome is a function of which guest holds the
6843    /// lock, and that is fixed by Detcore's deterministic schedule, so a given
6844    /// program and seed produce the same acquisition outcome every run.
6845    ///
6846    /// The scope is not decoration. Because this forwards to the kernel, a
6847    /// process OUTSIDE the container holding a lock on a guest-visible file
6848    /// does change the guest's result -- measured: with a host `flock -x`
6849    /// holder, a guest `LOCK_EX|LOCK_NB` returns `EWOULDBLOCK`, and acquires
6850    /// without one. That is a host-state leak, it is faithful to Linux, and it
6851    /// is the same leak `fcntl` record locks have always had here. Hermit
6852    /// already declines to make a mutating external filesystem deterministic,
6853    /// and lock state on a shared file is part of that state. Do not restate
6854    /// this as "no host state enters the decision": it does, and the previous
6855    /// bug in this very function came from writing down a determinism argument
6856    /// that was broader than the truth.
6857    ///
6858    /// Note that the no-op this replaced was not host-independent in any useful
6859    /// sense either -- it was host-independent by being wrong in all cases.
6860    ///
6861    /// # Why a blocking request is probed non-blockingly, and what that costs
6862    ///
6863    /// A guest thread parked inside a kernel `flock` is not visible to the
6864    /// deterministic scheduler as blocked, so nothing runs to release the lock
6865    /// and the whole container wedges -- measured: a four-way contention guest
6866    /// that completes natively hung indefinitely under a plain forwarding
6867    /// implementation. So a blocking operation is rewritten to `LOCK_NB` and,
6868    /// if it turns out to be contended, refused rather than hung.
6869    ///
6870    /// That rewrite is not free, and the cost is a *lock the guest already
6871    /// owns*. Linux converts an `flock` lock in place and the conversion is not
6872    /// atomic: `flock_lock_inode` deletes this open file description's existing
6873    /// lock **before** it scans for a conflict, so a contended `LOCK_SH` ->
6874    /// `LOCK_EX` conversion leaves the caller holding nothing and then reports
6875    /// `EWOULDBLOCK`. Natively the guest never observes that intermediate
6876    /// state, because a *blocking* request would sleep and eventually acquire.
6877    /// Under the rewrite it would: the guest asked to wait, got told "no", and
6878    /// silently lost the shared lock it was already relying on.
6879    ///
6880    /// So this handler restores the prior mode before refusing a *blocking*
6881    /// conversion, making the refusal side-effect-free. It deliberately does
6882    /// **not** restore when the guest itself passed `LOCK_NB`: there the drop is
6883    /// exactly what Linux does, and re-acquiring would be a divergence in the
6884    /// other direction. `DetFd::flock_mode` is what makes the two cases
6885    /// distinguishable -- it records the mode Detcore last saw the kernel grant
6886    /// for this open file description, so a first acquisition (nothing to lose)
6887    /// is not confused with a conversion (something to lose).
6888    ///
6889    /// That cache covers locks Detcore granted while it had sole knowledge of
6890    /// the open file description. State becomes permanently unknown when the
6891    /// descriptor is inherited across a process fork, discovered after tracing
6892    /// begins, or received through `SCM_RIGHTS` (recorded unobserved), because another process can
6893    /// change that shared kernel lock without updating this cache. A blocking
6894    /// conversion in unknown state is refused before the nonblocking probe, so
6895    /// the refusal cannot destroy a lock Detcore cannot restore.
6896    pub async fn handle_flock<G: Guest<Self>>(
6897        &self,
6898        guest: &mut G,
6899        call: syscalls::Flock,
6900    ) -> Result<i64, Error> {
6901        // A flock belongs to its open file description. Whatever the call
6902        // did (took, converted, released, or, refused, dropped a lock in a
6903        // non-atomic conversion), the description's own fdinfo now says which
6904        // locks it holds (`procfs::locks`).
6905        let fd = call.fd();
6906        // A shared or exclusive request can take a flock, which Linux
6907        // records with the caller as its taker; an unlock, none.
6908        let operation = call.operation();
6909        let class = (operation & libc::LOCK_UN == 0
6910            && operation & (libc::LOCK_SH | libc::LOCK_EX) != 0)
6911            .then_some("FLOCK");
6912        let baseline = self.lock_baseline(guest, fd, class);
6913        let result = self.flock_through_the_kernel(guest, call).await;
6914        self.refresh_description_locks(guest, fd, LockCall::Flock, baseline)
6915            .await?;
6916        result
6917    }
6918
6919    /// [`Self::handle_flock`]'s call itself.
6920    async fn flock_through_the_kernel<G: Guest<Self>>(
6921        &self,
6922        guest: &mut G,
6923        call: syscalls::Flock,
6924    ) -> Result<i64, Error> {
6925        const LOCK_NB: i32 = libc::LOCK_NB;
6926        /// `LOCK_SH`/`LOCK_EX`/`LOCK_UN` with `LOCK_NB` (and any padding) masked off.
6927        const MODE_MASK: i32 = libc::LOCK_SH | libc::LOCK_EX | libc::LOCK_UN;
6928
6929        let (fd, operation) = (call.fd(), call.operation());
6930        let requested = operation & MODE_MASK;
6931        let caller_wants_nonblocking = operation & LOCK_NB != 0;
6932        let releasing = requested == libc::LOCK_UN;
6933        let valid_operation = operation & !(MODE_MASK | LOCK_NB) == 0
6934            && matches!(requested, libc::LOCK_SH | libc::LOCK_EX | libc::LOCK_UN);
6935        let dettid = guest.thread_state().dettid;
6936
6937        // Preserve kernel validation for malformed operations. In particular,
6938        // an unknown descriptor with an invalid mode must report EINVAL rather
6939        // than being mistaken for a valid blocking request and refused with
6940        // ENOLCK.
6941        if !valid_operation {
6942            return self
6943                .record_or_replay(guest, call)
6944                .await
6945                .map_err(Error::from);
6946        }
6947
6948        // The mode Detcore last saw the kernel grant this open file description.
6949        let known_held = guest
6950            .thread_state()
6951            .with_detfd(fd, |detfd| detfd.known_flock_mode())
6952            .unwrap_or(None);
6953
6954        // A nonblocking conversion is allowed to have Linux's documented
6955        // non-atomic side effect. A blocking conversion is not: if this open
6956        // file description was inherited or received and its prior mode is
6957        // unknown, probing could silently drop a lock we cannot restore.
6958        // Validate the descriptor without changing lock state, then refuse the
6959        // uncertain blocking operation before issuing flock at all.
6960        if !caller_wants_nonblocking && !releasing && known_held.is_none() {
6961            guest
6962                .inject_with_retry(Syscall::Fcntl(
6963                    syscalls::Fcntl::new().with_fd(fd).with_cmd(F_GETFD),
6964                ))
6965                .await?;
6966            report_flock_refusal(&format!(
6967                "[dtid {dettid}] blocking flock(fd={fd}, operation={operation:#x}) refused: \
6968                 this open file description existed before Detcore observed its lock state, \
6969                 so a nonblocking probe could destroy a lock that cannot be restored. \
6970                 {FLOCK_REFUSAL_ADVICE}"
6971            ));
6972            return self
6973                .refuse_unserviceable_operation(guest, Sysno::flock, Errno::ENOLCK)
6974                .await;
6975        }
6976        let held = known_held.flatten();
6977
6978        // LOCK_UN cannot block, so it is forwarded exactly as the guest wrote it.
6979        let probe_operation = if releasing {
6980            operation
6981        } else {
6982            operation | LOCK_NB
6983        };
6984        let result = self
6985            .record_or_replay(guest, call.with_operation(probe_operation))
6986            .await;
6987        // A backend that cannot apply flock faithfully refuses it with a typed
6988        // refusal and no lock effect (the KVM backend refuses every lock
6989        // operation: https://github.com/rrnewton/reverie/issues/1069). Ask
6990        // before anything else is injected, since the next injection replaces
6991        // the backend's record. The refusal is reported as a refusal, never
6992        // handed to the guest as the backend's raw ENOSYS.
6993        let refusal = if result.is_err() {
6994            guest.take_unsupported_refusal()
6995        } else {
6996            None
6997        };
6998        if let Some(refusal) = refusal {
6999            let message = format!(
7000                "[dtid {dettid}] flock(fd={fd}, operation={operation:#x}) refused by the backend: {}. \
7001                 No lock was taken or released.",
7002                refusal.diagnostic
7003            );
7004            report_flock_refusal(&message);
7005            if !self.cfg.panic_on_unsupported_syscalls {
7006                crate::tool_global::report_unsupported_syscall(guest, Sysno::flock).await;
7007            }
7008            return self
7009                .refuse_unserviceable_operation_because(
7010                    guest,
7011                    Sysno::flock,
7012                    Errno::ENOLCK,
7013                    refusal.diagnostic,
7014                )
7015                .await;
7016        }
7017
7018        match result {
7019            Ok(value) => {
7020                let granted = if releasing { None } else { Some(requested) };
7021                let _ = guest
7022                    .thread_state()
7023                    .with_detfd(fd, |detfd| detfd.set_flock_mode(granted));
7024                trace!(
7025                    "flock(fd={}, operation={:#x}) served, open file now holds {:?}",
7026                    fd, operation, granted
7027                );
7028                Ok(value)
7029            }
7030            Err(Errno::EWOULDBLOCK) if caller_wants_nonblocking => {
7031                // Exactly what the guest asked for, including Linux's own
7032                // non-atomic conversion behavior: if this was a conversion, the
7033                // kernel really did drop the prior lock on the way to failing,
7034                // so the cache must forget it rather than claim a lock the
7035                // guest no longer holds.
7036                if held.is_some_and(|held| held != requested) {
7037                    let _ = guest
7038                        .thread_state()
7039                        .with_detfd(fd, |detfd| detfd.set_flock_mode(None));
7040                }
7041                trace!("flock(fd={}, operation={:#x}) would block", fd, operation);
7042                Err(Errno::EWOULDBLOCK.into())
7043            }
7044            Err(Errno::EWOULDBLOCK) => {
7045                // The guest asked to wait, and Detcore substituted a probe.
7046                // Undo the probe's collateral damage before refusing.
7047                if let Some(previous) = held.filter(|previous| *previous != requested) {
7048                    let restore = call.with_operation(previous | LOCK_NB);
7049                    match self.record_or_replay(guest, restore).await {
7050                        Ok(_) => {
7051                            warn!(
7052                                "[dtid {dettid}] contended blocking flock(fd={fd}, \
7053                                 operation={operation:#x}) refused; restored this open file's \
7054                                 prior {previous:#x} lock, which Linux's non-atomic conversion \
7055                                 had dropped. The guest holds exactly what it held before the \
7056                                 call."
7057                            );
7058                        }
7059                        Err(err) => {
7060                            let _ = guest
7061                                .thread_state()
7062                                .with_detfd(fd, |detfd| detfd.set_flock_mode(None));
7063                            error!(
7064                                "[dtid {dettid}] contended blocking flock(fd={fd}, \
7065                                 operation={operation:#x}) refused, AND this open file's prior \
7066                                 {previous:#x} lock could not be restored ({err}). Linux's \
7067                                 non-atomic conversion dropped it and something outside this \
7068                                 container took it in the interval. The guest has lost a lock \
7069                                 it held; treat any mutual exclusion it was protecting as \
7070                                 broken."
7071                            );
7072                        }
7073                    }
7074                }
7075                // Waiting faithfully needs a wait queue owned by the
7076                // deterministic scheduler, the way futexes are handled; until
7077                // that exists, refuse loudly. Returning success would recreate
7078                // the mutual-exclusion bug this handler was written to fix, and
7079                // blocking in the kernel would deadlock the container.
7080                report_flock_refusal(&format!(
7081                    "[dtid {dettid}] blocking flock(fd={fd}, operation={operation:#x}) is \
7082                     contended, and Detcore cannot yet park a thread on a file lock \
7083                     deterministically. Refusing rather than granting a lock another guest \
7084                     holds. {FLOCK_REFUSAL_ADVICE}"
7085                ));
7086                self.refuse_unserviceable_operation(guest, Sysno::flock, Errno::ENOLCK)
7087                    .await
7088            }
7089            Err(err) => {
7090                trace!(
7091                    "flock(fd={}, operation={:#x}) refused: {}",
7092                    fd, operation, err
7093                );
7094                Err(err.into())
7095            }
7096        }
7097    }
7098
7099    /// A process's `maps`, opened on descriptor `fd`, rendered from the
7100    /// backend's memory map (`Guest::memory_map`, `procfs::memory_map`)
7101    /// rather than read from the kernel's file, where the backend gives one
7102    /// (package K slice K9a of <https://github.com/rrnewton/hermit/issues/4213>):
7103    /// on ptrace the kernel's own text for the guest's address space; on KVM
7104    /// the mappings of the stack window that LinuxEmu keeps, `[stack]` and
7105    /// the parts a split left of it, each as Linux prints it. The rest of the
7106    /// address space a native process lists (the image, the interpreter, the
7107    /// vDSO, the heap, every `mmap(2)` mapping) is missing there, a named
7108    /// difference of the KVM provider
7109    /// (<https://github.com/rrnewton/reverie/issues/1211>).
7110    ///
7111    /// Linux's read first finds the task the open found, by the pid the
7112    /// file's inode holds (`m_start`), and answers ESRCH once that task is
7113    /// reaped, whichever task holds its number by then; it then prints the
7114    /// mappings of the address space the open took, whichever thread reads.
7115    /// Where the open bound the file (`ProcfsFile::bind_memory_map`), ESRCH
7116    /// where Detcore's process table no longer has the task the open found
7117    /// (`procfs::memory_map::Binding`): on every backend, whatever the
7118    /// backend's own file would give. On KVM a descriptor received over
7119    /// `SCM_RIGHTS` carries no binding, and reads LinuxEmu's file, a named
7120    /// difference (`docs/ARCHITECTURE.md`). Then, where the bound address
7121    /// space is still the reader's, the reader's map is the file's. `None`,
7122    /// to read the file as before, where it is not (another process that
7123    /// inherited the descriptor, or the same one after `exec`), where the
7124    /// open bound nothing, where Detcore renders no procfs file (under record
7125    /// and replay, after a `chroot`), where the descriptor cannot read (the
7126    /// kernel's read answers EBADF), and where the backend's map does not
7127    /// answer, lists nothing while incomplete, or lists a mapping the
7128    /// renderer does not know (`procfs::memory_map::render`).
7129    ///
7130    /// TODO(package K slice K10): on KVM the open still reaches LinuxEmu's
7131    /// synthetic `/proc/self/maps`, the last file of its synthetic path
7132    /// table, which the guest's descriptor is read through. Its one invented
7133    /// `[stack]` line is still the file wherever this rendering does not
7134    /// apply, and an `mmap(2)` of the descriptor still maps it where Linux
7135    /// refuses one (ENODEV); a `stat` of the descriptor reports Linux's size
7136    /// (`Self::with_procfs_maps_size`), but one of its path, or of its
7137    /// `/proc/self/fd` link, reports its length. K10 deletes the file, with
7138    /// the path table.
7139    async fn maps_from_memory_map<G: Guest<Self>>(
7140        &self,
7141        guest: &mut G,
7142        fd: RawFd,
7143    ) -> Result<Option<Vec<u8>>, Error> {
7144        let (binding, flags) = guest.thread_state().with_detfd(fd, |detfd| {
7145            (detfd.procfs_memory_map(), detfd.status_flags())
7146        })?;
7147        let Some(binding) = binding else {
7148            return Ok(None);
7149        };
7150        if !self.renders_procfs(guest) || !can_read(flags) {
7151            return Ok(None);
7152        }
7153        let answer = crate::tool_global::procfs_task(guest, binding.task()).await;
7154        if !binding.finds_its_task(&answer) {
7155            trace!("maps of fd {fd}: the task its open found is gone (ESRCH)");
7156            return Err(Errno::ESRCH.into());
7157        }
7158        if !binding.held_by(guest.thread_state().mm_id) {
7159            return Ok(None);
7160        }
7161        let rendered = match guest.memory_map(guest.tid()) {
7162            Ok(map) => {
7163                let rendered = crate::procfs::memory_map::render(&map);
7164                if rendered.is_none() {
7165                    trace!("maps of fd {fd} read from the kernel's file: memory map gives no file");
7166                }
7167                rendered
7168            }
7169            Err(error) => {
7170                trace!("maps of fd {fd} read from the kernel's file: memory map {error:?}");
7171                None
7172            }
7173        };
7174        if let Some(rendered) = &rendered {
7175            trace!(
7176                "maps of fd {fd} rendered from the memory map: {} bytes",
7177                rendered.len()
7178            );
7179        }
7180        Ok(rendered)
7181    }
7182
7183    async fn snapshot_procfs<G: Guest<Self>>(
7184        &self,
7185        guest: &mut G,
7186        call: syscalls::Read,
7187    ) -> Result<Vec<u8>, Error> {
7188        // A backend-owned read may have advanced the kernel cursor without
7189        // passing through Detcore's logical procfs cursor (KVM does this for
7190        // worker-shared descriptors). Rewind before taking the initial snapshot
7191        // so a later intercepted pread cannot snapshot from EOF.
7192        //
7193        // AUTONOMOUS-BOT-IMPLEMENTED
7194        // TODO-HUMAN-REVIEW(#1903): ESPIPE is not a failure here. Several procfs
7195        // files are legitimately non-seekable -- `/proc/net/*` single-release
7196        // seq_files return ESPIPE from `llseek` on the host, verified natively:
7197        // `lseek(fd, 0, SEEK_SET)` on `/proc/net/sockstat` gives ESPIPE while the
7198        // subsequent `read(2)` returns data. Propagating that ESPIPE made the
7199        // GUEST's `read` fail on a file Linux reads fine (`cat
7200        // /proc/net/sockstat` -> "Illegal seek"), which is a deviation from Linux
7201        // semantics, not a determinism requirement: the rewind is an internal
7202        // correction Detcore performs for its own benefit and the guest never
7203        // asked for it. A non-seekable fd also cannot have been advanced behind
7204        // our back by a seek, and a freshly opened one is already at offset 0, so
7205        // skipping the rewind loses nothing the rewind was protecting.
7206        match guest
7207            .inject_with_retry(Syscall::Lseek(
7208                syscalls::Lseek::new()
7209                    .with_fd(call.fd())
7210                    .with_offset(0)
7211                    .with_whence(Whence::SEEK_SET),
7212            ))
7213            .await
7214        {
7215            Ok(_) => {}
7216            Err(Errno::ESPIPE) => {}
7217            Err(err) => return Err(err.into()),
7218        }
7219
7220        // Capture into scratch memory, never into the caller's buffer. The
7221        // caller receives only the sanitized bytes, so every host byte a
7222        // capture read left in its buffer past that length would stay visible
7223        // after the read returns: all of a host `/proc/modules` chunk behind
7224        // its empty sanitized view
7225        // (https://github.com/rrnewton/hermit/issues/3815). Saving and
7226        // restoring the caller's buffer cannot cover every destination the
7227        // kernel may write, such as a PROT_WRITE-only page that the save
7228        // cannot read. The caller's buffer sees only the sanitized
7229        // publication, which checks its protection as the kernel's copy does.
7230        let (lists_address_space, lists_locks) =
7231            guest.thread_state().with_detfd(call.fd(), |detfd| {
7232                (
7233                    detfd.procfs_lists_address_space(),
7234                    detfd.procfs_lists_locks(),
7235                )
7236            })?;
7237        if lists_address_space {
7238            self.capture_procfs_on_stack(guest, call, lists_locks).await
7239        } else {
7240            self.capture_procfs_in_mapping(guest, call, lists_locks)
7241                .await
7242        }
7243    }
7244
7245    /// Capture through a private anonymous mapping that is unmapped before
7246    /// returning, so no host byte stays in guest memory.
7247    async fn capture_procfs_in_mapping<G: Guest<Self>>(
7248        &self,
7249        guest: &mut G,
7250        call: syscalls::Read,
7251        lists_locks: bool,
7252    ) -> Result<Vec<u8>, Error> {
7253        const CAPTURE_CHUNK_BYTES: usize = 64 * 1024;
7254
7255        let mapped = guest
7256            .inject_with_retry(Syscall::Mmap(
7257                syscalls::Mmap::new()
7258                    .with_addr(None)
7259                    .with_len(CAPTURE_CHUNK_BYTES)
7260                    .with_prot(ProtFlags::PROT_READ | ProtFlags::PROT_WRITE)
7261                    .with_flags(MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS)
7262                    .with_fd(-1)
7263                    .with_offset(0),
7264            ))
7265            .await?;
7266        let mapping = usize::try_from(mapped)
7267            .ok()
7268            .and_then(AddrMut::<u8>::from_raw)
7269            .ok_or(Errno::EFAULT)?;
7270        let result = self
7271            .drain_procfs(guest, call, mapping, CAPTURE_CHUNK_BYTES, lists_locks)
7272            .await;
7273        guest
7274            .inject_with_retry(Syscall::Munmap(
7275                syscalls::Munmap::new()
7276                    .with_addr(Some(Addr::from(mapping).cast()))
7277                    .with_len(CAPTURE_CHUNK_BYTES),
7278            ))
7279            .await?;
7280        result
7281    }
7282
7283    /// Capture a file that lists the address space (`maps`, `smaps`,
7284    /// `numa_maps`, `smaps_rollup`) through the guest stack scratch. A
7285    /// capture mapping would be listed in the snapshot, or merged into a
7286    /// neighbouring row, and then be unmapped before the guest sees it. The
7287    /// stack scratch changes no mapping; its original bytes are put back, so
7288    /// no host byte stays below the stack pointer either.
7289    ///
7290    /// The scratch below the red zone is not always there: a thread, fiber or
7291    /// alternate signal stack may put the stack pointer just above a guard page
7292    /// or the end of its mapping (compare [`Self::inject_fstat`]). The read
7293    /// needs no stack, so that must not fail it. The capture then goes through
7294    /// the caller's buffer, which this read writes anyway, with the same save
7295    /// and restore before the output is copied. If not even its first byte can
7296    /// be read and written, Linux copies nothing and the read fails `EFAULT`;
7297    /// it does so here without taking the snapshot.
7298    async fn capture_procfs_on_stack<G: Guest<Self>>(
7299        &self,
7300        guest: &mut G,
7301        call: syscalls::Read,
7302        lists_locks: bool,
7303    ) -> Result<Vec<u8>, Error> {
7304        const CAPTURE_CHUNK_BYTES: usize = 512;
7305
7306        let mut original = [0_u8; CAPTURE_CHUNK_BYTES];
7307        {
7308            let mut stack = guest.stack().await;
7309            let scratch = stack.reserve::<[u8; CAPTURE_CHUNK_BYTES]>().cast::<u8>();
7310            // `commit` writes the reserved bytes, so save them before it.
7311            if save_procfs_scratch(&mut guest.memory(), scratch, &mut original)
7312                && let Ok(guard) = stack.commit()
7313            {
7314                let result = self
7315                    .drain_procfs(guest, call, scratch, CAPTURE_CHUNK_BYTES, lists_locks)
7316                    .await;
7317                guest.memory().write_exact(scratch, &original)?;
7318                drop(guard);
7319                return result;
7320            }
7321        }
7322
7323        let buffer = call.buf().ok_or(Errno::EFAULT)?;
7324        for chunk in [call.len().min(CAPTURE_CHUNK_BYTES), 1] {
7325            let original = &mut original[..chunk];
7326            if save_procfs_scratch(&mut guest.memory(), buffer, original) {
7327                let result = self
7328                    .drain_procfs(guest, call, buffer, chunk, lists_locks)
7329                    .await;
7330                guest.memory().write_exact(buffer, original)?;
7331                return result;
7332            }
7333        }
7334        Err(Errno::EFAULT.into())
7335    }
7336
7337    /// Read the host file to EOF through `scratch`, `chunk` bytes at a time.
7338    ///
7339    /// For `/proc/locks` (`lists_locks`), each record that reads after the one
7340    /// whose length shows it reached the end of the kernel's lock list return
7341    /// is dropped, waiters and all, when its granted lock is one already read
7342    /// (`procfs::locks::locks_capture_without_repeats`): such records are rows
7343    /// the kernel prints again when the host adds a lock between two reads, at
7344    /// moments the host decides. Any other record is kept. Those reads are still made,
7345    /// through the recorder, until one returns 0, and the decision reads only
7346    /// their recorded lengths and bytes, so a replay drops the same bytes.
7347    async fn drain_procfs<G: Guest<Self>>(
7348        &self,
7349        guest: &mut G,
7350        call: syscalls::Read,
7351        scratch: AddrMut<'_, u8>,
7352        chunk: usize,
7353        lists_locks: bool,
7354    ) -> Result<Vec<u8>, Error> {
7355        const MAX_SNAPSHOT_BYTES: usize = 16 * 1024 * 1024;
7356
7357        let capture = call.with_buf(Some(scratch)).with_len(chunk);
7358        let mut contents = Vec::new();
7359        let mut reads = Vec::new();
7360        loop {
7361            let bytes_read = self.record_or_replay(guest, capture).await? as usize;
7362            if bytes_read == 0 {
7363                break;
7364            }
7365            if contents.len() + bytes_read > MAX_SNAPSHOT_BYTES {
7366                return Err(Errno::EFBIG.into());
7367            }
7368            let start = contents.len();
7369            contents.resize(start + bytes_read, 0);
7370            read_guest_exact(&guest.memory(), scratch, &mut contents[start..])?;
7371            reads.push(bytes_read);
7372        }
7373        if lists_locks {
7374            let kept =
7375                crate::procfs::locks::locks_capture_without_repeats(&contents, &reads, chunk);
7376            if kept.len() < contents.len() {
7377                // Not INFO: whether the host moved a lock between the reads is
7378                // host-timed, and INFO is compared between verified runs.
7379                tracing::debug!(
7380                    "[detcore] /proc/locks capture: dropped {} bytes of rows that reads after \
7381                     the end of the kernel's list printed again (reads {:?})",
7382                    contents.len() - kept.len(),
7383                    reads
7384                );
7385                return Ok(kept);
7386            }
7387        }
7388        Ok(contents)
7389    }
7390
7391    /// The procfs directory that the kernel beneath the backend names for
7392    /// descriptor `fd`, which this guest thread just opened: its
7393    /// `/proc/thread-self/fd/<fd>` link, read by a `readlinkat` injected in
7394    /// the guest thread, which the host kernel answers under ptrace and DBT
7395    /// and the KVM executor answers for its own `/proc`, if that names a
7396    /// directory Detcore may record (`procfs_directory_link`). `None`
7397    /// otherwise, and where the guest stack scratch cannot hold the call's
7398    /// buffers, the call fails, or the link does not fit in 256 bytes: every
7399    /// path accepted is far shorter, and a truncated one must not match.
7400    async fn procfs_directory_named_for<G: Guest<Self>>(
7401        guest: &mut G,
7402        fd: RawFd,
7403    ) -> Option<PathBuf> {
7404        let named = Self::descriptor_kernel_link(guest, fd).await?;
7405        procfs_directory_link(&named).then_some(named)
7406    }
7407
7408    /// The `/proc/thread-self/fd/<fd>` link of descriptor `fd`, which this
7409    /// guest thread holds, as the kernel beneath the backend reads it (one
7410    /// `readlinkat` injected in the guest thread; the host kernel under ptrace
7411    /// and DBT, the KVM executor for its own `/proc`). `None` where the guest
7412    /// stack scratch cannot hold the call's buffers, the call fails, or the
7413    /// link does not fit in 256 bytes.
7414    async fn descriptor_kernel_link<G: Guest<Self>>(guest: &mut G, fd: RawFd) -> Option<PathBuf> {
7415        const LINK_CAPACITY: usize = 40;
7416        const TARGET_CAPACITY: usize = 256;
7417        let mut stack = guest.stack().await;
7418        let link = stack.push(rendered_procfs::c_string::<LINK_CAPACITY>(
7419            format!("/proc/thread-self/fd/{fd}").as_bytes(),
7420        ));
7421        let target = stack.reserve::<[u8; TARGET_CAPACITY]>();
7422        let stack_guard = stack.commit().ok()?;
7423        let length = guest
7424            .inject(
7425                syscalls::Readlinkat::new()
7426                    .with_dirfd(libc::AT_FDCWD)
7427                    .with_path(PathPtr::from_ptr(link.as_raw() as *const libc::c_char))
7428                    .with_buf(Some(target.cast::<libc::c_char>()))
7429                    .with_buf_len(TARGET_CAPACITY),
7430            )
7431            .await;
7432        let bytes = match length.map(|length| length as usize) {
7433            Ok(length) if length < TARGET_CAPACITY => {
7434                let mut bytes = vec![0; length];
7435                guest
7436                    .memory()
7437                    .read_exact(target.cast::<u8>(), &mut bytes)
7438                    .ok()
7439                    .map(|()| bytes)
7440            }
7441            _ => None,
7442        };
7443        drop(stack_guard);
7444        Some(PathBuf::from(
7445            <std::ffi::OsString as std::os::unix::ffi::OsStringExt>::from_vec(bytes?),
7446        ))
7447    }
7448
7449    /// Whether the descriptor `fd` an open just returned is one of Linux's
7450    /// random devices (`crate::fd::is_random_device`), by the host `fstat`
7451    /// the open already took (`stat`, when metadata is virtualized) or by one
7452    /// `fstat` through the recording ([`Self::recorded_fstat`]). Under record
7453    /// and replay the latter is always used: replay's descriptor is a
7454    /// placeholder, so only the recorded result names the device.
7455    async fn opened_random_device<G: Guest<Self>>(
7456        &self,
7457        guest: &mut G,
7458        fd: RawFd,
7459        stat: Option<&libc::stat>,
7460    ) -> Result<bool, StatFailure> {
7461        let stat = match stat {
7462            Some(stat) if !self.cfg.recordreplay_modes => *stat,
7463            _ => self.recorded_fstat(guest, fd).await?,
7464        };
7465        Ok(crate::fd::is_random_device(stat.st_mode, stat.st_rdev))
7466    }
7467
7468    /// [`Self::recorded_fstat`] of a descriptor an `SCM_RIGHTS` receive
7469    /// installed (`Detcore::record_received_descriptors`). A failed call,
7470    /// recorded or not, is its errno, which leaves the descriptor unrecorded.
7471    /// Under record and replay a buffer that cannot be prepared ends the run
7472    /// instead, as an open's classification does: it recorded no event, so
7473    /// the other run could consume a different one.
7474    pub(crate) async fn received_descriptor_fstat<G: Guest<Self>>(
7475        &self,
7476        guest: &mut G,
7477        raw_fd: RawFd,
7478    ) -> Result<libc::stat, Error> {
7479        match self.recorded_fstat(guest, raw_fd).await {
7480            Ok(stat) => Ok(stat),
7481            Err(StatFailure::Scratch(errno)) if self.cfg.recordreplay_modes => {
7482                Err(Error::Tool(anyhow::anyhow!(
7483                    "cannot record descriptor {raw_fd}, received over SCM_RIGHTS, under \
7484                     record and replay: no buffer for its fstat ({errno})"
7485                )))
7486            }
7487            Err(failure) => Err(failure.errno().into()),
7488        }
7489    }
7490
7491    /// `fstat(fd)` through the recording, with [`Self::inject_fstat`]'s buffer
7492    /// placement and retry: recorded under record, served from the recording
7493    /// under replay, injected otherwise. Every attempt, including an
7494    /// interrupted one, is a recorded event. The buffer's placement consumes
7495    /// no event: the stack scratch is used or not by the guest's own stack
7496    /// pointer and mappings, which replay reproduces, and the transient page
7497    /// is an unrecorded private mapping of the same address space. If that
7498    /// mapping fails, `StatFailure::Scratch` reports it, so the caller can
7499    /// refuse to continue a recording or replay rather than consume a
7500    /// different event than the other run did.
7501    async fn recorded_fstat<G: Guest<Self>>(
7502        &self,
7503        guest: &mut G,
7504        raw_fd: RawFd,
7505    ) -> Result<libc::stat, StatFailure> {
7506        info!(
7507            "Taking fstat through the recording to classify fd {}.",
7508            raw_fd
7509        );
7510        let fstat = move |statptr: StatPtr<'_>| {
7511            Syscall::Fstat(
7512                syscalls::Fstat::new()
7513                    .with_fd(raw_fd)
7514                    .with_stat(Some(statptr)),
7515            )
7516        };
7517        Self::stat_with_fallback(guest, raw_fd, fstat, Some(self)).await
7518    }
7519
7520    /// Close descriptor `fd`, which an open just returned but Detcore could
7521    /// not classify, through the recording, and then forget it. A close that
7522    /// provably never ran (ERESTARTSYS: a signal stopped the injection before
7523    /// its `syscall` instruction) is retried; one that ran is not, as Linux
7524    /// releases the descriptor even when `close` reports EINTR or EIO. A close
7525    /// still refused after the retries, or refused otherwise, ends the run:
7526    /// the descriptor would stay open in the guest with no model.
7527    async fn close_unclassified<G: Guest<Self>>(
7528        &self,
7529        guest: &mut G,
7530        fd: RawFd,
7531    ) -> Result<(), Error> {
7532        const ATTEMPTS: usize = 16;
7533        let mut last = Errno::ERESTARTSYS;
7534        for _ in 0..ATTEMPTS {
7535            match self
7536                .record_or_replay(guest, syscalls::Close::new().with_fd(fd))
7537                .await
7538            {
7539                Ok(_) | Err(Errno::EINTR | Errno::EIO) => {
7540                    guest.thread_state().remove_fd(fd);
7541                    return Ok(());
7542                }
7543                Err(Errno::ERESTARTSYS) => last = Errno::ERESTARTSYS,
7544                Err(errno) => {
7545                    last = errno;
7546                    break;
7547                }
7548            }
7549        }
7550        Err(Error::Tool(anyhow::anyhow!(
7551            "cannot close fd {fd}, which an open returned but Detcore could not \
7552             classify: close answered {last}"
7553        )))
7554    }
7555
7556    /// What descriptor `fd` of the calling process names, as its
7557    /// `/proc/thread-self/fd` link reads, through the recording
7558    /// ([`Self::recorded_descriptor_link`]): `None` for an anonymous target,
7559    /// a link that cannot be read, one too long for the 256 bytes read (every
7560    /// path Detcore classifies is far shorter, and a truncated one must not
7561    /// match), or one Detcore had no buffer to read into, which must never
7562    /// fail the guest's open.
7563    async fn recorded_open_path<G: Guest<Self>>(
7564        &self,
7565        guest: &mut G,
7566        fd: RawFd,
7567    ) -> Result<Option<PathBuf>, Error> {
7568        let bytes = self
7569            .recorded_descriptor_link(guest, fd)
7570            .await
7571            .unwrap_or(None);
7572        Ok(bytes.and_then(|bytes| {
7573            let path = PathBuf::from(
7574                <std::ffi::OsString as std::os::unix::ffi::OsStringExt>::from_vec(bytes),
7575            );
7576            path.is_absolute().then_some(path)
7577        }))
7578    }
7579
7580    /// The bytes of descriptor `fd`'s `/proc/thread-self/fd` link, through the
7581    /// recording ([`Self::recorded_readlink`]): the recording reads the
7582    /// kernel's link and keeps its bytes, and replay, whose slot may hold a
7583    /// stand-in eventfd, returns the same bytes. `None` for a link that cannot
7584    /// be read or fills the 256 bytes read.
7585    ///
7586    /// The link's name and the buffer go in the guest stack scratch, below the
7587    /// red zone, or, where that memory is not writable (a stack pointer just
7588    /// above a guard page or the lowest page of the main-thread stack), in a
7589    /// transient private page, as `inject_fstat` does
7590    /// (<https://github.com/rrnewton/hermit/issues/3328>). Neither placement
7591    /// consumes an event. `Err` when neither could be prepared: no readlink
7592    /// ran, so no event was recorded or consumed, and a caller under record
7593    /// and replay decides whether that may end the run.
7594    pub(crate) async fn recorded_descriptor_link<G: Guest<Self>>(
7595        &self,
7596        guest: &mut G,
7597        fd: RawFd,
7598    ) -> Result<Option<Vec<u8>>, Errno> {
7599        const LINK_CAPACITY: usize = 32;
7600        const TARGET_CAPACITY: usize = 256;
7601        let link_name = rendered_procfs::c_string::<LINK_CAPACITY>(
7602            format!("/proc/thread-self/fd/{fd}").as_bytes(),
7603        );
7604        let mut stack = guest.stack().await;
7605        let link = stack.push(link_name);
7606        let target = stack.reserve::<[u8; TARGET_CAPACITY]>();
7607        match stack.commit() {
7608            Ok(stack_guard) => {
7609                let bytes = self
7610                    .recorded_readlink(guest, link.as_raw(), target.as_raw(), TARGET_CAPACITY)
7611                    .await;
7612                drop(stack_guard);
7613                Ok(bytes)
7614            }
7615            Err(_) => {
7616                info!(
7617                    "Guest stack scratch cannot hold the link of fd {}; using a transient page \
7618                     instead.",
7619                    fd
7620                );
7621                let len = LINK_CAPACITY + TARGET_CAPACITY;
7622                let mapped = guest
7623                    .inject_with_retry(Syscall::Mmap(
7624                        syscalls::Mmap::new()
7625                            .with_addr(None)
7626                            .with_len(len)
7627                            .with_prot(ProtFlags::PROT_READ | ProtFlags::PROT_WRITE)
7628                            .with_flags(MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS)
7629                            .with_fd(-1)
7630                            .with_offset(0),
7631                    ))
7632                    .await;
7633                let page = mapped.and_then(|mapped| {
7634                    usize::try_from(mapped)
7635                        .ok()
7636                        .and_then(AddrMut::<u8>::from_raw)
7637                        .ok_or(Errno::ENOMEM)
7638                })?;
7639                let bytes = if let Err(errno) = guest.memory().write_exact(page, &link_name) {
7640                    Err(errno)
7641                } else {
7642                    Ok(self
7643                        .recorded_readlink(
7644                            guest,
7645                            page.as_raw(),
7646                            page.as_raw() + LINK_CAPACITY,
7647                            TARGET_CAPACITY,
7648                        )
7649                        .await)
7650                };
7651                if let Err(errno) = guest
7652                    .inject_with_retry(Syscall::Munmap(
7653                        syscalls::Munmap::new()
7654                            .with_addr(Some(page.cast::<libc::c_void>().into()))
7655                            .with_len(len),
7656                    ))
7657                    .await
7658                {
7659                    warn!(
7660                        "[detcore] could not unmap the transient link page: {}",
7661                        errno
7662                    );
7663                }
7664                bytes
7665            }
7666        }
7667    }
7668
7669    /// `readlink(link, target, capacity)` through the recording, both at guest
7670    /// addresses: the bytes it read, or `None` if it failed or filled the
7671    /// buffer (a link that may be truncated). Retried while a signal stops it
7672    /// before it runs; every attempt is an event, so replay retries as the
7673    /// recording did.
7674    async fn recorded_readlink<G: Guest<Self>>(
7675        &self,
7676        guest: &mut G,
7677        link: usize,
7678        target: usize,
7679        capacity: usize,
7680    ) -> Option<Vec<u8>> {
7681        const ATTEMPTS: usize = 64;
7682        let target = AddrMut::<u8>::from_raw(target)?;
7683        let path = PathPtr::from_ptr(link as *const libc::c_char);
7684        let buf = Some(target.cast::<libc::c_char>());
7685        #[cfg(target_arch = "x86_64")]
7686        let call = syscalls::Readlink::new()
7687            .with_path(path)
7688            .with_buf(buf)
7689            .with_bufsize(capacity);
7690        // aarch64 has only `readlinkat`.
7691        #[cfg(not(target_arch = "x86_64"))]
7692        let call = syscalls::Readlinkat::new()
7693            .with_dirfd(libc::AT_FDCWD)
7694            .with_path(path)
7695            .with_buf(buf)
7696            .with_buf_len(capacity);
7697        let mut answer = Err(Errno::ERESTARTSYS);
7698        for _ in 0..ATTEMPTS {
7699            answer = self.record_or_replay(guest, call).await;
7700            if !matches!(answer, Err(Errno::EINTR | Errno::ERESTARTSYS)) {
7701                break;
7702            }
7703        }
7704        let length = answer.ok()?;
7705        let length = usize::try_from(length)
7706            .ok()
7707            .filter(|length| *length < capacity)?;
7708        let mut bytes = vec![0; length];
7709        guest.memory().read_exact(target, &mut bytes).ok()?;
7710        Some(bytes)
7711    }
7712
7713    /// A `read` (`offset` `None`, at the shared cursor, which it advances by
7714    /// the bytes copied) or a `pread64` (at `offset`) of the rendered
7715    /// `/proc/<pid>/cmdline` open as `fd`, in `vfs_read`'s order
7716    /// (fs/read_write.c): a descriptor that cannot read is EBADF; a buffer
7717    /// beyond the user address limit is EFAULT; `rw_verify_area` refuses a
7718    /// count above `SSIZE_MAX` or a position whose end overflows (EINVAL);
7719    /// then the file's own read, even for a count of zero, which still finds
7720    /// its task (ESRCH once it is gone), with the count cut to
7721    /// `MAX_RW_COUNT` (`Detcore::read_rendered_cmdline`).
7722    async fn read_rendered_cmdline_call<G: Guest<Self>>(
7723        &self,
7724        guest: &mut G,
7725        fd: RawFd,
7726        buf: Option<AddrMut<'_, u8>>,
7727        len: usize,
7728        offset: Option<u64>,
7729    ) -> Result<i64, Error> {
7730        let status = guest
7731            .thread_state()
7732            .with_detfd(fd, |detfd| detfd.status_flags())?;
7733        if !can_read(status) {
7734            return Err(Errno::EBADF.into());
7735        }
7736        validate_procfs_destination(
7737            || crate::iovecs::UserAddressLimit::from_query(guest.user_address_limit()),
7738            buf,
7739            len,
7740        )?;
7741        let pos = match offset {
7742            Some(offset) => offset,
7743            None => {
7744                self.adopt_rendered_procfs_position(guest, fd).await?;
7745                guest
7746                    .thread_state()
7747                    .with_detfd(fd, |detfd| detfd.procfs_position())?
7748                    .map_or(0, |(position, _)| position) as u64
7749            }
7750        };
7751        let ends = i64::try_from(len)
7752            .ok()
7753            .and_then(|len| i64::try_from(pos).ok()?.checked_add(len));
7754        if ends.is_none() {
7755            return Err(Errno::EINVAL.into());
7756        }
7757        let destination = buf.map_or(0, |buf| buf.as_raw());
7758        let copied = self
7759            .read_rendered_cmdline(
7760                guest,
7761                fd,
7762                destination,
7763                len.min(crate::page::max_rw_count()),
7764                pos,
7765            )
7766            .await?;
7767        if offset.is_none() {
7768            guest.thread_state().with_detfd(fd, |detfd| {
7769                detfd.commit_procfs_read(pos as usize, copied);
7770            })?;
7771            self.sync_rendered_procfs_position(guest, fd).await?;
7772        }
7773        Ok(copied as i64)
7774    }
7775
7776    /// Whether an operation on `fd` must be refused because its open file's
7777    /// record sequence is one Detcore cannot know here
7778    /// (`DetFd::record_sequence_unknown`): a read, positioned or vectored
7779    /// (`seek` false), through a descriptor that can read, or a seek
7780    /// (`seek` true) through one that has a position. A read through a
7781    /// descriptor that cannot read never reaches the sequence (`vfs_read`
7782    /// refuses it with EBADF first, fs/read_write.c:556-557 in 6.17), nor does
7783    /// a seek of an `O_PATH` descriptor (EBADF): those keep their answers.
7784    ///
7785    /// Also where a refusal that ends the run would strand it
7786    /// ([`refusal_would_strand_the_run`]): there the call gets EOPNOTSUPP
7787    /// ([`Self::refuse_unknown_record_sequence`]), so no read through the
7788    /// descriptor reaches a separate copy of the sequence or the kernel's own
7789    /// file. Before, SaBRe refused nothing here, and answered stale bytes, end
7790    /// of file and, for a file the kernel opened, the host's own unsanitized
7791    /// statm, with no message (Claude's final check of the kvm lane's slice
7792    /// N3).
7793    fn refuses_unknown_record_sequence<G: Guest<Self>>(guest: &G, fd: RawFd, seek: bool) -> bool {
7794        guest
7795            .thread_state()
7796            .with_detfd(fd, |detfd| {
7797                detfd.record_sequence_unknown() && {
7798                    let status = detfd.status_flags();
7799                    if seek {
7800                        status & libc::O_PATH == 0
7801                    } else {
7802                        can_read(status)
7803                    }
7804                }
7805            })
7806            .unwrap_or(false)
7807    }
7808
7809    /// Refuse `sysno` on `fd` by name ([`Self::refuses_unknown_record_sequence`]),
7810    /// through the unsupported-operation policy: a fail-closed run stops with
7811    /// the policy's exit status (122) and this reason (on DBT, whose runtime
7812    /// ends a refusal through its own exit and report path, 101 or 137 and the
7813    /// reason), and a run started with `--allow-unsupported-syscalls` gives the
7814    /// call EOPNOTSUPP. Where that stop would strand the run
7815    /// ([`refusal_would_strand_the_run`], SaBRe's capabilities), the call gets
7816    /// EOPNOTSUPP as with `--allow-unsupported-syscalls`, no process ends, and
7817    /// the reason goes to the guest's standard error ([`write_to_guest_stderr`]):
7818    /// the tool runs in the guest's process there and installs no log
7819    /// subscriber, so `error!` would print nothing. Boxed, so the per-syscall
7820    /// futures that call it stay within their bound
7821    /// (`hot_path_futures_stay_small`).
7822    fn refuse_unknown_record_sequence<'a, G: Guest<Self>>(
7823        &'a self,
7824        guest: &'a mut G,
7825        sysno: Sysno,
7826        fd: RawFd,
7827    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<i64, Error>> + Send + 'a>> {
7828        Box::pin(async move {
7829            let path = guest
7830                .thread_state()
7831                .with_detfd(fd, |detfd| detfd.path())
7832                .ok()
7833                .flatten();
7834            let reason = unknown_record_sequence_reason(fd, path.as_deref());
7835            let strands = refusal_would_strand_the_run(guest.config());
7836            if strands {
7837                write_to_guest_stderr(&format!(
7838                    "Detcore refuses {sysno}: {reason}; the call gets EOPNOTSUPP, as a \
7839                     refusal that ended the run would leave the run waiting on this process\n"
7840                ));
7841            } else {
7842                // Named where the run's own output shows it: a backend whose
7843                // tool runs in Hermit's process prints no stored reason.
7844                error!("Detcore refuses {sysno}: {reason}");
7845            }
7846            crate::syscalls::helpers::refuse_unserviceable_operation_with(
7847                guest,
7848                self.cfg.panic_on_unsupported_syscalls && !strands,
7849                sysno,
7850                Errno::EOPNOTSUPP,
7851                Some(&reason),
7852            )
7853            .await
7854        })
7855    }
7856
7857    /// The identity of `fd`'s open file description, where it is a per-process
7858    /// file whose record sequence a fork shares, so that the sequence lives in
7859    /// Detcore's global state between operations
7860    /// (`ProcfsFile::sequence_is_shared`), and this thread has not taken it
7861    /// already for the operation it is handling
7862    /// (`ThreadState::procfs_sequence_taken`).
7863    fn procfs_sequence_to_take<G: Guest<Self>>(guest: &G, fd: RawFd) -> Option<OpenFileId> {
7864        let id = guest
7865            .thread_state()
7866            .with_detfd(fd, |detfd| detfd.procfs_shared_sequence())
7867            .ok()
7868            .flatten()?;
7869        (guest.thread_state().procfs_sequence_taken != Some(id)).then_some(id)
7870    }
7871
7872    /// A vectored read of a per-process file whose record sequence a fork
7873    /// shares, as [`Self::read_with_shared_sequence`].
7874    #[allow(clippy::type_complexity, clippy::too_many_arguments)]
7875    fn readv_with_shared_sequence<'a, G: Guest<Self>>(
7876        &'a self,
7877        guest: &'a mut G,
7878        fd: RawFd,
7879        address: usize,
7880        count: usize,
7881        offset: Option<i64>,
7882        flags: i32,
7883        id: OpenFileId,
7884    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<i64, Error>> + Send + 'a>> {
7885        Box::pin(async move {
7886            self.take_procfs_sequence(guest, fd, id).await;
7887            let result = self
7888                .readv_rendered_procfs(guest, fd, address, count, offset, flags)
7889                .await;
7890            self.store_procfs_sequence(guest, fd, id).await;
7891            result
7892        })
7893    }
7894
7895    /// Before a read, positioned read, vectored read or seek of a per-process
7896    /// file whose description a fork copied where the tool runs in each guest
7897    /// process: take its record sequence (the cursor, where the sequence is,
7898    /// and the buffered record) from Detcore's global state, as the last such
7899    /// operation on the description, in whichever process, left it, so this
7900    /// one decides from the state Linux's one `struct file` and `seq_file`
7901    /// would hold (`seq_read_iter`, `seq_lseek`, fs/seq_file.c:171-206 and
7902    /// 308-341 in 6.17). The first operation after the fork finds none there
7903    /// and keeps this process's copy, which is the one the fork made
7904    /// (`ThreadState::share_procfs_sequences`). It runs in this thread's turn,
7905    /// as does [`Self::store_procfs_sequence`] after the operation, so every
7906    /// process's operations follow one another in the guest's order; the
7907    /// handler the operation runs again finds the sequence taken
7908    /// (`ThreadState::procfs_sequence_taken`) and takes nothing more.
7909    async fn take_procfs_sequence<G: Guest<Self>>(&self, guest: &mut G, fd: RawFd, id: OpenFileId) {
7910        let mut sequence = crate::tool_global::procfs_sequence(guest, id).await;
7911        if sequence.is_some() {
7912            // The descriptor is the one just looked up; nothing ran between.
7913            let _ = guest.thread_state().with_detfd(fd, |detfd| {
7914                if let Some(sequence) = sequence.take() {
7915                    detfd.adopt_procfs_sequence(sequence);
7916                }
7917            });
7918        }
7919        guest.thread_state_mut().procfs_sequence_taken = Some(id);
7920    }
7921
7922    /// After the operation [`Self::take_procfs_sequence`] preceded, whatever
7923    /// its result: keep the description's record sequence in Detcore's global
7924    /// state, for the next operation, in whichever process.
7925    async fn store_procfs_sequence<G: Guest<Self>>(
7926        &self,
7927        guest: &mut G,
7928        fd: RawFd,
7929        id: OpenFileId,
7930    ) {
7931        guest.thread_state_mut().procfs_sequence_taken = None;
7932        let sequence = guest
7933            .thread_state()
7934            .with_detfd(fd, |detfd| {
7935                (detfd.procfs_shared_sequence() == Some(id))
7936                    .then(|| detfd.procfs_sequence())
7937                    .flatten()
7938            })
7939            .ok()
7940            .flatten();
7941        if let Some(sequence) = sequence {
7942            crate::tool_global::store_procfs_sequence(guest, id, sequence).await;
7943        }
7944    }
7945
7946    /// Before a send that passes the descriptors its messages at `headers`
7947    /// name over `SCM_RIGHTS`, where the tool runs in each guest process:
7948    /// each that is a rendered per-process file following its record's
7949    /// sequence keeps that sequence in Detcore's global state from then on
7950    /// (`ThreadState::share_sent_procfs_sequence`), taken and stored there now,
7951    /// in the caller's turn, before the descriptor can reach another process:
7952    /// a receiver whose registry names the open file takes it from there and
7953    /// continues this process's record, as Linux's receiver does with the one
7954    /// `struct file` (`Detcore::adopt_received_placeholder`,
7955    /// `FileMetadata::adopt_placeholder`), and this process takes back what
7956    /// that one stores. A description already shared there takes the stored
7957    /// sequence first, so a stale copy never replaces it. Read from the
7958    /// messages before the call, as `refuse_sending_virtual_timerfds` reads
7959    /// them; a placeholder is never sent in a batch (`sendmmsg` refuses it).
7960    pub(crate) async fn share_sent_record_sequences<G: Guest<Self>>(
7961        &self,
7962        guest: &mut G,
7963        headers: &[usize],
7964    ) {
7965        if crate::syscalls::timerfd::detcore_runs_outside_the_guest(&self.cfg.backend) {
7966            return;
7967        }
7968        let (sent, _) = crate::syscalls::io::sent_descriptors(guest, headers);
7969        for fd in sent {
7970            let Some(id) = guest.thread_state().share_sent_procfs_sequence(fd) else {
7971                continue;
7972            };
7973            self.take_procfs_sequence(guest, fd, id).await;
7974            self.store_procfs_sequence(guest, fd, id).await;
7975        }
7976    }
7977
7978    /// The checks of the `show` of a per-process file's record, where a read
7979    /// or a seek at `position` runs it (`ProcfsFile::drop_stale_task_snapshot`,
7980    /// `ProcfsFile::seek_task_record`): for a rendered file the task must
7981    /// still be the one the file was bound to (ESRCH), and a task's `io` asks
7982    /// whether the reader may read it (EACCES)
7983    /// (`Self::require_rendered_task`), before the record is rendered; for
7984    /// one the kernel opened, nothing here: the new capture of the kernel's
7985    /// file that follows is the kernel's own `show`, which answers. Linux's
7986    /// `seq_read_iter` and `seq_lseek` run `show` at a read at 0 and at a read
7987    /// or seek elsewhere than the sequence, and never for a read that
7988    /// continues it, so a revoked `io` or a reaped task is found at those
7989    /// reads only, positioned or not (fs/seq_file.c:188-206 and 308-340 in
7990    /// 6.17). A process's `stat` and a task's `comm` find the task as they
7991    /// render (`Self::rendered_task_stat`): nothing more here. A failed `show`
7992    /// resets the sequence as `seq_read_iter` does after a failed traversal
7993    /// (`ProcfsFile::note_task_record_rendered`), and leaves the shared cursor.
7994    async fn check_record_show<G: Guest<Self>>(
7995        &self,
7996        guest: &mut G,
7997        fd: RawFd,
7998        position: usize,
7999    ) -> Result<(), Error> {
8000        if guest
8001            .thread_state()
8002            .with_detfd(fd, |detfd| detfd.procfs_renders_task_record())?
8003        {
8004            return Ok(());
8005        }
8006        if let Err(error) = self.require_rendered_task(guest, fd).await {
8007            guest.thread_state().with_detfd(fd, |detfd| {
8008                detfd.procfs_note_task_record_rendered(position, false)
8009            })?;
8010            return Err(error);
8011        }
8012        Ok(())
8013    }
8014
8015    async fn initialize_procfs_snapshot<G: Guest<Self>>(
8016        &self,
8017        guest: &mut G,
8018        call: syscalls::Read,
8019    ) -> Result<(), Error> {
8020        let format = guest
8021            .thread_state()
8022            .with_detfd(call.fd(), |fd| fd.procfs_private_mapping_format())?;
8023        let maps_target = if let Some(format) = format
8024            && guest.runtime_private_memory_regions().is_some()
8025        {
8026            let name = format.name();
8027            if !guest.config().sequentialize_threads {
8028                return Err(Error::Tool(anyhow::anyhow!(
8029                    "runtime-private {name} view unavailable: capture is not sequentialized"
8030                )));
8031            }
8032            let target = guest
8033                .thread_state()
8034                .with_detfd(call.fd(), |fd| fd.procfs_maps_target())?
8035                .ok_or_else(|| {
8036                    Error::Tool(anyhow::anyhow!(
8037                        "runtime-private {name} view unavailable: opened target image is unbound"
8038                    ))
8039                })?;
8040            if !guest.runtime_private_memory_is_current(&target.private) {
8041                return Err(Error::Tool(anyhow::anyhow!(
8042                    "runtime-private {name} view unavailable: target ownership retired"
8043                )));
8044            }
8045            Some((format, target))
8046        } else {
8047            None
8048        };
8049        let template = guest
8050            .thread_state()
8051            .with_detfd(call.fd(), |detfd| detfd.procfs_rendering_template())?;
8052        // A process's `mountinfo` or `mounts`, in a run with a container
8053        // mount table: the table's bytes, whoever opened the file.
8054        let mount_table = self.mount_table_snapshot(guest, call.fd())?;
8055        // `/proc/locks` where the guest's procfs is not the kernel's: the
8056        // lock ledger's rows, whoever opened the file (Detcore, or the
8057        // backend's own procfs through a spelling Detcore does not resolve,
8058        // such as one with `..`), instead of the backend's file (package K
8059        // slice K8-h).
8060        let ledger_locks = self.renders_proc_locks(guest)
8061            && guest
8062                .thread_state()
8063                .with_detfd(call.fd(), |detfd| detfd.procfs_lists_locks())?;
8064        let raw_contents = match template {
8065            // A descriptor that cannot read (`O_PATH`, write-only, or access
8066            // mode 3) reads nothing (EBADF), as the kernel's read of it,
8067            // which a rendered snapshot does not make, would answer.
8068            Some(_)
8069                if !can_read(
8070                    guest
8071                        .thread_state()
8072                        .with_detfd(call.fd(), |detfd| detfd.status_flags())?,
8073                ) =>
8074            {
8075                return Err(Errno::EBADF.into());
8076            }
8077            Some(template) => {
8078                // A per-process file's `show` finds its task again: a task
8079                // that is gone, or whose pid another task now holds, is ESRCH;
8080                // a task's `io` also asks whether the reader may read it. The
8081                // caller runs those checks where Linux runs `show`
8082                // (`Self::check_record_show`), except for a process's `stat`
8083                // and a task's `comm`, whose rendering finds the task itself
8084                // (`Self::rendered_task_stat`) and which are also checked here
8085                // at the cursor 0. A description with no snapshot at another
8086                // position is one rebuilt from a placeholder whose first read
8087                // happened in another process (`adopt_received_placeholder`,
8088                // `adopt_placeholder`), where Linux reads on from the buffer
8089                // that read filled, without looking for the task.
8090                let (position, task_record) =
8091                    guest.thread_state().with_detfd(call.fd(), |detfd| {
8092                        (
8093                            detfd.procfs_position().map_or(0, |(offset, _)| offset),
8094                            detfd.procfs_renders_task_record(),
8095                        )
8096                    })?;
8097                if position == 0 && task_record {
8098                    self.require_rendered_task(guest, call.fd()).await?;
8099                }
8100                // A rendered `/proc/diskstats` starts from the host's table
8101                // as the run first read it, from the coordinator, not from
8102                // its template: one rebuilt from a placeholder in another
8103                // process or image carries no snapshot, and asks for the
8104                // same table its open took. A table the run could not read
8105                // is a named failure, never a table of no devices.
8106                if guest
8107                    .thread_state()
8108                    .with_detfd(call.fd(), |detfd| detfd.procfs_renders_host_diskstats())?
8109                {
8110                    crate::tool_global::host_diskstats(guest)
8111                        .await
8112                        .map_err(|errno| {
8113                            Error::Tool(anyhow::anyhow!(
8114                                "the host's /proc/diskstats, which a rendered /proc/diskstats \
8115                                 is made from, could not be read: {errno}"
8116                            ))
8117                        })?
8118                } else {
8119                    // `/proc/locks` Detcore renders: its lock ledger's rows now
8120                    // (package K slice K8-h).
8121                    match mount_table {
8122                        Some(table) => table,
8123                        None if ledger_locks => Box::pin(self.ledger_locks_text(guest)).await,
8124                        None => template.to_vec(),
8125                    }
8126                }
8127            }
8128            // A mount table file the kernel opened takes no capture of the
8129            // kernel's bytes, which the table replaces; a descriptor that
8130            // cannot read reads nothing (EBADF), as the kernel's read of it
8131            // would answer.
8132            None if let Some(table) = mount_table => {
8133                if !can_read(
8134                    guest
8135                        .thread_state()
8136                        .with_detfd(call.fd(), |detfd| detfd.status_flags())?,
8137                ) {
8138                    return Err(Errno::EBADF.into());
8139                }
8140                table
8141            }
8142            None if ledger_locks => {
8143                if !can_read(
8144                    guest
8145                        .thread_state()
8146                        .with_detfd(call.fd(), |detfd| detfd.status_flags())?,
8147                ) {
8148                    return Err(Errno::EBADF.into());
8149                }
8150                Box::pin(self.ledger_locks_text(guest)).await
8151            }
8152            None => {
8153                if let Some((crate::procfs::private_maps::Format::NumaMaps, target)) = &maps_target
8154                {
8155                    if !can_read(
8156                        guest
8157                            .thread_state()
8158                            .with_detfd(call.fd(), |fd| fd.status_flags())?,
8159                    ) {
8160                        return Err(Errno::EBADF.into());
8161                    }
8162                    crate::tool_global::mapping_capture::capture(guest, target)?
8163                } else if let Some(rendered) = self.maps_from_memory_map(guest, call.fd()).await? {
8164                    // The kernel's file stays unread, so Detcore keeps its
8165                    // position at the cursor from now on, as a read of it
8166                    // would move it (`DetFd::procfs_keeps_kernel_position`).
8167                    guest.thread_state().with_detfd(call.fd(), |detfd| {
8168                        detfd.note_procfs_rendered_from_memory_map()
8169                    })?;
8170                    rendered
8171                } else {
8172                    self.snapshot_procfs(guest, call).await?
8173                }
8174            }
8175        };
8176        let raw_contents = if let Some((format, target)) = maps_target {
8177            let name = format.name();
8178            if !guest.runtime_private_memory_is_current(&target.private) {
8179                return Err(Error::Tool(anyhow::anyhow!(
8180                    "runtime-private {name} view unavailable: target ownership changed during capture"
8181                )));
8182            }
8183            if format == crate::procfs::private_maps::Format::NumaMaps {
8184                // The synchronous capture already projected its coherent pair
8185                // with this exact ownership fact. The existing initializer
8186                // below applies the unchanged NUMA sanitizer exactly once.
8187                raw_contents
8188            } else {
8189                format
8190                    .project(&raw_contents, &target.private.regions)
8191                    .map_err(|reason| {
8192                        Error::Tool(anyhow::anyhow!(
8193                            "runtime-private {name} view unavailable: {reason}"
8194                        ))
8195                    })?
8196            }
8197        } else {
8198            raw_contents
8199        };
8200        // The guest mount view is the launch namespace (guest mount/unshare/
8201        // setns are refused under Detcore). Ephemeral host FUSE seed rows
8202        // (per-process and named-tool seeds) are other tenants' propagated
8203        // runtime state, not part of that namespace; exclude the class at
8204        // capture so snapshot identity assignment and every later read agree
8205        // on the same membership.
8206        let contents = if guest
8207            .thread_state()
8208            .with_detfd(call.fd(), |detfd| detfd.procfs_needs_mountinfo_identities())?
8209        {
8210            crate::procfs::exclude_ephemeral_host_seed_mounts(&raw_contents)
8211        } else {
8212            raw_contents
8213        };
8214        // Another process's `stat` or `status`, where the kernel numbers
8215        // processes otherwise than Detcore (DBT), is renumbered to the
8216        // guest's numbers (slice B3a-4 of
8217        // https://github.com/rrnewton/hermit/issues/4266).
8218        let held = guest.thread_state().with_detfd(call.fd(), |detfd| {
8219            detfd.procfs_kernel_numbers_held(&contents)
8220        })?;
8221        // The init's files (`/proc/1/...`, Hermit's own process and threads)
8222        // keep the numbers the kernel gives them, as their paths do. Which
8223        // process's file it is, the kernel's name for the descriptor says
8224        // where the tool shares the guest's descriptors (DBT), whatever path
8225        // reached it.
8226        let path = if guest.config().backend.tool_shares_guest_descriptor_table {
8227            std::fs::read_link(format!("/proc/thread-self/fd/{}", call.fd())).ok()
8228        } else {
8229            guest
8230                .thread_state()
8231                .with_detfd(call.fd(), |detfd| detfd.path())?
8232        };
8233        let init = path.is_some_and(|path| crate::procfs::ids::names_init(&path));
8234        let held = if init { Vec::new() } else { held };
8235        if let Some(numbers) = Self::guest_procfs_numbers(guest, &held).await {
8236            guest.thread_state().with_detfd(call.fd(), |detfd| {
8237                detfd.set_procfs_guest_numbers(numbers.clone())
8238            })?;
8239        }
8240        let virtual_uptime_seconds = self.calculate_procfs_uptime(guest).await?;
8241        // A file Detcore renders at its first read: at the uptime an earlier
8242        // read of the same open file took its snapshot at, which a process
8243        // without Detcore's state of it (after `exec` or `fork` where the
8244        // tool runs in each guest process) finds on the placeholder.
8245        let virtual_uptime_seconds = match guest.thread_state().with_detfd(call.fd(), |detfd| {
8246            detfd
8247                .procfs_rendered_at_first_read()
8248                .then(|| detfd.procfs_rendered_path())
8249                .flatten()
8250        })? {
8251            Some(path) => {
8252                self.first_read_uptime(guest, call.fd(), path, virtual_uptime_seconds)
8253                    .await
8254            }
8255            None => virtual_uptime_seconds,
8256        };
8257        // Only `/proc/stat` renders btime. Computing it for every snapshot let
8258        // an unrepresentable boot instant refuse unrelated files such as
8259        // `/proc/uptime`.
8260        let needs_boot_time = guest
8261            .thread_state()
8262            .with_detfd(call.fd(), |detfd| detfd.procfs_needs_boot_time())?;
8263        let virtual_boot_time_seconds = needs_boot_time
8264            .then(|| self.calculate_procfs_boot_time())
8265            .transpose()?;
8266        let virtual_realtime_seconds = i64::try_from(thread_observe_time(guest).await.as_secs())
8267            .map_err(|_| Errno::EOVERFLOW)?;
8268        // TODO-HUMAN-REVIEW(PR-723): Review injected identity snapshot reads.
8269        let virtual_pid = guest.inject(syscalls::Getpid::new()).await? as i32;
8270        let virtual_ppid = guest.inject(syscalls::Getppid::new()).await? as i32;
8271        let virtual_pty_count = guest.thread_state().count_open_files_at_paths(&[
8272            std::path::Path::new("/dev/ptmx"),
8273            std::path::Path::new("/dev/pts/ptmx"),
8274        ]);
8275        let target_fd = guest
8276            .thread_state()
8277            .with_detfd(call.fd(), |detfd| detfd.procfs_target_fd())?;
8278        // The kernel's fdinfo for a procfs file Detcore renders describes the
8279        // memfd standing in for it. Linux's names the file: Detcore's cursor,
8280        // the guest's procfs mount and the file's inode. The cursor is the one
8281        // processes sharing the open file share.
8282        if let Some(target_fd) = target_fd {
8283            self.adopt_rendered_procfs_position(guest, target_fd)
8284                .await?;
8285        }
8286        let rendered_target = match target_fd {
8287            Some(target_fd) => guest
8288                .thread_state()
8289                .with_detfd(target_fd, |detfd| {
8290                    detfd.procfs_rendered_path().map(|path| {
8291                        (
8292                            path,
8293                            detfd.procfs_position().map_or(0, |(offset, _)| offset),
8294                        )
8295                    })
8296                })
8297                .ok()
8298                .flatten(),
8299            None => None,
8300        };
8301        // The kernel's fdinfo for a placeholder descriptor describes the
8302        // placeholder: its tag as the position, its tmpfs mount and inode.
8303        // Linux's names the directory it stands for: Detcore's position, the
8304        // guest's procfs mount and the directory's inode
8305        // (`descriptor_directory`).
8306        // Boxed, so that this call's future holds its answer alone.
8307        let placeholder = match target_fd {
8308            Some(target_fd) if rendered_target.is_none() => {
8309                Box::pin(self.placeholder_fdinfo(guest, target_fd)).await?
8310            }
8311            _ => None,
8312        };
8313        let placeholder_inode = placeholder.as_ref().map(|(_, inode)| *inode);
8314        let contents = match (rendered_target, placeholder) {
8315            (Some((_, offset)), _) => format!(
8316                "pos:\t{offset}\nflags:\t0\nmnt_id:\t{}\nino:\t0\n",
8317                rendered_procfs::guest_procfs_mount_id()?
8318            )
8319            .into_bytes(),
8320            (None, Some((placeholder, _))) => placeholder,
8321            (None, None) => contents,
8322        };
8323        let fdinfo_identity = if let Some(target_fd) = target_fd {
8324            let (cached_stat, logical_flags, open_file_id, fd_type, resource) =
8325                guest.thread_state().with_detfd(target_fd, |detfd| {
8326                    (
8327                        detfd.stat(),
8328                        detfd.status_flags(),
8329                        detfd.open_file_id(),
8330                        detfd.ty(),
8331                        detfd.resource(),
8332                    )
8333                })?;
8334            let stat = match cached_stat {
8335                Some(stat) => stat,
8336                None => DetStat::from(self.inject_fstat(guest, target_fd).await?),
8337            };
8338            // Boxed, so that this call's future holds its answer alone.
8339            let virtual_inode = Box::pin(self.fdinfo_inode(
8340                guest,
8341                target_fd,
8342                &stat,
8343                resource,
8344                rendered_target.map(|(path, _)| path),
8345                placeholder_inode,
8346            ))
8347            .await;
8348            let raw_mount_id =
8349                detcore_model::procfs::parse_fdinfo_mount_id(&contents).ok_or_else(|| {
8350                    Error::Tool(anyhow::anyhow!(
8351                        "kernel returned malformed /proc/*/fdinfo without one numeric mnt_id"
8352                    ))
8353                })?;
8354            // CLI container and recording paths provide the exact namespace's
8355            // row order. Replay intentionally retains the recording-time raw
8356            // IDs because ReadV2 supplies recording-time fdinfo bytes.
8357            let has_configured_mount_ids = guest.config().mountinfo_mount_ids_captured;
8358            let virtual_mount_id = if raw_mount_id == 0 {
8359                // Linux uses zero for anonymous objects such as memfd. Key the
8360                // equivalence on the observed value, not our descriptor type.
8361                0
8362            } else if !has_configured_mount_ids {
8363                // Public non-container callers have no pre-captured provenance.
8364                // Mount/unshare/setns are refused once Detcore starts, so a
8365                // tracer-side snapshot of this task's namespace is immutable.
8366                let mountinfo_path = format!("/proc/{}/mountinfo", guest.pid().as_raw());
8367                let mountinfo_contents = std::fs::read(&mountinfo_path).map_err(|error| {
8368                    Error::Tool(anyhow::anyhow!(
8369                        "failed to read {mountinfo_path} while validating fdinfo mnt_id: {error}"
8370                    ))
8371                })?;
8372                // Same guest mount model as the mountinfo capture path:
8373                // ephemeral host seed rows are not guest namespace members
8374                // and must not shift run-global mount-ID assignment.
8375                let mountinfo_contents =
8376                    crate::procfs::exclude_ephemeral_host_seed_mounts(&mountinfo_contents);
8377                let mountinfo_rows =
8378                    crate::procfs::parse_mountinfo(&mountinfo_contents).ok_or_else(|| {
8379                        Error::Tool(anyhow::anyhow!(
8380                            "kernel returned malformed {mountinfo_path} while validating fdinfo mnt_id"
8381                        ))
8382                    })?;
8383                let snapshot = MountInfoSnapshot::new(
8384                    mountinfo_rows,
8385                    &[],
8386                    false,
8387                    BTreeMap::new(),
8388                    BTreeMap::new(),
8389                )
8390                .ok_or_else(|| {
8391                    Error::Tool(anyhow::anyhow!(
8392                        "{mountinfo_path} failed strict identity validation for fdinfo"
8393                    ))
8394                })?;
8395                determinize_mount_id(guest, raw_mount_id, Some(snapshot.raw_mount_id_order()))
8396                    .await
8397                    .ok_or_else(|| {
8398                        Error::Tool(anyhow::anyhow!(
8399                            "mountinfo mount-ID order changed after the identity snapshot while resolving fdinfo mnt_id {raw_mount_id} for {fd_type:?} from {mountinfo_path}"
8400                        ))
8401                    })?
8402            } else {
8403                // A file on a mount the container mount table hides reports
8404                // its nearest shown ancestor (`ContainerMountTable::select`).
8405                let raw_mount_id = self.shown_mount_id(guest, raw_mount_id);
8406                determinize_mount_id(guest, raw_mount_id, None)
8407                    .await
8408                    .ok_or_else(|| {
8409                        Error::Tool(anyhow::anyhow!(
8410                            "recorded mount identity provenance was invalid while resolving fdinfo mnt_id {raw_mount_id} for {fd_type:?}"
8411                        ))
8412                    })?
8413            };
8414            Some((
8415                // Determinized immediately above (stdio-special or
8416                // `determinize_inode`); lowered to an integer only here, at the
8417                // point it is rendered into guest-visible fdinfo text.
8418                virtual_inode.as_raw(),
8419                logical_flags,
8420                open_file_id.deterministic_socket_cookie(),
8421                virtual_mount_id,
8422            ))
8423        } else {
8424            None
8425        };
8426        // The guest's CPUID instruction is answered from Detcore's table
8427        // exactly when CPUID is virtualized and the backend delivers the
8428        // instruction to Detcore (`handle_cpuid_event`), on ptrace and KVM
8429        // alike; without CPUID interception the guest sees the host's CPU,
8430        // and so does this file. There Detcore renders the file itself
8431        // (`ProcfsFile::rendered_path`); this is for a /proc/cpuinfo the
8432        // kernel opened: through a symbolic link, after `chroot`, or under
8433        // record and replay.
8434        // A placeholder (`procfs_rendered_path`), which exists only where the
8435        // session renders the file, is rendered from the table: it holds no
8436        // other bytes.
8437        let (is_cpuinfo, is_arch_status, placeholder) =
8438            guest.thread_state().with_detfd(call.fd(), |detfd| {
8439                (
8440                    detfd.procfs_is_cpuinfo(),
8441                    detfd.procfs_is_arch_status(),
8442                    detfd.procfs_rendered_path().is_some(),
8443                )
8444            })?;
8445        let renders_virtual_cpu =
8446            (is_cpuinfo || is_arch_status) && (placeholder || self.rendered_files(guest).cpuinfo);
8447        let virtual_cpuinfo = (is_cpuinfo && renders_virtual_cpu).then(|| {
8448            self.virtual_cpuid()
8449                .cpuinfo(guest.config().virtual_cpu_count())
8450        });
8451        // A task's `arch_status` describes the same CPU. Detcore renders it
8452        // from the same table exactly where it renders `/proc/cpuinfo`, and
8453        // there one the kernel opened describes the virtual CPU too.
8454        let virtual_arch_status =
8455            (is_arch_status && renders_virtual_cpu).then(|| self.virtual_cpuid().arch_status());
8456        let needs_random_uuid = guest
8457            .thread_state()
8458            .with_detfd(call.fd(), |detfd| detfd.procfs_needs_random_uuid())?;
8459        // AUTONOMOUS-BOT-IMPLEMENTED
8460        // TODO-HUMAN-REVIEW(PR-955): Review deterministic kernel UUID generation.
8461        let random_uuid =
8462            needs_random_uuid.then(|| guest.thread_state_mut().thread_prng().random::<[u8; 16]>());
8463        // ⚠️ DETERMINIZE HERE, IN THE CALLER, THROUGH THE SAME POOLS `stat` USES.
8464        //
8465        // The sanitizers in `crate::procfs` are pure functions of content and
8466        // hold no guest handle, so they cannot reach `InodePool`/`DevicePool`.
8467        // Minting an identity down there would make the maps column stable and
8468        // STILL DISAGREE with `stat` -- deterministic, reproducible and wrong.
8469        // This mirrors how `fdinfo_identity` is built a few lines above:
8470        // determinize with `determinize_inode`/`determinize_device`, then hand
8471        // the finished values down purely to be rendered.
8472        //
8473        // BOTH COLUMNS, not just the inode. `determinize_stat` sanitizes
8474        // `st_dev` as well, so rewriting only the inode would leave the device
8475        // disagreeing -- the same defect with the reported symptom removed.
8476        let needs_mapping_identities = guest
8477            .thread_state()
8478            .with_detfd(call.fd(), |detfd| detfd.procfs_needs_mapping_identities())?;
8479        let mut mapping_identities: BTreeMap<(u64, u64), (u64, u64)> = BTreeMap::new();
8480        if needs_mapping_identities {
8481            // A mapping backed by stdio must report the SAME inode fdinfo
8482            // reports for that fd, which is the fixed `deterministic_stdio_inode`
8483            // value rather than a pooled one. Matching is by raw inode, read
8484            // from the cached stat only: injecting an fstat here would add
8485            // syscalls to every maps read and perturb the very traces this
8486            // change is meant to keep consistent.
8487            //
8488            // The lowest descriptor wins a shared identity, as in
8489            // `deterministic_stdio_inode_for_raw`. Detcore gives all three
8490            // inherited descriptors the stat of its own stdin (`setup_stdio`),
8491            // so they share one until the guest replaces them; a mapping of
8492            // that file is a mapping of fd 0, and must report the 1000 that
8493            // fd 0's fdinfo reports, not stderr's 1002.
8494            let mut stdio_by_raw_inode: BTreeMap<RawInode, DetInode> = BTreeMap::new();
8495            for fd in libc::STDIN_FILENO..=libc::STDERR_FILENO {
8496                let cached = guest
8497                    .thread_state()
8498                    .with_detfd(fd, |detfd| {
8499                        let inode = deterministic_stdio_inode_for_resource(fd, detfd.resource())?;
8500                        detfd.stat().map(|stat| (stat.raw_inode(), inode))
8501                    })
8502                    .ok()
8503                    .flatten();
8504                if let Some((raw, det)) = cached {
8505                    stdio_by_raw_inode.entry(raw).or_insert(det);
8506                }
8507            }
8508            // One numbering request per file-backed line, in line order
8509            // (address order, which the guest's own mmaps decide), repeats and
8510            // stdio-backed lines included. The pool numbers files in request
8511            // order and every request consumes a number, so sorting by raw
8512            // `(dev, ino)` would let a host inode decide which file gets which
8513            // number, and skipping lines whose host pair repeats (hard-linked
8514            // libraries) or matches stdio would let host identity decide how
8515            // many numbers a read consumes
8516            // (https://github.com/rrnewton/hermit/issues/2897).
8517            //
8518            // The device a mapping names is the filesystem's own, which on
8519            // btrfs is not the per-subvolume device `stat` reports; the pair is
8520            // the identity either way, as it is for the device determinized
8521            // below.
8522            let raw_pairs: Vec<(u64, u64)> = String::from_utf8_lossy(&contents)
8523                .lines()
8524                .filter_map(crate::procfs::mapping_header_identity)
8525                .collect();
8526            let pooled = determinize_mapping_inodes(
8527                guest,
8528                raw_pairs
8529                    .iter()
8530                    .map(|&(raw_dev, raw_inode)| RawInode::new(raw_dev, raw_inode))
8531                    .collect(),
8532            )
8533            .await;
8534            for (&(raw_dev, raw_inode), pooled) in raw_pairs.iter().zip(pooled) {
8535                if mapping_identities.contains_key(&(raw_dev, raw_inode)) {
8536                    continue;
8537                }
8538                let det_inode = stdio_by_raw_inode
8539                    .get(&RawInode::new(raw_dev, raw_inode))
8540                    .copied()
8541                    .unwrap_or(pooled);
8542                // The device pool numbers first sightings only, so a repeated
8543                // device request changes nothing.
8544                let det_dev = determinize_device(guest, raw_dev).await;
8545                mapping_identities.insert((raw_dev, raw_inode), (det_dev, det_inode.as_raw()));
8546            }
8547        }
8548        let mountinfo = if guest
8549            .thread_state()
8550            .with_detfd(call.fd(), |detfd| detfd.procfs_needs_mountinfo_identities())?
8551        {
8552            let mut rows = crate::procfs::parse_mountinfo(&contents).ok_or_else(|| {
8553                Error::Tool(anyhow::anyhow!(
8554                    "kernel returned malformed /proc/*/mountinfo"
8555                ))
8556            })?;
8557            let mut device_rewrites = BTreeMap::new();
8558            for &(mountinfo_device, metadata_device) in &guest.config().mountinfo_device_rewrites {
8559                if device_rewrites
8560                    .insert(mountinfo_device, metadata_device)
8561                    .is_some()
8562                {
8563                    return Err(Error::Tool(anyhow::anyhow!(
8564                        "duplicate proven mountinfo device rewrite for raw device {mountinfo_device}"
8565                    )));
8566                }
8567            }
8568            for row in &mut rows {
8569                if let Some(metadata_device) = device_rewrites.get(&row.raw_device) {
8570                    row.raw_device = *metadata_device;
8571                }
8572            }
8573            let mut raw_devices = Vec::new();
8574            let mut seen_devices = BTreeSet::new();
8575            for row in &rows {
8576                if seen_devices.insert(row.raw_device) {
8577                    raw_devices.push(row.raw_device);
8578                }
8579            }
8580            let mut devices = BTreeMap::new();
8581            let virtualize_metadata = guest.config().virtualize_metadata;
8582            if virtualize_metadata {
8583                // Intentionally use snapshot row order to pre-populate the
8584                // same run-global DevicePool used by stat/statx. This makes
8585                // every later observation of a device agree within the run.
8586                // It does not promise that unlike host filesystem layouts
8587                // expose the same device equivalence classes or order.
8588                for raw in raw_devices {
8589                    devices.insert(raw, determinize_device(guest, raw).await);
8590                }
8591            }
8592            let mut root_rewrites = BTreeMap::new();
8593            for rewrite in &guest.config().mountinfo_root_rewrites {
8594                if root_rewrites
8595                    .insert(rewrite.raw_mount_id, rewrite.clone())
8596                    .is_some()
8597                {
8598                    return Err(Error::Tool(anyhow::anyhow!(
8599                        "duplicate proven mountinfo root rewrite for mount ID {}",
8600                        rewrite.raw_mount_id
8601                    )));
8602                }
8603            }
8604            let mut snapshot = MountInfoSnapshot::new(
8605                rows,
8606                if guest.config().mountinfo_mount_ids_captured {
8607                    &guest.config().mountinfo_mount_ids
8608                } else {
8609                    &[]
8610                },
8611                virtualize_metadata,
8612                devices,
8613                root_rewrites,
8614            )
8615            .ok_or_else(|| {
8616                Error::Tool(anyhow::anyhow!(
8617                    "mountinfo snapshot failed strict identity validation"
8618                ))
8619            })?;
8620            // Number this view's mount IDs through the same run-global pool
8621            // as fdinfo. IDs the run has already shown keep their numbers;
8622            // the rest are numbered in the order this view presents them
8623            // (rows, then parent-only IDs). The canonical order, which may
8624            // follow the captured namespace, only validates the view.
8625            let observation_order = snapshot.raw_mount_id_observation_order();
8626            let Some(virtual_ids) = resolve_mountinfo_identities(
8627                guest,
8628                snapshot.raw_mount_id_order(),
8629                observation_order.clone(),
8630            )
8631            .await
8632            else {
8633                return Err(Error::Tool(anyhow::anyhow!(
8634                    "mountinfo mount-ID order changed after the run-global identity snapshot"
8635                )));
8636            };
8637            if !snapshot.use_run_mount_ids(&observation_order, &virtual_ids) {
8638                return Err(Error::Tool(anyhow::anyhow!(
8639                    "run-global mount identities do not cover this mountinfo snapshot"
8640                )));
8641            }
8642            Some(snapshot)
8643        } else {
8644            None
8645        };
8646        // `/proc/locks` keeps the guest's own rows only: an OFD lock has no
8647        // owning task, so Linux shows every process's OFD locks from every
8648        // PID namespace (`procfs::locks`). Decided before the sanitizer
8649        // numbers identities, so a dropped row shifts no remaining id. The
8650        // capture above and this decision are two steps of one turn: with
8651        // sequentialized threads no other guest thread's lock call (and its
8652        // fdinfo record) runs between them. Parallel execution would need
8653        // them taken as one.
8654        let lock_rows_kept = if !ledger_locks
8655            && guest
8656                .thread_state()
8657                .with_detfd(call.fd(), |detfd| detfd.procfs_lists_locks())?
8658        {
8659            match crate::procfs::locks::lock_rows(&contents) {
8660                Some(rows) => match crate::tool_global::guest_lock_rows(guest, rows).await {
8661                    Ok(kept) => Some(kept),
8662                    Err(reason) => {
8663                        let reason = format!(
8664                            "/proc/locks: {reason}; that description's rows cannot be told \
8665                             from another process's"
8666                        );
8667                        self.refuse_lock_bookkeeping(guest, reason).await
8668                    }
8669                },
8670                // Rows that do not parse: the sanitizer refuses the snapshot.
8671                None => None,
8672            }
8673        } else {
8674            None
8675        };
8676        let machine = crate::machine::MachineDescription::of(guest.config());
8677        // A status reports the guest's own mask of a thread Detcore holds
8678        // under a private blocking mask, which that thread published to the
8679        // scheduler; the kernel's `SigBlk` is Detcore's mask (Codex review of
8680        // R2b-2 stage 2, P2; https://github.com/rrnewton/hermit/issues/4273).
8681        // Masks are published only where the scheduler models signal targets,
8682        // and under the in-guest delivery gate, where each request publishes
8683        // the guest's mask (A5 step 3c.1b) and the kernel's may be the in-guest
8684        // runtime's (`publishes_guest_masks`). With nothing published for the
8685        // thread, the kernel's `SigBlk` is reported, as before.
8686        let mask_thread = guest.thread_state().with_detfd(call.fd(), |detfd| {
8687            detfd.procfs_status_mask_thread(&contents)
8688        })?;
8689        let mask_facts = status_published_sigblk(guest, mask_thread).await?;
8690        // Under `--virtual-cpus` a status reports the CPU affinity of the
8691        // thread it describes. A thread the scheduler no longer knows (one
8692        // reaped while the file was open) shows every CPU.
8693        let cpus_allowed = match (self.cfg.virtual_cpus, mask_thread) {
8694            (Some(cpus), Some(thread)) => Some(
8695                crate::tool_global::cpu_affinity(guest, crate::types::DetTid::from_raw(thread))
8696                    .await
8697                    .cpus(cpus.get())
8698                    .unwrap_or_else(|_| crate::cpu_set::CpuSet::first(cpus.get())),
8699            ),
8700            _ => None,
8701        };
8702        // A task's `schedstat` shows its runtime where it is a thread of the
8703        // reader's own process (`Detcore::same_process_task_runtime`).
8704        let schedstat_thread = guest
8705            .thread_state()
8706            .with_detfd(call.fd(), |detfd| detfd.procfs_schedstat_thread())?;
8707        let schedstat_runtime = match schedstat_thread {
8708            Some(thread) => self.same_process_task_runtime(guest, thread).await,
8709            None => None,
8710        };
8711        // A process's `stat` or `comm` Detcore opened renders its task's
8712        // record in the process table as of this read (`procfs::task_stat`).
8713        // A descriptor's fdinfo Detcore opened renders that descriptor's
8714        // state as of this read (package K slice K8-h); no other file renders
8715        // a task record then. Boxed, so that this call's future holds its
8716        // answer alone.
8717        let (task_stat, fdinfo_rendering) = if guest
8718            .thread_state()
8719            .with_detfd(call.fd(), |detfd| detfd.procfs_is_rendered_fdinfo())?
8720        {
8721            let Some(rendering) = Box::pin(self.render_own_fdinfo(guest, call.fd())).await? else {
8722                return Err(Error::Tool(anyhow::anyhow!(
8723                    "descriptor {} is a rendered fdinfo Detcore has no rendering of",
8724                    call.fd()
8725                )));
8726            };
8727            (None, Some(rendering))
8728        } else {
8729            (
8730                self.rendered_task_stat(guest, call.fd(), virtual_ppid)
8731                    .await?,
8732                None,
8733            )
8734        };
8735        guest.thread_state().with_detfd(call.fd(), |detfd| {
8736            detfd.initialize_procfs(
8737                contents.clone(),
8738                ProcfsSnapshotContext {
8739                    mapping_identities: mapping_identities.clone(),
8740                    mountinfo: mountinfo.clone(),
8741                    virtual_uptime_seconds,
8742                    virtual_boot_time_seconds,
8743                    virtual_realtime_seconds,
8744                    virtual_pid,
8745                    virtual_ppid,
8746                    virtual_pty_count,
8747                    fdinfo_identity,
8748                    random_uuid,
8749                    virtual_cpuinfo: virtual_cpuinfo.clone(),
8750                    virtual_arch_status: virtual_arch_status.clone(),
8751                    machine: machine.clone(),
8752                    schedstat_runtime,
8753                    published_sigblk: mask_facts.sigblk,
8754                    // A5 step 3c.1d, design D37: the pending ignored signals
8755                    // the guest's mask lets in are left out, under the gate
8756                    // only; those a temporary-mask publication keeps from its
8757                    // call's entry stay, each queue by its own held mask (A5
8758                    // step 3c.1e A).
8759                    status_hides_ignored: guest.config().in_guest_signal_delivery,
8760                    status_held_ignored: (mask_facts.held_private, mask_facts.held_shared),
8761                    cpus_allowed,
8762                    lock_rows_kept: lock_rows_kept.clone(),
8763                    task_stat,
8764                    fdinfo_rendering: fdinfo_rendering.clone(),
8765                },
8766            );
8767        })?;
8768        Ok(())
8769    }
8770
8771    /// The inode number a descriptor's fdinfo shows for descriptor
8772    /// `target_fd`, whose raw stat is `stat`: keyed as `fstat` keys the
8773    /// descriptor, so the two numbers agree. A standard stream's fixed number
8774    /// (`deterministic_stdio_inode_for_resource`); a procfs file Detcore
8775    /// renders (`rendered`), the number Detcore gave it; a directory Detcore
8776    /// serves on a placeholder, the number its `fstat` shows
8777    /// (`placeholder_inode`); any other file, its number in the run's pool.
8778    pub(super) async fn fdinfo_inode<G: Guest<Self>>(
8779        &self,
8780        guest: &mut G,
8781        target_fd: RawFd,
8782        stat: &DetStat,
8783        resource: Option<ResourceID>,
8784        rendered: Option<&'static Path>,
8785        placeholder_inode: Option<u64>,
8786    ) -> DetInode {
8787        match deterministic_stdio_inode_for_resource(target_fd, resource) {
8788            Some(inode) => inode,
8789            // Detcore numbered it when it rendered the file.
8790            None if let Some(path) = rendered => DetInode::mint(procfs_inode::rendered_inode(path)),
8791            // The directory a placeholder stands for, as its `fstat` numbers
8792            // it.
8793            None if let Some(inode) = placeholder_inode => DetInode::mint(inode),
8794            None => {
8795                let procfs =
8796                    Self::procfs_entry(guest, stat, StatTarget::Descriptor(target_fd)).await;
8797                let files = self.rendered_files(guest);
8798                if let Some(path) =
8799                    rendered_entry(&procfs, stat, guest.config().recordreplay_modes, files)
8800                {
8801                    DetInode::mint(procfs_inode::rendered_inode(path))
8802                } else {
8803                    determinize_inode_observing_mtime(
8804                        guest,
8805                        stat.raw_inode(),
8806                        procfs.name(stat.dev),
8807                        ObservedMtime::Unobserved,
8808                        InodeSighting::Descriptor,
8809                    )
8810                    .await
8811                    .0
8812                }
8813            }
8814        }
8815    }
8816
8817    /// The mount ID an fdinfo Detcore renders shows for a file on mount
8818    /// `raw_mount_id` (package K slice K8-h): the number the guest's own
8819    /// `statx` reports for it (`Self::fill_statx_mount_id`), a hidden
8820    /// mount's nearest shown ancestor (`Self::shown_mount_id`) through the
8821    /// run's mount-ID pool, and 0 where that `statx` reports none: a mount
8822    /// the backend does not report (`None`), a run with no mount provenance
8823    /// to number it by (`Config::mountinfo_mount_ids_captured`), or one the
8824    /// pool cannot number. A raw 0 stays 0. That 0 is Detcore's convention,
8825    /// as the sanitizer of a kernel fdinfo prints it for an object on a mount
8826    /// the guest's `mountinfo` does not show (a memfd's internal mount);
8827    /// Linux prints that mount's own nonzero number.
8828    pub(super) async fn fdinfo_mount_id<G: Guest<Self>>(
8829        &self,
8830        guest: &mut G,
8831        raw_mount_id: Option<u64>,
8832    ) -> u64 {
8833        match raw_mount_id {
8834            Some(raw_mount_id)
8835                if raw_mount_id != 0 && guest.config().mountinfo_mount_ids_captured =>
8836            {
8837                let shown = self.shown_mount_id(guest, raw_mount_id);
8838                determinize_mount_id(guest, shown, None).await.unwrap_or(0)
8839            }
8840            _ => 0,
8841        }
8842    }
8843
8844    /// The bytes of the procfs file open as `fd`, if it is a process's
8845    /// `mountinfo` or `mounts` and the run has a container mount table
8846    /// (`Config::container_mount_table`): the table's `mountinfo`, with raw
8847    /// identities that the snapshot then numbers as it numbers the kernel's,
8848    /// or its `mounts`. Wherever the file was opened from: rendered by Detcore,
8849    /// or opened by the kernel (under record and replay, through a spelling
8850    /// Detcore does not resolve, on a backend's own procfs), except one the
8851    /// kernel opened after the process it names changed its root, whose view
8852    /// the kernel decides (`chroot` keeps the kernel's path, as for every
8853    /// rendered file). `None` for every other file.
8854    fn mount_table_snapshot<G: Guest<Self>>(
8855        &self,
8856        guest: &G,
8857        fd: RawFd,
8858    ) -> Result<Option<Vec<u8>>, Errno> {
8859        let Some(table) = guest.config().container_mount_table.as_ref() else {
8860            return Ok(None);
8861        };
8862        let (rendering, rendered, pinned) = guest.thread_state().with_detfd(fd, |detfd| {
8863            (
8864                detfd.procfs_mount_table_rendering(),
8865                detfd.procfs_rendered_path().is_some(),
8866                detfd.procfs_pinned_mount_view(),
8867            )
8868        })?;
8869        let Some(rendering) = rendering else {
8870            return Ok(None);
8871        };
8872        // A rendered file is the table, as its open decided. A file the kernel
8873        // opened reads the view its open pinned; one nothing pinned (a
8874        // descriptor Detcore learned of otherwise), the reader's.
8875        let kernel_view = !rendered
8876            && pinned.unwrap_or_else(|| {
8877                guest
8878                    .thread_state()
8879                    .changed_root
8880                    .load(std::sync::atomic::Ordering::SeqCst)
8881            });
8882        if kernel_view {
8883            return Ok(None);
8884        }
8885        Ok(Some(match rendering {
8886            crate::procfs::MountTableRendering::Mountinfo => table.mountinfo(),
8887            crate::procfs::MountTableRendering::Mounts => {
8888                table.mounts(&guest.config().mountinfo_root_rewrites)
8889            }
8890        }))
8891    }
8892
8893    /// The runtime of `thread`, its user plus system time, where it is the
8894    /// reader or another thread of the reader's own process
8895    /// (`ThreadState::task_cpu_time`, the accounting the CPU-time clocks
8896    /// read), and Detcore renders the reader's own `stat`, so that the two
8897    /// files agree: `None` under record and replay, after `chroot` and in a
8898    /// nested PID namespace, where the kernel serves `stat` with its CPU
8899    /// times zeroed, and for a thread of another process, whose accounting
8900    /// Detcore keeps in that process's own threads.
8901    async fn same_process_task_runtime<G: Guest<Self>>(
8902        &self,
8903        guest: &mut G,
8904        thread: crate::types::DetTid,
8905    ) -> Option<crate::types::LogicalTime> {
8906        if !self.renders_procfs(guest) || guest.thread_state().nested_pid_namespace {
8907            return None;
8908        }
8909        if thread != guest.thread_state().dettid {
8910            let own = guest.thread_state().detpid?;
8911            let process = crate::tool_global::signal_target_process(guest, thread).await;
8912            if process != Some(own) {
8913                return None;
8914            }
8915        }
8916        let (user, system) = guest.thread_state().task_cpu_time(thread);
8917        Some(user + system)
8918    }
8919
8920    /// Whether the process's `mountinfo` or `mounts` the kernel opened as
8921    /// `file` reads the kernel's view: the task the path names has a changed
8922    /// root (`tool_global::root_changed`, which follows filesystem groups
8923    /// shared through CLONE_FS), or Detcore cannot tell which task it names.
8924    /// As on Linux, where `mounts_open_common` takes the target task's root,
8925    /// the reader's own root does not decide it. The kernel's view is
8926    /// Linux's own answer for whatever task the path names, so it is the
8927    /// answer whenever the task is unknown: never the launch table.
8928    async fn mount_view_is_kernel<G: Guest<Self>>(&self, guest: &mut G, file: &ProcfsFile) -> bool {
8929        // In a nested PID namespace the path's numbers are that namespace's
8930        // (a child that kept a `/proc` directory descriptor across
8931        // CLONE_NEWPID and opens `self/mountinfo` through it, for one), and
8932        // Detcore does not map them here.
8933        let Some((pid, tid)) = self.procfs_caller_ids(guest) else {
8934            return true;
8935        };
8936        let mut bound = file.clone();
8937        bound.bind_task(pid, tid);
8938        match bound.bound_task() {
8939            Some(task) => {
8940                crate::tool_global::root_changed(
8941                    guest,
8942                    detcore_model::pid::DetPid::from_raw(task.thread.unwrap_or(task.process)),
8943                )
8944                .await
8945            }
8946            None => true,
8947        }
8948    }
8949
8950    /// The raw mount ID a file on mount `raw_mount_id` reports to the guest:
8951    /// with a container mount table, and until the process changes its root,
8952    /// a hidden mount's nearest shown ancestor
8953    /// (`ContainerMountTable::shown_mount_id`); otherwise the mount itself.
8954    pub(crate) fn shown_mount_id<G: Guest<Self>>(&self, guest: &G, raw_mount_id: u64) -> u64 {
8955        match guest.config().container_mount_table.as_ref() {
8956            Some(table)
8957                if !guest
8958                    .thread_state()
8959                    .changed_root
8960                    .load(std::sync::atomic::Ordering::SeqCst) =>
8961            {
8962                table.shown_mount_id(raw_mount_id)
8963            }
8964            _ => raw_mount_id,
8965        }
8966    }
8967
8968    /// Write `stat`, a procfs file Detcore renders, as `statx`'s result, with
8969    /// the guest's procfs mount's ID, as its fdinfo `mnt_id` shows it.
8970    async fn write_rendered_statx<G: Guest<Self>>(
8971        &self,
8972        guest: &mut G,
8973        call: syscalls::Statx,
8974        stat: DetStat,
8975    ) -> Result<i64, Error> {
8976        let statptr = call.statx().ok_or(Errno::EFAULT)?;
8977        let mut statx: reverie::syscalls::linux_abi::Statx = stat.into();
8978        let raw_mount_id = rendered_procfs::guest_procfs_mount_id().ok();
8979        let unique = statx_requested_mask(call) & STATX_MNT_ID_UNIQUE != 0;
8980        self.fill_statx_mount_id(guest, &mut statx, raw_mount_id, unique)
8981            .await;
8982        guest.memory().write_value(statptr.0, &statx)?;
8983        Ok(0)
8984    }
8985
8986    // TODO-HUMAN-REVIEW(https://github.com/rrnewton/hermit/issues/4213):
8987    // Review STATX_MNT_ID numbered as fdinfo's mnt_id, a hidden mount's file
8988    // reporting its nearest shown ancestor.
8989    /// Fill `statx`'s mount ID for a file on mount `raw_mount_id` (a classic
8990    /// ID), as Linux fills it whether or not the caller asked for it
8991    /// (`vfs_statx_path`, v6.12 fs/stat.c:248-259): the number its fdinfo
8992    /// `mnt_id` and its `mountinfo` row show, through the run's mount-ID
8993    /// pool, a hidden mount's file showing its nearest shown ancestor's
8994    /// ([`Self::shown_mount_id`]), with `STATX_MNT_ID`. If the caller asked
8995    /// for `STATX_MNT_ID_UNIQUE` (`unique`), Linux reports the mount's unique
8996    /// ID and that bit instead; here that is the same number plus
8997    /// [`MNT_UNIQUE_ID_OFFSET`], which is above every classic ID, the same
8998    /// for every file of the mount, different for every other mount, and
8999    /// the first valid unique ID for the first mount numbered. Where there is
9000    /// no such number (no mount provenance to number it by, outside Hermit's
9001    /// container, or no mount ID from the kernel), the field is 0 and both
9002    /// mask bits are cleared, so no caller reads a number that names nothing.
9003    ///
9004    /// Where this differs from Linux: the pool is keyed by classic IDs, which
9005    /// Linux can give a new mount after an unmount (an IDA), while a unique
9006    /// ID is never reused. A mount made after another's unmount, on the
9007    /// freed classic ID, shows that mount's unique ID here too.
9008    async fn fill_statx_mount_id<G: Guest<Self>>(
9009        &self,
9010        guest: &mut G,
9011        statx: &mut reverie::syscalls::linux_abi::Statx,
9012        raw_mount_id: Option<u64>,
9013        unique: bool,
9014    ) {
9015        let guest_mount_id = match raw_mount_id {
9016            Some(raw_mount_id) if guest.config().mountinfo_mount_ids_captured => {
9017                let raw_mount_id = self.shown_mount_id(guest, raw_mount_id);
9018                determinize_mount_id(guest, raw_mount_id, None).await
9019            }
9020            _ => None,
9021        };
9022        let reported = match guest_mount_id {
9023            Some(id) if unique => id
9024                .checked_add(MNT_UNIQUE_ID_OFFSET)
9025                .map(|id| (id, STATX_MNT_ID_UNIQUE)),
9026            Some(id) => Some((id, reverie::syscalls::linux_abi::STATX_MNT_ID)),
9027            None => None,
9028        };
9029        statx.stx_mask &= !(reverie::syscalls::linux_abi::STATX_MNT_ID | STATX_MNT_ID_UNIQUE);
9030        match reported {
9031            Some((id, bit)) => {
9032                statx.stx_mnt_id = id;
9033                statx.stx_mask |= bit;
9034            }
9035            None => statx.stx_mnt_id = 0,
9036        }
9037    }
9038
9039    /// SYS_read system call (MAYHANG), of a per-process file whose record
9040    /// sequence a fork shares, description `id` (`Self::procfs_sequence_to_take`):
9041    /// [`Self::handle_read`] with the sequence taken from Detcore's global
9042    /// state and stored back (`Self::take_procfs_sequence`). Boxed here, so
9043    /// that the handler's own future stays as it was.
9044    #[allow(clippy::type_complexity)]
9045    fn read_with_shared_sequence<'a, G: Guest<Self>>(
9046        &'a self,
9047        guest: &'a mut G,
9048        call: syscalls::Read,
9049        id: OpenFileId,
9050    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<i64, Error>> + Send + 'a>> {
9051        Box::pin(async move {
9052            self.take_procfs_sequence(guest, call.fd(), id).await;
9053            let result = self.handle_read(guest, call).await;
9054            self.store_procfs_sequence(guest, call.fd(), id).await;
9055            result
9056        })
9057    }
9058
9059    /// SYS_read system call (MAYHANG).
9060    pub async fn handle_read<G: Guest<Self>>(
9061        &self,
9062        guest: &mut G,
9063        call: syscalls::Read,
9064    ) -> Result<i64, Error> {
9065        if Self::refuses_unknown_record_sequence(guest, call.fd(), false) {
9066            return self
9067                .refuse_unknown_record_sequence(guest, Sysno::read, call.fd())
9068                .await;
9069        }
9070        if let Some(id) = Self::procfs_sequence_to_take(guest, call.fd()) {
9071            return self.read_with_shared_sequence(guest, call, id).await;
9072        }
9073        if self.timer_slack_binding(guest, call.fd())?.is_some() {
9074            return self
9075                .read_timer_slack(guest, call.fd(), call.buf(), call.len())
9076                .await;
9077        }
9078        // A rendered `cmdline`: every read, of any count, reads memory.
9079        if guest
9080            .thread_state()
9081            .with_detfd(call.fd(), |detfd| detfd.procfs_reads_memory_at_every_read())
9082            .unwrap_or(false)
9083        {
9084            return self
9085                .read_rendered_cmdline_call(guest, call.fd(), call.buf(), call.len(), None)
9086                .await;
9087        }
9088
9089        if call.len() == 0 {
9090            if let Ok(Some(status_flags)) = guest.thread_state().with_detfd(call.fd(), |detfd| {
9091                (detfd.ty() == FdType::Rng).then(|| detfd.status_flags())
9092            }) {
9093                // Replay reserves random-device fd slots with eventfds. A
9094                // physical zero-length read of that placeholder returns EINVAL
9095                // even though the logical random-device read must return zero.
9096                require_random_device_read_access(status_flags)?;
9097                let limit =
9098                    crate::iovecs::UserAddressLimit::from_query(guest.user_address_limit())?;
9099                // vfs_read still checks access_ok for a zero-length buffer:
9100                // NULL is valid, but an address beyond TASK_SIZE is EFAULT.
9101                limit.validate(&[crate::iovecs::ImportedIovec {
9102                    base: call.buf().map_or(0, |address| address.as_raw()),
9103                    len: 0,
9104                }])?;
9105                return Ok(0);
9106            }
9107            // A zero-count read transfers nothing on a file or stream, but on a
9108            // datagram or SEQPACKET socket it consumes a pending message. Its
9109            // result goes through record/replay: a replayed descriptor may be
9110            // a placeholder whose own zero-count read fails differently.
9111            // An inotify instance is the exception: its read waits for an
9112            // event even for a zero count, and then fails with EINVAL, as no
9113            // event fits (`inotify_read`), so its zero-count read waits as
9114            // its other reads do, below.
9115            // AUTONOMOUS-BOT-IMPLEMENTED
9116            // TODO-HUMAN-REVIEW(PR-3601)
9117            let inotify = guest
9118                .thread_state()
9119                .with_detfd(call.fd(), |detfd| detfd.ty() == FdType::Inotify)
9120                .unwrap_or(false);
9121            if !inotify {
9122                return self
9123                    .record_or_replay_preserving_tool_errors(guest, call)
9124                    .await;
9125            }
9126        }
9127
9128        let (serves_procfs_snapshot, cannot_read) =
9129            guest.thread_state().with_detfd(call.fd(), |detfd| {
9130                (
9131                    detfd.procfs_serves_snapshot(),
9132                    !can_read(detfd.status_flags()),
9133                )
9134            })?;
9135        // A descriptor that cannot read (`O_PATH`, write-only, access mode 3)
9136        // reads nothing (EBADF), before its buffer is looked at, as Linux's
9137        // `vfs_read` checks, whether or not a rendered file's snapshot is
9138        // taken yet (`/proc/uptime`'s is taken at its first read).
9139        if serves_procfs_snapshot && cannot_read {
9140            return Err(Errno::EBADF.into());
9141        }
9142        if serves_procfs_snapshot {
9143            validate_procfs_destination(
9144                || crate::iovecs::UserAddressLimit::from_query(guest.user_address_limit()),
9145                call.buf(),
9146                call.len(),
9147            )?;
9148        }
9149        // A rendered per-process file runs its record's `show` where the read
9150        // does not continue the record's sequence: at the cursor 0, and
9151        // after a `pread` or a `preadv` moved the sequence, the shared cursor
9152        // is elsewhere (`ProcfsFile::drop_stale_task_snapshot`,
9153        // `Self::check_record_show`). Only after `vfs_read`'s checks: the
9154        // descriptor's access (above), the destination (`access_ok`, above)
9155        // and the range (`rw_verify_area`: a cursor plus count past LLONG_MAX
9156        // is EINVAL).
9157        let task_record_cursor = if guest
9158            .thread_state()
9159            .with_detfd(call.fd(), |detfd| detfd.procfs_follows_record_sequence())?
9160        {
9161            self.adopt_rendered_procfs_position(guest, call.fd())
9162                .await?;
9163            let cursor = guest.thread_state().with_detfd(call.fd(), |detfd| {
9164                detfd.procfs_position().map_or(0, |(offset, _)| offset)
9165            })?;
9166            verify_procfs_read_range(cursor, call.len())?;
9167            if guest.thread_state().with_detfd(call.fd(), |detfd| {
9168                detfd.procfs_drop_stale_task_snapshot(cursor)
9169            })? {
9170                self.check_record_show(guest, call.fd(), cursor).await?;
9171            }
9172            Some(cursor)
9173        } else {
9174            None
9175        };
9176        // A task's `schedstat` shows the runtime anew at a read at 0
9177        // (`ProcfsFile::drop_schedstat_snapshot_for_read_at`).
9178        guest.thread_state().with_detfd(call.fd(), |detfd| {
9179            let cursor = detfd.procfs_position().map_or(0, |(offset, _)| offset);
9180            detfd.procfs_drop_schedstat_snapshot_for_read_at(cursor as u64)
9181        })?;
9182        let needs_procfs_snapshot = guest
9183            .thread_state()
9184            .with_detfd(call.fd(), |detfd| detfd.procfs_needs_snapshot())?;
9185        if needs_procfs_snapshot {
9186            let rendered = self.initialize_procfs_snapshot(guest, call).await;
9187            if let Some(cursor) = task_record_cursor {
9188                guest.thread_state().with_detfd(call.fd(), |detfd| {
9189                    detfd.procfs_note_task_record_rendered(cursor, rendered.is_ok())
9190                })?;
9191            }
9192            rendered?;
9193        }
9194
9195        self.adopt_rendered_procfs_position(guest, call.fd())
9196            .await?;
9197        // Like a seq_file read, advance the shared cursor only by the bytes
9198        // actually copied: a read that faults before copying anything leaves
9199        // it where it was.
9200        let procfs_preview = guest
9201            .thread_state()
9202            .with_detfd(call.fd(), |detfd| detfd.preview_procfs(call.len()))?;
9203        if let Some((offset, bytes)) = procfs_preview {
9204            let faults_on_a_short_copy = guest
9205                .thread_state()
9206                .with_detfd(call.fd(), |detfd| detfd.procfs_faults_on_a_short_copy())?;
9207            let copied = copy_procfs_read(
9208                &mut guest.memory(),
9209                call.buf(),
9210                &bytes,
9211                faults_on_a_short_copy,
9212            )?;
9213            guest.thread_state().with_detfd(call.fd(), |detfd| {
9214                detfd.commit_procfs_read(offset, copied);
9215            })?;
9216            self.sync_rendered_procfs_position(guest, call.fd()).await?;
9217            return Ok(copied as i64);
9218        }
9219
9220        self.fifo_polls_if_a_guest_holds_its_peer(guest, call.fd(), FifoPeer::Writer)
9221            .await;
9222        let (fd_type, resource, descriptor) =
9223            guest.thread_state_mut().with_detfd(call.fd(), |detfd| {
9224                (detfd.ty(), detfd.resource(), detfd.clone())
9225            })?;
9226
9227        if let Some(resource) = resource {
9228            let request = guest.thread_state().mk_request(resource, Permission::R);
9229            resource_request(guest, request).await;
9230        }
9231
9232        let res = match fd_type {
9233            FdType::Rng => {
9234                trace!("Read call RNG fd {}, simulating...", call.fd());
9235                let status_flags = descriptor.status_flags();
9236                let stream = descriptor.random_device_stream();
9237                descriptor
9238                    .with_random_device_stream(|offset| {
9239                        require_random_device_read_access(status_flags)?;
9240                        let remote_buf = call.buf().ok_or(Errno::EFAULT)?;
9241                        self.fill_random_device_bytes(guest, stream, remote_buf, call.len(), offset)
9242                    })
9243                    .map(|n| n as i64)
9244            }
9245            FdType::Regular | FdType::Pipe if descriptor.inherited_external_read().is_some() => {
9246                // An authenticated controller input or terminal can block
9247                // after this Device grant.
9248                // Announce one external kernel read even when our cached flags
9249                // say nonblocking: a fork/exec peer can change the shared OFD's
9250                // flags while the process-local model remains stale (the
9251                // pre-existing https://github.com/rrnewton/hermit/issues/3520).
9252                // The kernel keeps its actual O_NONBLOCK semantics, including
9253                // immediate EAGAIN. Never fill an external short read to len.
9254                self.record_or_replay_blocking(guest, call.into()).await
9255            }
9256            FdType::Regular => {
9257                if guest.config().deterministic_io {
9258                    self.deterministic_read(guest, call).await
9259                } else {
9260                    Ok(self.record_or_replay(guest, call).await?)
9261                }
9262            }
9263            FdType::Signalfd
9264                if call.len() >= 2 * crate::syscalls::signal::SIGNALFD_RECORD
9265                    && self.signalfd_reads_per_record(guest, call.fd()).await =>
9266            {
9267                self.read_signalfd_per_record(guest, call).await
9268            }
9269            FdType::Signalfd => self.read_signalfd(guest, call, true).await,
9270            FdType::Timerfd => {
9271                trace!("Possibly blocking read call on timerfd {}", call.fd());
9272                // Expirations up to this thread's virtual now make the host
9273                // descriptor readable before a nonblocking read's one attempt;
9274                // a blocking read's retries publish again at each turn.
9275                crate::syscalls::timerfd::publish_expired(guest).await;
9276                let result = self.execute_nonblockable_fd_syscall(guest, call).await;
9277                crate::syscalls::timerfd::finish_read(guest, call.fd(), call.buf(), result)
9278            }
9279            FdType::Eventfd => {
9280                trace!(
9281                    "Possibly blocking read call on notification fd {}, type {:?}",
9282                    call.fd(),
9283                    fd_type
9284                );
9285                self.execute_nonblockable_fd_syscall(guest, call).await
9286            }
9287            FdType::Inotify => {
9288                trace!(
9289                    "Possibly blocking read call on notification fd {}, type {:?}",
9290                    call.fd(),
9291                    fd_type
9292                );
9293                let destination = crate::syscalls::inotify::Destination::buffer(
9294                    call.buf().map_or(0, |buf| buf.as_raw()),
9295                    call.len(),
9296                );
9297                // A blocking read with nothing queued waits in Detcore's turns,
9298                // on the instance `handle_inotify_init1` made physically
9299                // nonblocking, as a pipe's or an eventfd's does.
9300                self.inotify_read_part(guest, Sysno::read, call.fd(), call, Some(destination))
9301                    .await
9302                    .0
9303            }
9304            FdType::Memfd | FdType::Pidfd | FdType::Userfaultfd | FdType::Epoll => {
9305                trace!("Read call on unusual fd {}, type {:?}", call.fd(), fd_type);
9306                Ok(self.record_or_replay(guest, call).await?)
9307            }
9308
9309            FdType::Socket | FdType::Pipe => {
9310                trace!(
9311                    "Possibly blocking read call on {:?} fd {}",
9312                    fd_type,
9313                    call.fd()
9314                );
9315                self.execute_nonblockable_fd_syscall(guest, call).await
9316            }
9317        };
9318        // O2: a signalfd read (even one that faulted copying out a record
9319        // it had already dequeued) may have taken the process's ITIMER_REAL
9320        // copy (https://github.com/rrnewton/hermit/issues/4139).
9321        if fd_type == FdType::Signalfd {
9322            crate::tool_global::reconcile_alarm(guest).await;
9323        }
9324        resource_release_all(guest).await;
9325        res
9326    }
9327
9328    /// One read of signalfd `call`, as the kernel answers it, with each
9329    /// record put through the child-exit ledger and the discarded-at-send
9330    /// record (`reconcile_signalfd_records`). A read whose every record was
9331    /// dropped reads again if `may_read_again`, as if those copies had never
9332    /// been queued; otherwise it answers 0.
9333    async fn read_signalfd<G: Guest<Self>>(
9334        &self,
9335        guest: &mut G,
9336        call: syscalls::Read,
9337        may_read_again: bool,
9338    ) -> Result<i64, Error> {
9339        loop {
9340            trace!("Possibly blocking read call on signalfd {}", call.fd());
9341            let extents = [(call.buf().map_or(0, |buf| buf.as_raw()), call.len())];
9342            let before =
9343                crate::syscalls::signal::capture_signalfd_buffer(guest, &extents, call.len());
9344            let result = self.execute_nonblockable_fd_syscall(guest, call).await;
9345            // A SIGCHLD read here goes through the child-exit ledger as
9346            // one delivered to a handler does. A read whose every record
9347            // was a dropped duplicate reads again, as if those copies had
9348            // never been queued.
9349            match (result, call.buf()) {
9350                (Ok(read), Some(buf))
9351                    if read > 0 && self.cfg.backend.reports_child_exit_publication =>
9352                {
9353                    let Some(kept) = self
9354                        .reconcile_signalfd_records(guest, buf, read as usize, &before)
9355                        .await
9356                    else {
9357                        break self
9358                            .refuse_unrestorable_signalfd_record(guest, Sysno::read)
9359                            .await;
9360                    };
9361                    if self
9362                        .settle_faulted_signalfd_record(
9363                            guest,
9364                            call.fd(),
9365                            &extents,
9366                            Some(read as usize),
9367                        )
9368                        .await
9369                    {
9370                        break self
9371                            .refuse_faulted_signalfd_discard(guest, Sysno::read)
9372                            .await;
9373                    }
9374                    if kept > 0 {
9375                        break Ok(kept as i64);
9376                    }
9377                    if !may_read_again {
9378                        break Ok(0);
9379                    }
9380                }
9381                (Err(Error::Errno(Errno::EFAULT)), _) => {
9382                    if self
9383                        .settle_faulted_signalfd_record(guest, call.fd(), &extents, None)
9384                        .await
9385                    {
9386                        break self
9387                            .refuse_faulted_signalfd_discard(guest, Sysno::read)
9388                            .await;
9389                    }
9390                    break Err(Errno::EFAULT.into());
9391                }
9392                (result, _) => break result,
9393            }
9394        }
9395    }
9396
9397    /// Whether a signalfd read of `fd` takes one record per kernel read
9398    /// (4262 design §4.4): some signal it covers has an entry behind the
9399    /// front of its list, or a stale timer entry, in the calling thread's or
9400    /// its process's queue. A read must then dequeue one record at a time:
9401    /// sending a list's next entry before the next record, as Linux's
9402    /// `signalfd_dequeue` loop takes them in its order, and refilling the slot
9403    /// of a stale record it drops, as Linux skips such an entry inside the
9404    /// dequeue. With only single valid entries, every entry is physically
9405    /// queued and none is dropped, so one kernel read returns what Linux
9406    /// returns, in count and in order.
9407    async fn signalfd_reads_per_record<G: Guest<Self>>(&self, guest: &mut G, fd: i32) -> bool {
9408        use crate::scheduler::discarded_at_send::DiscardedSignalAnswer;
9409        use crate::scheduler::discarded_at_send::DiscardedSignalControl;
9410        if !crate::scheduler::discarded_at_send::records(guest.config()) {
9411            return false;
9412        }
9413        let Some(mask) = crate::syscalls::signal::signalfd_mask(guest.pid().as_raw(), fd) else {
9414            return false;
9415        };
9416        matches!(
9417            crate::tool_global::discarded_signal(guest, DiscardedSignalControl::Queued).await,
9418            DiscardedSignalAnswer::Bits { thread, process } if (thread | process) & mask != 0
9419        )
9420    }
9421
9422    /// A signalfd read taken one record at a time
9423    /// (`signalfd_reads_per_record`): the first record's read may block, as
9424    /// the call would; each later one is read only while a signal the
9425    /// descriptor covers is pending, so it never blocks (fs/signalfd.c reads
9426    /// later records without waiting). A fault after k records returns
9427    /// k records, as the kernel's copy-out does.
9428    async fn read_signalfd_per_record<G: Guest<Self>>(
9429        &self,
9430        guest: &mut G,
9431        call: syscalls::Read,
9432    ) -> Result<i64, Error> {
9433        const RECORD: usize = crate::syscalls::signal::SIGNALFD_RECORD;
9434        let Some(buf) = call.buf() else {
9435            return self.read_signalfd(guest, call, true).await;
9436        };
9437        let mask = crate::syscalls::signal::signalfd_mask(guest.pid().as_raw(), call.fd());
9438        let mut done = 0usize;
9439        // A read whose records were all dropped (stale timer entries) frees
9440        // its slot. Each such read dequeued a physical record and popped its
9441        // entry, and the lists are finite, so the refills end without a cap
9442        // (the Codex re-review of
9443        // https://github.com/rrnewton/hermit/pull/4491, P2: 80 stale entries).
9444        while done + RECORD <= call.len() {
9445            if done > 0 {
9446                let pending =
9447                    crate::syscalls::threads::read_kernel_signal_state(guest.pid(), guest.tid())
9448                        .map(|state| state.thread_pending | state.shared_pending)
9449                        .unwrap_or(0);
9450                if mask.is_none_or(|mask| pending & mask == 0) {
9451                    break;
9452                }
9453            }
9454            // A guest address, never dereferenced here.
9455            let at = AddrMut::<u8>::from_raw(buf.as_raw() + done);
9456            let record = syscalls::Read::new()
9457                .with_fd(call.fd())
9458                .with_buf(at)
9459                .with_len(RECORD);
9460            match self.read_signalfd(guest, record, done == 0).await {
9461                Ok(read) if read > 0 => done += read as usize,
9462                Ok(read) if done == 0 => return Ok(read),
9463                // Linux skips a stale entry inside the dequeue and fills the
9464                // slot with the next pending signal (the Claude review of
9465                // https://github.com/rrnewton/hermit/pull/4491, P2-1).
9466                Ok(_) => {}
9467                Err(error) if done == 0 => return Err(error),
9468                Err(_) => break,
9469            }
9470        }
9471        Ok(done as i64)
9472    }
9473
9474    /// SYS_pread64 system call of a per-process file whose record sequence a
9475    /// fork shares, as [`Self::read_with_shared_sequence`].
9476    #[allow(clippy::type_complexity)]
9477    fn pread64_with_shared_sequence<'a, G: Guest<Self>>(
9478        &'a self,
9479        guest: &'a mut G,
9480        call: syscalls::Pread64,
9481        id: OpenFileId,
9482    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<i64, Error>> + Send + 'a>> {
9483        Box::pin(async move {
9484            self.take_procfs_sequence(guest, call.fd(), id).await;
9485            let result = self.handle_pread64(guest, call).await;
9486            self.store_procfs_sequence(guest, call.fd(), id).await;
9487            result
9488        })
9489    }
9490
9491    /// SYS_pread64 system call.
9492    pub async fn handle_pread64<G: Guest<Self>>(
9493        &self,
9494        guest: &mut G,
9495        call: syscalls::Pread64,
9496    ) -> Result<i64, Error> {
9497        if Self::refuses_unknown_record_sequence(guest, call.fd(), false) {
9498            return self
9499                .refuse_unknown_record_sequence(guest, Sysno::pread64, call.fd())
9500                .await;
9501        }
9502        if let Some(id) = Self::procfs_sequence_to_take(guest, call.fd()) {
9503            return self.pread64_with_shared_sequence(guest, call, id).await;
9504        }
9505        if self.timer_slack_binding(guest, call.fd())?.is_some() {
9506            return self
9507                .pread_timer_slack(guest, call.fd(), call.buf(), call.len(), call.offset())
9508                .await;
9509        }
9510        // A rendered `cmdline`: `ksys_pread64` refuses a negative position
9511        // first, then every read, of any count, reads memory.
9512        if guest
9513            .thread_state()
9514            .with_detfd(call.fd(), |detfd| detfd.procfs_reads_memory_at_every_read())
9515            .unwrap_or(false)
9516        {
9517            let offset = u64::try_from(call.offset()).map_err(|_| Errno::EINVAL)?;
9518            return self
9519                .read_rendered_cmdline_call(guest, call.fd(), call.buf(), call.len(), Some(offset))
9520                .await;
9521        }
9522
9523        if call.len() == 0 {
9524            // As for read, a zero-count pread's result goes through
9525            // record/replay.
9526            // AUTONOMOUS-BOT-IMPLEMENTED
9527            // TODO-HUMAN-REVIEW(PR-3601)
9528            return self
9529                .record_or_replay_preserving_tool_errors(guest, call)
9530                .await;
9531        }
9532
9533        let offset = usize::try_from(call.offset()).map_err(|_| Errno::EINVAL)?;
9534        let (serves_procfs_snapshot, cannot_read, follows_record) =
9535            guest.thread_state().with_detfd(call.fd(), |detfd| {
9536                (
9537                    detfd.procfs_serves_snapshot(),
9538                    !can_read(detfd.status_flags()),
9539                    detfd.procfs_follows_record_sequence(),
9540                )
9541            })?;
9542        // A descriptor that cannot read (`O_PATH`, write-only, access mode 3)
9543        // reads nothing (EBADF), before its buffer is looked at, as Linux's
9544        // `vfs_read` checks, whether or not a rendered file's snapshot is
9545        // taken yet (`/proc/uptime`'s is taken at its first read).
9546        if serves_procfs_snapshot && cannot_read {
9547            return Err(Errno::EBADF.into());
9548        }
9549        if serves_procfs_snapshot {
9550            validate_procfs_destination(
9551                || crate::iovecs::UserAddressLimit::from_query(guest.user_address_limit()),
9552                call.buf(),
9553                call.len(),
9554            )?;
9555        }
9556        // A rendered per-process file runs its record's `show` where the read
9557        // does not continue the record's sequence: a `pread` at 0, and one
9558        // elsewhere than where the sequence is, whatever the shared cursor
9559        // (`ProcfsFile::drop_stale_task_snapshot`, `Self::check_record_show`),
9560        // only after `vfs_read`'s checks: access, destination (above) and
9561        // range (`rw_verify_area`: a position plus count past LLONG_MAX is
9562        // EINVAL).
9563        if follows_record {
9564            verify_procfs_read_range(offset, call.len())?;
9565            if guest.thread_state().with_detfd(call.fd(), |detfd| {
9566                detfd.procfs_drop_stale_task_snapshot(offset)
9567            })? {
9568                self.check_record_show(guest, call.fd(), offset).await?;
9569            }
9570        }
9571        // A task's `schedstat` shows the runtime anew at a read at 0
9572        // (`ProcfsFile::drop_schedstat_snapshot_for_read_at`).
9573        guest.thread_state().with_detfd(call.fd(), |detfd| {
9574            detfd.procfs_drop_schedstat_snapshot_for_read_at(offset as u64)
9575        })?;
9576        let needs_procfs_snapshot = guest
9577            .thread_state()
9578            .with_detfd(call.fd(), |detfd| detfd.procfs_needs_snapshot())?;
9579        if needs_procfs_snapshot {
9580            let read = syscalls::Read::new()
9581                .with_fd(call.fd())
9582                .with_buf(call.buf())
9583                .with_len(call.len());
9584            let rendered = self.initialize_procfs_snapshot(guest, read).await;
9585            guest.thread_state().with_detfd(call.fd(), |detfd| {
9586                detfd.procfs_note_task_record_rendered(offset, rendered.is_ok())
9587            })?;
9588            rendered?;
9589        }
9590
9591        let procfs_bytes = guest
9592            .thread_state()
9593            .with_detfd(call.fd(), |detfd| detfd.take_procfs_at(offset, call.len()))?;
9594        if let Some(bytes) = procfs_bytes {
9595            let faults_on_a_short_copy = guest
9596                .thread_state()
9597                .with_detfd(call.fd(), |detfd| detfd.procfs_faults_on_a_short_copy())?;
9598            let copied = copy_procfs_read(
9599                &mut guest.memory(),
9600                call.buf(),
9601                &bytes,
9602                faults_on_a_short_copy,
9603            )?;
9604            guest.thread_state().with_detfd(call.fd(), |detfd| {
9605                detfd.procfs_note_read_end(offset + copied)
9606            })?;
9607            return Ok(copied as i64);
9608        }
9609
9610        let (fd_type, resource, status_flags, stream) =
9611            guest.thread_state_mut().with_detfd(call.fd(), |detfd| {
9612                (
9613                    detfd.ty(),
9614                    detfd.resource(),
9615                    detfd.status_flags(),
9616                    detfd.random_device_stream(),
9617                )
9618            })?;
9619
9620        if let Some(resource) = resource {
9621            let request = guest.thread_state().mk_request(resource, Permission::R);
9622            resource_request(guest, request).await;
9623        }
9624
9625        let res = match fd_type {
9626            FdType::Rng => (|| -> Result<i64, Error> {
9627                trace!("Pread64 call RNG fd {}, simulating...", call.fd());
9628                // A random device open for writing only reads nothing
9629                // (EBADF), before its buffer is looked at, as `vfs_read`
9630                // checks FMODE_READ first; read and readv already check.
9631                require_random_device_read_access(status_flags)?;
9632                let remote_buf = call.buf().ok_or(Errno::EFAULT)?;
9633                let n = self.fill_random_device_bytes(
9634                    guest,
9635                    stream,
9636                    remote_buf,
9637                    call.len(),
9638                    offset as u64,
9639                )?;
9640                Ok(n as i64)
9641            })(),
9642            FdType::Regular if guest.config().deterministic_io => {
9643                self.deterministic_pread64(guest, call).await
9644            }
9645            _ => match self.record_or_replay(guest, call).await {
9646                Ok(value) => Ok(value),
9647                Err(error) => Err(error.into()),
9648            },
9649        };
9650
9651        resource_release_all(guest).await;
9652        res
9653    }
9654
9655    /// Whether `fd`, whose open recorded the host stat `cached`, is a
9656    /// directory that is not on procfs (see [`Self::handle_lseek`]). procfs
9657    /// keeps the host's seek behavior, as it keeps the host's sizes. A
9658    /// descriptor's type and device never change, so the stat its open took
9659    /// answers without another `fstat` (Claude re-check of
9660    /// https://github.com/rrnewton/hermit/pull/4369). Its block size can (a
9661    /// FUSE server reports it), so it only rules procfs out: the kernel's
9662    /// procfs always has 1024-byte blocks, and a directory that may be procfs
9663    /// is proved so by naming its entry, as a stat does. (KVM's `/proc`
9664    /// directories are Detcore's placeholders, which answer `lseek`
9665    /// themselves.) Under DBT, where
9666    /// Detcore names no entries, one with procfs's stat fields keeps the
9667    /// host's seek unproved ([`procfs_inode::unprovable_procfs_signature`]).
9668    /// With metadata virtualized every descriptor the guest opened has a stat;
9669    /// those without are Detcore's own pipes and sockets.
9670    async fn is_seekable_directory<G: Guest<Self>>(
9671        guest: &mut G,
9672        fd: RawFd,
9673        cached: Option<DetStat>,
9674    ) -> bool {
9675        let Some(stat) = cached.filter(|stat| stat.mode & libc::S_IFMT == libc::S_IFDIR) else {
9676            return false;
9677        };
9678        if procfs_inode::unprovable_procfs_signature(
9679            &guest.config().backend,
9680            guest.thread_state().physical_tid,
9681            stat.dev,
9682            stat.blksize,
9683        ) {
9684            return false;
9685        }
9686        !Self::procfs_entry(guest, &stat, StatTarget::Descriptor(fd))
9687            .await
9688            .is_procfs()
9689    }
9690
9691    /// SYS_lseek system call of a per-process file whose record sequence a
9692    /// fork shares, as [`Self::read_with_shared_sequence`].
9693    #[allow(clippy::type_complexity)]
9694    fn lseek_with_shared_sequence<'a, G: Guest<Self>>(
9695        &'a self,
9696        guest: &'a mut G,
9697        call: syscalls::Lseek,
9698        id: OpenFileId,
9699    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<i64, Error>> + Send + 'a>> {
9700        Box::pin(async move {
9701            self.take_procfs_sequence(guest, call.fd(), id).await;
9702            let result = self.handle_lseek(guest, call).await;
9703            self.store_procfs_sequence(guest, call.fd(), id).await;
9704            result
9705        })
9706    }
9707
9708    /// SYS_lseek system call.
9709    pub async fn handle_lseek<G: Guest<Self>>(
9710        &self,
9711        guest: &mut G,
9712        call: syscalls::Lseek,
9713    ) -> Result<i64, Error> {
9714        if Self::refuses_unknown_record_sequence(guest, call.fd(), true) {
9715            return self
9716                .refuse_unknown_record_sequence(guest, Sysno::lseek, call.fd())
9717                .await;
9718        }
9719        if let Some(id) = Self::procfs_sequence_to_take(guest, call.fd()) {
9720            return self.lseek_with_shared_sequence(guest, call, id).await;
9721        }
9722        let timer_slack_binding = self.timer_slack_binding(guest, call.fd())?;
9723        // A seek must not move the position under a `getdents` that is
9724        // reading or serving this open file's directory.
9725        let directory_lock = guest
9726            .thread_state()
9727            .with_detfd(call.fd(), |detfd| detfd.directory_lock())?;
9728        let _seeking = directory_lock.lock().await;
9729        self.adopt_rendered_procfs_position(guest, call.fd())
9730            .await?;
9731        let (fd_type, status_flags, procfs_position, resource, directory_stream, host_order) =
9732            guest.thread_state().with_detfd(call.fd(), |detfd| {
9733                (
9734                    detfd.ty(),
9735                    detfd.status_flags(),
9736                    detfd.procfs_position(),
9737                    detfd.resource(),
9738                    detfd.has_directory_stream(),
9739                    detfd.directory_in_host_order(),
9740                )
9741            })?;
9742        if fd_type == FdType::Rng {
9743            return random_device_lseek_result(status_flags, call.whence()).map_err(Into::into);
9744        }
9745        if is_inherited_container_output(resource) {
9746            return Err(Errno::ESPIPE.into());
9747        }
9748        if timer_slack_binding.is_some() && status_flags & libc::O_PATH != 0 {
9749            return Err(Errno::EBADF.into());
9750        }
9751        if directory_stream {
9752            // Positions in a sorted directory stream are entry indices (see
9753            // `DirectoryStream`), not host cookies. The kernel position only
9754            // follows the stream, for descriptors Detcore does not track. Like
9755            // tmpfs's `dcache_dir_lseek`, only SEEK_SET and SEEK_CUR are
9756            // accepted; SEEK_END, SEEK_DATA and SEEK_HOLE get `EINVAL`, where
9757            // ext4 and btrfs accept SEEK_END
9758            // (https://github.com/rrnewton/hermit/issues/3724).
9759            let current = guest.thread_state().with_detfd(call.fd(), |detfd| {
9760                detfd.with_directory_stream(|stream| stream.position())
9761            })??;
9762            let requested = i128::from(call.offset());
9763            let position = match call.whence() {
9764                Whence::SEEK_SET => requested,
9765                Whence::SEEK_CUR => i128::from(current) + requested,
9766                _ => return Err(Errno::EINVAL.into()),
9767            };
9768            let result = i64::try_from(position)
9769                .ok()
9770                .filter(|position| *position >= 0)
9771                .ok_or(Errno::EINVAL)?;
9772            let target = guest.thread_state().with_detfd(call.fd(), |detfd| {
9773                detfd.with_directory_stream(|stream| {
9774                    stream.seek(result as u64);
9775                    stream.kernel_target()
9776                })
9777            })??;
9778            self.move_directory_kernel_position(guest, call.fd() as RawFd, target)
9779                .await;
9780            return Ok(result);
9781        }
9782        let Some((current, snapshot_len)) = procfs_position else {
9783            // A directory without a stream yet: SEEK_END would return the
9784            // host directory's size, which st_size no longer reports and
9785            // which changes as anything outside the run adds or removes an
9786            // entry (https://github.com/rrnewton/hermit/issues/4372). Answer
9787            // as a stream does (above, tmpfs's `dcache_dir_lseek`), from the
9788            // descriptor's first use. An `O_PATH` descriptor goes on to the
9789            // kernel, which refuses its seek with EBADF whatever the whence.
9790            if guest.config().virtualize_metadata
9791                && status_flags & libc::O_PATH == 0
9792                && matches!(
9793                    call.whence(),
9794                    Whence::SEEK_END | Whence::SEEK_DATA | Whence::SEEK_HOLE
9795                )
9796            {
9797                let cached = guest
9798                    .thread_state()
9799                    .with_detfd(call.fd(), |detfd| detfd.stat())?;
9800                if Self::is_seekable_directory(guest, call.fd(), cached).await {
9801                    return Err(Errno::EINVAL.into());
9802                }
9803            }
9804            // AUTONOMOUS-BOT-IMPLEMENTED
9805            // TODO-HUMAN-REVIEW(#1044): Regular-file lseek must
9806            // flow through record_or_replay, exactly as handle_read does for
9807            // FdType::Regular. Injecting the seek live here is correct for run
9808            // and --verify (the inner NoopTool just re-injects), but under
9809            // record/replay the replay descriptor is a virtual placeholder
9810            // whose kernel position never advances (reads are served from the
9811            // log, not the file). A live SEEK_CUR then returned Ok(0) on replay
9812            // versus the recorded offset (e.g. glibc's __tzfile_read rewinds
9813            // /etc/localtime with lseek(fd, -N, SEEK_CUR)), diverging the
9814            // guest's control flow and desynchronizing the event stream. Routing
9815            // through record_or_replay records the offset once and substitutes
9816            // the recorded value on replay, keeping the two runs identical.
9817            // A placeholder descriptor Detcore does not know is never moved
9818            // into the range of the positions that tag one
9819            // (`descriptor_directory`), where it would pass for another.
9820            if self
9821                .seek_would_place_a_tag(
9822                    guest,
9823                    call.fd() as RawFd,
9824                    call.offset(),
9825                    Syscall::Lseek(call).into_parts().1.arg2 as i32,
9826                )
9827                .await
9828            {
9829                return Err(Errno::EINVAL.into());
9830            }
9831            let position = self.record_or_replay(guest, call).await?;
9832            if host_order && position == 0 {
9833                // Back at the start, the next `getdents` can read the whole
9834                // directory again and serve it as a sorted stream.
9835                guest
9836                    .thread_state()
9837                    .with_detfd(call.fd(), |detfd| detfd.use_directory_stream())?;
9838            }
9839            return Ok(position);
9840        };
9841
9842        if let Some(binding) = timer_slack_binding {
9843            // Linux exposes this file through seq_lseek, which accepts only
9844            // SEEK_SET and SEEK_CUR. Keep that position entirely in the
9845            // virtual open-file description even before the first read.
9846            let requested = i128::from(call.offset());
9847            let new_offset = match call.whence() {
9848                Whence::SEEK_SET => requested,
9849                Whence::SEEK_CUR => current as i128 + requested,
9850                _ => return Err(Errno::EINVAL.into()),
9851            };
9852            let new_offset = usize::try_from(new_offset).map_err(|_| Errno::EINVAL)?;
9853            let result = i64::try_from(new_offset).map_err(|_| Errno::EOVERFLOW)?;
9854            // seq_lseek does not call the show callback for a no-op or a reset
9855            // to zero. Only a traversal to another positive position observes
9856            // the target task and therefore performs lifetime/access checks.
9857            if new_offset != 0 && new_offset != current {
9858                self.require_current_timer_slack_target(guest, binding)
9859                    .await?;
9860            }
9861            guest
9862                .thread_state()
9863                .with_detfd(call.fd(), |detfd| detfd.set_procfs_offset(new_offset))?;
9864            return Ok(result);
9865        }
9866
9867        // A procfs file Detcore renders, whichever opened it: an `O_PATH`
9868        // descriptor has no position (EBADF), before the seek's arguments are
9869        // looked at; otherwise a sysctl file's `default_llseek` on a file of
9870        // size 0, whatever the snapshot holds, or a `seq_file`'s `seq_lseek`,
9871        // which seeks only from the start or the position.
9872        let files = self.rendered_files(guest);
9873        let rendered = guest
9874            .thread_state()
9875            .with_detfd(call.fd(), |detfd| detfd.procfs_renderable_path(files))?;
9876        if let Some(rendered) = rendered {
9877            if status_flags & libc::O_PATH != 0 {
9878                return Err(Errno::EBADF.into());
9879            }
9880            let seek = if crate::procfs::rendered_path_is_seq_file(rendered) {
9881                RenderedSeek::SeqFile
9882            } else if crate::procfs::per_process_name(rendered)
9883                .is_some_and(|name| matches!(name.filename, "auxv" | "cmdline"))
9884            {
9885                RenderedSeek::Generic
9886            } else {
9887                RenderedSeek::Default
9888            };
9889            let new_offset = rendered_procfs_seek(seek, call.whence(), call.offset(), current)?;
9890            let result = i64::try_from(new_offset).map_err(|_| Errno::EINVAL)?;
9891            // A rendered per-process file traverses at the seek wherever its
9892            // record's sequence is not (`ProcfsFile::seek_task_record`): to a
9893            // positive position its `show` runs here, in the seeking thread's
9894            // turn (ESRCH once its task is gone, EACCES where a task's `io`
9895            // refuses the reader), and the record is rendered, except through
9896            // a descriptor that cannot read, which runs those checks only
9897            // (`Self::seek_record`); a rewind or a seek to where the sequence
9898            // is runs none. A failed `show` (ESRCH, EACCES, or the named
9899            // refusal) leaves the cursor and the sequence at 0 with nothing
9900            // buffered, as `seq_lseek` does (fs/seq_file.c:327-332 in 6.17), so
9901            // the next read runs `show` again.
9902            if guest
9903                .thread_state()
9904                .with_detfd(call.fd(), |detfd| detfd.procfs_follows_record_sequence())?
9905            {
9906                return self.seek_record(guest, call.fd(), new_offset, result).await;
9907            }
9908            guest
9909                .thread_state()
9910                .with_detfd(call.fd(), |detfd| detfd.seek_procfs(new_offset))?;
9911            self.sync_rendered_procfs_position(guest, call.fd()).await?;
9912            return Ok(result);
9913        }
9914
9915        // A per-process file the kernel opened that follows its record's
9916        // sequence (through `..` or a symbolic link, after `chroot`, under
9917        // record and replay, or `arch_status` where the guest's CPUID
9918        // instruction is not intercepted) seeks as a rendered one does
9919        // (above), whether or not Detcore holds a snapshot of it (none on a
9920        // fresh descriptor, nor after a failed `show`): from Detcore's own
9921        // cursor, as `seq_lseek` starts SEEK_CUR from the file's position
9922        // (fs/seq_file.c:308-341 in 6.17), with a traverse wherever the
9923        // sequence is not, whose `show` is a new capture of the kernel's file
9924        // (`Self::seek_record`). Never by the kernel's file itself, whose
9925        // position and sequence Detcore's own captures moved
9926        // (`Self::snapshot_procfs` rewinds it and reads it to its end), except
9927        // through a descriptor that cannot read, which Detcore never captures
9928        // through (`Self::seek_unreadable_kernel_record`). An `O_PATH`
9929        // descriptor has no position (EBADF), as above.
9930        if guest
9931            .thread_state()
9932            .with_detfd(call.fd(), |detfd| detfd.procfs_follows_record_sequence())?
9933        {
9934            if status_flags & libc::O_PATH != 0 {
9935                return Err(Errno::EBADF.into());
9936            }
9937            let new_offset =
9938                rendered_procfs_seek(RenderedSeek::SeqFile, call.whence(), call.offset(), current)?;
9939            let result = i64::try_from(new_offset).map_err(|_| Errno::EINVAL)?;
9940            if !can_read(status_flags) {
9941                return self
9942                    .seek_unreadable_kernel_record(guest, call.fd(), new_offset, result)
9943                    .await;
9944            }
9945            return self.seek_record(guest, call.fd(), new_offset, result).await;
9946        }
9947
9948        let Some(snapshot_len) = snapshot_len else {
9949            let offset = guest.inject(Syscall::from(call)).await?;
9950            let offset = usize::try_from(offset).map_err(|_| Errno::EINVAL)?;
9951            guest
9952                .thread_state()
9953                .with_detfd(call.fd(), |detfd| detfd.set_procfs_offset(offset))?;
9954            return Ok(offset as i64);
9955        };
9956
9957        // A `seq_file` seeks only from the start or the position, before and
9958        // after the snapshot (`ProcfsFile::seeks_only_from_start_or_position`).
9959        let seq_file = guest.thread_state().with_detfd(call.fd(), |detfd| {
9960            detfd.procfs_seeks_only_from_start_or_position()
9961        })?;
9962        let requested = i128::from(call.offset());
9963        let new_offset = match call.whence() {
9964            Whence::SEEK_SET => requested,
9965            Whence::SEEK_CUR => current as i128 + requested,
9966            _ if seq_file => return Err(Errno::EINVAL.into()),
9967            Whence::SEEK_END => snapshot_len as i128 + requested,
9968            Whence::SEEK_DATA => {
9969                if requested < 0 || requested >= snapshot_len as i128 {
9970                    return Err(Errno::ENXIO.into());
9971                }
9972                requested
9973            }
9974            Whence::SEEK_HOLE => {
9975                if requested < 0 || requested >= snapshot_len as i128 {
9976                    return Err(Errno::ENXIO.into());
9977                }
9978                snapshot_len as i128
9979            }
9980            _ => return Err(Errno::EINVAL.into()),
9981        };
9982        let new_offset = usize::try_from(new_offset).map_err(|_| Errno::EINVAL)?;
9983        let result = i64::try_from(new_offset).map_err(|_| Errno::EOVERFLOW)?;
9984        guest
9985            .thread_state()
9986            .with_detfd(call.fd(), |detfd| detfd.set_procfs_offset(new_offset))?;
9987        self.sync_rendered_procfs_position(guest, call.fd()).await?;
9988        Ok(result)
9989    }
9990
9991    /// A seek to `new_offset` (returning `result`) of a per-process file that
9992    /// follows its record's sequence (`ProcfsFile::follows_record_sequence`),
9993    /// as Linux's `seq_lseek` moves it (fs/seq_file.c:308-340 in 6.17): to
9994    /// anywhere but where the sequence is it traverses, which runs the
9995    /// record's `show` here, in the seeking thread's turn, for a positive
9996    /// position (`Self::check_record_show`, then the record rendered or, for
9997    /// a file the kernel opened, captured anew); a rewind or a seek to where
9998    /// the sequence is runs none. A failed `show` (ESRCH, EACCES, or the
9999    /// named refusal) leaves the cursor and the sequence at 0 with nothing
10000    /// buffered, as `seq_lseek` does (fs/seq_file.c:327-332), so the next read
10001    /// runs `show` again.
10002    ///
10003    /// `seq_lseek` never looks at the descriptor's access mode (nor does
10004    /// `vfs_llseek`, which checks only `FMODE_LSEEK`, fs/read_write.c:385-390),
10005    /// so a rendered file's descriptor that cannot read (write-only, or access
10006    /// mode 3) traverses the same way and runs the same checks, the task's
10007    /// (ESRCH) and an `io`'s reader's (EACCES), through
10008    /// `Self::require_rendered_task` (`proc_single_show` and
10009    /// `do_io_accounting`, fs/proc/base.c:800-816 and 3027-3039), and renders
10010    /// nothing: nothing can read through it (`vfs_read` refuses it with EBADF
10011    /// first, fs/read_write.c:556-557), and a snapshot is refused to it
10012    /// (`Self::initialize_procfs_snapshot`). Such a descriptor of a file the
10013    /// kernel opened goes to the kernel instead
10014    /// (`Self::seek_unreadable_kernel_record`).
10015    async fn seek_record<G: Guest<Self>>(
10016        &self,
10017        guest: &mut G,
10018        fd: RawFd,
10019        new_offset: usize,
10020        result: i64,
10021    ) -> Result<i64, Error> {
10022        let (shows, readable) = guest.thread_state().with_detfd(fd, |detfd| {
10023            (
10024                detfd.seek_procfs_task_record(new_offset),
10025                can_read(detfd.status_flags()),
10026            )
10027        })?;
10028        if shows {
10029            let shown = if !readable {
10030                self.require_rendered_task(guest, fd).await
10031            } else {
10032                match self.check_record_show(guest, fd, new_offset).await {
10033                    Ok(()) => {
10034                        self.initialize_procfs_snapshot(guest, syscalls::Read::new().with_fd(fd))
10035                            .await
10036                    }
10037                    Err(error) => Err(error),
10038                }
10039            };
10040            if let Err(error) = shown {
10041                guest
10042                    .thread_state()
10043                    .with_detfd(fd, |detfd| detfd.reset_procfs_task_record())?;
10044                self.sync_rendered_procfs_position(guest, fd).await?;
10045                return Err(error);
10046            }
10047        }
10048        self.sync_rendered_procfs_position(guest, fd).await?;
10049        Ok(result)
10050    }
10051
10052    /// A seek to `new_offset` (returning `result`) of a per-process file the
10053    /// kernel opened that follows its record's sequence, through a descriptor
10054    /// that cannot read (write-only, or access mode 3): the kernel's own
10055    /// `seq_lseek` answers, on the kernel's file, through the recording.
10056    /// Detcore never captures through such a descriptor (its reads are EBADF
10057    /// before anything, as `vfs_read`'s are, fs/read_write.c:556-557), so the
10058    /// kernel's file holds this description's position and sequence exactly:
10059    /// its traverse runs `show` where Linux's does, and nowhere else (ESRCH
10060    /// for a task gone, EACCES for an `io` the opener may not read), and a
10061    /// failed one leaves the position and the sequence at 0
10062    /// (fs/seq_file.c:322-337 in 6.17). Every seek of such a descriptor comes
10063    /// here, so the two never part. Detcore's record of the description
10064    /// follows the answer, for a later SEEK_CUR and for the processes that
10065    /// share the description.
10066    async fn seek_unreadable_kernel_record<G: Guest<Self>>(
10067        &self,
10068        guest: &mut G,
10069        fd: RawFd,
10070        new_offset: usize,
10071        result: i64,
10072    ) -> Result<i64, Error> {
10073        let sought = self
10074            .record_or_replay(
10075                guest,
10076                syscalls::Lseek::new()
10077                    .with_fd(fd)
10078                    .with_offset(result)
10079                    .with_whence(Whence::SEEK_SET),
10080            )
10081            .await;
10082        match sought {
10083            Ok(position) => {
10084                let position_usize = usize::try_from(position).unwrap_or(new_offset);
10085                guest
10086                    .thread_state()
10087                    .with_detfd(fd, |detfd| detfd.seek_procfs_task_record(position_usize))?;
10088                Ok(position)
10089            }
10090            Err(errno) => {
10091                guest
10092                    .thread_state()
10093                    .with_detfd(fd, |detfd| detfd.reset_procfs_task_record())?;
10094                Err(errno.into())
10095            }
10096        }
10097    }
10098
10099    /// Helper for performing a deterministic read that retries until it gets all its
10100    /// bytes.
10101    // AUTONOMOUS-BOT-IMPLEMENTED
10102    // TODO-HUMAN-REVIEW(#689): Confirm partial reads take precedence over later errors.
10103    async fn deterministic_read<G: Guest<Self>>(
10104        &self,
10105        guest: &mut G,
10106        mut call: syscalls::Read,
10107    ) -> Result<i64, Error> {
10108        let mut total_read_bytes = 0;
10109        let mut remaining_buf = call.len();
10110
10111        trace!(
10112            "[detcore/det_io]: Requested read buffer size: {:?}",
10113            remaining_buf
10114        );
10115
10116        loop {
10117            match guest.inject_with_retry(call).await {
10118                Ok(res) => {
10119                    remaining_buf -= res as usize;
10120                    total_read_bytes += res;
10121
10122                    trace!(
10123                        "[detcore/det_io]: Remaining read buffer size: {:?}",
10124                        remaining_buf
10125                    );
10126
10127                    if res == 0 || remaining_buf == 0 {
10128                        break Ok(total_read_bytes);
10129                    }
10130
10131                    // Buf is guaranteed to exist as we already issued a syscall.
10132                    let old_ptr = call.buf().unwrap().as_raw();
10133                    call = call
10134                        .with_len(remaining_buf)
10135                        .with_buf(AddrMut::<u8>::from_raw(old_ptr + res as usize));
10136                }
10137                Err(error) if total_read_bytes > 0 => {
10138                    trace!("[detcore/det_io]: returning {total_read_bytes} bytes before {error}");
10139                    break Ok(total_read_bytes);
10140                }
10141                Err(error) => break Err(error.into()),
10142            }
10143        }
10144    }
10145
10146    /// Perform a positional read until the requested buffer is full or EOF is reached.
10147    async fn deterministic_pread64<G: Guest<Self>>(
10148        &self,
10149        guest: &mut G,
10150        mut call: syscalls::Pread64,
10151    ) -> Result<i64, Error> {
10152        let mut total_read_bytes = 0;
10153        let mut remaining_buf = call.len();
10154
10155        trace!(
10156            "[detcore/det_io]: Requested pread64 buffer size: {:?}",
10157            remaining_buf
10158        );
10159
10160        loop {
10161            match guest.inject_with_retry(call).await {
10162                Ok(res) => {
10163                    remaining_buf -= res as usize;
10164                    total_read_bytes += res;
10165
10166                    trace!(
10167                        "[detcore/det_io]: Remaining pread64 buffer size: {:?}",
10168                        remaining_buf
10169                    );
10170
10171                    if res == 0 || remaining_buf == 0 {
10172                        break Ok(total_read_bytes);
10173                    }
10174
10175                    let old_ptr = call
10176                        .buf()
10177                        .expect("successful pread64 requires a valid guest buffer")
10178                        .as_raw();
10179                    let offset = call.offset().checked_add(res).ok_or(Errno::EOVERFLOW)?;
10180                    call = call
10181                        .with_len(remaining_buf)
10182                        .with_buf(AddrMut::<u8>::from_raw(old_ptr + res as usize))
10183                        .with_offset(offset);
10184                }
10185                Err(error) => break Err(error.into()),
10186            }
10187        }
10188    }
10189
10190    // AUTONOMOUS-BOT-IMPLEMENTED
10191    // TODO-HUMAN-REVIEW(PR-838): Review regular-file sendfile mediation.
10192    /// Copy data between tracked regular files or memfds.
10193    ///
10194    /// The kernel advances the input offset (or the explicit offset pointer) and
10195    /// destination offset atomically with the copy. Detcore serializes destination
10196    /// writes while the strict scheduler orders the stable input read, and routes
10197    /// the syscall through record/replay so that the result and offset update stay
10198    /// ordered with other file operations. Socket and pipe destinations can block
10199    /// and need the nonblocking scheduler path; return ENOSYS for those endpoint
10200    /// types so libc/application fallbacks use Detcore's existing read/write
10201    /// handlers instead.
10202    pub async fn handle_sendfile<G: Guest<Self>>(
10203        &self,
10204        guest: &mut G,
10205        call: syscalls::Sendfile,
10206    ) -> Result<i64, Error> {
10207        let in_type = guest
10208            .thread_state()
10209            .with_detfd(call.in_fd(), |detfd| detfd.ty())?;
10210        let (out_type, out_resource, out_inode) =
10211            guest.thread_state().with_detfd(call.out_fd(), |detfd| {
10212                (
10213                    detfd.ty(),
10214                    detfd.resource(),
10215                    detfd.stat().map(|stat| stat.raw_inode()),
10216                )
10217            })?;
10218
10219        // AUTONOMOUS-BOT-IMPLEMENTED
10220        // TODO-HUMAN-REVIEW(PR-973): Refuse sendfile from a procfs input so it
10221        // cannot bypass the deterministic ProcfsFile snapshot. A procfs fd is
10222        // classified `FdType::Regular`, so it would otherwise pass the type
10223        // guard below and copy live kernel bytes straight to the output fd,
10224        // reintroducing the nondeterminism the mediated read()/write() path
10225        // sanitizes. Failing closed with ENOSYS makes callers fall back to
10226        // that mediated path (glibc's sendfile does exactly this).
10227        // A per-process file whose record's sequence Detcore cannot know here
10228        // (`DetFd::record_sequence_unknown`) is one too: the caller's
10229        // fallback, a read, is then refused by name.
10230        let in_is_procfs = guest.thread_state().with_detfd(call.in_fd(), |detfd| {
10231            detfd.record_sequence_unknown() || detfd.procfs_position().is_some()
10232        })?;
10233        if in_is_procfs {
10234            return Err(Errno::ENOSYS.into());
10235        }
10236
10237        if !matches!(in_type, FdType::Regular | FdType::Memfd)
10238            || !matches!(out_type, FdType::Regular | FdType::Memfd)
10239        {
10240            return Err(Errno::ENOSYS.into());
10241        }
10242
10243        let dettid = guest.thread_state().dettid;
10244        let mut resources = Resources::new(dettid);
10245        // `out_inode` is the fd's cached HOST inode, so it must be
10246        // determinized before naming a resource. It is deliberately left raw
10247        // for the `touch_file` call below, which takes a `RawInode`.
10248        let out_resource = match out_resource {
10249            Some(resource) => Some(resource),
10250            None => match out_inode {
10251                Some(raw_ino) => Some(ResourceID::FileContents(
10252                    determinize_inode(guest, raw_ino).await.0,
10253                )),
10254                None => None,
10255            },
10256        };
10257        if let Some(resource) = out_resource {
10258            resources.insert(resource, Permission::W);
10259        }
10260        resources.fyi("sendfile");
10261        resource_request(guest, resources).await;
10262
10263        let result = self
10264            .record_or_replay(guest, call)
10265            .await
10266            .map_err(Error::from);
10267        if guest.config().virtualize_metadata && matches!(&result, Ok(copied) if *copied > 0) {
10268            let inode = out_inode.expect("virtualized metadata requires stat data for sendfile");
10269            touch_file(guest, inode).await;
10270        }
10271        resource_release_all(guest).await;
10272        result
10273    }
10274
10275    /// SYS_write system call.
10276    pub async fn handle_write<G: Guest<Self>>(
10277        &self,
10278        guest: &mut G,
10279        mut call: syscalls::Write,
10280    ) -> Result<i64, Error> {
10281        // D41 (A5 step 3c.1c), at the handler's entry: a write can be a
10282        // kernel-run call (execute_nonblockable_fd_syscall's blocking branch
10283        // files it), and its virtual mtime bump below (touch_file) is a
10284        // guest-visible effect a call not run must not leave; a default-fatal
10285        // signal deliverable here arrived before the call, a schedule Linux
10286        // permits for any write. The check at the filing point stays for a
10287        // signal sent while this call waits for its turn below.
10288        if let Some(refused) = super::helpers::kernel_run_entry_refusal(guest)? {
10289            return Err(refused);
10290        }
10291        if self.timer_slack_binding(guest, call.fd())?.is_some() {
10292            return self
10293                .write_timer_slack(guest, call.fd(), call.buf(), call.len())
10294                .await;
10295        }
10296
10297        if call.len() > 0 {
10298            self.fifo_polls_if_a_guest_holds_its_peer(guest, call.fd(), FifoPeer::Reader)
10299                .await;
10300        }
10301        let (
10302            fd_type,
10303            physically_nonblocking,
10304            logically_nonblocking,
10305            open_file_id,
10306            resource,
10307            raw_ino,
10308        ) = guest.thread_state().with_detfd(call.fd(), |detfd| {
10309            (
10310                detfd.ty(),
10311                detfd.physically_nonblocking(),
10312                detfd.is_nonblocking(),
10313                detfd.open_file_id(),
10314                detfd.resource(),
10315                detfd.stat().map(|x| x.raw_inode()),
10316            )
10317        })?;
10318        // It doesn't matter much where the linearization point for this mtime bump falls:
10319        if guest.config().virtualize_metadata {
10320            let r =
10321                raw_ino.expect("Expect that when virtualize_metadata, DetFd's stat is populated!");
10322            touch_file(guest, r).await;
10323        }
10324
10325        if let Some(resource) = resource {
10326            let container_output = crate::sigalrm_phase1::is_container_output(&resource);
10327            let request = guest.thread_state().mk_request(resource, Permission::W);
10328            resource_request(guest, request).await;
10329            // Signal phase 1, design closure 1: a write to inherited stdout or
10330            // stderr may sleep with no scheduler admission. Once the grant is
10331            // taken no SIGALRM commits until this call ends, so one already due
10332            // is the only one it can strand: recorded as a determinism loss.
10333            if guest.thread_state().sigalrm_handled && container_output {
10334                sigalrm_refuses(guest, SigalrmControl::UnadmittedStdioIo).await;
10335            }
10336        }
10337
10338        // Only route writes through the nonblockable-fd path when the fd is actually
10339        // physically nonblocking. Detcore-created pipes are physically nonblocking in every
10340        // sequential mode, including record/replay, so their logically blocking writes use
10341        // the completion helper below. A physically blocking fd instead uses the original
10342        // synchronous path: treating an internal pipe as BlockingExternalIO would assume the
10343        // writer and reader were independent and could deadlock the scheduler.
10344        let res = if physically_nonblocking && fd_type == FdType::Pipe && !logically_nonblocking {
10345            self.execute_blocking_pipe_write(guest, call, open_file_id)
10346                .await
10347        } else if physically_nonblocking
10348            && matches!(fd_type, FdType::Socket | FdType::Pipe | FdType::Eventfd)
10349        {
10350            self.execute_nonblockable_fd_syscall(guest, call).await
10351        } else if guest.config().deterministic_io {
10352            let mut total_written_bytes = 0;
10353            let mut remaining_buf = call.len();
10354
10355            trace!(
10356                "[detcore/det_io]: Requested write buffer size: {:?}",
10357                remaining_buf
10358            );
10359
10360            loop {
10361                match self
10362                    .record_or_replay_preserving_tool_errors(guest, call)
10363                    .await
10364                {
10365                    Ok(res) => {
10366                        remaining_buf -= res as usize;
10367                        total_written_bytes += res;
10368
10369                        trace!(
10370                            "[detcore/det_io]: Remaining write buffer size: {:?}",
10371                            remaining_buf
10372                        );
10373
10374                        if res == 0 || remaining_buf == 0 {
10375                            break Ok(total_written_bytes);
10376                        }
10377
10378                        // Buf is guaranteed to exist as we already issued a syscall.
10379                        let old_ptr = call.buf().unwrap().as_raw();
10380                        call = call
10381                            .with_len(remaining_buf)
10382                            .with_buf(Addr::<u8>::from_raw(old_ptr + res as usize));
10383                    }
10384                    Err(error) => {
10385                        break finish_partial_record_or_replay_write(total_written_bytes, error);
10386                    }
10387                }
10388            }
10389        } else {
10390            self.record_or_replay_preserving_tool_errors(guest, call)
10391                .await
10392        };
10393
10394        resource_release_all(guest).await;
10395        res
10396    }
10397
10398    // AUTONOMOUS-BOT-IMPLEMENTED
10399    // TODO-HUMAN-REVIEW(#683): Confirm positional-write ordering and replay semantics.
10400    /// SYS_pwrite64 system call.
10401    pub async fn handle_pwrite64<G: Guest<Self>>(
10402        &self,
10403        guest: &mut G,
10404        mut call: syscalls::Pwrite64,
10405    ) -> Result<i64, Error> {
10406        if self.timer_slack_binding(guest, call.fd())?.is_some() {
10407            return Err(if call.offset() < 0 {
10408                Errno::EINVAL.into()
10409            } else {
10410                Errno::ESPIPE.into()
10411            });
10412        }
10413
10414        let (resource, raw_ino) = guest.thread_state().with_detfd(call.fd(), |detfd| {
10415            (detfd.resource(), detfd.stat().map(|stat| stat.raw_inode()))
10416        })?;
10417        // The fd's cached `DetStat` carries the HOST inode (`DetStat` is built
10418        // straight from `fstat`/`statx`), so it must be determinized before it
10419        // can name a guest-visible resource. Passing it through directly used
10420        // to type-check only because `DetInode` was an alias for `RawInode`.
10421        let resource = match resource {
10422            Some(resource) => Some(resource),
10423            None => match raw_ino {
10424                Some(raw_ino) => Some(ResourceID::FileContents(
10425                    determinize_inode(guest, raw_ino).await.0,
10426                )),
10427                None => None,
10428            },
10429        };
10430
10431        if let Some(resource) = resource {
10432            let request = guest.thread_state().mk_request(resource, Permission::W);
10433            resource_request(guest, request).await;
10434        }
10435
10436        let result = if guest.config().deterministic_io {
10437            let mut total_written = 0_i64;
10438            let mut remaining = call.len();
10439
10440            loop {
10441                match self
10442                    .record_or_replay_preserving_tool_errors(guest, call)
10443                    .await
10444                {
10445                    Ok(written) => {
10446                        let Ok(written) = usize::try_from(written) else {
10447                            break Err(Errno::EIO.into());
10448                        };
10449                        let Ok(written_i64) = i64::try_from(written) else {
10450                            break Err(Errno::EIO.into());
10451                        };
10452                        if written > remaining {
10453                            break Err(Errno::EIO.into());
10454                        }
10455                        remaining -= written;
10456                        let Some(next_total) = total_written.checked_add(written_i64) else {
10457                            break Err(Errno::EIO.into());
10458                        };
10459                        total_written = next_total;
10460
10461                        if written == 0 || remaining == 0 {
10462                            break Ok(total_written);
10463                        }
10464
10465                        let Some(old_buf) = call.buf() else {
10466                            break Err(Errno::EFAULT.into());
10467                        };
10468                        let Some(next_buf) = old_buf.as_raw().checked_add(written) else {
10469                            break Err(Errno::EFAULT.into());
10470                        };
10471                        let Some(next_offset) = call.offset().checked_add(written_i64) else {
10472                            break Err(Errno::EFBIG.into());
10473                        };
10474                        let Some(next_buf) = Addr::<u8>::from_raw(next_buf) else {
10475                            break Err(Errno::EFAULT.into());
10476                        };
10477                        call = call
10478                            .with_buf(Some(next_buf))
10479                            .with_len(remaining)
10480                            .with_offset(next_offset);
10481                    }
10482                    Err(error) => {
10483                        break finish_partial_record_or_replay_write(total_written, error);
10484                    }
10485                }
10486            }
10487        } else {
10488            self.record_or_replay_preserving_tool_errors(guest, call)
10489                .await
10490        };
10491
10492        if guest.config().virtualize_metadata && matches!(&result, Ok(written) if *written > 0) {
10493            let inode = raw_ino.expect("virtualized metadata requires stat data for tracked fds");
10494            touch_file(guest, inode).await;
10495        }
10496
10497        resource_release_all(guest).await;
10498        result
10499    }
10500
10501    // AUTONOMOUS-BOT-IMPLEMENTED
10502    // TODO-HUMAN-REVIEW(#547)
10503    /// SYS_writev system call.
10504    ///
10505    /// Preserve the initial writev as one kernel operation so its iovec order remains intact.
10506    /// Detcore adds open-file resource ordering and nonblocking scheduler integration; a
10507    /// blocking pipe short write is completed by the helper because Hermit injected O_NONBLOCK.
10508    pub async fn handle_writev<G: Guest<Self>>(
10509        &self,
10510        guest: &mut G,
10511        call: syscalls::Writev,
10512    ) -> Result<i64, Error> {
10513        if self.timer_slack_binding(guest, call.fd())?.is_some() {
10514            self.require_timer_slack_access(guest, call.fd(), true)?;
10515            let iovecs = read_iovecs(&guest.memory(), call.iov(), call.len())?;
10516            return self.writev_timer_slack(guest, call.fd(), iovecs, 0).await;
10517        }
10518
10519        self.fifo_polls_if_a_guest_holds_its_peer(guest, call.fd(), FifoPeer::Reader)
10520            .await;
10521        let (
10522            fd_type,
10523            physically_nonblocking,
10524            logically_nonblocking,
10525            open_file_id,
10526            resource,
10527            raw_ino,
10528        ) = guest.thread_state().with_detfd(call.fd(), |detfd| {
10529            (
10530                detfd.ty(),
10531                detfd.physically_nonblocking(),
10532                detfd.is_nonblocking(),
10533                detfd.open_file_id(),
10534                detfd.resource(),
10535                detfd.stat().map(|x| x.raw_inode()),
10536            )
10537        })?;
10538
10539        if let Some(resource) = resource {
10540            let request = guest.thread_state().mk_request(resource, Permission::W);
10541            resource_request(guest, request).await;
10542        }
10543
10544        let result = if physically_nonblocking && fd_type == FdType::Pipe && !logically_nonblocking
10545        {
10546            self.execute_blocking_pipe_writev(guest, call, open_file_id)
10547                .await
10548        } else if physically_nonblocking
10549            && matches!(fd_type, FdType::Socket | FdType::Pipe | FdType::Eventfd)
10550        {
10551            self.execute_nonblockable_fd_syscall(guest, call).await
10552        } else {
10553            self.record_or_replay_preserving_tool_errors(guest, call)
10554                .await
10555        };
10556
10557        if guest.config().virtualize_metadata && matches!(&result, Ok(written) if *written > 0) {
10558            let inode =
10559                raw_ino.expect("virtualized metadata requires stat data for every tracked fd");
10560            touch_file(guest, inode).await;
10561        }
10562
10563        resource_release_all(guest).await;
10564        result
10565    }
10566
10567    // AUTONOMOUS-BOT-IMPLEMENTED
10568    // TODO-HUMAN-REVIEW(#794)
10569    /// A vectored read (`readv`, or `preadv2` at the current offset) of a
10570    /// signalfd, whose
10571    /// SIGCHLD records go through the child-exit ledger as `read`'s do
10572    /// (`reconcile_signalfd_extents`).
10573    ///
10574    /// Linux imports the `count` iovecs at `address` before it writes any record,
10575    /// and the records can overwrite the array itself, so the geometry is
10576    /// captured here, before the read, and used for the reconciliation and
10577    /// for the output observation (`observed`) alike (Codex re-check #6 of
10578    /// https://github.com/rrnewton/hermit/pull/4031). A read whose every
10579    /// record is dropped reads again, with the array's bytes put back first.
10580    /// An array the tracer cannot read is a proven kernel failure (EINVAL, or
10581    /// EFAULT with some byte outside every mapping: the kernel copies the whole
10582    /// array), forwarded unchanged; a wholly mapped one it cannot read, or one
10583    /// whose mappings cannot be listed, is refused by name, so no record is
10584    /// returned unreconciled.
10585    async fn vectored_signalfd_read<G: Guest<Self>, C>(
10586        &self,
10587        guest: &mut G,
10588        sysno: Sysno,
10589        call: C,
10590        address: usize,
10591        count: usize,
10592        observed: &mut Option<Vec<crate::io_buffers::BufferExtent>>,
10593    ) -> Result<i64, Error>
10594    where
10595        C: Into<Syscall> + Copy,
10596    {
10597        const IOVEC: usize = std::mem::size_of::<libc::iovec>();
10598        let fd = match call.into() {
10599            Syscall::Readv(call) => call.fd(),
10600            Syscall::Preadv2(call) => call.fd(),
10601            _ => -1,
10602        };
10603        // Linux passes the count on as an `unsigned int` (Codex re-check #7).
10604        let count = crate::io_buffers::linux_iovec_count(count as u64);
10605        if count > libc::UIO_MAXIOV as usize {
10606            return self
10607                .record_or_replay_preserving_tool_errors(guest, call)
10608                .await;
10609        }
10610        let mut raw = vec![0u8; count * IOVEC];
10611        if count > 0 {
10612            let read = AddrMut::<u8>::from_raw(address)
10613                .is_some_and(|at| guest.memory().read_exact(at, &mut raw).is_ok())
10614                || {
10615                    use std::os::unix::fs::FileExt;
10616                    std::fs::File::open(format!("/proc/{}/mem", guest.tid().as_raw()))
10617                        .and_then(|mem| mem.read_exact_at(&mut raw, address as u64))
10618                        .is_ok()
10619                };
10620            if !read {
10621                // A maps listing that cannot be read decides nothing: refuse.
10622                let maps = std::fs::read_to_string(format!("/proc/{}/maps", guest.tid().as_raw()));
10623                if let Ok(maps) = &maps
10624                    && !signalfd_range_is_mapped(maps, address, raw.len())
10625                {
10626                    return self
10627                        .record_or_replay_preserving_tool_errors(guest, call)
10628                        .await;
10629                }
10630                tracing::error!(
10631                    "[detcore, dtid {}] {} of a signalfd with its iovec array at {:#x} is not \
10632                     supported: the memory is mapped, but Hermit cannot read it to reconcile \
10633                     the SIGCHLD records (https://github.com/rrnewton/hermit/issues/3994)",
10634                    guest.thread_state().dettid,
10635                    sysno,
10636                    address,
10637                );
10638                return self
10639                    .refuse_unserviceable_operation(guest, sysno, Errno::ENOSYS)
10640                    .await;
10641            }
10642        }
10643        let extents: Vec<(usize, usize)> = raw
10644            .chunks(IOVEC)
10645            .map(|iovec| {
10646                let word = |at: usize| {
10647                    usize::from_ne_bytes(iovec[at..at + 8].try_into().expect("eight bytes"))
10648                };
10649                (word(0), word(8))
10650            })
10651            .collect();
10652        // A vectored read of a signalfd covering a signal with an entry behind
10653        // its list's front would need one record per kernel read, each into
10654        // its own sub-extents (`read_signalfd_per_record`), which Detcore does
10655        // not do for a vector; refused by name rather than return records out
10656        // of Linux's order (<https://github.com/rrnewton/hermit/issues/4262>).
10657        let total: usize = extents.iter().map(|(_, len)| *len).sum();
10658        if total >= 2 * crate::syscalls::signal::SIGNALFD_RECORD
10659            && self.signalfd_reads_per_record(guest, fd).await
10660        {
10661            tracing::error!(
10662                "[detcore, dtid {}] {} of a signalfd while a signal it covers has a POSIX timer's entry queued behind another is not supported: Linux returns them one by one in its dequeue order, which Detcore produces only for read (https://github.com/rrnewton/hermit/issues/4262)",
10663                guest.thread_state().dettid,
10664                sysno,
10665            );
10666            return self
10667                .refuse_unserviceable_operation(guest, sysno, Errno::ENOSYS)
10668                .await;
10669        }
10670        loop {
10671            let before =
10672                crate::syscalls::signal::capture_signalfd_buffer(guest, &extents, usize::MAX);
10673            let result = self
10674                .record_or_replay_preserving_tool_errors(guest, call)
10675                .await;
10676            match result {
10677                Ok(read) if read > 0 => {
10678                    let Some(kept) = self
10679                        .reconcile_signalfd_extents(guest, &extents, read as usize, &before)
10680                        .await
10681                    else {
10682                        break self.refuse_unrestorable_signalfd_record(guest, sysno).await;
10683                    };
10684                    if self
10685                        .settle_faulted_signalfd_record(guest, fd, &extents, Some(read as usize))
10686                        .await
10687                    {
10688                        break self.refuse_faulted_signalfd_discard(guest, sysno).await;
10689                    }
10690                    if kept > 0 {
10691                        *observed = Some(
10692                            crate::syscalls::signal::signalfd_extent_pieces(&extents, kept)
10693                                .into_iter()
10694                                .map(|(addr, len)| crate::io_buffers::BufferExtent {
10695                                    addr: addr as u64,
10696                                    len: len as u64,
10697                                })
10698                                .collect(),
10699                        );
10700                        break Ok(kept as i64);
10701                    }
10702                    if let Some(at) = AddrMut::<u8>::from_raw(address)
10703                        && guest.memory().write_exact(at, &raw).is_err()
10704                    {
10705                        break Err(Errno::EFAULT.into());
10706                    }
10707                }
10708                Err(Error::Errno(Errno::EFAULT)) => {
10709                    if self
10710                        .settle_faulted_signalfd_record(guest, fd, &extents, None)
10711                        .await
10712                    {
10713                        break self.refuse_faulted_signalfd_discard(guest, sysno).await;
10714                    }
10715                    break Err(Errno::EFAULT.into());
10716                }
10717                result => break result,
10718            }
10719        }
10720    }
10721
10722    /// Linux's signalfd read dequeues each signal before it copies its record
10723    /// out, and stops at the first copy that faults, so that signal is lost:
10724    /// the read fails with EFAULT if it was the first record, or returns the
10725    /// records before it. The SIGCHLD such a record may have held is settled
10726    /// with the child-exit ledger as a faulted `rt_sigtimedwait`'s is
10727    /// (`WaitFaulted`; the Codex re-check of
10728    /// https://github.com/rrnewton/hermit/pull/4131 at 8692836e). `extents`
10729    /// are the read's buffers and `read` the bytes it returned, or `None` for
10730    /// an EFAULT.
10731    ///
10732    /// An EFAULT dequeued nothing when a buffer is not a user address at all
10733    /// (`access_ok`, checked before any dequeue). After a positive read, a
10734    /// record was lost only if the buffers had room for another and some
10735    /// byte of it is not writable; even then no signal may have been left to
10736    /// take, so the shared queue's pending bit decides that case.
10737    ///
10738    /// Returns whether a record may have been lost that way, and its copy may
10739    /// have been one Linux discarded when it was sent, which it would never
10740    /// have dequeued: the caller refuses the call by name
10741    /// (`refuse_faulted_signalfd_discard`), since the copy cannot be told
10742    /// (<https://github.com/rrnewton/hermit/issues/4293>).
10743    async fn settle_faulted_signalfd_record<G: Guest<Self>>(
10744        &self,
10745        guest: &mut G,
10746        fd: i32,
10747        extents: &[(usize, usize)],
10748        read: Option<usize>,
10749    ) -> bool {
10750        if !self.cfg.backend.reports_child_exit_publication || !guest.config().sequentialize_threads
10751        {
10752            return false;
10753        }
10754        match read {
10755            None => {
10756                let user_end = user_address_end();
10757                if extents.iter().any(|(address, len)| {
10758                    address.checked_add(*len).is_none_or(|end| end > user_end)
10759                }) {
10760                    return false;
10761                }
10762            }
10763            Some(read) => {
10764                let Some(next) = signalfd_next_record(extents, read) else {
10765                    return false;
10766                };
10767                let Ok(maps) =
10768                    std::fs::read_to_string(format!("/proc/{}/maps", guest.tid().as_raw()))
10769                else {
10770                    return false;
10771                };
10772                if next
10773                    .iter()
10774                    .all(|(address, len)| signalfd_range_is_writable(&maps, *address, *len))
10775                {
10776                    return false;
10777                }
10778            }
10779        }
10780        let mask = signalfd_sigmask(guest.pid().as_raw(), fd);
10781        let lost_a_discarded_copy = {
10782            use crate::scheduler::discarded_at_send::DiscardedSignalAnswer;
10783            use crate::scheduler::discarded_at_send::DiscardedSignalControl;
10784            use crate::scheduler::discarded_at_send::STANDARD_SIGNALS;
10785            matches!(
10786                crate::tool_global::discarded_signal(guest, DiscardedSignalControl::Bits).await,
10787                DiscardedSignalAnswer::Bits { thread, process }
10788                    if (thread | process) & mask.unwrap_or(!0) & STANDARD_SIGNALS != 0
10789            )
10790        };
10791        let sigchld = crate::syscalls::threads::kernel_sigset_bit(libc::SIGCHLD);
10792        if mask.is_some_and(|mask| mask & sigchld == 0) {
10793            return lost_a_discarded_copy;
10794        }
10795        let Ok(state) =
10796            crate::syscalls::threads::read_thread_wait_signal_state(guest.pid(), guest.tid())
10797        else {
10798            return lost_a_discarded_copy;
10799        };
10800        // A failed read of a descriptor for SIGCHLD alone took a SIGCHLD.
10801        let shared_pending = if read.is_none() && mask == Some(sigchld) {
10802            false
10803        } else {
10804            state.shared_pending & sigchld != 0
10805        };
10806        crate::tool_global::child_exit_sigchld(
10807            guest,
10808            crate::scheduler::child_exit_sigchld::ChildExitSigchldControl::WaitFaulted {
10809                private_pending: state.thread_pending & sigchld != 0,
10810                shared_pending,
10811            },
10812        )
10813        .await;
10814        lost_a_discarded_copy
10815    }
10816
10817    /// Refuse by name a signalfd `sysno` whose copy-out faulted after a
10818    /// dequeue that may have taken a copy Linux discarded when it was sent
10819    /// (`settle_faulted_signalfd_record`): Linux would not have taken it, and
10820    /// which copy the lost record held cannot be told.
10821    async fn refuse_faulted_signalfd_discard<G: Guest<Self>>(
10822        &self,
10823        guest: &mut G,
10824        sysno: Sysno,
10825    ) -> Result<i64, Error> {
10826        tracing::error!(
10827            "[detcore, dtid {}] {} of a signalfd faulted copying out a record that may hold a \
10828             signal Linux discarded when it was sent, which Linux would never have dequeued; \
10829             Hermit cannot tell which copy was lost \
10830             (https://github.com/rrnewton/hermit/issues/4293)",
10831            guest.thread_state().dettid,
10832            sysno,
10833        );
10834        self.refuse_unserviceable_operation(guest, sysno, Errno::ENOSYS)
10835            .await
10836    }
10837
10838    /// Refuse by name a signalfd `sysno` that read a record to drop (a copy
10839    /// Linux discarded at send, or a duplicate child-exit copy) whose bytes
10840    /// the capture before the read does not reach: more records than
10841    /// `SIGNALFD_CAPTURED` holds, which only real-time signals can make.
10842    /// Linux would leave those bytes as they were, and Hermit does not invent
10843    /// them (the Codex re-check of https://github.com/rrnewton/hermit/pull/4351,
10844    /// P2-2).
10845    async fn refuse_unrestorable_signalfd_record<G: Guest<Self>>(
10846        &self,
10847        guest: &mut G,
10848        sysno: Sysno,
10849    ) -> Result<i64, Error> {
10850        tracing::error!(
10851            "[detcore, dtid {}] {} of a signalfd read a record Hermit drops, beyond the bytes \
10852             captured before the read, so it cannot leave the caller's buffer as Linux would \
10853             (https://github.com/rrnewton/hermit/issues/4293)",
10854            guest.thread_state().dettid,
10855            sysno,
10856        );
10857        self.refuse_unserviceable_operation(guest, sysno, Errno::ENOSYS)
10858            .await
10859    }
10860
10861    /// SYS_readv system call: the vectored form of `read`.
10862    ///
10863    /// Mirrors [`Self::handle_writev`] for the read direction. Detcore adds
10864    /// open-file resource ordering and, for physically nonblocking pipe/socket
10865    /// fds, the nonblocking scheduler integration. Random devices use the shared
10866    /// canonical cursor; other descriptors retain their recorded kernel operation.
10867    pub async fn handle_readv<G: Guest<Self>>(
10868        &self,
10869        guest: &mut G,
10870        call: syscalls::Readv,
10871    ) -> Result<i64, Error> {
10872        self.handle_readv_with_output(guest, call, &mut None).await
10873    }
10874
10875    /// Import after the resource wait and retain that geometry through copyout.
10876    /// The shared-cursor closure is synchronous and takes no metadata locks.
10877    fn read_random_vectors<G: Guest<Self>>(
10878        &self,
10879        guest: &mut G,
10880        detfd: &DetFd,
10881        request: RandomVectoredRead,
10882        rng_output: &mut Option<Vec<crate::io_buffers::BufferExtent>>,
10883    ) -> Result<i64, Error> {
10884        require_random_device_read_access(detfd.status_flags())?;
10885        let iovecs = crate::iovecs::import_read_iovecs(
10886            &guest.memory(),
10887            request.address,
10888            request.count,
10889            || crate::iovecs::UserAddressLimit::from_query(guest.user_address_limit()),
10890        )?;
10891        let total = iovecs.iter().map(|iov| iov.len).sum();
10892        validate_random_vector_read(request.offset, total, request.flags)?;
10893        let stream = detfd.random_device_stream();
10894        let written = if let Some(offset) = request.offset {
10895            self.fill_random_device_iovecs(guest, stream, &iovecs, offset)?
10896        } else {
10897            detfd.with_random_device_stream(|offset| {
10898                self.fill_random_device_iovecs(guest, stream, &iovecs, offset)
10899            })?
10900        };
10901        if written > 0 && self.cfg.detlog_io_buffers && crate::detlog_observed!() {
10902            *rng_output = Some(crate::io_buffers::rng_readv_extents(&iovecs, written)?);
10903        }
10904        Ok(written as i64)
10905    }
10906
10907    pub(crate) async fn handle_readv_with_output<G: Guest<Self>>(
10908        &self,
10909        guest: &mut G,
10910        call: syscalls::Readv,
10911        rng_output: &mut Option<Vec<crate::io_buffers::BufferExtent>>,
10912    ) -> Result<i64, Error> {
10913        if Self::refuses_unknown_record_sequence(guest, call.fd(), false) {
10914            return self
10915                .refuse_unknown_record_sequence(guest, Sysno::readv, call.fd())
10916                .await;
10917        }
10918        if self.timer_slack_binding(guest, call.fd())?.is_some() {
10919            self.require_timer_slack_access(guest, call.fd(), false)?;
10920            let iovecs = read_iovecs(&guest.memory(), call.iov(), call.len())?;
10921            return self
10922                .readv_timer_slack(guest, call.fd(), iovecs, None, 0)
10923                .await;
10924        }
10925        let files = self.rendered_files(guest);
10926        if guest.thread_state().with_detfd(call.fd(), |detfd| {
10927            detfd.procfs_renderable_path(files).is_some() || detfd.procfs_follows_record_sequence()
10928        })? {
10929            let address = call.iov().map_or(0, |address| address.as_raw());
10930            return self
10931                .readv_rendered_procfs(guest, call.fd(), address, call.len(), None, 0)
10932                .await;
10933        }
10934
10935        let is_procfs = guest
10936            .thread_state()
10937            .with_detfd(call.fd(), |detfd| detfd.procfs_position().is_some())?;
10938        if is_procfs {
10939            return Err(Errno::ENOSYS.into());
10940        }
10941
10942        self.fifo_polls_if_a_guest_holds_its_peer(guest, call.fd(), FifoPeer::Writer)
10943            .await;
10944        let (fd_type, physically_nonblocking, resource, detfd) =
10945            guest.thread_state().with_detfd(call.fd(), |detfd| {
10946                (
10947                    detfd.ty(),
10948                    detfd.physically_nonblocking(),
10949                    detfd.resource(),
10950                    detfd.clone(),
10951                )
10952            })?;
10953
10954        if let Some(resource) = resource {
10955            let request = guest.thread_state().mk_request(resource, Permission::R);
10956            resource_request(guest, request).await;
10957        }
10958
10959        let res = if fd_type == FdType::Rng {
10960            self.read_random_vectors(
10961                guest,
10962                &detfd,
10963                RandomVectoredRead {
10964                    address: call.iov().map_or(0, |addr| addr.as_raw()),
10965                    count: call.len(),
10966                    offset: None,
10967                    flags: 0,
10968                },
10969                rng_output,
10970            )
10971        } else if detfd.inherited_external_read().is_some() {
10972            // The same one-kernel-read rule as scalar input: preserve the
10973            // kernel's iovec/short-read/error semantics and announce blocking.
10974            self.record_or_replay_blocking(guest, call.into()).await
10975        } else if fd_type == FdType::Timerfd && crate::syscalls::timerfd::enabled(guest) {
10976            // As `read` of a timerfd on virtual time: publish its expirations,
10977            // retry a logically blocking read in turns, and return the
10978            // virtual count (`crate::syscalls::timerfd`).
10979            crate::syscalls::timerfd::publish_expired(guest).await;
10980            let result = self.execute_nonblockable_fd_syscall(guest, call).await;
10981            crate::syscalls::timerfd::finish_readv(guest, call.fd(), call.iov(), call.len(), result)
10982        } else if physically_nonblocking
10983            && matches!(fd_type, FdType::Socket | FdType::Pipe | FdType::Eventfd)
10984        {
10985            self.execute_nonblockable_fd_syscall(guest, call).await
10986        } else if fd_type == FdType::Signalfd && self.cfg.backend.reports_child_exit_publication {
10987            // The SIGCHLD records a vectored read scatters go through the
10988            // child-exit ledger as `read`'s do (`reconcile_signalfd_extents`).
10989            self.vectored_signalfd_read(
10990                guest,
10991                Sysno::readv,
10992                call,
10993                call.iov().map_or(0, |iov| iov.as_raw()),
10994                call.len(),
10995                rng_output,
10996            )
10997            .await
10998        } else if fd_type == FdType::Inotify {
10999            // As `read`: a blocking readv with nothing queued waits in
11000            // Detcore's turns, never in the kernel holding this thread's turn
11001            // (https://github.com/rrnewton/hermit/issues/4768), and it reads
11002            // on into its later iovecs as Linux's does.
11003            self.readv_inotify(guest, call).await
11004        } else {
11005            self.record_or_replay_preserving_tool_errors(guest, call)
11006                .await
11007        };
11008
11009        // O2: a signalfd read (even one that faulted copying out a record
11010        // it had already dequeued) may have taken the process's ITIMER_REAL
11011        // copy (https://github.com/rrnewton/hermit/issues/4139).
11012        if fd_type == FdType::Signalfd {
11013            crate::tool_global::reconcile_alarm(guest).await;
11014        }
11015        resource_release_all(guest).await;
11016        res
11017    }
11018
11019    // AUTONOMOUS-BOT-IMPLEMENTED
11020    // TODO-HUMAN-REVIEW(#794)
11021    /// SYS_preadv system call: the vectored form of `pread64`.
11022    ///
11023    /// RNG reads use the canonical stream at the explicit offset without
11024    /// advancing its shared cursor. Other files retain their kernel operation.
11025    pub async fn handle_preadv<G: Guest<Self>>(
11026        &self,
11027        guest: &mut G,
11028        call: syscalls::Preadv,
11029    ) -> Result<i64, Error> {
11030        self.handle_preadv_with_output(guest, call, &mut None).await
11031    }
11032
11033    pub(crate) async fn handle_preadv_with_output<G: Guest<Self>>(
11034        &self,
11035        guest: &mut G,
11036        call: syscalls::Preadv,
11037        rng_output: &mut Option<Vec<crate::io_buffers::BufferExtent>>,
11038    ) -> Result<i64, Error> {
11039        // Linux rejects negative offsets before descriptor lookup or import.
11040        let offset = vectored_offset(call.pos_l(), call.pos_h());
11041        if offset < 0 {
11042            return Err(Errno::EINVAL.into());
11043        }
11044        if Self::refuses_unknown_record_sequence(guest, call.fd(), false) {
11045            return self
11046                .refuse_unknown_record_sequence(guest, Sysno::preadv, call.fd())
11047                .await;
11048        }
11049        if self.timer_slack_binding(guest, call.fd())?.is_some() {
11050            self.require_timer_slack_access(guest, call.fd(), false)?;
11051            let iovecs = read_iovecs(&guest.memory(), call.iov(), call.iov_len())?;
11052            return self
11053                .readv_timer_slack(guest, call.fd(), iovecs, Some(offset), 0)
11054                .await;
11055        }
11056        let files = self.rendered_files(guest);
11057        if guest.thread_state().with_detfd(call.fd(), |detfd| {
11058            detfd.procfs_renderable_path(files).is_some() || detfd.procfs_follows_record_sequence()
11059        })? {
11060            let address = call.iov().map_or(0, |address| address.as_raw());
11061            return self
11062                .readv_rendered_procfs(guest, call.fd(), address, call.iov_len(), Some(offset), 0)
11063                .await;
11064        }
11065
11066        let is_procfs = guest
11067            .thread_state()
11068            .with_detfd(call.fd(), |detfd| detfd.procfs_position().is_some())?;
11069        if is_procfs {
11070            return Err(Errno::ENOSYS.into());
11071        }
11072
11073        let detfd = guest
11074            .thread_state()
11075            .with_detfd(call.fd(), |detfd| detfd.clone())?;
11076
11077        if let Some(resource) = detfd.resource() {
11078            let request = guest.thread_state().mk_request(resource, Permission::R);
11079            resource_request(guest, request).await;
11080        }
11081
11082        let res = if detfd.ty() == FdType::Rng {
11083            self.read_random_vectors(
11084                guest,
11085                &detfd,
11086                RandomVectoredRead {
11087                    address: call.iov().map_or(0, |addr| addr.as_raw()),
11088                    count: call.iov_len(),
11089                    offset: Some(offset as u64),
11090                    flags: 0,
11091                },
11092                rng_output,
11093            )
11094        } else {
11095            self.record_or_replay_preserving_tool_errors(guest, call)
11096                .await
11097        };
11098        resource_release_all(guest).await;
11099        res
11100    }
11101
11102    // AUTONOMOUS-BOT-IMPLEMENTED
11103    // TODO-HUMAN-REVIEW(#794)
11104    /// SYS_preadv2 system call: positioned vectors, or the shared stream when
11105    /// offset is -1. RNG flag validation precedes output copying.
11106    pub async fn handle_preadv2<G: Guest<Self>>(
11107        &self,
11108        guest: &mut G,
11109        call: syscalls::Preadv2,
11110    ) -> Result<i64, Error> {
11111        self.handle_preadv2_with_output(guest, call, &mut None)
11112            .await
11113    }
11114
11115    pub(crate) async fn handle_preadv2_with_output<G: Guest<Self>>(
11116        &self,
11117        guest: &mut G,
11118        call: syscalls::Preadv2,
11119        rng_output: &mut Option<Vec<crate::io_buffers::BufferExtent>>,
11120    ) -> Result<i64, Error> {
11121        let offset = vectored_offset(call.pos_l(), call.pos_h());
11122        if offset < -1 {
11123            return Err(Errno::EINVAL.into());
11124        }
11125        if Self::refuses_unknown_record_sequence(guest, call.fd(), false) {
11126            return self
11127                .refuse_unknown_record_sequence(guest, Sysno::preadv2, call.fd())
11128                .await;
11129        }
11130        if self.timer_slack_binding(guest, call.fd())?.is_some() {
11131            self.require_timer_slack_access(guest, call.fd(), false)?;
11132            let count = usize::try_from(call.iov_len()).map_err(|_| Errno::EINVAL)?;
11133            let iovecs = read_iovecs(&guest.memory(), call.iov(), count)?;
11134            return if offset == -1 {
11135                self.readv_timer_slack(guest, call.fd(), iovecs, None, call.flags())
11136                    .await
11137            } else {
11138                self.readv_timer_slack(guest, call.fd(), iovecs, Some(offset), call.flags())
11139                    .await
11140            };
11141        }
11142        let files = self.rendered_files(guest);
11143        if guest.thread_state().with_detfd(call.fd(), |detfd| {
11144            detfd.procfs_renderable_path(files).is_some() || detfd.procfs_follows_record_sequence()
11145        })? {
11146            let address = call.iov().map_or(0, |address| address.as_raw());
11147            let offset = (offset != -1).then_some(offset);
11148            return self
11149                .readv_rendered_procfs(
11150                    guest,
11151                    call.fd(),
11152                    address,
11153                    call.iov_len() as usize,
11154                    offset,
11155                    call.flags(),
11156                )
11157                .await;
11158        }
11159
11160        let is_procfs = guest
11161            .thread_state()
11162            .with_detfd(call.fd(), |detfd| detfd.procfs_position().is_some())?;
11163        if is_procfs {
11164            return Err(Errno::ENOSYS.into());
11165        }
11166
11167        // At the current position (-1) and with no flags, `preadv2` is
11168        // `readv` (`preadv2` calls `do_readv`). On a descriptor whose blocking
11169        // read Detcore runs in turns it is run as one, so it waits as `readv`
11170        // does: the single kernel attempt below fails with EAGAIN on the
11171        // descriptor Detcore made physically nonblocking
11172        // (https://github.com/rrnewton/hermit/issues/4822). So it is on an
11173        // inotify instance, whose blocking `readv` also reads on into its
11174        // later iovecs (`Detcore::readv_inotify`), with RWF_HIPRI too, the one
11175        // flag Linux lets through to its per-iovec reads
11176        // (`do_loop_readv_writev`).
11177        let inotify = guest
11178            .thread_state()
11179            .with_detfd(call.fd(), |detfd| detfd.ty() == FdType::Inotify)
11180            .unwrap_or(false);
11181        if offset == -1
11182            && ((inotify && call.flags() & !libc::RWF_HIPRI == 0)
11183                || (call.flags() == 0 && self.waits_in_turns(guest, call.fd())))
11184        {
11185            let readv = syscalls::Readv::new()
11186                .with_fd(call.fd())
11187                .with_iov(call.iov())
11188                .with_len(call.iov_len() as usize);
11189            return self
11190                .handle_readv_with_output(guest, readv, rng_output)
11191                .await;
11192        }
11193
11194        let detfd = guest
11195            .thread_state()
11196            .with_detfd(call.fd(), |detfd| detfd.clone())?;
11197
11198        if let Some(resource) = detfd.resource() {
11199            let request = guest.thread_state().mk_request(resource, Permission::R);
11200            resource_request(guest, request).await;
11201        }
11202
11203        let res = if detfd.ty() == FdType::Rng {
11204            self.read_random_vectors(
11205                guest,
11206                &detfd,
11207                RandomVectoredRead {
11208                    address: call.iov().map_or(0, |addr| addr.as_raw()),
11209                    count: call.iov_len() as usize,
11210                    offset: (offset != -1).then_some(offset as u64),
11211                    flags: call.flags(),
11212                },
11213                rng_output,
11214            )
11215        } else if detfd.ty() == FdType::Signalfd
11216            && offset == -1
11217            && self.cfg.backend.reports_child_exit_publication
11218        {
11219            // At the current offset (-1) a signalfd reads as `readv` does,
11220            // and its records go through the child-exit ledger
11221            // (`reconcile_signalfd_extents`). At any other offset, as with
11222            // `preadv`, the kernel fails with ESPIPE before it imports the
11223            // array, so that call goes to it unchanged below.
11224            self.vectored_signalfd_read(
11225                guest,
11226                Sysno::preadv2,
11227                call,
11228                call.iov().map_or(0, |iov| iov.as_raw()),
11229                usize::try_from(call.iov_len()).unwrap_or(0),
11230                rng_output,
11231            )
11232            .await
11233        } else if detfd.ty() == FdType::Inotify && offset == -1 {
11234            // At the current offset (-1) an inotify descriptor reads as
11235            // `readv` does (`do_preadv2` calls `do_readv`), so its rename
11236            // cookies are renumbered as `readv`'s are. At any other offset
11237            // the kernel fails with ESPIPE (an anonymous inode has no
11238            // FMODE_PREAD), so that call goes to it unchanged below.
11239            let destination = crate::syscalls::inotify::Destination::iovecs(
11240                &guest.memory(),
11241                call.iov().map_or(0, |iov| iov.as_raw()),
11242                call.iov_len() as usize,
11243            );
11244            // Only flags Linux refuses (EOPNOTSUPP, `do_loop_readv_writev`) come
11245            // here, which it refuses before any wait; on an instance Detcore
11246            // made physically nonblocking the first attempt is refused alike.
11247            crate::syscalls::inotify::begin_count(guest, call.fd(), call).await;
11248            let result = self.execute_nonblockable_fd_syscall(guest, call).await;
11249            let queued = crate::syscalls::inotify::end_count(guest);
11250            crate::syscalls::inotify::finish(
11251                guest,
11252                Sysno::preadv2,
11253                call.fd(),
11254                destination,
11255                queued,
11256                result,
11257            )
11258            .await
11259        } else {
11260            self.record_or_replay_preserving_tool_errors(guest, call)
11261                .await
11262        };
11263        // O2: a signalfd read (even one that faulted copying out a record
11264        // it had already dequeued) may have taken the process's ITIMER_REAL
11265        // copy (https://github.com/rrnewton/hermit/issues/4139).
11266        if detfd.ty() == FdType::Signalfd {
11267            crate::tool_global::reconcile_alarm(guest).await;
11268        }
11269        resource_release_all(guest).await;
11270        res
11271    }
11272
11273    // AUTONOMOUS-BOT-IMPLEMENTED
11274    // TODO-HUMAN-REVIEW(#794)
11275    /// SYS_pwritev system call: the vectored form of `pwrite64`.
11276    ///
11277    /// Positioned writes target seekable files and do not block, so this mirrors
11278    /// [`Self::handle_pwrite64`]'s ordering, records/replays the single kernel
11279    /// operation, and bumps the virtual mtime on a successful write.
11280    pub async fn handle_pwritev<G: Guest<Self>>(
11281        &self,
11282        guest: &mut G,
11283        call: syscalls::Pwritev,
11284    ) -> Result<i64, Error> {
11285        if self.timer_slack_binding(guest, call.fd())?.is_some() {
11286            let offset = vectored_offset(call.pos_l(), call.pos_h());
11287            return if offset < 0 {
11288                Err(Errno::EINVAL.into())
11289            } else {
11290                Err(Errno::ESPIPE.into())
11291            };
11292        }
11293
11294        let (resource, raw_ino) = guest.thread_state().with_detfd(call.fd(), |detfd| {
11295            (detfd.resource(), detfd.stat().map(|stat| stat.raw_inode()))
11296        })?;
11297        // The fd's cached `DetStat` carries the HOST inode (`DetStat` is built
11298        // straight from `fstat`/`statx`), so it must be determinized before it
11299        // can name a guest-visible resource. Passing it through directly used
11300        // to type-check only because `DetInode` was an alias for `RawInode`.
11301        let resource = match resource {
11302            Some(resource) => Some(resource),
11303            None => match raw_ino {
11304                Some(raw_ino) => Some(ResourceID::FileContents(
11305                    determinize_inode(guest, raw_ino).await.0,
11306                )),
11307                None => None,
11308            },
11309        };
11310
11311        if let Some(resource) = resource {
11312            let request = guest.thread_state().mk_request(resource, Permission::W);
11313            resource_request(guest, request).await;
11314        }
11315
11316        let result = self
11317            .record_or_replay_preserving_tool_errors(guest, call)
11318            .await;
11319
11320        if guest.config().virtualize_metadata && matches!(&result, Ok(written) if *written > 0) {
11321            let inode = raw_ino.expect("virtualized metadata requires stat data for tracked fds");
11322            touch_file(guest, inode).await;
11323        }
11324
11325        resource_release_all(guest).await;
11326        result
11327    }
11328
11329    /// Whether a blocking transfer on `fd` waits in Detcore's turns, or may
11330    /// once the `readv` and `writev` handlers look at it: an internal pipe,
11331    /// socket or eventfd Detcore made physically nonblocking, whose `readv`
11332    /// and `writev` it polls (`execute_nonblockable_fd_syscall`,
11333    /// `execute_blocking_pipe_writev`); a blocking FIFO opened by path, which
11334    /// those handlers poll as a pipe once they find a guest at its other end
11335    /// (`fifo_polls_if_a_guest_holds_its_peer`); or a timerfd on virtual time.
11336    /// A descriptor Detcore cannot look up is left to the kernel.
11337    fn waits_in_turns<G: Guest<Self>>(&self, guest: &G, fd: RawFd) -> bool {
11338        guest
11339            .thread_state()
11340            .with_detfd(fd, |detfd| {
11341                (detfd.physically_nonblocking()
11342                    && matches!(detfd.ty(), FdType::Socket | FdType::Pipe | FdType::Eventfd))
11343                    || (detfd.fifo_awaiting_guest_peer() && !detfd.is_nonblocking())
11344                    || (detfd.ty() == FdType::Timerfd && crate::syscalls::timerfd::enabled(guest))
11345            })
11346            .unwrap_or(false)
11347    }
11348
11349    // AUTONOMOUS-BOT-IMPLEMENTED
11350    // TODO-HUMAN-REVIEW(#794)
11351    /// SYS_pwritev2 system call: `pwritev` with a trailing per-call flags
11352    /// argument, which record/replay forwards unchanged.
11353    pub async fn handle_pwritev2<G: Guest<Self>>(
11354        &self,
11355        guest: &mut G,
11356        call: syscalls::Pwritev2,
11357    ) -> Result<i64, Error> {
11358        if self.timer_slack_binding(guest, call.fd())?.is_some() {
11359            let offset = vectored_offset(call.pos_l(), call.pos_h());
11360            if offset < -1 {
11361                return Err(Errno::EINVAL.into());
11362            }
11363            if offset >= 0 {
11364                return Err(Errno::ESPIPE.into());
11365            }
11366            self.require_timer_slack_access(guest, call.fd(), true)?;
11367            let count = usize::try_from(call.iov_len()).map_err(|_| Errno::EINVAL)?;
11368            let iovecs = read_iovecs(&guest.memory(), call.iov(), count)?;
11369            return self
11370                .writev_timer_slack(guest, call.fd(), iovecs, call.flags())
11371                .await;
11372        }
11373
11374        // At the current position (-1) and with no flags, `pwritev2` is
11375        // `writev` (`pwritev2` calls `do_writev`): on a descriptor whose
11376        // blocking write Detcore runs in turns it is run as one, so it waits
11377        // for room and writes in full as `writev` does
11378        // (https://github.com/rrnewton/hermit/issues/4822).
11379        if vectored_offset(call.pos_l(), call.pos_h()) == -1
11380            && call.flags() == 0
11381            && self.waits_in_turns(guest, call.fd())
11382        {
11383            let writev = syscalls::Writev::new()
11384                .with_fd(call.fd())
11385                .with_iov(call.iov())
11386                .with_len(call.iov_len() as usize);
11387            return self.handle_writev(guest, writev).await;
11388        }
11389
11390        let (resource, raw_ino) = guest.thread_state().with_detfd(call.fd(), |detfd| {
11391            (detfd.resource(), detfd.stat().map(|stat| stat.raw_inode()))
11392        })?;
11393        // The fd's cached `DetStat` carries the HOST inode (`DetStat` is built
11394        // straight from `fstat`/`statx`), so it must be determinized before it
11395        // can name a guest-visible resource. Passing it through directly used
11396        // to type-check only because `DetInode` was an alias for `RawInode`.
11397        let resource = match resource {
11398            Some(resource) => Some(resource),
11399            None => match raw_ino {
11400                Some(raw_ino) => Some(ResourceID::FileContents(
11401                    determinize_inode(guest, raw_ino).await.0,
11402                )),
11403                None => None,
11404            },
11405        };
11406
11407        if let Some(resource) = resource {
11408            let request = guest.thread_state().mk_request(resource, Permission::W);
11409            resource_request(guest, request).await;
11410        }
11411
11412        let result = self
11413            .record_or_replay_preserving_tool_errors(guest, call)
11414            .await;
11415
11416        if guest.config().virtualize_metadata && matches!(&result, Ok(written) if *written > 0) {
11417            let inode = raw_ino.expect("virtualized metadata requires stat data for tracked fds");
11418            touch_file(guest, inode).await;
11419        }
11420
11421        resource_release_all(guest).await;
11422        result
11423    }
11424
11425    /// SYS_mmap system call.
11426    pub async fn handle_mmap<G: Guest<Self>>(
11427        &self,
11428        guest: &mut G,
11429        call: syscalls::Mmap,
11430    ) -> Result<i64, Error> {
11431        enum SharedBacking {
11432            Anonymous,
11433            File {
11434                object: SharedMemoryObjectId,
11435                offset: u64,
11436            },
11437        }
11438
11439        let backing = if call.flags().contains(MapFlags::MAP_SHARED) {
11440            if call.fd() == -1 {
11441                Some(SharedBacking::Anonymous)
11442            } else {
11443                let offset = u64::try_from(call.offset()).map_err(|_| Errno::EINVAL)?;
11444                guest
11445                    .thread_state()
11446                    .with_detfd(call.fd(), |fd| {
11447                        let object = fd.stat().map_or_else(
11448                            || SharedMemoryObjectId::OpenFile {
11449                                id: fd.open_file_id(),
11450                            },
11451                            |stat| SharedMemoryObjectId::File {
11452                                device: stat.dev,
11453                                inode: stat.inode,
11454                            },
11455                        );
11456                        SharedBacking::File { object, offset }
11457                    })
11458                    .ok()
11459            }
11460        } else {
11461            None
11462        };
11463        let len = call.len();
11464        let result = self.record_or_replay(guest, call).await?;
11465        let start = usize::try_from(result).expect("a successful mmap must return an address");
11466
11467        guest.thread_state().unmap_memory(start, len);
11468        match backing {
11469            Some(SharedBacking::Anonymous) => {
11470                guest.thread_state().map_shared_anonymous(start, len);
11471            }
11472            Some(SharedBacking::File { object, offset }) => {
11473                guest
11474                    .thread_state()
11475                    .map_shared_object(start, len, object, offset);
11476            }
11477            None => {}
11478        }
11479        Ok(result)
11480    }
11481
11482    /// SYS_munmap system call.
11483    pub async fn handle_munmap<G: Guest<Self>>(
11484        &self,
11485        guest: &mut G,
11486        call: syscalls::Munmap,
11487    ) -> Result<i64, Error> {
11488        let start = call.addr().map(Addr::as_raw).unwrap_or(0);
11489        let len = call.len();
11490        let result = self.record_or_replay(guest, call).await?;
11491        guest.thread_state().unmap_memory(start, len);
11492        Ok(result)
11493    }
11494
11495    /// SYS_mremap system call.
11496    pub async fn handle_mremap<G: Guest<Self>>(
11497        &self,
11498        guest: &mut G,
11499        call: syscalls::Mremap,
11500    ) -> Result<i64, Error> {
11501        let old_start = call.addr().map(AddrMut::as_raw).unwrap_or(0);
11502        let old_len = call.old_len();
11503        let new_len = call.new_len();
11504        // MREMAP_DONTUNMAP (Linux 5.7): the old mapping stays in place.
11505        let keep_source = call.flags() & 4 != 0;
11506        let result = self.record_or_replay(guest, call).await?;
11507        let new_start =
11508            usize::try_from(result).expect("a successful mremap must return an address");
11509        guest
11510            .thread_state()
11511            .remap_memory(old_start, old_len, new_start, new_len, keep_source);
11512        Ok(result)
11513    }
11514
11515    /// What Detcore learns of the file `stat` describes, which the guest
11516    /// named as `target`, as a procfs entry: its path within procfs when its
11517    /// device is a procfs mount in the guest's mount table (see
11518    /// [`procfs_inode`]). A failure to tell or to name a procfs entry records
11519    /// a determinism loss, as the entry's number then depends on whether the
11520    /// host rebuilt it.
11521    ///
11522    /// Never under record and replay, which reproduces the recorded host
11523    /// numbers instead: Replayer's descriptor is a placeholder, and its guest
11524    /// has no `/proc`. Nor when the guest's thread IDs do not name host tasks
11525    /// (see [`procfs_inode::guest_tids_name_host_tasks`]), as there is then no
11526    /// `/proc/<tid>` to read.
11527    ///
11528    /// Nor without sequentialized threads (see [`Self::name_procfs_entry`]).
11529    ///
11530    /// Naming a path injects an `openat` and a `close` into the guest (see
11531    /// [`Self::with_object_link`]), and Detcore trusts the host kernel to run
11532    /// them as Linux defines them. A
11533    /// seccomp filter the guest inherited from whatever started it (which
11534    /// `Config::inherited_seccomp` records) may refuse them with an error,
11535    /// such as `EPERM`, and Detcore then names nothing and records a
11536    /// determinism loss. A filter, or a user-space supervisor
11537    /// (`SECCOMP_RET_USER_NOTIF`), that returns a result without running the
11538    /// call stands in for the kernel, which Detcore does not support.
11539    ///
11540    /// The `O_PATH` descriptor that names a path is open in the guest for one
11541    /// turn. A call the scheduler runs in the background meanwhile, such as
11542    /// an `accept` on a descriptor Detcore does not track, can allocate a
11543    /// descriptor in the same table at a moment the host chooses, and so take
11544    /// another number than without the stat, as it can beside any other
11545    /// descriptor allocation (https://github.com/rrnewton/hermit/issues/3988).
11546    ///
11547    /// The guest's `errno` is what it was: under SaBRe, Detcore shares libc,
11548    /// and so `errno`, with the guest thread it handles (see
11549    /// [`procfs_inode::preserving_errno`]).
11550    async fn procfs_entry<G: Guest<Self>>(
11551        guest: &mut G,
11552        stat: &DetStat,
11553        target: StatTarget<'_>,
11554    ) -> ProcfsEntry {
11555        procfs_inode::preserving_errno(Self::name_procfs_entry(guest, stat, target)).await
11556    }
11557
11558    /// Maps/Smaps ownership needs the actual opened object even where scheduler
11559    /// IDs are virtual. Use the existing same-view task directory capability;
11560    /// never pass a virtual TID to the host-only procfs naming path.
11561    async fn maps_descriptor_entry<G: Guest<Self>>(
11562        &self,
11563        guest: &G,
11564        stat: &DetStat,
11565        fd: RawFd,
11566    ) -> ProcfsEntry {
11567        procfs_inode::preserving_errno(async {
11568            if guest.config().recordreplay_modes || !guest.config().sequentialize_threads {
11569                return ProcfsEntry::Unknown;
11570            }
11571            let Some(task) = self.guest_task_directory(guest) else {
11572                return ProcfsEntry::Unknown;
11573            };
11574            match procfs_inode::descriptor_procfs_path(&task, fd, stat.dev, stat.inode) {
11575                Ok(Some(within)) => ProcfsEntry::At(within),
11576                Ok(None) => ProcfsEntry::NotProcfs,
11577                Err(_) => ProcfsEntry::Unknown,
11578            }
11579        })
11580        .await
11581    }
11582
11583    async fn maps_guest_target<G: Guest<Self>>(
11584        &self,
11585        guest: &mut G,
11586        actual: &Path,
11587    ) -> Option<crate::procfs::BoundTask> {
11588        let numbers = if Self::translates_procfs_numbers(guest) {
11589            let ids = crate::procfs::ids::named_ids(actual);
11590            Some(Self::guest_procfs_numbers(guest, &ids).await?)
11591        } else {
11592            None
11593        };
11594        let (pid, tid) = self.procfs_caller_ids(guest)?;
11595        Self::maps_target_with_numbers(actual, numbers.as_ref(), pid, tid)
11596    }
11597
11598    /// Preserve an authenticated spelling when its complete numeric mapping
11599    /// is the identity. Missing target components cannot authorize a binding.
11600    fn maps_target_with_numbers(
11601        actual: &Path,
11602        numbers: Option<&BTreeMap<i32, i32>>,
11603        pid: i32,
11604        tid: i32,
11605    ) -> Option<crate::procfs::BoundTask> {
11606        let translated;
11607        let actual = if let Some(numbers) = numbers {
11608            if crate::procfs::ids::named_ids(actual)
11609                .iter()
11610                .any(|id| !numbers.contains_key(id))
11611            {
11612                return None;
11613            }
11614            translated = crate::procfs::ids::translate(actual, |id| numbers.get(&id).copied());
11615            translated.as_deref().unwrap_or(actual)
11616        } else {
11617            actual
11618        };
11619        crate::procfs::maps_task_at(actual, pid, tid)
11620    }
11621
11622    /// [`Self::procfs_entry`], which restores the guest's `errno` afterwards.
11623    ///
11624    /// Without sequentialized threads, another guest thread can change what a
11625    /// descriptor, the working directory or a path leads to between any two
11626    /// of the steps that name an entry, and under SaBRe it shares the
11627    /// descriptor table Detcore reads the mount table through. So Detcore
11628    /// names nothing then, before any of those steps, and records a
11629    /// determinism loss for any file that may be on procfs (see
11630    /// [`procfs_inode::may_be_procfs`]), devpts files among them.
11631    async fn name_procfs_entry<G: Guest<Self>>(
11632        guest: &mut G,
11633        stat: &DetStat,
11634        target: StatTarget<'_>,
11635    ) -> ProcfsEntry {
11636        if guest.config().recordreplay_modes
11637            || !procfs_inode::guest_tids_name_host_tasks(
11638                &guest.config().backend,
11639                guest.thread_state().physical_tid,
11640            )
11641            || !procfs_inode::may_be_procfs(stat.dev, stat.blksize)
11642        {
11643            return ProcfsEntry::NotProcfs;
11644        }
11645        if !guest.config().sequentialize_threads {
11646            Self::record_procfs_loss(
11647                guest,
11648                "the guest's threads are not sequentialized, so Detcore does not name procfs files"
11649                    .into(),
11650            )
11651            .await;
11652            return ProcfsEntry::Unknown;
11653        }
11654        let tid = guest.tid().as_raw();
11655        let mounts = match procfs_inode::procfs_mounts(tid, stat.dev, false) {
11656            Ok(Some(mounts)) => mounts,
11657            // No procfs mount in the guest's mount table is on the device, but
11658            // a procfs file can be, opened outside the guest's mounts: one
11659            // inherited from Hermit's own `/proc`, or received through
11660            // SCM_RIGHTS. It has no path within the guest's procfs, so it goes
11661            // unnamed. Its filesystem type tells.
11662            Ok(None) => {
11663                let on_procfs = Self::with_object_link(guest, target, |link| {
11664                    procfs_inode::is_on_procfs(link, stat.dev, stat.inode)
11665                })
11666                .await;
11667                return match on_procfs {
11668                    Ok(false) => ProcfsEntry::NotProcfs,
11669                    Ok(true) => {
11670                        Self::record_procfs_loss(
11671                            guest,
11672                            "a procfs file outside the guest's procfs mounts".into(),
11673                        )
11674                        .await;
11675                        ProcfsEntry::Unnamed
11676                    }
11677                    Err(reason) => {
11678                        Self::record_procfs_loss(guest, reason).await;
11679                        ProcfsEntry::Unknown
11680                    }
11681                };
11682            }
11683            Err(error) => {
11684                Self::record_procfs_loss(
11685                    guest,
11686                    format!(
11687                        "cannot read the guest's mount table: {}",
11688                        crate::util::describe_io_error(&error)
11689                    ),
11690                )
11691                .await;
11692                return ProcfsEntry::Unknown;
11693            }
11694        };
11695        let (canonical, mount_id) = match Self::procfs_canonical_path(guest, stat, target).await {
11696            Ok(found) => found,
11697            Err(reason) => {
11698                Self::record_procfs_loss(guest, reason).await;
11699                return ProcfsEntry::Unnamed;
11700            }
11701        };
11702        let reread = || procfs_inode::procfs_mounts(tid, stat.dev, true);
11703        match procfs_inode::path_within(
11704            &mounts,
11705            mount_id,
11706            &canonical,
11707            reread,
11708            procfs_inode::mount_root,
11709        ) {
11710            Ok(within) => ProcfsEntry::At(within),
11711            // The guest reached the file through a mount that its mount
11712            // table, read again, does not list at a mount point where Detcore
11713            // finds the mount: one outside the guest's root, or one whose
11714            // mount point reads in another frame than the file's path.
11715            Err(reason) => {
11716                Self::record_procfs_loss(guest, reason).await;
11717                ProcfsEntry::Unnamed
11718            }
11719        }
11720    }
11721
11722    /// The canonical path of the procfs file `stat` describes, which the
11723    /// guest named as `target`, from a magic link to it (see
11724    /// [`Self::with_object_link`]), if that is still the file `stat`
11725    /// describes, and the ID of the mount it was reached through. An entry
11726    /// the host rebuilt between the guest's stat and an `O_PATH` open, a
11727    /// window of one injected call, is not, and goes unnamed.
11728    async fn procfs_canonical_path<G: Guest<Self>>(
11729        guest: &mut G,
11730        stat: &DetStat,
11731        target: StatTarget<'_>,
11732    ) -> Result<(PathBuf, u64), String> {
11733        Self::with_object_link(guest, target, |link| {
11734            procfs_inode::inspect_link(link, stat.dev, stat.inode)
11735        })
11736        .await
11737    }
11738
11739    /// Run `inspect` on a magic link to the file the guest named as
11740    /// `target`: that of the descriptor or working directory in the guest
11741    /// thread's `/proc/<tid>`. For a path, Detcore opens an `O_PATH`
11742    /// descriptor in the guest with the guest's own path, directory and
11743    /// flags, which resolves `self` and `thread-self` to the guest, and
11744    /// closes it again (see [`Self::open_object_descriptor`] and
11745    /// [`Self::close_object_descriptor`]). An error is the reason Detcore
11746    /// learned nothing, or that the guest's descriptors may have changed.
11747    ///
11748    /// It runs only with sequentialized threads (see
11749    /// [`Self::name_procfs_entry`]). Only then do the open, the look at the
11750    /// link and the close run in one turn, and does the guest's descriptor or
11751    /// working directory lead to one file throughout; otherwise another guest
11752    /// thread can close the descriptor Detcore opened and take its number for
11753    /// one of its own in between, which Detcore would then read, and close.
11754    async fn with_object_link<G: Guest<Self>, R>(
11755        guest: &mut G,
11756        target: StatTarget<'_>,
11757        inspect: impl FnOnce(&Path) -> Result<R, String>,
11758    ) -> Result<R, String> {
11759        let tid = guest.tid().as_raw();
11760        match target {
11761            StatTarget::Descriptor(fd) => inspect(Path::new(&format!("/proc/{tid}/fd/{fd}"))),
11762            StatTarget::Cwd => inspect(Path::new(&format!("/proc/{tid}/cwd"))),
11763            StatTarget::Overwritten => Err("the call's result overwrote the guest's path".into()),
11764            StatTarget::Path {
11765                dirfd,
11766                path,
11767                follow,
11768            } => {
11769                let mut flags = OFlag::O_PATH | OFlag::O_CLOEXEC;
11770                if !follow {
11771                    flags |= OFlag::O_NOFOLLOW;
11772                }
11773                let open = syscalls::Openat::new()
11774                    .with_dirfd(dirfd)
11775                    .with_path(path)
11776                    .with_flags(flags);
11777                let fd = Self::open_object_descriptor(guest, open).await?;
11778                let found = inspect(Path::new(&format!("/proc/{tid}/fd/{fd}")));
11779                Self::close_object_descriptor(guest, fd).await?;
11780                found
11781            }
11782        }
11783    }
11784
11785    /// Open the `O_PATH` descriptor `open` describes in the guest. An open
11786    /// that opened no descriptor and reported a restart or `EINTR` (see
11787    /// [`procfs_inode::read_open`]) is tried again, but only
11788    /// `OBJECT_DESCRIPTOR_ATTEMPTS` times, as a seccomp filter the guest
11789    /// inherited can return a restart every time without running the call.
11790    async fn open_object_descriptor<G: Guest<Self>>(
11791        guest: &mut G,
11792        open: syscalls::Openat,
11793    ) -> Result<RawFd, String> {
11794        let mut last = Errno::EINTR.to_string();
11795        for _ in 0..OBJECT_DESCRIPTOR_ATTEMPTS {
11796            // The guest's path, respelled for this injection where it leads
11797            // below a descriptor directory, as the guest's own call is
11798            // (`Self::inject_translated`).
11799            match procfs_inode::read_open(Self::inject_translated(guest, Syscall::from(open)).await)
11800            {
11801                procfs_inode::Injected::Ran(fd) => return Ok(fd),
11802                procfs_inode::Injected::NotRun(reported) => last = reported,
11803                procfs_inode::Injected::Failed(reason) => return Err(reason),
11804            }
11805        }
11806        Err(format!(
11807            "cannot open the guest's path again: {last} in each of {OBJECT_DESCRIPTOR_ATTEMPTS} attempts"
11808        ))
11809    }
11810
11811    /// Close the `O_PATH` descriptor `fd` that Detcore opened in the guest. A
11812    /// close that did not run (see [`procfs_inode::read_close`]) is tried
11813    /// again, but only `OBJECT_DESCRIPTOR_ATTEMPTS` times. One that failed
11814    /// leaves the descriptor open, where it changes the numbers the guest's
11815    /// later descriptors take, so it is an error.
11816    async fn close_object_descriptor<G: Guest<Self>>(
11817        guest: &mut G,
11818        fd: RawFd,
11819    ) -> Result<(), String> {
11820        let close = syscalls::Close::new().with_fd(fd);
11821        for _ in 0..OBJECT_DESCRIPTOR_ATTEMPTS {
11822            match procfs_inode::read_close(guest.inject(close).await) {
11823                procfs_inode::Injected::Ran(()) => return Ok(()),
11824                procfs_inode::Injected::NotRun(_) => {}
11825                procfs_inode::Injected::Failed(reason) => return Err(reason),
11826            }
11827        }
11828        Err(format!(
11829            "close of the guest's O_PATH descriptor did not run in {OBJECT_DESCRIPTOR_ATTEMPTS} attempts"
11830        ))
11831    }
11832
11833    /// Records the determinism loss of a procfs entry Detcore could not name,
11834    /// once per process, through the global tool, whose process is the one
11835    /// whose losses verification reads under every backend.
11836    async fn record_procfs_loss<G: Guest<Self>>(guest: &mut G, reason: String) {
11837        static RECORDED: AtomicBool = AtomicBool::new(false);
11838        if !RECORDED.swap(true, Ordering::Relaxed) {
11839            report_determinism_loss(guest, format!("procfs inode numbers: {reason}")).await;
11840        }
11841    }
11842
11843    // Determinize stat by doing:
11844    //   - using virtual inode instead of real inodes. The virtual inodes
11845    //     increase monolitically and won't be re-used (like ext4)
11846    //   - use logical modtime which could be used by program like GNU make
11847    //     to determine file changes. A file first seen with a canonical host
11848    //     mtime (`CANONICAL_FILE_MTIME_SECONDS`, e.g. the Nix store's 1) keeps
11849    //     it; any other first-seen file reports the epoch.
11850    //   - atime, ctime and btime always report the epoch: the kernel sets ctime
11851    //     and btime itself, so even for a Nix store file they are real
11852    //     timestamps, and Hermit keeps no per-file atime.
11853    async fn determinize_stat<G, S>(
11854        &self,
11855        guest: &mut G,
11856        stat: S,
11857        inode_override: Option<DetInode>,
11858        procfs: ProcfsEntry,
11859    ) -> Result<DetStat, Error>
11860    where
11861        G: Guest<Self>,
11862        S: Into<DetStat>,
11863    {
11864        // The two fields this reads, copied: cloning the whole Config here
11865        // copied its mount-table lists, one allocation per container mount, on
11866        // every stat-family call.
11867        let recordreplay_modes = guest.config().recordreplay_modes;
11868        let epoch = guest.config().epoch;
11869
11870        let mut stat: DetStat = stat.into();
11871        // Detcore's placeholder, however the guest reached it: the rendered
11872        // file it stands in for.
11873        if inode_override.is_none()
11874            && !recordreplay_modes
11875            && let Some(path) = rendered_procfs::placeholder_identity(stat.dev, stat.inode)
11876        {
11877            return self.rendered_metadata(guest, path).await;
11878        }
11879        let (d_ino, global_mtime) = match inode_override {
11880            // The container's stdio streams have fixed inodes and always
11881            // report the epoch: whatever backs them on the host (a pipe, a
11882            // terminal, a redirected file) is not part of the guest's view.
11883            Some(inode) => {
11884                let nanos = epoch
11885                    .timestamp_nanos_opt()
11886                    .expect("epoch cannot be represented in nanoseconds")
11887                    as u64;
11888                (inode, LogicalTime::from_nanos(nanos))
11889            }
11890            None => {
11891                // statx fills stx_mtime only when it reports STATX_MTIME.
11892                let observed = if stat.mask.contains(StatxMask::STATX_MTIME) {
11893                    ObservedMtime::from_host_mtime(stat.mtime.tv_sec, stat.mtime.tv_nsec)
11894                } else {
11895                    ObservedMtime::Unobserved
11896                };
11897                // A file with a link cannot be one a retired mapping describes:
11898                // Linux refuses to link an unlinked file again (only an
11899                // `O_TMPFILE` file, which is never retired, may get a name).
11900                // So a nonzero link count is a name sighting however the file
11901                // was reached, by path, descriptor or working directory, and a
11902                // zero count (`fstat` after `unlink`, `/proc/self/fd/N`) is not.
11903                let sighting = if stat.mask.contains(StatxMask::STATX_NLINK) && stat.nlink > 0 {
11904                    InodeSighting::Name
11905                } else {
11906                    InodeSighting::Descriptor
11907                };
11908                // A procfs root counts the host's processes as its links.
11909                // Report 1, which file systems that do not count a
11910                // directory's links report, so that a walker such as `find`
11911                // infers nothing from it.
11912                if procfs.is_procfs()
11913                    && stat.inode == procfs_inode::PROC_ROOT_INO
11914                    && stat.mode & libc::S_IFMT == libc::S_IFDIR
11915                    && stat.mask.contains(StatxMask::STATX_NLINK)
11916                {
11917                    stat.nlink = 1;
11918                }
11919                match rendered_entry(
11920                    &procfs,
11921                    &stat,
11922                    recordreplay_modes,
11923                    self.rendered_files(guest),
11924                ) {
11925                    // A file Detcore renders, reached through a spelling the
11926                    // kernel resolved (a symbolic link): the number its path
11927                    // names, and the epoch.
11928                    Some(path) => {
11929                        let nanos = epoch
11930                            .timestamp_nanos_opt()
11931                            .expect("epoch cannot be represented in nanoseconds")
11932                            as u64;
11933                        (
11934                            DetInode::mint(procfs_inode::rendered_inode(path)),
11935                            LogicalTime::from_nanos(nanos),
11936                        )
11937                    }
11938                    None => {
11939                        determinize_inode_observing_mtime(
11940                            guest,
11941                            stat.raw_inode(),
11942                            procfs.name(stat.dev),
11943                            observed,
11944                            sighting,
11945                        )
11946                        .await
11947                    }
11948                }
11949            }
11950        };
11951        let host_inode = stat.inode;
11952        stat.inode = d_ino.as_raw(); // Reveal only the deterministic inode.
11953
11954        // AUTONOMOUS-BOT-IMPLEMENTED
11955        // TODO-HUMAN-REVIEW(PR-1056): Deterministic st_dev remapping.
11956        // The raw st_dev leaks the kernel's host-wide anonymous block-device
11957        // number for procfs/sysfs/tmpfs mounts, which drifts between runs (and
11958        // between the two runs of `--verify`). Reveal only a deterministic
11959        // device id.
11960        let host_device = stat.dev;
11961        stat.dev = determinize_device(guest, stat.dev).await;
11962        // A procfs root that Detcore could not name (`ProcfsEntry::Unknown`, as
11963        // on a backend that does not read the guest's mount table) reaches here
11964        // with the host's process count as its link count, which changes
11965        // whenever a host process starts or exits. Its device is Detcore's
11966        // procfs device by now, so it reports 1 like a named one (above).
11967        // Measured on DBT: two runs of one guest saw 6724 and 6770.
11968        if stat.dev == procfs_inode::VIRTUAL_PROC_DEVICE
11969            && host_inode == procfs_inode::PROC_ROOT_INO
11970            && stat.mode & libc::S_IFMT == libc::S_IFDIR
11971            && stat.mask.contains(StatxMask::STATX_NLINK)
11972        {
11973            stat.nlink = 1;
11974        }
11975
11976        // A directory's size is whatever its file system makes of its
11977        // entries: btrfs sums the names' lengths, tmpfs counts entries, ext4
11978        // rounds up to its blocks. So it changes whenever any process adds or
11979        // removes an entry, and differs between file systems. Builds stat
11980        // every ancestor of their working directory (kbuild, configure,
11981        // cargo, CMake's look at $HOME), so the host's value broke
11982        // `--verify --verify-strict` of ordinary builds on a shared machine
11983        // and reproducibility across hosts
11984        // (https://github.com/rrnewton/hermit/issues/4162). Report one
11985        // 4096-byte block, as ext4 does for a small directory, whatever the
11986        // directory holds; btrfs reports an empty directory as 0, so even
11987        // an empty one is set. A procfs directory keeps the host's value
11988        // (0, or a descriptor count for /proc/<pid>/fd), which Detcore
11989        // already virtualizes there. Only a proved procfs directory keeps
11990        // it: one whose entry Detcore names (`ProcfsEntry`, on the backends
11991        // whose guest threads are host tasks). (KVM's `/proc` directories are
11992        // Detcore's placeholders, whose stat Detcore answers.) Its stat fields alone
11993        // prove nothing: an anonymous device with 1024-byte blocks is also
11994        // devpts, autofs or a FUSE mount reporting those blocks (Codex
11995        // re-check of https://github.com/rrnewton/hermit/pull/4369). Under
11996        // DBT, where Detcore names no procfs entries, such a directory keeps
11997        // the host's value unproved
11998        // (`procfs_inode::unprovable_procfs_signature`). sysfs and cgroup
11999        // directories, which Linux also reports as 0, report 4096.
12000        if stat.mode & libc::S_IFMT == libc::S_IFDIR
12001            && stat.mask.contains(StatxMask::STATX_SIZE)
12002            && !procfs.is_procfs()
12003            && !procfs_inode::unprovable_procfs_signature(
12004                &guest.config().backend,
12005                guest.thread_state().physical_tid,
12006                host_device,
12007                stat.blksize,
12008            )
12009        {
12010            stat.size = 4096;
12011            if stat.mask.contains(StatxMask::STATX_BLOCKS) {
12012                stat.blocks = 8;
12013            }
12014        }
12015
12016        // A directory's link count is the same kind of host value: on file
12017        // systems that count a directory's links (ext4, and tmpfs too) it
12018        // is two plus the number of subdirectories, so it changes whenever
12019        // any process adds or removes a subdirectory; only file systems
12020        // that do not count them (btrfs) report 1 for every directory. The
12021        // ancestors a build stats include shared directories such as
12022        // `/home`, whose subdirectory count churns under parallel work on
12023        // a shared machine, so the host's count broke the same `--verify`
12024        // comparisons as the size above, through the hashed stat buffer
12025        // the io-buffer log records. Report 1, as the procfs root above
12026        // already does. GNU find's leaf optimization expects `nlink - 2`
12027        // subdirectories and stats entries until it has seen that many;
12028        // a count below 2 disables the optimization, so 1 makes a walker
12029        // infer nothing and stat every entry -- the safe direction.
12030        //
12031        // Unlike the size pin above, this pin does not inherit the
12032        // unprovable-procfs exclusion: a procfs directory's size 0 matters
12033        // to SEEK_END, but its link count has no such consumer, and the
12034        // procfs root already reports 1 on every backend, so 1 cannot be
12035        // wrong for a directory that might be procfs. An anonymous-device
12036        // directory with 1024-byte blocks (autofs `/home`, devpts, a FUSE
12037        // mount) is therefore pinned on DBT too, where the size pin still
12038        // leaves it alone. Only a proved procfs directory keeps its host
12039        // count: one Detcore knows is procfs (`ProcfsEntry::At` or
12040        // `ProcfsEntry::Unnamed`; see `ProcfsEntry::is_procfs`). For
12041        // example `/proc/<pid>/fd` reports nlink 2 and carries its
12042        // descriptor count in its size, and both are preserved here.
12043        // Synthetic procfs directories Detcore serves itself (the
12044        // `descriptor_directory` placeholders answered by
12045        // `rendered_procfs_stat` before `determinize_stat` runs) bypass
12046        // this pin entirely and keep their synthesized metadata.
12047        //
12048        // A deleted directory keeps 0: `mkdir`, `open(O_DIRECTORY)`,
12049        // `rmdir`, then `fstat` reports `st_nlink == 0` on Linux (ext4's
12050        // rmdir clears the count), and that zero is guest-visible state,
12051        // not host churn. Only a positive count is pinned.
12052        if stat.mode & libc::S_IFMT == libc::S_IFDIR
12053            && stat.mask.contains(StatxMask::STATX_NLINK)
12054            && stat.nlink >= 1
12055            && !procfs.is_procfs()
12056        {
12057            stat.nlink = 1;
12058        }
12059
12060        let epoch_tp = Timespec {
12061            tv_sec: epoch.timestamp(),
12062            tv_nsec: epoch.timestamp_subsec_nanos() as i64,
12063        };
12064
12065        let mtime: Timespec = global_mtime.into();
12066        stat.atime = epoch_tp;
12067        stat.ctime = epoch_tp;
12068        stat.btime = epoch_tp;
12069
12070        stat.mtime = mtime;
12071
12072        Ok(stat)
12073    }
12074
12075    /// `stat`, of the file `target` names, with Linux's size where that is a
12076    /// process's `maps` that Detcore may render from the backend's memory map
12077    /// (`Self::maps_from_memory_map`), named by its descriptor: 0, and no
12078    /// blocks, as for every file procfs prints at read time, whatever the
12079    /// backend's own file holds. On KVM that is LinuxEmu's in-memory file,
12080    /// whose size is its invented line's length, while a read gives Detcore's
12081    /// rendering; elsewhere the kernel's file already reports 0.
12082    fn with_procfs_maps_size<G: Guest<Self>>(
12083        guest: &G,
12084        target: StatTarget<'_>,
12085        mut stat: DetStat,
12086    ) -> DetStat {
12087        let rendered = target.descriptor().is_some_and(|fd| {
12088            guest
12089                .thread_state()
12090                .with_detfd(fd, |detfd| detfd.procfs_memory_map().is_some())
12091                .unwrap_or(false)
12092        });
12093        if rendered {
12094            stat.size = 0;
12095            stat.blocks = 0;
12096        }
12097        stat
12098    }
12099
12100    /// Handles all stat syscalls.
12101    pub async fn handle_stat_family<G: Guest<Self>>(
12102        &self,
12103        guest: &mut G,
12104        call: StatFamily,
12105    ) -> Result<i64, Error> {
12106        if guest.config().virtualize_metadata {
12107            // NB: let kernel handle error codes, it's not easy to do so without
12108            // kernel because there're many corner cases. i.e.: even access
12109            // filepath from tracer may cause tracer to hang under certain fuse
12110            // filesystem (squashfs_ll).
12111            // The target is found before the call, which can overwrite its path.
12112            // A procfs file Detcore renders is answered from the path as named,
12113            // as Linux reads the path before it writes the result, once the
12114            // call's own arguments passed.
12115            let target = stat_family_target(&guest.memory(), &call);
12116            if !stat_family_arguments_valid(&guest.memory(), &call) {
12117                if !matches!(self.rendered_procfs_stat(guest, target).await, Ok(None)) {
12118                    return Err(Errno::EINVAL.into());
12119                }
12120            } else if let Some(stat) = self.rendered_procfs_stat(guest, target).await? {
12121                let statptr = call.stat().ok_or(Errno::EFAULT)?;
12122                guest.memory().write_value(statptr.0, &stat.into())?;
12123                return Ok(0);
12124            }
12125            let target = match call.stat() {
12126                Some(result) => target.unless_overwritten(
12127                    &guest.memory(),
12128                    result.0.as_raw(),
12129                    std::mem::size_of::<libc::stat>(),
12130                ),
12131                None => target,
12132            };
12133            let before = Self::stat_buffer_before(
12134                guest,
12135                call.stat().map(|result| result.0.as_raw()),
12136                std::mem::size_of::<libc::stat>(),
12137            );
12138            Self::inject_translated(guest, Syscall::from(call)).await?;
12139            let statptr = call.stat().ok_or(Errno::EFAULT)?;
12140            let inode_override = target
12141                .descriptor()
12142                .and_then(|fd| stdio_inode_override(guest, fd));
12143            let stat: libc::stat = guest.memory().read_value(statptr.0)?;
12144            // The placeholder directory, reached through a spelling the
12145            // `--proc` level table could not resolve: hidden, as `/proc/fs`
12146            // is (`descriptor_directory`). Until its identity is learnt, the
12147            // question is asked in a boxed future, after which the result is
12148            // read again, so that no stat is kept across it.
12149            let (mode, dev, ino) = (stat.st_mode, stat.st_dev, stat.st_ino);
12150            let stat = match Self::hidden_placeholder_known(guest, mode, dev, ino) {
12151                Some(true) => {
12152                    return Self::hidden_placeholder_stat(guest, statptr.0.as_raw(), before);
12153                }
12154                Some(false) => stat,
12155                None => {
12156                    if Box::pin(Self::hidden_placeholder_reached(guest, mode, dev, ino)).await {
12157                        return Self::hidden_placeholder_stat(guest, statptr.0.as_raw(), before);
12158                    }
12159                    guest.memory().read_value(statptr.0)?
12160                }
12161            };
12162            let mut memory = guest.memory();
12163            // A placeholder another process made, or an earlier image of this
12164            // one, becomes known by its identity (`determinize_stat`).
12165            if inode_override.is_none() {
12166                let _ = self
12167                    .recognize_placeholder(guest, &DetStat::from(&stat), target)
12168                    .await;
12169            }
12170            let procfs = match inode_override {
12171                Some(_) => ProcfsEntry::NotProcfs,
12172                None => Self::procfs_entry(guest, &DetStat::from(&stat), target).await,
12173            };
12174            let stat = self
12175                .determinize_stat(guest, stat, inode_override, procfs)
12176                .await?;
12177            let stat = Self::with_procfs_maps_size(guest, target, stat);
12178            memory.write_value(statptr.0, &stat.into())?;
12179            Ok(0)
12180        } else {
12181            // A procfs file Detcore renders has no kernel object to ask.
12182            let target = stat_family_target(&guest.memory(), &call);
12183            if !stat_family_arguments_valid(&guest.memory(), &call) {
12184                if !matches!(self.rendered_procfs_stat(guest, target).await, Ok(None)) {
12185                    return Err(Errno::EINVAL.into());
12186                }
12187            } else if let Some(stat) = self.rendered_procfs_stat(guest, target).await? {
12188                let statptr = call.stat().ok_or(Errno::EFAULT)?;
12189                guest.memory().write_value(statptr.0, &stat.into())?;
12190                return Ok(0);
12191            }
12192            let before = Self::stat_buffer_before(
12193                guest,
12194                call.stat().map(|result| result.0.as_raw()),
12195                std::mem::size_of::<libc::stat>(),
12196            );
12197            let ret = self.record_or_replay(guest, call).await?;
12198            // The placeholder directory is hidden here too (record and replay
12199            // have none, so a recording is unchanged).
12200            if ret == 0
12201                && before.is_some()
12202                && let Some(statptr) = call.stat()
12203            {
12204                let stat: libc::stat = guest.memory().read_value(statptr.0)?;
12205                let (mode, dev, ino) = (stat.st_mode, stat.st_dev, stat.st_ino);
12206                if Self::reaches_hidden_placeholder(guest, mode, dev, ino).await {
12207                    return Self::hidden_placeholder_stat(guest, statptr.0.as_raw(), before);
12208                }
12209            }
12210            Ok(ret)
12211        }
12212    }
12213
12214    /// statx system call
12215    pub async fn handle_statx<G: Guest<Self>>(
12216        &self,
12217        guest: &mut G,
12218        call: syscalls::Statx,
12219    ) -> Result<i64, Error> {
12220        // Whether `statx` accepts its arguments, which it checks before it
12221        // looks at any file.
12222        let renderable = statx_arguments_valid(
12223            call.flags().bits(),
12224            call.mask().bits(),
12225            names_no_path(&guest.memory(), call.path(), call.flags()),
12226        );
12227        if guest.config().virtualize_metadata {
12228            // NB: let kernel handle error codes, it's not easy to do so without kernel
12229            // because there're many corner cases. i.e.: even access filepath from tracer
12230            // may cause tracer to hang under certain fuse filesystem (squashfs_ll).
12231            // The target is found before the call, which can overwrite its path.
12232            // A procfs file Detcore renders is answered from the path as named,
12233            // as Linux reads the path before it writes the result, once the
12234            // call's own arguments passed (which `statx` checks before it
12235            // looks up any file).
12236            let target = StatTarget::at(&guest.memory(), call.dirfd(), call.path(), call.flags());
12237            if !renderable {
12238                if !matches!(self.rendered_procfs_stat(guest, target).await, Ok(None)) {
12239                    return Err(Errno::EINVAL.into());
12240                }
12241            } else if let Some(stat) = self.rendered_procfs_stat(guest, target).await? {
12242                return self.write_rendered_statx(guest, call, stat).await;
12243            }
12244            let target = match call.statx() {
12245                Some(result) => target.unless_overwritten(
12246                    &guest.memory(),
12247                    result.0.as_raw(),
12248                    std::mem::size_of::<reverie::syscalls::linux_abi::Statx>(),
12249                ),
12250                None => target,
12251            };
12252            // A call that asks for the unique mount ID asks the kernel for the
12253            // classic one: the run's mount-ID pool numbers classic IDs, the
12254            // ones mountinfo and fdinfo show, and the guest's unique ID is
12255            // derived from that number (`fill_statx_mount_id`). Linux fills
12256            // both mount-ID fields in `vfs_statx_path` (v6.12
12257            // fs/stat.c:248-259) after the filesystem's getattr, which sees
12258            // `STATX_MNT_ID` in place of `STATX_MNT_ID_UNIQUE`.
12259            let unique = statx_requested_mask(call) & STATX_MNT_ID_UNIQUE != 0;
12260            let before = Self::stat_buffer_before(
12261                guest,
12262                call.statx().map(|result| result.0.as_raw()),
12263                std::mem::size_of::<reverie::syscalls::linux_abi::Statx>(),
12264            );
12265            Self::inject_translated(guest, Syscall::from(statx_asking_classic_mount_id(call)))
12266                .await?;
12267            let statptr = call.statx().ok_or(Errno::EFAULT)?;
12268            let inode_override = target
12269                .descriptor()
12270                .and_then(|fd| stdio_inode_override(guest, fd));
12271            let stat: reverie::syscalls::linux_abi::Statx = guest.memory().read_value(statptr.0)?;
12272            // The placeholder directory, reached through a spelling the
12273            // `--proc` level table could not resolve: hidden, as `/proc/fs`
12274            // is (`descriptor_directory`), asked as for the other stat calls
12275            // (`handle_stat_family`).
12276            let (mode, dev, ino) = {
12277                let identity = DetStat::from(stat);
12278                (identity.mode, identity.dev, identity.inode)
12279            };
12280            let stat = match Self::hidden_placeholder_known(guest, mode, dev, ino) {
12281                Some(true) => {
12282                    return Self::hidden_placeholder_stat(guest, statptr.0.as_raw(), before);
12283                }
12284                Some(false) => stat,
12285                None => {
12286                    if Box::pin(Self::hidden_placeholder_reached(guest, mode, dev, ino)).await {
12287                        return Self::hidden_placeholder_stat(guest, statptr.0.as_raw(), before);
12288                    }
12289                    guest.memory().read_value(statptr.0)?
12290                }
12291            };
12292            let mut memory = guest.memory();
12293            // A placeholder another process made, or an earlier image of this
12294            // one, becomes known by its identity (`determinize_stat`).
12295            let placeholder = match inode_override {
12296                None => {
12297                    self.recognize_placeholder(guest, &DetStat::from(stat), target)
12298                        .await
12299                }
12300                Some(_) => None,
12301            };
12302            let procfs = match inode_override {
12303                Some(_) => ProcfsEntry::NotProcfs,
12304                None => Self::procfs_entry(guest, &DetStat::from(stat), target).await,
12305            };
12306            // A rendered file reached through any spelling (a symbolic link to
12307            // its descriptor link) is on the guest's procfs mount, not on the
12308            // placeholder's memfd's.
12309            let raw_mount_id = match placeholder {
12310                Some(_) => rendered_procfs::guest_procfs_mount_id().ok(),
12311                None => (stat.stx_mask & reverie::syscalls::linux_abi::STATX_MNT_ID != 0)
12312                    .then_some(stat.stx_mnt_id),
12313            };
12314            let stat = self
12315                .determinize_stat(guest, stat, inode_override, procfs)
12316                .await?;
12317            let stat = Self::with_procfs_maps_size(guest, target, stat);
12318            let mut statx: reverie::syscalls::linux_abi::Statx = stat.into();
12319            self.fill_statx_mount_id(guest, &mut statx, raw_mount_id, unique)
12320                .await;
12321            memory.write_value(statptr.0, &statx)?;
12322            Ok(0)
12323        } else {
12324            // A procfs file Detcore renders has no kernel object to ask.
12325            let target = StatTarget::at(&guest.memory(), call.dirfd(), call.path(), call.flags());
12326            // Arguments `statx` refuses before it looks up any file, here a
12327            // file Detcore renders.
12328            if !renderable {
12329                if !matches!(self.rendered_procfs_stat(guest, target).await, Ok(None)) {
12330                    return Err(Errno::EINVAL.into());
12331                }
12332            } else if let Some(stat) = self.rendered_procfs_stat(guest, target).await? {
12333                return self.write_rendered_statx(guest, call, stat).await;
12334            }
12335            let before = Self::stat_buffer_before(
12336                guest,
12337                call.statx().map(|result| result.0.as_raw()),
12338                std::mem::size_of::<reverie::syscalls::linux_abi::Statx>(),
12339            );
12340            let ret = self.record_or_replay(guest, call).await?;
12341            // The same in recording, in replay and without either: a recording
12342            // keeps only the fields Detcore models, so what else the kernel
12343            // wrote must not reach the guest when it is recorded either.
12344            if ret == 0
12345                && let Some(statptr) = call.statx()
12346            {
12347                // The placeholder directory is hidden here too
12348                // (`handle_stat_family`), before the tail is canonicalized.
12349                if before.is_some() {
12350                    let stat: reverie::syscalls::linux_abi::Statx =
12351                        guest.memory().read_value(statptr.0)?;
12352                    let identity = DetStat::from(stat);
12353                    let (mode, dev, ino) = (identity.mode, identity.dev, identity.inode);
12354                    if Self::reaches_hidden_placeholder(guest, mode, dev, ino).await {
12355                        return Self::hidden_placeholder_stat(guest, statptr.0.as_raw(), before);
12356                    }
12357                }
12358                canonicalize_statx_tail(&mut guest.memory(), statptr.0.as_raw())?;
12359            }
12360            Ok(ret)
12361        }
12362    }
12363
12364    /// Whether a guest's F_SETFL or FIONBIO on this descriptor changes only its
12365    /// logical (guest-visible) O_NONBLOCK, leaving it physically nonblocking for
12366    /// the scheduler's nonblockize-and-retry.
12367    ///
12368    /// Outside record/replay that holds for every scheduler-managed type. In
12369    /// record/replay it holds only for a container-internal channel that is
12370    /// already physically nonblocking: a pipe, which is how `handle_pipe2`
12371    /// leaves every guest-created pipe in both phases, or an internal socket (a
12372    /// socketpair endpoint, which is how `handle_socketpair` leaves both ends, or
12373    /// a socket whose peer `classify_socket_peer` found to be a guest, which it
12374    /// makes physically nonblocking). Forwarding the guest's
12375    /// clear would make the recording's next read on that channel a blocking
12376    /// read on an fd that `syscall_targets_internal_fd` calls internal, which
12377    /// deadlocks the sequentialized scheduler while the writer waits for its
12378    /// turn. Both phases take the same branch, because `fd_type`,
12379    /// `internal_socket` and `physically_nonblocking` are Detcore
12380    /// bookkeeping that record and replay evolve identically: an F_SETFL is
12381    /// still recorded and replayed, with the kernel given `flags | O_NONBLOCK`
12382    /// in record and the recorded result returned in replay; a FIONBIO returns
12383    /// before `record_or_replay`, so neither phase records or applies it. Other
12384    /// sockets, eventfds and inotify instances stay external in record/replay
12385    /// (`handle_eventfd2` and `handle_inotify_init1` make those two physically
12386    /// nonblocking only outside it). A pipe whose
12387    /// physical O_NONBLOCK came from the guest rather than from Detcore is also
12388    /// kept nonblocking; in record/replay the only such pipes are `pipe2` pipes,
12389    /// which Detcore has already made nonblocking.
12390    fn keeps_physically_nonblocking(
12391        &self,
12392        fd_type: FdType,
12393        internal_socket: bool,
12394        physically_nonblocking: bool,
12395        inherited_external_read: bool,
12396    ) -> bool {
12397        self.cfg.use_nonblocking_sockets()
12398            && !inherited_external_read
12399            && match fd_type {
12400                FdType::Pipe => !self.cfg.recordreplay_modes || physically_nonblocking,
12401                FdType::Socket if internal_socket => {
12402                    !self.cfg.recordreplay_modes || physically_nonblocking
12403                }
12404                FdType::Socket | FdType::Eventfd | FdType::Inotify => !self.cfg.recordreplay_modes,
12405                _ => false,
12406            }
12407    }
12408
12409    /// fcntl system call
12410    pub async fn handle_fcntl<G: Guest<Self>>(
12411        &self,
12412        guest: &mut G,
12413        call: syscalls::Fcntl,
12414    ) -> Result<i64, Error> {
12415        let fd = call.fd();
12416        // A reserved in-guest signal as the descriptor's I/O signal; 0, the
12417        // default SIGIO, is never reserved. Like the owner screen below, it
12418        // comes first, so a refused call has no other effect. Linux's fdget
12419        // comes before it: a descriptor the guest does not hold, or an O_PATH
12420        // one (which `check_fcntl_cmd` admits only for F_DUPFD*, F_GETFD,
12421        // F_SETFD and F_GETFL), is EBADF.
12422        if let F_SETSIG(signal) = call.cmd()
12423            && guest.config().reserves_in_guest_signal(signal.into())
12424        {
12425            let o_path = guest
12426                .thread_state()
12427                .with_detfd(fd, |detfd| detfd.status_flags() & libc::O_PATH != 0)?;
12428            if o_path {
12429                return Err(Errno::EBADF.into());
12430            }
12431            Self::refuse_reserved_signal_arming(
12432                guest,
12433                crate::refused_signal_controls::ReservedArming::FcntlSetsig,
12434                signal.into(),
12435            )?;
12436        }
12437        // On an in-guest run's default route, the one operation that would
12438        // complete a host-timed signal producer (D28), before the call reaches
12439        // the kernel. It comes before the owner screen below, so that its own
12440        // reads of the descriptor's state finish before the screen takes its
12441        // copy of an `F_SETOWN_EX` owner on the scratch stack: one scratch
12442        // frame at a time. It reads that owner from guest memory itself; the
12443        // screen's later copy is the same value, since only a thread of the
12444        // caller's process could change it in between, and the in-guest
12445        // backends, the only ones that refuse producers, run no second thread
12446        // (they refuse thread creation).
12447        if let Some((arming, owner)) = self.fcntl_producer_arming(guest, call) {
12448            self.refuse_host_timed_producer(guest, fd, arming, owner)
12449                .await?;
12450        }
12451        // Before anything else, so a refused call has no other effect.
12452        let (call, _owner_copy) = self.screen_fcntl_owner(guest, call).await?;
12453        if matches!(call.cmd(), F_SETSIG(signal) if signal == libc::SIGKILL) {
12454            // Owner/O_ASYNC may be configured later or through an alias.
12455            // Failed configuration cannot restore the lost lifetime proof.
12456            crate::reserved_input::expose_global(guest, "async-descriptor-sigkill")?;
12457        }
12458        // The owner of an asynchronous-I/O descriptor may be any process or
12459        // process group, and Linux signals it when data arrives, at a moment
12460        // set by host timing. Record the signals in this turn, before the call
12461        // reaches the kernel, for every process of the container, and never
12462        // forget them (`Scheduler::record_host_timed_signals`).
12463        let signals = fcntl_host_timed_signals(call.cmd());
12464        if signals & kernel_sigset_bit(libc::SIGALRM) != 0 {
12465            refuse_sigalrm(guest, SigalrmControl::ArmProducer).await?;
12466        }
12467        // A lease makes another process's open of the file wait for the
12468        // lease break, interruptibly, with no scheduler admission.
12469        if matches!(call.cmd(), F_SETLEASE(lease) if lease != libc::F_UNLCK) {
12470            refuse_sigalrm(guest, SigalrmControl::ArmProducer).await?;
12471        }
12472        if signals != 0 && guest.config().backend.host_timed_signals_are_physical {
12473            record_host_timed_signals(guest, HostTimedSignalScope::Container, signals).await;
12474        }
12475        let o_cloexec = match call.cmd() {
12476            F_DUPFD_CLOEXEC(_) => OFlag::O_CLOEXEC,
12477            _ => OFlag::empty(),
12478        };
12479        match call.cmd() {
12480            F_GETFL => {
12481                let physical_flags = self.record_or_replay(guest, call).await?;
12482                let (logical_nonblocking, rendered_nofollow, mode_unknown) =
12483                    guest.thread_state().with_detfd(fd, |detfd| {
12484                        (
12485                            detfd.is_nonblocking(),
12486                            detfd.procfs_rendered_path().is_some()
12487                                && detfd.status_flags() & libc::O_NOFOLLOW != 0,
12488                            detfd.blocking_mode_unknown(),
12489                        )
12490                    })?;
12491                // An inotify instance received without the open file
12492                // description its sender modelled may carry Detcore's own
12493                // O_NONBLOCK (`DetFd::blocking_mode_unknown`): refused by
12494                // name under the fail-closed policy rather than answered
12495                // with a mode that may be wrong.
12496                if mode_unknown && self.cfg.panic_on_unsupported_syscalls {
12497                    let reason = format!(
12498                        "Detcore cannot tell the blocking mode of inotify descriptor {fd}: it \
12499                         was received over SCM_RIGHTS without the open file description its \
12500                         sender modelled, so its O_NONBLOCK may be Detcore's own rather than \
12501                         the guest's"
12502                    );
12503                    tracing::error!("Detcore refuses fcntl(F_GETFL): {reason}");
12504                    return self
12505                        .refuse_unserviceable_operation_because(
12506                            guest,
12507                            Sysno::fcntl,
12508                            Errno::EINVAL,
12509                            &reason,
12510                        )
12511                        .await;
12512                }
12513                Ok(guest_status_flags(
12514                    physical_flags,
12515                    logical_nonblocking,
12516                    rendered_nofollow,
12517                ))
12518            }
12519            F_SETFL(flags) => {
12520                let (fd_type, internal_socket, physically_nonblocking, inherited_external_read) =
12521                    guest.thread_state().with_detfd(fd, |detfd| {
12522                        (
12523                            detfd.ty(),
12524                            detfd.is_internal_socket(),
12525                            detfd.physically_nonblocking(),
12526                            detfd.inherited_external_read().is_some(),
12527                        )
12528                    })?;
12529                let force_nonblocking = self.keeps_physically_nonblocking(
12530                    fd_type,
12531                    internal_socket,
12532                    physically_nonblocking,
12533                    inherited_external_read,
12534                );
12535                let physical_flags = if force_nonblocking {
12536                    flags | OFlag::O_NONBLOCK.bits()
12537                } else {
12538                    flags
12539                };
12540                let result = self
12541                    .record_or_replay(guest, call.with_cmd(F_SETFL(physical_flags)))
12542                    .await?;
12543                guest.thread_state().with_detfd(fd, |detfd| {
12544                    // Record the guest's *logical* status flags (derives logical
12545                    // nonblocking); when we forced O_NONBLOCK physically without the
12546                    // guest asking, mark the description physically nonblocking too.
12547                    detfd.set_status_flags(flags);
12548                    if force_nonblocking {
12549                        detfd.set_physically_nonblocking();
12550                    }
12551                })?;
12552                Ok(result)
12553            }
12554            F_DUPFD(_) | F_DUPFD_CLOEXEC(_) => {
12555                let newfd = self.record_or_replay(guest, call).await? as RawFd;
12556                let lock_tracked = self.lock_tracked_descriptions(guest, newfd..=newfd);
12557                let replaced = guest.thread_state_mut().dup_fd(fd, newfd, o_cloexec)?;
12558                if let Some(open_file_id) = replaced {
12559                    self.release_port_for_open_file(guest, open_file_id).await;
12560                }
12561                self.report_lock_references(guest, lock_tracked).await;
12562                Ok(newfd as i64)
12563            }
12564            F_SETFD(flags) => {
12565                let result = self.record_or_replay(guest, call).await?;
12566                let cloexec = flags & libc::FD_CLOEXEC != 0;
12567                // A descriptor Detcore lists without modelling keeps its flag
12568                // too (`FileMetadata::present_unmodelled`).
12569                if !guest.thread_state().set_unmodelled_cloexec(fd, cloexec) {
12570                    guest.thread_state().with_detfd(fd, |detfd| {
12571                        detfd.set_cloexec(cloexec);
12572                    })?;
12573                }
12574                Ok(result)
12575            }
12576            // AUTONOMOUS-BOT-IMPLEMENTED
12577            // TODO-HUMAN-REVIEW(PR-2568): Review refusing guest pipe growth.
12578            // Raising a pipe above the pinned capacity is the ONE syscall that
12579            // defeats the pin, and whether it succeeds is decided by the host's
12580            // `/proc/sys/fs/pipe-max-size`. On a default host (1 MiB ceiling)
12581            // the guest gets a 1048576-byte pipe; on a hardened host (64 KiB)
12582            // the identical guest gets EPERM and keeps 8192. Same binary, same
12583            // `--strict`, guest-visible return value and pipe capacity decided
12584            // by a host sysctl -- a determinism leak by this project's own
12585            // definition, and it survived because the capacity pin was applied
12586            // at creation and never defended afterwards.
12587            //
12588            // Refuse deterministically instead of asking Linux. EPERM is the
12589            // errno Linux itself returns when that ceiling binds, so the guest
12590            // sees a shape it must already handle rather than a novel one, and
12591            // it is the answer the hardened host would have given.
12592            //
12593            // SHRINKING IS DELIBERATELY LEFT ALONE. It is always permitted for
12594            // an unprivileged process, it is process-local with no host-derived
12595            // input, and `tests/c/pipe_capacity.c` locks
12596            // it as a guest-visible contract: that fixture shrinks to one page
12597            // and requires the value to round-trip. Clamping every
12598            // `F_SETPIPE_SZ` to the pinned capacity would break that contract
12599            // while fixing nothing that is actually nondeterministic.
12600            // A host-backed pipe's capacity is host state
12601            // (`reported_pipe_capacity`); the guest reads it no larger than
12602            // the ceiling it may raise a pipe to.
12603            F_GETPIPE_SZ => {
12604                let host = self.record_or_replay(guest, call).await?;
12605                if !host_pipe_floor() {
12606                    self.refuse_below_two_page_pipe_floor(
12607                        guest,
12608                        &format!("F_GETPIPE_SZ on fd {fd} read {host} bytes"),
12609                    )
12610                    .await;
12611                }
12612                Ok(reported_pipe_capacity(host))
12613            }
12614            F_SETPIPE_SZ(requested) if pipe_capacity_request_exceeds_ceiling(requested) => {
12615                trace!(
12616                    "[detcore] refusing F_SETPIPE_SZ({}) above the deterministic pipe ceiling {}",
12617                    requested,
12618                    deterministic_pipe_capacity_bytes()
12619                );
12620                Err(Errno::EPERM.into())
12621            }
12622            // An OFD lock belongs to its open file description, and Linux
12623            // shows it in `/proc/locks` from every PID namespace, the host's
12624            // included, as owner -1. After each call, the description's own
12625            // fdinfo says which locks it holds (`procfs::locks`); the lock
12626            // itself is the kernel's, as for POSIX locks.
12627            F_OFD_SETLK(_) | F_OFD_SETLKW(_) => {
12628                let result = self.record_or_replay(guest, call).await?;
12629                self.refresh_description_locks(guest, fd, LockCall::Fcntl, None)
12630                    .await?;
12631                Ok(result)
12632            }
12633            // Where the ledger takes the backend's fact (package K slice
12634            // K8-h; `Self::ledger_takes_fact`), there is no kernel
12635            // `/proc/locks` to keep a record lock's or a lease's row by its
12636            // owner: the ledger is the record, and Detcore renders the rows
12637            // from it. A record lock the backend holds as an OFD lock (KVM's
12638            // provider: https://github.com/rrnewton/reverie/issues/1223) is
12639            // shown nowhere: a `/proc/locks` snapshot or a fdinfo read that
12640            // would show it is refused by name
12641            // (`Self::note_record_lock_held_as_ofd`).
12642            F_SETLK(_) | F_SETLKW(_) | F_SETLK64(_) | F_SETLKW64(_) | F_SETLEASE(_)
12643                if self.ledger_takes_fact(guest) =>
12644            {
12645                // The class of lock the request can take, which Linux
12646                // records with the caller as its taker; an unlock, none.
12647                let class = match call.cmd() {
12648                    F_SETLEASE(kind) => requested_lock_class(kind, "LEASE"),
12649                    F_SETLK(Some(lock)) | F_SETLKW(Some(lock)) => guest
12650                        .memory()
12651                        .read_value(lock)
12652                        .ok()
12653                        .and_then(|lock: libc::flock| {
12654                            requested_lock_class(i32::from(lock.l_type), "POSIX")
12655                        }),
12656                    F_SETLK64(Some(lock)) | F_SETLKW64(Some(lock)) => guest
12657                        .memory()
12658                        .read_value(lock)
12659                        .ok()
12660                        .and_then(|lock: libc::flock64| {
12661                            requested_lock_class(i32::from(lock.l_type), "POSIX")
12662                        }),
12663                    _ => None,
12664                };
12665                let baseline = self.lock_baseline(guest, fd, class);
12666                let result = self.record_or_replay(guest, call).await?;
12667                Box::pin(self.refresh_description_locks(guest, fd, LockCall::Fcntl, baseline))
12668                    .await?;
12669                if class == Some("POSIX") {
12670                    self.note_record_lock_held_as_ofd(guest, fd).await;
12671                }
12672                Ok(result)
12673            }
12674            _ => {
12675                trace!(
12676                    "[detcore-finishme]: fcntl unhandled cases: {:?}",
12677                    call.cmd()
12678                );
12679                Ok(self.record_or_replay(guest, call).await?)
12680            }
12681        }
12682    }
12683
12684    /// Record which OFD and flock locks the open file description `fd` names
12685    /// holds now, after a call through it that may have changed them
12686    /// (`procfs::locks`), from the `lock:` lines of its fdinfo, which name
12687    /// exactly that description's locks with the device `/proc/locks`
12688    /// prints. A descriptor Detcore keeps no model of is left out.
12689    ///
12690    /// The fdinfo is read only through an anchor proved to be procfs, by a
12691    /// sequence of calls through the recorder ([`Self::read_own_fdinfo`]):
12692    /// the `/proc/locks` rows a replay filters are the recording's, and
12693    /// replay takes no real lock, so the locks they are matched against must
12694    /// be the recording's too. A recording therefore holds, after each such
12695    /// call, the anchor's `openat`, `fstatfs` and `fstat`, the entry's
12696    /// `openat2`, `fstatfs` and `fstat`, the reads and two `close`s
12697    /// (hermit-cli's `RECORD_VERSION` names the change).
12698    ///
12699    /// The guest's `call` is never affected. A read that fails for a cause
12700    /// the guest's own filesystem view or the backend decides (no `/proc`,
12701    /// not procfs, an entry outside it or across a mount, a backend's
12702    /// refusal) makes the description untrackable: it contributes no lock. A
12703    /// read that fails for ambient or resource state (a full descriptor table
12704    /// or file table, no memory, an interruption, an I/O error, a kernel
12705    /// without `openat2`, a permission refusal) leaves its locks unknown, and
12706    /// a later `/proc/locks` read
12707    /// refuses while a guest table references the description
12708    /// (`procfs::locks::LockLedger::unknown`).
12709    ///
12710    /// A lock or unlock through a descriptor whose description the ledger
12711    /// may know under another identity (one Detcore keeps no model of, such
12712    /// as a descriptor received over `SCM_RIGHTS` and not recorded
12713    /// (`Detcore::record_received_descriptors`), or one recorded on
12714    /// reception as a second model of the sender's description, not one that
12715    /// joined an open file its table models) can leave that other
12716    /// identity's entry stale, so no lock on that file's inode number counts
12717    /// from then on (`procfs::locks::LockLedger::alias_inode`): hidden, never
12718    /// a host's row admitted. If that descriptor's fdinfo cannot be read for
12719    /// ambient or resource state, every later `/proc/locks` read refuses
12720    /// instead ([`Self::note_alias`]).
12721    pub(crate) async fn refresh_description_locks<G: Guest<Self>>(
12722        &self,
12723        guest: &mut G,
12724        fd: RawFd,
12725        call: LockCall,
12726        baseline: Option<LockBaseline>,
12727    ) -> Result<(), Error> {
12728        let model = guest.thread_state().with_detfd(fd, |detfd| {
12729            detfd.set_lock_tracked();
12730            if matches!(
12731                call,
12732                LockCall::Recvmsg | LockCall::Recvmmsg | LockCall::BinderWriteRead
12733            ) {
12734                detfd.set_received_for_locks();
12735            }
12736            if call == LockCall::Fcntl {
12737                detfd.set_may_hold_ofd_lock();
12738            }
12739            (
12740                detfd.open_file_id(),
12741                detfd.is_received_for_locks(),
12742                detfd.stdio_alias_group(),
12743            )
12744        });
12745        let Ok((open_file, received, alias_group)) = model else {
12746            if call.changes_locks() {
12747                let fdinfo = self.read_own_fdinfo(guest, fd, call.name()).await?;
12748                Self::note_alias(guest, &fdinfo).await;
12749            }
12750            return Ok(());
12751        };
12752        let fdinfo = self.read_own_fdinfo(guest, fd, call.name()).await?;
12753        if received && call.changes_locks() {
12754            Self::note_alias(guest, &fdinfo).await;
12755        }
12756        let held = matches!(&fdinfo, FdinfoRead::Contents(_) | FdinfoRead::Facts(..));
12757        let from_fact = matches!(&fdinfo, FdinfoRead::Facts(..));
12758        let locks = match fdinfo {
12759            FdinfoRead::Contents(contents) => {
12760                Ok(crate::procfs::locks::held_locks_in_fdinfo(&contents))
12761            }
12762            FdinfoRead::Facts(facts) => Ok(facts.locks),
12763            FdinfoRead::Unknown(reason) => Err(reason),
12764            // Hidden, never admitted: the description contributes no lock
12765            // until a later read through a proved procfs.
12766            FdinfoRead::Untrackable(reason) => {
12767                info!("[detcore] /proc/locks: {reason}; its locks are not shown");
12768                Ok(Vec::new())
12769            }
12770        };
12771        if !held
12772            || locks
12773                .as_ref()
12774                .is_ok_and(|locks| locks.iter().any(|lock| lock.class == "OFDLCK"))
12775        {
12776            let _ = guest
12777                .thread_state()
12778                .with_detfd(fd, |detfd| detfd.set_may_hold_ofd_lock());
12779        }
12780        let slots = guest.thread_state().slots_naming(open_file);
12781        // From the fact, after a call that can take a lock, the ledger also
12782        // learns which locks this process proved it took, whose lines name it
12783        // (`procfs::locks::LockLedger::note_locks_taken`), and what else names
12784        // the open file description: possibly another identity, on reception
12785        // (`procfs::locks::LockLedger::second_identities`), or the other
12786        // standard streams of its alias group, which are that description
12787        // (`procfs::locks::LockLedger::alias_groups`). A receipt that joined
12788        // the description its sender's table modeled is that description, so
12789        // the ledger learns only this table's slots and the locks it holds.
12790        match locks {
12791            Ok(locks) if from_fact && call.changes_locks() => {
12792                let taken = locks_taken(baseline.as_ref(), &locks);
12793                let identity = if received {
12794                    crate::procfs::locks::LedgerIdentity::Received
12795                } else if let Some(group) = alias_group {
12796                    crate::procfs::locks::LedgerIdentity::StandardStreams {
12797                        group,
12798                        members: Self::alias_group_members(guest, group),
12799                    }
12800                } else {
12801                    crate::procfs::locks::LedgerIdentity::Own
12802                };
12803                crate::tool_global::note_description_locks_taken(
12804                    guest, open_file, locks, taken, slots, identity,
12805                )
12806                .await;
12807            }
12808            Ok(locks) if from_fact && call != LockCall::Joined => {
12809                crate::tool_global::note_second_identity_locks(guest, open_file, locks, slots)
12810                    .await;
12811            }
12812            locks => {
12813                crate::tool_global::note_description_locks(guest, open_file, locks, slots).await;
12814            }
12815        }
12816        Ok(())
12817    }
12818
12819    /// The identities of alias group `group` (the container's standard
12820    /// streams that are one open file description, `FileMetadata::setup_stdio`)
12821    /// that the caller's table names, each once, with how many of its slots
12822    /// name it. Each is marked as one whose locks the ledger follows
12823    /// (`DetFd::set_lock_tracked`), as the caller's own description is, so
12824    /// that its closes, its replacements and its survival of an exec reach the
12825    /// ledger too.
12826    fn alias_group_members<G: Guest<Self>>(guest: &G, group: OpenFileId) -> Vec<(OpenFileId, u32)> {
12827        let (numbers, _, _) = guest.thread_state().descriptor_table();
12828        let mut members: Vec<(OpenFileId, u32)> = Vec::new();
12829        for fd in numbers {
12830            let Ok(Some(member)) = guest.thread_state().with_detfd(fd, |detfd| {
12831                (detfd.stdio_alias_group() == Some(group)).then(|| {
12832                    detfd.set_lock_tracked();
12833                    detfd.open_file_id()
12834                })
12835            }) else {
12836                continue;
12837            };
12838            if !members.iter().any(|(known, _)| *known == member) {
12839                members.push((member, guest.thread_state().slots_naming(member)));
12840            }
12841        }
12842        members
12843    }
12844
12845    /// The description's locks now, before a lock call that can take a
12846    /// lock of `class`, from the backend's fact, where the ledger takes it
12847    /// ([`LockBaseline`]); `None` elsewhere, or where the backend gives no
12848    /// fact for the descriptor, so that nothing is attributed to the call.
12849    fn lock_baseline<G: Guest<Self>>(
12850        &self,
12851        guest: &G,
12852        fd: RawFd,
12853        class: Option<&'static str>,
12854    ) -> Option<LockBaseline> {
12855        let class = class?;
12856        if !self.ledger_takes_fact(guest) {
12857            return None;
12858        }
12859        let status = guest.descriptor_status(fd).ok()?;
12860        Some(LockBaseline {
12861            class,
12862            held: held_locks_of_status(&status),
12863        })
12864    }
12865
12866    /// After a guest's record-lock request (`F_SETLK`, `F_SETLKW`) through
12867    /// `fd` succeeded, where the ledger takes the backend's fact: whether the
12868    /// backend holds the lock it took as an OFD lock of the description, as
12869    /// KVM's provider holds every record lock
12870    /// (https://github.com/rrnewton/reverie/issues/1223), which the ledger
12871    /// then shows on no `/proc/locks` row and no fdinfo line
12872    /// (`procfs::locks::LockLedger::record_locks_held_as_ofd`). The fact says,
12873    /// after the call ([`record_lock_files_held_as_ofd`]). Where the backend
12874    /// gives no fact for the descriptor (`NotProvided`, an object it keeps
12875    /// itself), no lock through it is shown anywhere, so nothing is noted;
12876    /// where the fact fails for another cause, the file is unknown, and every
12877    /// OFD lock may be such a lock.
12878    async fn note_record_lock_held_as_ofd<G: Guest<Self>>(&self, guest: &mut G, fd: RawFd) {
12879        let files = match guest.descriptor_status(fd) {
12880            Ok(status) => {
12881                let files = record_lock_files_held_as_ofd(&held_locks_of_status(&status));
12882                if files.is_empty() {
12883                    return;
12884                }
12885                Some(files)
12886            }
12887            Err(reverie::FactError::Errno(errno)) if errno != Errno::EBADF => None,
12888            Err(_) => return,
12889        };
12890        crate::tool_global::note_record_locks_held_as_ofd(guest, files).await;
12891    }
12892
12893    /// After a lock or unlock through a descriptor whose description the
12894    /// ledger may know under another identity: no lock on its inode number
12895    /// counts from now on (`procfs::locks::LockLedger::alias_inode`). If its
12896    /// fdinfo could not be read for ambient or resource state, which
12897    /// identities it changed is unknown, and every later `/proc/locks`
12898    /// snapshot is refused by name (`procfs::locks::LockLedger::refuse`); if
12899    /// the guest's `/proc` could not be proved to be procfs, no lock counts
12900    /// at all (an inode of `None`), which only hides.
12901    async fn note_alias<G: Guest<Self>>(guest: &mut G, fdinfo: &FdinfoRead) {
12902        match fdinfo {
12903            FdinfoRead::Unknown(reason) => {
12904                crate::tool_global::note_lock_ledger_refused(guest, reason.clone()).await;
12905            }
12906            FdinfoRead::Contents(_) | FdinfoRead::Facts(..) | FdinfoRead::Untrackable(_) => {
12907                crate::tool_global::note_aliased_inode(guest, fdinfo.inode()).await;
12908            }
12909        }
12910    }
12911
12912    /// The descriptions named by the slots `fds` of this thread's table
12913    /// whose locks `procfs::locks::LockLedger` follows, taken before a close
12914    /// or replacement of those slots.
12915    fn lock_tracked_descriptions<G: Guest<Self>>(
12916        &self,
12917        guest: &G,
12918        fds: std::ops::RangeInclusive<RawFd>,
12919    ) -> Vec<OpenFileId> {
12920        let thread = guest.thread_state();
12921        let candidates = if fds.start() == fds.end() {
12922            vec![*fds.start()]
12923        } else {
12924            thread
12925                .open_fds()
12926                .into_iter()
12927                .filter(|fd| fds.contains(fd))
12928                .collect()
12929        };
12930        let mut tracked = candidates
12931            .into_iter()
12932            .filter_map(|fd| {
12933                thread
12934                    .with_detfd(fd, |detfd| {
12935                        detfd.is_lock_tracked().then(|| detfd.open_file_id())
12936                    })
12937                    .ok()
12938                    .flatten()
12939            })
12940            .collect::<Vec<_>>();
12941        tracked.sort();
12942        tracked.dedup();
12943        tracked
12944    }
12945
12946    /// After a close or replacement of slots that named `descriptions`,
12947    /// report how many slots of this thread's table still name each
12948    /// (`procfs::locks::LockLedger::release`).
12949    async fn report_lock_references<G: Guest<Self>>(
12950        &self,
12951        guest: &mut G,
12952        descriptions: Vec<OpenFileId>,
12953    ) {
12954        for open_file in descriptions {
12955            let remaining = guest.thread_state().slots_naming(open_file);
12956            crate::tool_global::note_table_released(guest, open_file, remaining).await;
12957        }
12958    }
12959
12960    /// The text of `/proc/thread-self/fdinfo/<fd>`, read through the guest's
12961    /// procfs only, by a recorded sequence staged in a private page mapped for
12962    /// this call and unmapped before it returns ([`Self::read_staged_fdinfo`]).
12963    ///
12964    /// The guest chooses its own root, so a path alone proves nothing: under a
12965    /// `chroot` its `/proc` can be any tree, with any text where an fdinfo is
12966    /// expected, and lock lines that matched a host's lock would admit the
12967    /// host's `/proc/locks` row. So the read starts from an anchor, the
12968    /// guest's `/proc`, which must be procfs (`fstatfs`), opens the entry
12969    /// below it with `openat2`'s `RESOLVE_BENEATH | RESOLVE_NO_XDEV` (no step
12970    /// out of the anchor, no mount crossed, so nothing bound over an entry),
12971    /// and requires the entry itself on procfs and on the anchor's device
12972    /// (`fstatfs` and `fstat` of both). Every step goes through the
12973    /// recorder, so record and replay decide alike. The read holds two
12974    /// descriptors at once, the anchor and the entry. These checks rely on
12975    /// the serialized schedule, under which no other guest thread runs
12976    /// between the calls; with concurrent threads the entry check narrows a
12977    /// `dup2` race on the descriptor numbers but does not close it
12978    /// ([`Self::read_opened_entry`]).
12979    ///
12980    /// No page for a guest that mapped its whole address space away, and a
12981    /// failed step whose errno is ambient or resource state
12982    /// ([`fdinfo_failure_hides`] is false), leave the description's locks
12983    /// unknown ([`FdinfoRead::Unknown`], whose reason names the guest's
12984    /// `call`, [`fdinfo_refusal`]). Every other outcome that gives no proved
12985    /// fdinfo (no `/proc`, not procfs, an entry outside it or across a
12986    /// mount, a device mismatch, a guest-decided or backend-decided errno)
12987    /// makes the description untrackable ([`FdinfoRead::Untrackable`]).
12988    async fn read_own_fdinfo<G: Guest<Self>>(
12989        &self,
12990        guest: &mut G,
12991        fd: RawFd,
12992        call: &str,
12993    ) -> Result<FdinfoRead, Error> {
12994        // A backend whose `/proc` is its own (KVM) gives no procfs anchor:
12995        // its `descriptor_status` fact reports the description's locks
12996        // instead, where the ledger may take it
12997        // ([`Self::ledger_descriptor_status`]).
12998        if !procfs_inode::tool_can_name_guest_descriptors(&self.cfg.backend) {
12999            return Ok(self.ledger_descriptor_status(guest, fd, call));
13000        }
13001        let mapped = guest
13002            .inject_with_retry(Syscall::Mmap(
13003                syscalls::Mmap::new()
13004                    .with_addr(None)
13005                    .with_len(FDINFO_PAGE)
13006                    .with_prot(ProtFlags::PROT_READ | ProtFlags::PROT_WRITE)
13007                    .with_flags(MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS)
13008                    .with_fd(-1)
13009                    .with_offset(0),
13010            ))
13011            .await;
13012        let Some(page) = mapped
13013            .ok()
13014            .and_then(|mapped| usize::try_from(mapped).ok())
13015            .filter(|page| *page != 0)
13016        else {
13017            return Ok(FdinfoRead::Unknown(format!(
13018                "after {call} on fd {fd}, Detcore could not map the page it reads that \
13019                 descriptor's fdinfo through ({mapped:?}), so it does not know which locks \
13020                 the description holds"
13021            )));
13022        };
13023        let result = self.read_staged_fdinfo(guest, page, fd).await;
13024        if let Err(errno) = guest
13025            .inject_with_retry(Syscall::Munmap(
13026                syscalls::Munmap::new()
13027                    .with_addr(Addr::from_raw(page))
13028                    .with_len(FDINFO_PAGE),
13029            ))
13030            .await
13031        {
13032            warn!(
13033                "[detcore] could not unmap the transient fdinfo page: {}",
13034                errno
13035            );
13036        }
13037        Ok(match result? {
13038            Ok(contents) => FdinfoRead::Contents(contents),
13039            // Ambient or resource state says nothing about the locks.
13040            Err(FdinfoFailure::Failed(step, errno)) if !fdinfo_failure_hides(errno) => {
13041                FdinfoRead::Unknown(fdinfo_refusal(call, fd, step, errno))
13042            }
13043            Err(FdinfoFailure::Failed(step, errno)) => FdinfoRead::Untrackable(format!(
13044                "after {call} on fd {fd}, the guest's /proc did not give a procfs fdinfo \
13045                 ({step}: {errno})"
13046            )),
13047            Err(FdinfoFailure::NotProcfs(why)) => FdinfoRead::Untrackable(format!(
13048                "after {call} on fd {fd}, the guest's /proc did not give a procfs fdinfo \
13049                 ({why})"
13050            )),
13051        })
13052    }
13053
13054    /// Whether the lock ledger takes the backend's `descriptor_status` fact
13055    /// for a description's locks (package K slice K8-h): where the guest's
13056    /// procfs is not the kernel's, so no fdinfo can be read through it
13057    /// (`procfs_inode::tool_can_name_guest_descriptors`), and only where
13058    /// Detcore renders procfs (`Detcore::renders_procfs`), which it never
13059    /// does under record and replay. The fact is a read of the backend's
13060    /// state that no recording holds: consulted under replay, it would decide
13061    /// the ledger, and with it a `/proc/locks` snapshot and the lock
13062    /// bookkeeping's refusal, from the replaying host's state instead of the
13063    /// recording's (package K design section 8, amendment 1). Elsewhere the
13064    /// description stays untrackable, as before the fact existed.
13065    pub(crate) fn ledger_takes_fact<G: Guest<Self>>(&self, guest: &G) -> bool {
13066        ledger_takes_fact_where(
13067            procfs_inode::tool_can_name_guest_descriptors(&self.cfg.backend),
13068            self.renders_procfs(guest),
13069        )
13070    }
13071
13072    /// [`Self::read_own_fdinfo`] where the guest's procfs is not the
13073    /// kernel's: the locks the backend's `descriptor_status` fact reports the
13074    /// description named by `fd` holds, if the ledger may take it
13075    /// ([`Self::ledger_takes_fact`]), with the inode number of its file (from
13076    /// a lock, else Detcore's raw stat of the descriptor), for
13077    /// `procfs::locks::LockLedger::alias_inode`. Nothing is injected.
13078    ///
13079    /// Where it may not, or the backend does not report the fact for this
13080    /// descriptor (`NotProvided`: an object the backend keeps itself), or
13081    /// says it is not open (`EBADF`, or an errno the guest's own view or the
13082    /// backend decides, [`fdinfo_failure_hides`]), the description is
13083    /// untrackable, as it was before the fact existed; any other errno leaves
13084    /// its locks unknown, so a later `/proc/locks` read refuses by name.
13085    fn ledger_descriptor_status<G: Guest<Self>>(
13086        &self,
13087        guest: &G,
13088        fd: RawFd,
13089        call: &str,
13090    ) -> FdinfoRead {
13091        if !self.ledger_takes_fact(guest) {
13092            return FdinfoRead::Untrackable(format!(
13093                "after {call} on fd {fd}, the guest's /proc did not give a procfs fdinfo \
13094                 (the backend's /proc is not the kernel's procfs)"
13095            ));
13096        }
13097        match guest.descriptor_status(fd) {
13098            Ok(status) => {
13099                let locks = held_locks_of_status(&status);
13100                let inode = status
13101                    .locks
13102                    .first()
13103                    .map(|lock| lock.raw_inode)
13104                    .or_else(|| {
13105                        guest
13106                            .thread_state()
13107                            .with_detfd(fd, |detfd| detfd.stat().map(|stat| stat.inode))
13108                            .ok()
13109                            .flatten()
13110                    })
13111                    .map(|inode| inode.to_string());
13112                FdinfoRead::Facts(Box::new(FactLocks { locks, inode }))
13113            }
13114            Err(reverie::FactError::Errno(errno))
13115                if errno != Errno::EBADF && !fdinfo_failure_hides(errno) =>
13116            {
13117                FdinfoRead::Unknown(fdinfo_refusal(
13118                    call,
13119                    fd,
13120                    "read the backend's descriptor_status of",
13121                    errno,
13122                ))
13123            }
13124            Err(error) => FdinfoRead::Untrackable(format!(
13125                "after {call} on fd {fd}, the backend gave no descriptor_status of it \
13126                 ({error:?})"
13127            )),
13128        }
13129    }
13130
13131    /// Whether every `/proc/locks` snapshot is the lock ledger's rows
13132    /// ([`Self::ledger_locks_text`]): where the guest's procfs is not the
13133    /// kernel's (`procfs_inode::tool_can_name_guest_descriptors`, KVM), so no
13134    /// kernel file holds the guest's rows to keep, and where Detcore renders
13135    /// procfs (`Self::renders_procfs`), the gate under which the ledger takes
13136    /// the backend's fact ([`Self::ledger_takes_fact`]), so that the rows are
13137    /// never a ledger's that took no fact. Elsewhere the kernel's file is
13138    /// filtered to the guest's rows, as before
13139    /// (`procfs::locks::guest_lock_rows`).
13140    pub(crate) fn renders_proc_locks<G: Guest<Self>>(&self, guest: &G) -> bool {
13141        !procfs_inode::tool_can_name_guest_descriptors(&self.cfg.backend)
13142            && self.renders_procfs(guest)
13143    }
13144
13145    /// The `/proc/locks` rows Detcore renders from its lock ledger where the
13146    /// guest's procfs is not the kernel's (package K slice K8-h;
13147    /// `procfs::locks::LockLedger::rendered_rows`), as the kernel would
13148    /// spell them before the sanitizer numbers their identities: one row per
13149    /// lock, `N: CLASS MODE ACCESS OWNER MAJOR:MINOR:INODE START END`. While
13150    /// a description some guest table references has locks Detcore does not
13151    /// know, no snapshot can be decided, and the run stops by name, as for a
13152    /// kernel snapshot.
13153    async fn ledger_locks_text<G: Guest<Self>>(&self, guest: &mut G) -> Vec<u8> {
13154        match crate::tool_global::ledger_lock_rows(guest).await {
13155            Ok(rows) => rows
13156                .iter()
13157                .enumerate()
13158                .map(|(index, (lock, owner))| {
13159                    format!(
13160                        "{}: {} {} {} {owner} {} {} {}\n",
13161                        index + 1,
13162                        lock.class,
13163                        lock.mode,
13164                        lock.access,
13165                        lock.object,
13166                        lock.start,
13167                        lock.end
13168                    )
13169                })
13170                .collect::<String>()
13171                .into_bytes(),
13172            Err(reason) => {
13173                let reason =
13174                    format!("/proc/locks: {reason}; Detcore cannot render that description's rows");
13175                self.refuse_lock_bookkeeping(guest, reason).await
13176            }
13177        }
13178    }
13179
13180    /// Stop the run, by name, with the policy-refusal exit, because Detcore
13181    /// cannot tell a `/proc/locks` snapshot's guest rows from a host's while a
13182    /// description whose locks it could not read is referenced
13183    /// (`procfs::locks::LockLedger::unknown`).
13184    async fn refuse_lock_bookkeeping<G: Guest<Self>>(&self, guest: &mut G, reason: String) -> ! {
13185        error!("[detcore] {reason}");
13186        crate::tool_global::report_policy_refusal(guest, reason).await;
13187        crate::tool_global::unrecoverable_shutdown(guest, detcore_model::HERMIT_POLICY_REFUSAL_EXIT)
13188            .await
13189    }
13190
13191    /// [`Self::read_own_fdinfo`]'s recorded sequence, in the mapped `page`
13192    /// (layout: the path at [`FDINFO_PATH`], `openat2`'s `struct open_how` at
13193    /// [`FDINFO_HOW`], `fstatfs` and `fstat` buffers at [`FDINFO_STAT`], the
13194    /// read buffer from [`FDINFO_READ`]): open the anchor, prove it procfs,
13195    /// open the entry beneath it on its mount, compare devices, read, and
13196    /// close both descriptors.
13197    async fn read_staged_fdinfo<G: Guest<Self>>(
13198        &self,
13199        guest: &mut G,
13200        page: usize,
13201        fd: RawFd,
13202    ) -> Result<Result<Vec<u8>, FdinfoFailure>, Error> {
13203        // A backend whose `/proc` is its own (KVM) gives no procfs anchor:
13204        // nothing is opened there, and the description is untrackable, as an
13205        // anchor that is not procfs makes it.
13206        if !procfs_inode::tool_can_name_guest_descriptors(&self.cfg.backend) {
13207            return Ok(Err(FdinfoFailure::NotProcfs(
13208                "the backend's /proc is not the kernel's procfs",
13209            )));
13210        }
13211        let path_ptr =
13212            PathPtr::from_ptr((page + FDINFO_PATH) as *const libc::c_char).ok_or(Errno::EFAULT)?;
13213        self.write_fdinfo_path(guest, page, "/proc")?;
13214        let anchor = syscalls::Openat::new()
13215            .with_dirfd(libc::AT_FDCWD)
13216            .with_path(Some(path_ptr))
13217            .with_flags(OFlag::O_PATH | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC)
13218            .with_mode(None);
13219        let anchor = match self.record_or_replay(guest, Syscall::Openat(anchor)).await {
13220            Ok(anchor) => anchor as RawFd,
13221            Err(errno) => return Ok(Err(FdinfoFailure::Failed("open", errno))),
13222        };
13223        let result = self.read_beneath_anchor(guest, page, anchor, fd).await;
13224        if let Err(errno) = self
13225            .record_or_replay(guest, syscalls::Close::new().with_fd(anchor))
13226            .await
13227        {
13228            trace!("[detcore] closing the fdinfo anchor failed: {errno}");
13229        }
13230        result
13231    }
13232
13233    /// [`Self::read_staged_fdinfo`] once its `anchor` is open.
13234    async fn read_beneath_anchor<G: Guest<Self>>(
13235        &self,
13236        guest: &mut G,
13237        page: usize,
13238        anchor: RawFd,
13239        fd: RawFd,
13240    ) -> Result<Result<Vec<u8>, FdinfoFailure>, Error> {
13241        let anchor_device = match self.recorded_procfs_device(guest, page, anchor).await? {
13242            Ok(device) => device,
13243            Err(failure) => return Ok(Err(failure)),
13244        };
13245        self.write_fdinfo_path(guest, page, &format!("thread-self/fdinfo/{fd}"))?;
13246        // struct open_how { u64 flags; u64 mode; u64 resolve; }
13247        let how = (libc::O_RDONLY | libc::O_CLOEXEC) as u64;
13248        let mut open_how = [0u8; FDINFO_HOW_SIZE];
13249        open_how[..8].copy_from_slice(&how.to_ne_bytes());
13250        open_how[16..].copy_from_slice(&FDINFO_RESOLVE.to_ne_bytes());
13251        let how_ptr = AddrMut::<u8>::from_raw(page + FDINFO_HOW).ok_or(Errno::EFAULT)?;
13252        guest.memory().write_exact(how_ptr, &open_how)?;
13253        let opened = match self
13254            .record_or_replay(
13255                guest,
13256                Syscall::from_raw(
13257                    Sysno::openat2,
13258                    procfs_levels::raw_args([
13259                        anchor as usize,
13260                        page + FDINFO_PATH,
13261                        page + FDINFO_HOW,
13262                        FDINFO_HOW_SIZE,
13263                        0,
13264                        0,
13265                    ]),
13266                ),
13267            )
13268            .await
13269        {
13270            Ok(opened) => opened as RawFd,
13271            // The entry is outside the anchor or across a mount.
13272            Err(Errno::EXDEV) => {
13273                return Ok(Err(FdinfoFailure::NotProcfs(
13274                    "the entry is outside /proc or across a mount",
13275                )));
13276            }
13277            Err(errno) => return Ok(Err(FdinfoFailure::Failed("open", errno))),
13278        };
13279        // Closed on every path, the error ones included.
13280        let result = self
13281            .read_opened_entry(guest, page, opened, anchor_device)
13282            .await;
13283        if let Err(errno) = self
13284            .record_or_replay(guest, syscalls::Close::new().with_fd(opened))
13285            .await
13286        {
13287            trace!("[detcore] closing fdinfo failed: {errno}");
13288        }
13289        result
13290    }
13291
13292    /// The text of the `opened` fdinfo entry, once it too is shown to be on
13293    /// procfs and on the anchor's device. The checks run on the entry's own
13294    /// descriptor, so a sibling thread that replaces the anchor's descriptor
13295    /// number between calls (`dup2` of a fake directory) cannot pass them.
13296    ///
13297    /// The defence relies on Detcore's serialized schedule: no other guest
13298    /// thread runs between these injected calls. Under
13299    /// `--no-sequentialize-threads` a sibling could still `dup2` onto the
13300    /// entry's own descriptor number between its checks and the read; the
13301    /// entry check narrows that race but does not close it.
13302    async fn read_opened_entry<G: Guest<Self>>(
13303        &self,
13304        guest: &mut G,
13305        page: usize,
13306        opened: RawFd,
13307        anchor_device: u64,
13308    ) -> Result<Result<Vec<u8>, FdinfoFailure>, Error> {
13309        match self.recorded_procfs_device(guest, page, opened).await? {
13310            Ok(device) if device == anchor_device => {}
13311            Ok(_) => {
13312                return Ok(Err(FdinfoFailure::NotProcfs(
13313                    "the entry is not on /proc's device",
13314                )));
13315            }
13316            Err(failure) => return Ok(Err(failure)),
13317        }
13318        let scratch = AddrMut::<u8>::from_raw(page + FDINFO_READ).ok_or(Errno::EFAULT)?;
13319        match self
13320            .drain_procfs(
13321                guest,
13322                syscalls::Read::new().with_fd(opened),
13323                scratch,
13324                FDINFO_PAGE - FDINFO_READ,
13325                false,
13326            )
13327            .await
13328        {
13329            Ok(contents) => Ok(Ok(contents)),
13330            Err(Error::Errno(errno)) => Ok(Err(FdinfoFailure::Failed("read", errno))),
13331            Err(error) => Err(error),
13332        }
13333    }
13334
13335    /// The device of the descriptor `fd`, once `fstatfs` shows it is on
13336    /// procfs, both through the recorder into the stat buffer of the fdinfo
13337    /// page. A failed call keeps its errno, for the common classification
13338    /// ([`fdinfo_failure_hides`]).
13339    async fn recorded_procfs_device<G: Guest<Self>>(
13340        &self,
13341        guest: &mut G,
13342        page: usize,
13343        fd: RawFd,
13344    ) -> Result<Result<u64, FdinfoFailure>, Error> {
13345        let stat = page + FDINFO_STAT;
13346        if let Err(errno) = self
13347            .record_or_replay(
13348                guest,
13349                Syscall::from_raw(
13350                    Sysno::fstatfs,
13351                    procfs_levels::raw_args([fd as usize, stat, 0, 0, 0, 0]),
13352                ),
13353            )
13354            .await
13355        {
13356            return Ok(Err(FdinfoFailure::Failed("fstatfs", errno)));
13357        }
13358        // `f_type` is the first field of `struct statfs`.
13359        if self.read_fdinfo_word(guest, stat)? != libc::PROC_SUPER_MAGIC as u64 {
13360            return Ok(Err(FdinfoFailure::NotProcfs("not procfs")));
13361        }
13362        if let Err(errno) = self
13363            .record_or_replay(
13364                guest,
13365                Syscall::from_raw(
13366                    Sysno::fstat,
13367                    procfs_levels::raw_args([fd as usize, stat, 0, 0, 0, 0]),
13368                ),
13369            )
13370            .await
13371        {
13372            return Ok(Err(FdinfoFailure::Failed("fstat", errno)));
13373        }
13374        // `st_dev` is the first field of `struct stat`.
13375        Ok(Ok(self.read_fdinfo_word(guest, stat)?))
13376    }
13377
13378    /// Write `path`, NUL-terminated, at the fdinfo page's path slot.
13379    fn write_fdinfo_path<G: Guest<Self>>(
13380        &self,
13381        guest: &mut G,
13382        page: usize,
13383        path: &str,
13384    ) -> Result<(), Error> {
13385        assert!(
13386            path.len() < FDINFO_HOW - FDINFO_PATH,
13387            "fdinfo path {path:?} outgrew its room"
13388        );
13389        let start = AddrMut::<u8>::from_raw(page + FDINFO_PATH).ok_or(Errno::EFAULT)?;
13390        let mut bytes = path.as_bytes().to_vec();
13391        bytes.push(0);
13392        guest.memory().write_exact(start, &bytes)?;
13393        Ok(())
13394    }
13395
13396    /// The first 8 bytes at `address`, a native-endian `u64`.
13397    fn read_fdinfo_word<G: Guest<Self>>(
13398        &self,
13399        guest: &mut G,
13400        address: usize,
13401    ) -> Result<u64, Error> {
13402        let mut word = [0u8; 8];
13403        let start = Addr::<u8>::from_raw(address).ok_or(Errno::EFAULT)?;
13404        guest.memory().read_exact(start, &mut word)?;
13405        Ok(u64::from_ne_bytes(word))
13406    }
13407
13408    /// Refuse with EPERM a call that would make one of Hermit's tasks the
13409    /// owner of a descriptor's signals (https://github.com/rrnewton/hermit/issues/4083).
13410    /// Linux would accept it and later signal Hermit whenever the descriptor is
13411    /// ready; with `F_SETSIG(SIGTERM)` that ended Hermit's run. Refusing is a
13412    /// policy, the answer `pidfd_open` of Hermit's process gets.
13413    async fn refuse_hermit_owner<G: Guest<Self>>(
13414        &self,
13415        guest: &mut G,
13416        fd: RawFd,
13417        command: &str,
13418        owner: libc::c_int,
13419    ) -> Result<(), Error> {
13420        if owner <= 0 || !crate::tool_global::names_hermit_task(guest, None, owner).await {
13421            return Ok(());
13422        }
13423        tracing::info!(
13424            "[detcore, dtid {}] {command} on fd {fd} names task {owner}, one of Hermit's own, \
13425             as the owner of its signals: refused with EPERM \
13426             (https://github.com/rrnewton/hermit/issues/4083)",
13427            guest.thread_state().dettid,
13428        );
13429        Err(Errno::EPERM.into())
13430    }
13431
13432    /// The operation `call`, an `fcntl` as the kernel will receive it, would
13433    /// perform that can complete a host-timed signal producer
13434    /// ([`crate::host_timed_producers`]), where the run refuses them
13435    /// (`Config::in_guest_refuses_host_timed_producers`); `None` for every
13436    /// other call, for every disarm (an owner of 0, `F_SETLEASE` with
13437    /// `F_UNLCK`, `F_NOTIFY` with no event bit, `F_SETFL` without `O_ASYNC`), for
13438    /// arguments the kernel refuses before it arms anything (`F_SETOWN` of
13439    /// `INT_MIN`, an unknown `F_SETOWN_EX` type or lease type), and always
13440    /// where the run does not refuse them. `F_SETSIG` alone completes nothing:
13441    /// it changes which signal an owner gets. With an owner-setting command
13442    /// comes the pid number the kernel looks up for the owner (`find_vpid`):
13443    /// `F_SETOWN`'s owner, negated for a process group, and `F_SETOWN_EX`'s
13444    /// pid as given.
13445    fn fcntl_producer_arming<G: Guest<Self>>(
13446        &self,
13447        guest: &mut G,
13448        call: syscalls::Fcntl,
13449    ) -> Option<(
13450        crate::host_timed_producers::ProducerArming,
13451        Option<crate::host_timed_producers::OwnerLookup>,
13452    )> {
13453        use crate::host_timed_producers::F_OWNER_PGRP;
13454        use crate::host_timed_producers::F_OWNER_PID;
13455        use crate::host_timed_producers::F_OWNER_TID;
13456        use crate::host_timed_producers::ProducerArming;
13457        // dnotify's event bits, `DN_ACCESS` through `DN_ATTRIB`: the only ones
13458        // `convert_arg` turns into events (`fs/notify/dnotify/dnotify.c:201-221`);
13459        // every other bit (`DN_MULTISHOT`, an unknown one) makes none.
13460        const DN_EVENTS: libc::c_int = 0x3f;
13461        if !guest.config().in_guest_refuses_host_timed_producers {
13462            return None;
13463        }
13464        match call.cmd() {
13465            F_SETOWN => {
13466                let owner = call.into_parts().1.arg2 as libc::c_int;
13467                (owner != 0 && owner != libc::c_int::MIN).then_some((
13468                    ProducerArming::SetOwner("fcntl(F_SETOWN)"),
13469                    Some(crate::host_timed_producers::OwnerLookup::of_setown(owner)),
13470                ))
13471            }
13472            F_SETOWN_EX(Some(owner)) => {
13473                let owner: Addr<[libc::c_int; 2]> = owner.cast();
13474                let [kind, pid] = guest.memory().read_value(owner).ok()?;
13475                ([F_OWNER_TID, F_OWNER_PID, F_OWNER_PGRP].contains(&kind) && pid != 0).then_some((
13476                    ProducerArming::SetOwner("fcntl(F_SETOWN_EX)"),
13477                    Some(crate::host_timed_producers::OwnerLookup {
13478                        who: pid,
13479                        group: kind == F_OWNER_PGRP,
13480                    }),
13481                ))
13482            }
13483            F_SETFL(flags) => (flags & libc::O_ASYNC != 0)
13484                .then_some((ProducerArming::SetAsync("fcntl(F_SETFL)"), None)),
13485            F_SETLEASE(lease) => [libc::F_RDLCK, libc::F_WRLCK]
13486                .contains(&lease)
13487                .then_some((ProducerArming::Lease, None)),
13488            F_NOTIFY(mask) => (mask & DN_EVENTS != 0).then_some((ProducerArming::Notify, None)),
13489            _ => None,
13490        }
13491    }
13492
13493    /// As [`Self::fcntl_producer_arming`], for an `ioctl` as the kernel will
13494    /// receive it: `FIOSETOWN` or `SIOCSPGRP` with a nonzero owner, with the
13495    /// pid number the kernel looks up for it (`f_setown`), and `FIOASYNC`
13496    /// turning `O_ASYNC` on. An unreadable argument is left to the kernel,
13497    /// which answers EBADF or EFAULT and arms nothing.
13498    fn ioctl_producer_arming<G: Guest<Self>>(
13499        &self,
13500        guest: &mut G,
13501        call: syscalls::Ioctl,
13502    ) -> Option<(
13503        crate::host_timed_producers::ProducerArming,
13504        Option<crate::host_timed_producers::OwnerLookup>,
13505    )> {
13506        use crate::host_timed_producers::ProducerArming;
13507        if !guest.config().in_guest_refuses_host_timed_producers {
13508            return None;
13509        }
13510        let (call_name, owner) = match call.request() {
13511            syscalls::ioctl::Request::FIOASYNC(on) => {
13512                let on: libc::c_int = guest.memory().read_value(on?).ok()?;
13513                return (on != 0).then_some((ProducerArming::SetAsync("ioctl(FIOASYNC)"), None));
13514            }
13515            syscalls::ioctl::Request::FIOSETOWN(owner) => ("ioctl(FIOSETOWN)", owner),
13516            syscalls::ioctl::Request::SIOCSPGRP(owner) => ("ioctl(SIOCSPGRP)", owner),
13517            _ => return None,
13518        };
13519        let owner: libc::c_int = guest.memory().read_value(owner?).ok()?;
13520        (owner != 0 && owner != libc::c_int::MIN).then_some((
13521            ProducerArming::SetSocketOwner(call_name),
13522            Some(crate::host_timed_producers::OwnerLookup::of_setown(owner)),
13523        ))
13524    }
13525
13526    /// Refuses with EPERM and a WARN `arming` on descriptor `fd` if it would
13527    /// complete a host-timed signal producer
13528    /// ([`crate::host_timed_producers::ProducerArming::completes`]), reading
13529    /// the descriptor's state in the caller's turn
13530    /// ([`Self::descriptor_producer_state`]). Linux's answers that come
13531    /// before any arming come first: EBADF for a descriptor the caller does
13532    /// not hold or an `O_PATH` one, ENOTDIR for `F_NOTIFY` on a file that is
13533    /// not a directory (`fcntl_dirnotify`, `fs/notify/dnotify/dnotify.c:288-292`)
13534    /// and EINVAL for a lease on a file that is not regular (`vfs_setlease`,
13535    /// `fs/locks.c:2021-2022`). An owner-setting operation naming an owner
13536    /// no signal can reach (`owner`, [`Self::owner_can_be_signalled`])
13537    /// completes nothing, and is left to the kernel, which answers ESRCH for
13538    /// one that does not exist, as `f_setown` and `f_setown_ex` do, and sets
13539    /// one that exists but can never be signalled. Called only where the run
13540    /// refuses producers.
13541    async fn refuse_host_timed_producer<G: Guest<Self>>(
13542        &self,
13543        guest: &mut G,
13544        fd: RawFd,
13545        arming: crate::host_timed_producers::ProducerArming,
13546        owner: Option<crate::host_timed_producers::OwnerLookup>,
13547    ) -> Result<(), Error> {
13548        use crate::host_timed_producers::ProducerArming;
13549        let (state, file_type) = self.descriptor_producer_state(guest, fd).await?;
13550        match arming {
13551            ProducerArming::Notify if file_type != libc::S_IFDIR => {
13552                return Err(Errno::ENOTDIR.into());
13553            }
13554            ProducerArming::Lease if file_type != libc::S_IFREG => {
13555                return Err(Errno::EINVAL.into());
13556            }
13557            _ => {}
13558        }
13559        if !arming.completes(state) {
13560            return Ok(());
13561        }
13562        if let Some(owner) = owner
13563            && !self.owner_can_be_signalled(guest, owner).await?
13564        {
13565            return Ok(());
13566        }
13567        tracing::warn!(
13568            "{}",
13569            crate::host_timed_producers::producer_refused_warning(
13570                arming,
13571                state,
13572                fd,
13573                guest.thread_state().dettid.as_raw(),
13574            )
13575        );
13576        Err(Errno::EPERM.into())
13577    }
13578
13579    /// Whether a signal for `owner`, the owner an owner-setting call names,
13580    /// can reach a guest process, now or after a later call of the guest's,
13581    /// so that setting it would complete a producer. Two questions are kept
13582    /// apart. The kernel sets any owner whose pid number a task, process
13583    /// group or session still holds (`find_vpid`; ESRCH otherwise: `f_setown`,
13584    /// `fs/fcntl.c:177-181`, and `f_setown_ex`, `:251-253`). Whether a signal
13585    /// can then reach anyone depends on the owner's type (`send_sigio`,
13586    /// `fs/fcntl.c:917-928`):
13587    /// - a process or thread owner (`F_OWNER_PID`, `F_OWNER_TID`, a positive
13588    ///   `F_SETOWN`) reaches the task with that pid, asked by `kill(who, 0)`.
13589    ///   A number that only a group or session holds names no task, and none
13590    ///   can take it while the owner holds it, so it can never be signalled.
13591    /// - a process-group owner reaches the members of that group
13592    ///   (`fs/fcntl.c:923-928`). It can be signalled when the group exists,
13593    ///   asked by `kill(-who, 0)`, and also when only a live task with that
13594    ///   pid exists, asked by `kill(who, 0)` and Detcore's exit model: that
13595    ///   task can still form the group, by `setpgid(0, 0)` or `setsid()`
13596    ///   (whose new group is the task's own pid, `kernel/sys.c:1128`), which
13597    ///   Detcore admits on every backend, and Linux would then signal it, so
13598    ///   arming it now completes a producer that a later call activates. A
13599    ///   number no task holds as its pid or group (one a session alone still
13600    ///   holds), or one only an exited task holds (a zombie), can never be
13601    ///   signalled.
13602    ///   `kill(-1, 0)` names every process, not group 1, so group 1 is asked
13603    ///   by `getpgid(1)`: pid 1 is Hermit's container init on the in-guest
13604    ///   backends, which never forms a group of its own later, so group 1
13605    ///   exists exactly when its leader is pid 1 (a group outside the PID
13606    ///   namespace reads as 0).
13607    ///
13608    /// Each probe sends nothing and runs in the caller's turn through
13609    /// `record_or_replay`. A probe answered (0, or EPERM for a task the
13610    /// caller may not signal) means it exists; ESRCH means it does not; any
13611    /// other errno (a security module's refusal, say) fails closed: the owner
13612    /// counts as reachable, so the call is refused by name rather than armed
13613    /// unchecked.
13614    async fn owner_can_be_signalled<G: Guest<Self>>(
13615        &self,
13616        guest: &mut G,
13617        owner: crate::host_timed_producers::OwnerLookup,
13618    ) -> Result<bool, Error> {
13619        let who = owner.who;
13620        if who <= 0 {
13621            return Ok(false);
13622        }
13623        let mut probe = async |sysno: Sysno, target: libc::c_int| {
13624            let call = Syscall::from_raw(
13625                sysno,
13626                procfs_levels::raw_args([target as isize as usize, 0, 0, 0, 0, 0]),
13627            );
13628            self.record_or_replay(&mut *guest, call).await
13629        };
13630        let answered = |result: Result<i64, Errno>| !matches!(result, Err(Errno::ESRCH));
13631        if owner.group && who == 1 {
13632            return Ok(match probe(Sysno::getpgid, 1).await {
13633                Ok(leader) => leader == 1,
13634                Err(Errno::ESRCH) => false,
13635                Err(_) => true,
13636            });
13637        }
13638        if owner.group && answered(probe(Sysno::kill, -who).await) {
13639            return Ok(true);
13640        }
13641        if !answered(probe(Sysno::kill, who).await) {
13642            return Ok(false);
13643        }
13644        if !owner.group {
13645            return Ok(true);
13646        }
13647        // A task holds the number as its pid, but leads no group: it can form
13648        // the group later only while it lives. One Detcore saw exit (a zombie
13649        // its parent has not reaped, or one already gone) never will, so its
13650        // number as a group owner can never be signalled. A task Detcore does
13651        // not model (none of the guest's) counts as able to.
13652        let (targets, exited) =
13653            crate::tool_global::resolve_kill_targets_and_exit(guest, DetPid::from_raw(who)).await;
13654        Ok(!targets.is_empty() || exited.is_none())
13655    }
13656
13657    /// What descriptor `fd` already arms toward a host-timed signal producer,
13658    /// and its file type (`st_mode & S_IFMT`), read in the caller's turn by
13659    /// `F_GETFL`, `F_GETOWN_EX` and `fstat`, each through `record_or_replay`,
13660    /// so a replay reads the recorded answers. The guest's own calls set a
13661    /// descriptor's owner and status flags in their turns (an inherited
13662    /// producer is refused at launch), and the reads run in the caller's
13663    /// turn, so the answers are the same in every run. The exception is an
13664    /// open file description the guest shares with a process outside the
13665    /// container (an inherited descriptor whose other holder is still
13666    /// running), which that process can change at a moment host timing
13667    /// decides: outside input of the same class as an external kill, which
13668    /// Hermit does not make deterministic. A descriptor the caller does not
13669    /// hold is EBADF from the first read, and an `O_PATH` one is EBADF, as
13670    /// Linux answers every arming command on one (`check_fcntl_cmd`,
13671    /// `fs/fcntl.c:556-568` and `:571-591`).
13672    async fn descriptor_producer_state<G: Guest<Self>>(
13673        &self,
13674        guest: &mut G,
13675        fd: RawFd,
13676    ) -> Result<(crate::host_timed_producers::ProducerState, libc::mode_t), Error> {
13677        let fcntl = |command: libc::c_int, address: usize| {
13678            Syscall::from_raw(
13679                Sysno::fcntl,
13680                procfs_levels::raw_args([fd as usize, command as usize, address, 0, 0, 0]),
13681            )
13682        };
13683        let flags = self
13684            .record_or_replay(guest, fcntl(libc::F_GETFL, 0))
13685            .await? as libc::c_int;
13686        if flags & libc::O_PATH != 0 {
13687            return Err(Errno::EBADF.into());
13688        }
13689        let mut stack = guest.stack().await;
13690        let owner = stack.reserve::<[libc::c_int; 2]>();
13691        let stat = stack.reserve::<libc::stat>();
13692        let guard = stack.commit()?;
13693        self.record_or_replay(
13694            guest,
13695            fcntl(crate::host_timed_producers::F_GETOWN_EX, owner.as_raw()),
13696        )
13697        .await?;
13698        self.record_or_replay(
13699            guest,
13700            Syscall::from_raw(
13701                Sysno::fstat,
13702                procfs_levels::raw_args([fd as usize, stat.as_raw(), 0, 0, 0, 0]),
13703            ),
13704        )
13705        .await?;
13706        let [kind, pid] = guest.memory().read_value(Addr::from(owner))?;
13707        let stat: libc::stat = guest.memory().read_value(Addr::from(stat))?;
13708        drop(guard);
13709        let file_type = stat.st_mode & libc::S_IFMT;
13710        Ok((
13711            crate::host_timed_producers::ProducerState {
13712                o_async: flags & libc::O_ASYNC != 0,
13713                owner: crate::host_timed_producers::names_an_owner(kind, pid),
13714                socket: file_type == libc::S_IFSOCK,
13715            },
13716            file_type,
13717        ))
13718    }
13719
13720    /// `call`, unless it is an `fcntl` that sets a descriptor's signal owner
13721    /// to one of Hermit's tasks ([`Self::refuse_hermit_owner`]): `F_SETOWN`
13722    /// with a positive pid, or `F_SETOWN_EX` naming a thread (`F_OWNER_TID`)
13723    /// or a process (`F_OWNER_PID`).
13724    ///
13725    /// `F_SETOWN` passes the owner by value; it is read from the raw argument,
13726    /// because the pinned Reverie's typed `FcntlCmd::F_SETOWN` carries none.
13727    /// `F_SETOWN_EX` passes it through guest memory, which another guest
13728    /// thread could change between this check and the kernel's own read, so
13729    /// the `f_owner_ex` is read once and the kernel is handed Detcore's copy,
13730    /// on this thread's scratch stack. An `f_owner_ex` that cannot be read is
13731    /// EFAULT here, as the kernel would answer, rather than a pointer passed
13732    /// on for the kernel to read later. The returned guard keeps the copy
13733    /// alive (SaBRe's scratch stack is an arena it owns) and must be held
13734    /// until the call has run.
13735    async fn screen_fcntl_owner<G: Guest<Self>>(
13736        &self,
13737        guest: &mut G,
13738        call: syscalls::Fcntl,
13739    ) -> Result<(syscalls::Fcntl, Option<<G::Stack as Stack>::StackGuard>), Error> {
13740        // Linux's include/uapi/asm-generic/fcntl.h; the pinned libc lacks them.
13741        const F_OWNER_TID: libc::c_int = 0;
13742        const F_OWNER_PID: libc::c_int = 1;
13743        match call.cmd() {
13744            F_SETOWN => {
13745                let (_, args) = call.into_parts();
13746                self.refuse_hermit_owner(guest, call.fd(), "F_SETOWN", args.arg2 as libc::c_int)
13747                    .await?;
13748                Ok((call, None))
13749            }
13750            F_SETOWN_EX(owner) => {
13751                let owner: Addr<[libc::c_int; 2]> = owner.ok_or(Errno::EFAULT)?.cast();
13752                let copy = guest.memory().read_value(owner)?;
13753                if matches!(copy[0], F_OWNER_TID | F_OWNER_PID) {
13754                    self.refuse_hermit_owner(guest, call.fd(), "F_SETOWN_EX", copy[1])
13755                        .await?;
13756                }
13757                let mut stack = guest.stack().await;
13758                let copied: Addr<[libc::c_int; 2]> = stack.push(copy);
13759                let guard = stack.commit()?;
13760                Ok((call.with_cmd(F_SETOWN_EX(Some(copied.cast()))), Some(guard)))
13761            }
13762            _ => Ok((call, None)),
13763        }
13764    }
13765
13766    /// `call`, unless it is an `ioctl` that sets a descriptor's signal owner
13767    /// to one of Hermit's tasks ([`Self::refuse_hermit_owner`]): `FIOSETOWN`
13768    /// or `SIOCSPGRP` with a positive pid, which set the owner as `F_SETOWN`
13769    /// does. The owner is passed through guest memory, so, as for
13770    /// `F_SETOWN_EX` ([`Self::screen_fcntl_owner`]), it is read once and the
13771    /// kernel is handed Detcore's copy, kept alive by the returned guard;
13772    /// an unreadable one is EFAULT.
13773    async fn screen_ioctl_owner<G: Guest<Self>>(
13774        &self,
13775        guest: &mut G,
13776        call: syscalls::Ioctl,
13777    ) -> Result<(syscalls::Ioctl, Option<<G::Stack as Stack>::StackGuard>), Error> {
13778        let (command, owner) = match call.request() {
13779            syscalls::ioctl::Request::FIOSETOWN(owner) => ("FIOSETOWN", owner),
13780            syscalls::ioctl::Request::SIOCSPGRP(owner) => ("SIOCSPGRP", owner),
13781            _ => return Ok((call, None)),
13782        };
13783        let copy: libc::c_int = guest.memory().read_value(owner.ok_or(Errno::EFAULT)?)?;
13784        self.refuse_hermit_owner(guest, call.fd(), command, copy)
13785            .await?;
13786        let mut stack = guest.stack().await;
13787        let copied = stack.push(copy);
13788        let guard = stack.commit()?;
13789        let request = match call.request() {
13790            syscalls::ioctl::Request::FIOSETOWN(_) => {
13791                syscalls::ioctl::Request::FIOSETOWN(Some(copied))
13792            }
13793            _ => syscalls::ioctl::Request::SIOCSPGRP(Some(copied)),
13794        };
13795        Ok((call.with_request(request), Some(guard)))
13796    }
13797
13798    /// ioctl system call
13799    pub async fn handle_ioctl<G: Guest<Self>>(
13800        &self,
13801        guest: &mut G,
13802        call: syscalls::Ioctl,
13803    ) -> Result<i64, Error> {
13804        let fd = call.fd();
13805        // Setting a virtual timerfd's host tick count would make the host
13806        // disagree with Detcore's count (`crate::syscalls::timerfd`).
13807        if Syscall::Ioctl(call).into_parts().1.arg1 as u64
13808            == crate::syscalls::timerfd::TFD_IOC_SET_TICKS
13809            && crate::syscalls::timerfd::enabled(guest)
13810            && crate::syscalls::timerfd::is_timerfd(guest, fd)
13811        {
13812            crate::syscalls::timerfd::refuse(format_args!(
13813                "ioctl(TFD_IOC_SET_TICKS) on timerfd {fd} would set the host's tick count"
13814            ));
13815        }
13816        // As in `handle_fcntl`: the one request that would complete a
13817        // host-timed signal producer, on an in-guest run's default route,
13818        // before the owner screen takes its copy on the scratch stack.
13819        if let Some((arming, owner)) = self.ioctl_producer_arming(guest, call) {
13820            self.refuse_host_timed_producer(guest, fd, arming, owner)
13821                .await?;
13822        }
13823        // Before anything else, so a refused call has no other effect.
13824        let (call, _owner_copy) = self.screen_ioctl_owner(guest, call).await?;
13825        // As in `handle_fcntl`: an asynchronous-I/O owner, and a session whose
13826        // controlling terminal hangs up, are signalled at a moment set by host
13827        // timing, so hold their signals in every process.
13828        let signals = ioctl_host_timed_signals(call.request());
13829        if signals != 0 && guest.config().backend.host_timed_signals_are_physical {
13830            record_host_timed_signals(guest, HostTimedSignalScope::Container, signals).await;
13831        }
13832        let (cloexec, nonblocking) = match call.request() {
13833            // AUTONOMOUS-BOT-IMPLEMENTED
13834            // TODO-HUMAN-REVIEW(PR-1142): Review deterministic SIOCETHTOOL rejection.
13835            // Ethernet link state belongs to the host network namespace and can change between
13836            // runs. Match record/replay's established policy instead of exposing that state.
13837            syscalls::ioctl::Request::SIOCETHTOOL(_) => return Err(Errno::ENODEV.into()),
13838            syscalls::ioctl::Request::FIOCLEX => (Some(true), None),
13839            syscalls::ioctl::Request::FIONCLEX => (Some(false), None),
13840            syscalls::ioctl::Request::FIONBIO(value) => {
13841                let enabled = guest.memory().read_value(value.ok_or(Errno::EFAULT)?)? != 0;
13842                (None, Some(enabled))
13843            }
13844            _ => (None, None),
13845        };
13846
13847        // AUTONOMOUS-BOT-IMPLEMENTED
13848        // TODO-HUMAN-REVIEW(PR-1013): Review logical FIONBIO handling for forced fds.
13849        // Detcore already keeps scheduler-managed fds physically nonblocking. Satisfy
13850        // FIONBIO logically instead of forwarding it: some backends cannot apply the
13851        // ioctl to their proxied pipe fd, and clearing it would violate the scheduler's
13852        // nonblockize-and-retry invariant. This mirrors F_SETFL's forced state split.
13853        if let Some(enabled) = nonblocking {
13854            let (fd_type, internal_socket, physically_nonblocking, inherited_external_read) =
13855                guest.thread_state().with_detfd(fd, |detfd| {
13856                    (
13857                        detfd.ty(),
13858                        detfd.is_internal_socket(),
13859                        detfd.physically_nonblocking(),
13860                        detfd.inherited_external_read().is_some(),
13861                    )
13862                })?;
13863            if self.keeps_physically_nonblocking(
13864                fd_type,
13865                internal_socket,
13866                physically_nonblocking,
13867                inherited_external_read,
13868            ) && physically_nonblocking
13869            {
13870                guest.thread_state().with_detfd(fd, |detfd| {
13871                    detfd.set_logical_nonblocking(enabled);
13872                })?;
13873                return Ok(0);
13874            }
13875        }
13876
13877        let request = Syscall::Ioctl(call).into_parts().1.arg1 as u64;
13878        let argument = Syscall::Ioctl(call).into_parts().1.arg2;
13879        // A binder read can install descriptors the transaction carried, which
13880        // no call names as new (`crate::syscalls::binder`). Read the call's
13881        // read_consumed before it runs, so only the commands it writes are
13882        // parsed; the in-guest backends discover such a descriptor
13883        // themselves, and parse nothing here.
13884        // AUTONOMOUS-BOT-IMPLEMENTED
13885        // Record/replay restores only the outer ioctl structure, not the
13886        // nested command buffer or installed descriptors. Preserve that
13887        // existing Binder limitation without adding receipt-probe events.
13888        let binder_read = crate::syscalls::binder::is_write_read(request)
13889            && !self.cfg.recordreplay_modes
13890            && !self.discovers_received_descriptors(guest);
13891        let already_consumed = if binder_read {
13892            crate::syscalls::binder::consumed_before(&guest.memory(), argument)
13893        } else {
13894            0
13895        };
13896        // A transaction may pass any descriptor of this table.
13897        if crate::syscalls::binder::is_write_read(request) {
13898            self.note_binder_transaction(guest);
13899        }
13900        let result = self.record_or_replay(guest, call).await?;
13901        // `handle_ioctl` runs the call in the caller's turn, so no other
13902        // task can have closed or replaced a delivered number yet.
13903        // TODO-HUMAN-REVIEW(PR-4736)
13904        if binder_read && result >= 0 {
13905            let delivered =
13906                crate::syscalls::binder::delivered(&guest.memory(), argument, already_consumed);
13907            let recorded = self
13908                .record_received_descriptors(
13909                    guest,
13910                    &delivered,
13911                    crate::syscalls::scm_rights::Channel::Binder,
13912                    true,
13913                    // Binder installs every descriptor it delivers
13914                    // close-on-exec (`binder_apply_fd_fixups`).
13915                    true,
13916                    crate::syscalls::scm_rights::ReceivedFrom {
13917                        socket: fd,
13918                        peek: false,
13919                        // No `SCM_RIGHTS` message is queued to a binder
13920                        // descriptor, so no count of messages taken matters.
13921                        messages: 1,
13922                    },
13923                )
13924                .await?;
13925            self.refresh_received_locks(guest, &recorded, LockCall::BinderWriteRead)
13926                .await?;
13927        }
13928        // A descriptor Detcore lists without modelling keeps its
13929        // close-on-exec flag too (`FileMetadata::present_unmodelled`).
13930        let unmodelled =
13931            cloexec.is_some_and(|enabled| guest.thread_state().set_unmodelled_cloexec(fd, enabled));
13932        if !unmodelled && (cloexec.is_some() || nonblocking.is_some()) {
13933            guest.thread_state().with_detfd(fd, |detfd| {
13934                if let Some(enabled) = cloexec {
13935                    detfd.set_cloexec(enabled);
13936                }
13937                if let Some(enabled) = nonblocking {
13938                    detfd.set_nonblocking(enabled);
13939                }
13940            })?;
13941        }
13942        Ok(result)
13943    }
13944
13945    /// statfs: report deterministic filesystem statistics.
13946    ///
13947    /// The kernel's `statfs` reflects live host state: the free-block counts
13948    /// (`f_bfree`, `f_bavail`), the free-inode count (`f_ffree`) and the device
13949    /// id (`f_fsid`) all vary between runs as the underlying host filesystem
13950    /// fills and drains, which makes a bare passthrough diverge under `--verify`
13951    /// (e.g. `tar` calls statfs on its target filesystem). The static geometry
13952    /// of the mount (`f_type`, `f_bsize`, `f_blocks`, `f_namelen`, ...) is
13953    /// reproducible, so we run the real syscall and then canonicalize only the
13954    /// volatile fields.
13955    pub async fn handle_statfs<G: Guest<Self>>(
13956        &self,
13957        guest: &mut G,
13958        call: syscalls::Statfs,
13959    ) -> Result<i64, Error> {
13960        if let Some(pointer) = call.path() {
13961            let path: PathBuf = pointer.read(&guest.memory())?;
13962            Self::prepare_guest_cwd(guest, libc::AT_FDCWD, &path).await;
13963            let observed = Self::observed_path(guest, libc::AT_FDCWD, &path)?;
13964            if self.rendered_procfs_at(guest, &observed).await?.is_some()
13965                || Box::pin(Self::names_placeholder_directory(guest, &observed)).await
13966                || self
13967                    .placeholder_reached(guest, libc::AT_FDCWD, Some(pointer), true)
13968                    .await
13969                    .is_some()
13970            {
13971                self.write_procfs_statfs(guest, call.buf())?;
13972                return Ok(0);
13973            }
13974        }
13975        let ret = self.record_or_replay(guest, call).await?;
13976        self.canonicalize_statfs_buf(guest, call.buf())?;
13977        Ok(ret)
13978    }
13979
13980    /// fstatfs: same determinization as [`Self::handle_statfs`], keyed on an fd.
13981    pub async fn handle_fstatfs<G: Guest<Self>>(
13982        &self,
13983        guest: &mut G,
13984        call: syscalls::Fstatfs,
13985    ) -> Result<i64, Error> {
13986        let rendered = guest
13987            .thread_state()
13988            .with_detfd(call.fd(), |detfd| detfd.procfs_rendered_path().is_some())
13989            .unwrap_or(false);
13990        if rendered {
13991            self.write_procfs_statfs(guest, call.buf())?;
13992            return Ok(0);
13993        }
13994        let ret = self.record_or_replay(guest, call).await?;
13995        self.canonicalize_statfs_buf(guest, call.buf())?;
13996        Ok(ret)
13997    }
13998
13999    // AUTONOMOUS-BOT-IMPLEMENTED
14000    // TODO-HUMAN-REVIEW(#1851): Determinized ownership mutation. Emulate the
14001    // IDENTITY half of the call and let Linux answer the ARGUMENT half.
14002    /// The `chown` family (`chown`, `fchown`, `fchownat`, `lchown`).
14003    ///
14004    /// Detcore presents a fixed virtual-root identity, so the *permission*
14005    /// answer must be the one a real root gets: success, for any uid. But root
14006    /// privilege affects only the ownership permission check — it does not
14007    /// waive pathname, descriptor, or flag errors. A real root's
14008    /// `chown("/does/not/exist", 0, 0)` still fails with `ENOENT`.
14009    ///
14010    /// So this does not fabricate a bare `Ok(0)`. It translates the mutation
14011    /// into a side-effect-free metadata lookup with the same target-selection
14012    /// arguments, and reports success only if that lookup succeeds:
14013    ///
14014    /// * `F_GETFL` validates `fchown`'s descriptor and distinguishes an
14015    ///   `O_PATH` descriptor (valid for `fstat`, invalid for `fchown`);
14016    ///   `newfstatat` performs the corresponding path walk for the three
14017    ///   pathname variants, preserving `ENOENT`, `ENOTDIR`, `ELOOP`,
14018    ///   `ENAMETOOLONG`, `EFAULT`, and `EBADF`;
14019    /// * `fchownat` flags are checked explicitly before the lookup, so an
14020    ///   unsupported flag still returns `EINVAL` rather than being accepted by
14021    ///   a metadata syscall with a wider flag vocabulary;
14022    /// * the ownership assignment itself is not performed, but the *other*
14023    ///   consequences of a successful chown are, because Linux applies them
14024    ///   even when ownership does not change. Measured on this host with
14025    ///   `chown(path, -1, -1)`: mode `06755`, `04755` and `02755` all become
14026    ///   `0755`; `02644` keeps `S_ISGID` because the file is not
14027    ///   group-executable; a directory at `06755` keeps both bits; and ctime
14028    ///   moves in every one of those cases, including the plain `0644` file
14029    ///   with nothing to clear. Skipping that is a privilege-containment
14030    ///   regression, not a bookkeeping one: a guest that builds a setuid
14031    ///   binary and chowns it would see the setuid bit survive under hermit
14032    ///   and be cleared on the kernel.
14033    ///
14034    /// Rather than reimplement that rule, the consequence is delegated to the
14035    /// kernel by reissuing the *same* call from the same family with the
14036    /// `(-1, -1)` sentinel, which is precisely the operation whose only effects
14037    /// are `ATTR_CTIME | ATTR_KILL_SUID | ATTR_KILL_SGID`. Delegation gets the
14038    /// directory exemption, the group-executable condition on `S_ISGID`, and
14039    /// symlink handling right for free, and cannot drift from the kernel the
14040    /// way a transcribed rule would.
14041    ///
14042    /// Routed through `record_or_replay` rather than `inject`, so a replay does
14043    /// not need the guest's filesystem to still exist.
14044    ///
14045    /// **Semantic boundary, stated explicitly.** Detcore does not model
14046    /// per-file ownership, so the success is not observable through a later
14047    /// `stat`. A guest that chowns to a foreign uid and reads the owner back
14048    /// sees the unchanged owner — a divergence a single-uid container cannot
14049    /// avoid, and strictly smaller than the status quo in which the guest
14050    /// believes it is root and cannot chown at all.
14051    ///
14052    /// **Residual, also stated.** This emulates ownership permission and target
14053    /// validation, not every write-time filesystem policy. In particular a
14054    /// target on a read-only mount can pass the metadata lookup where a real
14055    /// chown would return `EROFS`. Path resolution also still requires search
14056    /// permission on the parent directories, so `EACCES` remains a function of
14057    /// the host identity under `--no-namespace`. That exposure is shared with
14058    /// every pass-through filesystem syscall (`open`, `stat`, `chmod`) and is
14059    /// not introduced here; it is recorded so the boundary is not overstated.
14060    ///
14061    /// **Residual introduced by the delegation, stated too.** The sentinel call
14062    /// needs the same `inode_owner_or_capable` permission the mode change does,
14063    /// so on a target the guest does not own it returns `EPERM` and that errno
14064    /// is propagated. Reporting a successful chown while silently failing to
14065    /// apply the consequence the kernel guarantees would be the same defect in
14066    /// a narrower form, so this fails closed instead. It is not a regression:
14067    /// that is exactly the case in which the pass-through implementation also
14068    /// returned `EPERM`. The case this change exists to fix — a guest chowning
14069    /// a file it created — is the owning case, and it succeeds.
14070    ///
14071    /// The behavioural contract is bracketed end to end by
14072    /// `hermit-cli/tests/chown_virtual_root_identity.rs`; the unit tests in
14073    /// `syscall_classification` pin membership only and cannot see this
14074    /// function's result.
14075    pub async fn handle_ownership_change_noop<G: Guest<Self>>(
14076        &self,
14077        guest: &mut G,
14078        call: Syscall,
14079    ) -> Result<i64, Error> {
14080        // Unreachable while `is_ownership_change_noop_syscall` and the matches
14081        // below name the same four syscalls. Fail closed if they drift: "not
14082        // attempted" must never be observationally identical to a validated
14083        // emulated success.
14084        let ownership_change = match call {
14085            Syscall::Fchown(_) | Syscall::Fchownat(_) => true,
14086            #[cfg(target_arch = "x86_64")]
14087            Syscall::Chown(_) | Syscall::Lchown(_) => true,
14088            _ => false,
14089        };
14090        if !ownership_change {
14091            warn!(
14092                "ownership-change no-op reached with an unexpected syscall {:?}; \
14093                 refusing unvalidated success",
14094                call.number()
14095            );
14096            return Err(Error::Errno(Errno::ENOSYS));
14097        }
14098
14099        // Flag errors precede path resolution in the kernel, so check first.
14100        if let Syscall::Fchownat(c) = &call {
14101            let allowed = AtFlags::AT_EMPTY_PATH | AtFlags::AT_SYMLINK_NOFOLLOW;
14102            if c.flags().bits() & !allowed.bits() != 0 {
14103                return Err(Error::Errno(Errno::EINVAL));
14104            }
14105        }
14106
14107        // Argument half. `F_GETFL` answers it for `fchown`: it validates the
14108        // descriptor and distinguishes an `O_PATH` descriptor, which `fstat`
14109        // accepts and `fchown` rejects. For the three pathname variants a
14110        // `newfstatat` with the same target-selection arguments performs the
14111        // corresponding path walk.
14112        if let Syscall::Fchown(c) = &call {
14113            let flags = self
14114                .record_or_replay(
14115                    guest,
14116                    syscalls::Fcntl::new().with_fd(c.fd()).with_cmd(F_GETFL),
14117                )
14118                .await?;
14119            if flags & i64::from(OFlag::O_PATH.bits()) != 0 {
14120                return Err(Error::Errno(Errno::EBADF));
14121            }
14122        } else {
14123            let mut stack = guest.stack().await;
14124            let statptr: StatPtr = StatPtr(stack.reserve());
14125            stack.commit()?;
14126
14127            let validate = match &call {
14128                #[cfg(target_arch = "x86_64")]
14129                Syscall::Chown(c) => crate::arch::fstatat_syscall(
14130                    syscalls::Fstatat::new()
14131                        .with_dirfd(libc::AT_FDCWD)
14132                        .with_path(c.path())
14133                        .with_stat(Some(statptr))
14134                        .with_flags(AtFlags::empty()),
14135                ),
14136                #[cfg(target_arch = "x86_64")]
14137                Syscall::Lchown(c) => crate::arch::fstatat_syscall(
14138                    syscalls::Fstatat::new()
14139                        .with_dirfd(libc::AT_FDCWD)
14140                        .with_path(c.path())
14141                        .with_stat(Some(statptr))
14142                        .with_flags(AtFlags::AT_SYMLINK_NOFOLLOW),
14143                ),
14144                Syscall::Fchownat(c) => crate::arch::fstatat_syscall(
14145                    syscalls::Fstatat::new()
14146                        .with_dirfd(c.dirfd())
14147                        .with_path(c.path())
14148                        .with_stat(Some(statptr))
14149                        .with_flags(c.flags()),
14150                ),
14151                _ => return Err(Error::Errno(Errno::ENOSYS)),
14152            };
14153
14154            // The errno of the side-effect-free validating call is the guest's
14155            // answer; only an actually executed successful validation becomes the
14156            // emulated success. Clear the scratch output on both paths.
14157            let result = self.record_or_replay(guest, validate).await;
14158            guest
14159                .memory()
14160                .write_exact(statptr.0.cast(), &[0; std::mem::size_of::<libc::stat>()])?;
14161            result?;
14162        }
14163
14164        // Metadata half, delegated to the kernel. The identity assignment is
14165        // deliberately not performed, but everything else a successful chown
14166        // does is, by reissuing the same call with the `(-1, -1)` sentinel:
14167        // clear `S_ISUID`, clear `S_ISGID` on a group-executable file, exempt
14168        // directories, and move ctime unconditionally. Letting Linux apply its
14169        // own rule keeps it from drifting here.
14170        const KEEP_ID: libc::uid_t = libc::uid_t::MAX;
14171        let consequence = match &call {
14172            Syscall::Fchown(c) => Syscall::Fchown(
14173                syscalls::Fchown::new()
14174                    .with_fd(c.fd())
14175                    .with_owner(KEEP_ID)
14176                    .with_group(KEEP_ID),
14177            ),
14178            #[cfg(target_arch = "x86_64")]
14179            Syscall::Chown(c) => Syscall::Chown(
14180                syscalls::Chown::new()
14181                    .with_path(c.path())
14182                    .with_owner(KEEP_ID)
14183                    .with_group(KEEP_ID),
14184            ),
14185            #[cfg(target_arch = "x86_64")]
14186            Syscall::Lchown(c) => Syscall::Lchown(
14187                syscalls::Lchown::new()
14188                    .with_path(c.path())
14189                    .with_owner(KEEP_ID)
14190                    .with_group(KEEP_ID),
14191            ),
14192            Syscall::Fchownat(c) => Syscall::Fchownat(
14193                syscalls::Fchownat::new()
14194                    .with_dirfd(c.dirfd())
14195                    .with_path(c.path())
14196                    .with_owner(KEEP_ID)
14197                    .with_group(KEEP_ID)
14198                    .with_flags(c.flags()),
14199            ),
14200            _ => return Err(Error::Errno(Errno::ENOSYS)),
14201        };
14202        self.record_or_replay(guest, consequence).await?;
14203        Ok(0)
14204    }
14205
14206    /// Overwrite the host-varying fields of a `statfs` result buffer with fixed
14207    /// values, leaving the static per-mount geometry intact. Shared by statfs
14208    /// and fstatfs. A null buffer (only possible on an error return, which the
14209    /// caller has already propagated) is a no-op.
14210    fn canonicalize_statfs_buf<G: Guest<Self>>(
14211        &self,
14212        guest: &mut G,
14213        buf: Option<AddrMut<libc::statfs>>,
14214    ) -> Result<(), Error> {
14215        // Fixed *caps* for the volatile counters. The exact values are
14216        // arbitrary; they only need to be constant so repeated runs agree. We
14217        // clamp each free count to the mount's (static) total so we never report
14218        // the impossible "free > total": a filesystem may be smaller than the
14219        // cap, and some (e.g. overlayfs) report no inode accounting at all
14220        // (`f_files == 0`).
14221        const FREE_BLOCKS_CAP: libc::fsblkcnt_t = 1_000_000;
14222        const FREE_INODES_CAP: libc::fsfilcnt_t = 500_000;
14223
14224        if let Some(buf) = buf {
14225            let mut sf = guest.memory().read_value(buf)?;
14226            let free_blocks = FREE_BLOCKS_CAP.min(sf.f_blocks);
14227            sf.f_bfree = free_blocks;
14228            sf.f_bavail = free_blocks;
14229            // `f_files == 0` means the filesystem does not track inodes; keep the
14230            // free count at 0 rather than inventing free inodes on a mount that
14231            // reports none.
14232            sf.f_ffree = if sf.f_files == 0 {
14233                0
14234            } else {
14235                FREE_INODES_CAP.min(sf.f_files)
14236            };
14237            // f_fsid is a device-dependent filesystem identifier; zero it. An
14238            // all-zero bit pattern is a valid `fsid_t` (a POD id pair).
14239            sf.f_fsid = unsafe { std::mem::zeroed() };
14240            guest.memory().write_value(buf, &sf)?;
14241        }
14242        Ok(())
14243    }
14244
14245    /// dup system call.
14246    pub async fn handle_dup<G: Guest<Self>>(
14247        &self,
14248        guest: &mut G,
14249        call: syscalls::Dup,
14250    ) -> Result<i64, Errno> {
14251        let old_fd = call.oldfd();
14252        let new_fd = self.record_or_replay(guest, call).await? as RawFd;
14253        let lock_tracked = self.lock_tracked_descriptions(guest, new_fd..=new_fd);
14254        let replaced = guest
14255            .thread_state_mut()
14256            .dup_fd(old_fd, new_fd, OFlag::empty())?;
14257        if let Some(open_file_id) = replaced {
14258            self.release_port_for_open_file(guest, open_file_id).await;
14259        }
14260        self.report_lock_references(guest, lock_tracked).await;
14261        Ok(new_fd as i64)
14262    }
14263
14264    /// dup2 system call.
14265    #[cfg(target_arch = "x86_64")]
14266    pub async fn handle_dup2<G: Guest<Self>>(
14267        &self,
14268        guest: &mut G,
14269        call: syscalls::Dup2,
14270    ) -> Result<i64, Error> {
14271        let old_fd = call.oldfd();
14272        let new_fd = call.newfd();
14273        if let Some(refusal) = self
14274            .refuse_dup_over_waited_listener(guest, Sysno::dup2, old_fd, new_fd)
14275            .await
14276        {
14277            return refusal;
14278        }
14279        // `dup2` over another open descriptor closes it.
14280        let closed_end = (old_fd != new_fd)
14281            .then(|| self.pipe_channel_end(guest, new_fd))
14282            .flatten();
14283        let res = self
14284            .record_or_replay(guest, call)
14285            .await
14286            .inspect_err(|errno| Self::note_failed_dup(guest, old_fd, new_fd, *errno))?;
14287        // Reported as soon as the kernel replaced `new_fd`, before Detcore's
14288        // own table update, which can still fail for a descriptor it does not
14289        // track (`Scheduler::note_descriptors_replaced`).
14290        if old_fd != new_fd
14291            && let Ok(new_fd) = u32::try_from(new_fd)
14292        {
14293            crate::tool_global::descriptors_replaced(guest, new_fd, new_fd).await;
14294        }
14295        let lock_tracked = if old_fd == new_fd {
14296            Vec::new()
14297        } else {
14298            self.lock_tracked_descriptions(guest, new_fd..=new_fd)
14299        };
14300        let replaced = self
14301            .dup_fd_after_replacement(guest, old_fd, new_fd, OFlag::empty(), lock_tracked.clone())
14302            .await?;
14303        if let Some(open_file_id) = replaced {
14304            self.release_port_for_open_file(guest, open_file_id).await;
14305        }
14306        self.report_lock_references(guest, lock_tracked).await;
14307        self.note_channel_end_closed(guest, closed_end).await;
14308        Ok(res)
14309    }
14310
14311    /// A `dup2` or `dup3` of `old_fd` to `new_fd` that failed with `errno`:
14312    /// one that failed with `EBADF` for an old descriptor that is not open
14313    /// grew the table to hold `new_fd` first, as `ksys_dup3` expands it
14314    /// before it looks `old_fd` up
14315    /// (<https://github.com/torvalds/linux/blob/v6.17/fs/file.c#L1408-L1415>),
14316    /// unless `new_fd` was at or above `RLIMIT_NOFILE`, which it refuses
14317    /// first (`ThreadState::note_failed_dup_target`). The flags, and the same
14318    /// descriptor twice, are refused before (`EINVAL`).
14319    fn note_failed_dup<G: Guest<Self>>(guest: &G, old_fd: RawFd, new_fd: RawFd, errno: Errno) {
14320        if errno == Errno::EBADF && old_fd != new_fd {
14321            guest.thread_state().note_failed_dup_target(new_fd);
14322        }
14323    }
14324
14325    /// Detcore's table update after the kernel's `dup2` or `dup3` put
14326    /// `old_fd`'s description in slot `new_fd`. If Detcore keeps no model of
14327    /// `old_fd` (one received over `SCM_RIGHTS` and not recorded,
14328    /// `Detcore::record_received_descriptors`), the update fails,
14329    /// but the kernel replaced the slot all the same: its old description is
14330    /// dropped from the model and `lock_tracked` (what the slot named) is
14331    /// reported to `procfs::locks::LockLedger`, so no stale entry can admit
14332    /// a host's identical `/proc/locks` row. The error is returned as before.
14333    async fn dup_fd_after_replacement<G: Guest<Self>>(
14334        &self,
14335        guest: &mut G,
14336        old_fd: RawFd,
14337        new_fd: RawFd,
14338        flags: OFlag,
14339        lock_tracked: Vec<OpenFileId>,
14340    ) -> Result<Option<OpenFileId>, Error> {
14341        match guest.thread_state_mut().dup_fd(old_fd, new_fd, flags) {
14342            Ok(replaced) => Ok(replaced),
14343            Err(errno) => {
14344                if old_fd != new_fd {
14345                    let displaced = guest.thread_state().remove_fd(new_fd);
14346                    if let Some(open_file_id) = displaced {
14347                        self.release_port_for_open_file(guest, open_file_id).await;
14348                    }
14349                    self.report_lock_references(guest, lock_tracked).await;
14350                }
14351                Err(errno.into())
14352            }
14353        }
14354    }
14355
14356    /// Refuse a `dup2` or `dup3` that would close `new_fd` while a blocking
14357    /// accept waits on it. With `old_fd == new_fd` neither call closes
14358    /// anything. The check comes before the call, so a call that would fail
14359    /// for another reason (a bad `old_fd`) is refused as well.
14360    async fn refuse_dup_over_waited_listener<G: Guest<Self>>(
14361        &self,
14362        guest: &mut G,
14363        sysno: Sysno,
14364        old_fd: RawFd,
14365        new_fd: RawFd,
14366    ) -> Option<Result<i64, Error>> {
14367        if old_fd == new_fd {
14368            return None;
14369        }
14370        let new_fd = u32::try_from(new_fd).ok()?;
14371        self.refuse_close_of_waited_listener(guest, sysno, new_fd..=new_fd)
14372            .await
14373    }
14374
14375    /// dup3 system call.
14376    pub async fn handle_dup3<G: Guest<Self>>(
14377        &self,
14378        guest: &mut G,
14379        call: syscalls::Dup3,
14380    ) -> Result<i64, Error> {
14381        let old_fd = call.oldfd();
14382        let new_fd = call.newfd();
14383        let flags = call.flags();
14384        if let Some(refusal) = self
14385            .refuse_dup_over_waited_listener(guest, Sysno::dup3, old_fd, new_fd)
14386            .await
14387        {
14388            return refusal;
14389        }
14390        // `dup3` over another open descriptor closes it.
14391        let closed_end = (old_fd != new_fd)
14392            .then(|| self.pipe_channel_end(guest, new_fd))
14393            .flatten();
14394        let res = self
14395            .record_or_replay(guest, call)
14396            .await
14397            .inspect_err(|errno| Self::note_failed_dup(guest, old_fd, new_fd, *errno))?;
14398        // Reported before Detcore's own table update, as for `dup2`.
14399        if let Ok(new_fd) = u32::try_from(new_fd) {
14400            crate::tool_global::descriptors_replaced(guest, new_fd, new_fd).await;
14401        }
14402        let lock_tracked = self.lock_tracked_descriptions(guest, new_fd..=new_fd);
14403        let replaced = self
14404            .dup_fd_after_replacement(guest, old_fd, new_fd, flags, lock_tracked.clone())
14405            .await?;
14406        if let Some(open_file_id) = replaced {
14407            self.release_port_for_open_file(guest, open_file_id).await;
14408        }
14409        self.report_lock_references(guest, lock_tracked).await;
14410        self.note_channel_end_closed(guest, closed_end).await;
14411        Ok(res)
14412    }
14413
14414    /// pipe2 system call.
14415    pub async fn handle_pipe2<G: Guest<Self>>(
14416        &self,
14417        guest: &mut G,
14418        call: syscalls::Pipe2,
14419    ) -> Result<i64, Error> {
14420        // Pipes are unambiguously container-internal: both endpoints are owned by
14421        // guest processes. Make them physically nonblocking whenever we sequentialize
14422        // threads -- INCLUDING record/replay modes. This lets a potentially-blocking
14423        // pipe read follow the deterministic nonblockize-and-retry (InternalIOPolling)
14424        // path instead of being descheduled as BlockingExternalIO. A pipe reader and
14425        // its paired writer are NOT independent, so treating an internal pipe as
14426        // "external blocking IO" (safe to run in the background and rejoin whenever)
14427        // deadlocks the sequentialized scheduler in R/R (the documented pipe hang). The
14428        // physical O_NONBLOCK is Detcore-internal and invisible to the guest (F_GETFL is
14429        // virtualized), and mirrors what `hermit run --strict` already does for pipes.
14430        let internally_nonblocking = self.cfg.use_nonblocking_sockets();
14431        let injected = if internally_nonblocking {
14432            call.with_flags(call.flags() | OFlag::O_NONBLOCK)
14433        } else {
14434            call
14435        };
14436        // NO PRE-CALL READ OF `pipefd`. This is load-bearing on three backends, not a style
14437        // choice. `record_or_replay` below returns early on failure, so every guest memory
14438        // access AFTER it runs only when pipe2 SUCCEEDED -- and a successful pipe2 means the
14439        // kernel itself wrote two ints there, which proves the address valid. A pre-call
14440        // snapshot would be the only access to a kernel-UNVALIDATED address, and `LocalMemory`
14441        // (reverie-dbt, reverie-e9patch, reverie-liteinst) implements reads as an unsafe
14442        // `copy_nonoverlapping` that always returns `Ok`: a bad pointer is a hardware fault
14443        // that `Result::ok` cannot catch, so the guest would die with SIGSEGV before Linux
14444        // could report EFAULT. Letting the kernel touch `pipefd` first is also what keeps its
14445        // argument-validation precedence intact -- flags are checked before the pointer, so a
14446        // bad pointer with bad flags is EINVAL and with good flags EFAULT.
14447        // A C guest asserting that precedence directly is being added separately.
14448        let res = self.record_or_replay(guest, injected).await?;
14449        let memory = guest.memory();
14450
14451        if let Some(pipefd) = call.pipefd() {
14452            let fds: [i32; 2] = memory.read_value(pipefd)?;
14453            if internally_nonblocking {
14454                // Where host load decides a new pipe's size, no pin is the same in every
14455                // run, even one that happens to succeed (a pipe created under the quota
14456                // already has two pages).
14457                let capacity_result = if host_pipe_floor() {
14458                    guest
14459                        .inject(
14460                            syscalls::Fcntl::new()
14461                                .with_fd(fds[0])
14462                                .with_cmd(F_SETPIPE_SZ(deterministic_pipe_capacity_bytes())),
14463                        )
14464                        .await
14465                } else {
14466                    error!(
14467                        "{}",
14468                        below_two_page_pipe_floor("a new pipe cannot be pinned")
14469                    );
14470                    Err(Errno::EPERM)
14471                };
14472                if let Some(failure) = pipe_capacity_failure(fds, capacity_result) {
14473                    // Release the descriptors Linux already created, THEN stop the run.
14474                    //
14475                    // Returning `Err` here -- what this code did before -- unwinds without
14476                    // closing them. `pipe2` has already succeeded, so both descriptors are
14477                    // live in the guest and are not yet registered with `add_fd`, which means
14478                    // Detcore's own bookkeeping never learns they exist. Closing them is the
14479                    // only way the guest's descriptor table matches Detcore's model.
14480                    //
14481                    // We do NOT fabricate a `pipe2` errno. Linux leaves `pipefd` untouched
14482                    // when `pipe2` fails, so inventing a failure would oblige us to restore
14483                    // the caller's buffer, which needs the pre-call snapshot the comment above
14484                    // explains we must never take. And returning success with an unpinned pipe
14485                    // silently restores exactly the host-dependent capacity this path exists
14486                    // to remove. An unpinnable pipe means determinism is unavailable for this
14487                    // run, so fail closed and loudly rather than quietly.
14488                    //
14489                    // Defensive, not expected: pinning on a freshly created EMPTY pipe is a
14490                    // shrink or a no-op. EBUSY needs buffered data and EPERM needs to exceed
14491                    // `pipe-max-size`; neither can hold here (below a two-page
14492                    // `pipe-max-size` the pin is refused before it is tried).
14493                    for close in failure.close_syscalls() {
14494                        let _ = guest.inject(close).await;
14495                    }
14496                    error!(
14497                        "[detcore] cannot pin scheduler-managed pipe to {} bytes (fds {:?}): {}. \
14498                         Determinism is unavailable for this run.",
14499                        deterministic_pipe_capacity_bytes(),
14500                        failure.created_fds,
14501                        failure.error,
14502                    );
14503                    // Fail-closed policy: determinism is unavailable for this run.
14504                    unrecoverable_shutdown(guest, detcore_model::HERMIT_POLICY_REFUSAL_EXIT).await;
14505                }
14506            }
14507            self.add_fd(guest, fds[0], call.flags(), FdType::Pipe)
14508                .await?;
14509            self.add_fd(guest, fds[1], call.flags(), FdType::Pipe)
14510                .await?;
14511            // The guest holds both ends (`DetFd::is_guest_held`).
14512            for fd in fds {
14513                guest
14514                    .thread_state()
14515                    .with_detfd(fd, |detfd| detfd.set_guest_held())?;
14516            }
14517            if internally_nonblocking {
14518                self.maybe_set_nonblocking_fd(guest, fds[0]);
14519                self.maybe_set_nonblocking_fd(guest, fds[1]);
14520            }
14521            // Both ends name the pipe by the read end's open file description,
14522            // for `--pipe-wake-latency` only (`crate::pipe_wake`).
14523            if self.cfg.pipe_wake_latency_in_effect().is_some() {
14524                let read_end = guest
14525                    .thread_state()
14526                    .with_detfd(fds[0], |detfd| detfd.open_file_id())?;
14527                let channel = crate::pipe_wake::ChannelId::Pipe(read_end);
14528                for (fd, reads) in [(fds[0], true), (fds[1], false)] {
14529                    let end = crate::pipe_wake::ChannelEnd {
14530                        channel,
14531                        reads,
14532                        writes: !reads,
14533                    };
14534                    guest
14535                        .thread_state()
14536                        .with_detfd(fd, |detfd| detfd.set_channel(end))?;
14537                }
14538            }
14539        }
14540
14541        Ok(res)
14542    }
14543
14544    /// utime syscall: update access/modification time on a file
14545    #[cfg(target_arch = "x86_64")]
14546    pub async fn handle_utime<G: Guest<Self>>(
14547        &self,
14548        guest: &mut G,
14549        call: syscalls::Utime,
14550    ) -> Result<i64, Errno> {
14551        let times = match call.times() {
14552            None => {
14553                let now: Timespec = thread_observe_time(guest).await.into();
14554                [now, now]
14555            }
14556            Some(times) => {
14557                let utimbuf = guest.memory().read_value(times)?;
14558                [
14559                    Timespec {
14560                        tv_sec: utimbuf.actime,
14561                        tv_nsec: 0,
14562                    },
14563                    Timespec {
14564                        tv_sec: utimbuf.modtime,
14565                        tv_nsec: 0,
14566                    },
14567                ]
14568            }
14569        };
14570
14571        let utimensat = syscalls::Utimensat::new()
14572            .with_dirfd(libc::AT_FDCWD)
14573            .with_path(call.path());
14574
14575        self.set_file_times(guest, utimensat, Some(times)).await
14576    }
14577
14578    /// utimes syscall
14579    #[cfg(target_arch = "x86_64")]
14580    pub async fn handle_utimes<G: Guest<Self>>(
14581        &self,
14582        guest: &mut G,
14583        call: syscalls::Utimes,
14584    ) -> Result<i64, Errno> {
14585        let utimensat = syscalls::Utimensat::new()
14586            .with_dirfd(libc::AT_FDCWD)
14587            .with_path(call.filename());
14588
14589        match call.times() {
14590            None => {
14591                let now: Timespec = thread_observe_time(guest).await.into();
14592                self.set_file_times(guest, utimensat, Some([now, now]))
14593                    .await
14594            }
14595            Some(times) => {
14596                // Convert the timeval array to a timespec array.
14597                let mut memory = guest.memory();
14598                let tvs = memory.read_value(times)?;
14599                let tp: Addr<[Timespec; 2]> = times.cast();
14600
14601                // Safety: The address could point to read-only memory and the
14602                // write below could fail.
14603                let tp = unsafe { tp.into_mut() };
14604
14605                memory.write_value(tp, &[tvs[0].into(), tvs[1].into()])?;
14606                self.set_file_times(guest, utimensat.with_times(Some(tp.into())), None)
14607                    .await
14608            }
14609        }
14610    }
14611
14612    /// ustimensat syscall
14613    pub async fn handle_utimensat<G: Guest<Self>>(
14614        &self,
14615        guest: &mut G,
14616        call: syscalls::Utimensat,
14617    ) -> Result<i64, Errno> {
14618        self.set_file_times(guest, call, None).await
14619    }
14620
14621    /// Performs a utimensat call and copies the mtime it sets into the virtual
14622    /// mtime. With `staged` the times are first pushed onto the guest's scratch
14623    /// stack and replace the call's `times` pointer; utime and utimes(NULL)
14624    /// build their times that way.
14625    async fn set_file_times<G: Guest<Self>>(
14626        &self,
14627        guest: &mut G,
14628        call: syscalls::Utimensat,
14629        staged: Option<[Timespec; 2]>,
14630    ) -> Result<i64, Errno> {
14631        if !guest.config().virtualize_metadata {
14632            return self.utimensat_without_lookup(guest, call, staged).await;
14633        }
14634        // Without thread sequentialization another guest thread can swap the
14635        // target away and back around the call, so that both lookups name a
14636        // file the kernel did not update. No lookup can tell the two apart,
14637        // so the virtual mtime is left alone.
14638        if !guest.config().sequentialize_threads {
14639            return self.utimensat_without_lookup(guest, call, staged).await;
14640        }
14641        // The scratch stack starts 128 bytes below the stack pointer and its
14642        // addresses are computed by subtraction, so a stack pointer too close
14643        // to zero to hold the staged times and the lookup buffer would stop
14644        // Hermit rather than reach Linux.
14645        let scratch = 128
14646            + staged.map_or(0, |_| std::mem::size_of::<[Timespec; 2]>())
14647            + std::mem::size_of::<libc::stat>();
14648        if usize::try_from(crate::arch::stack_pointer(&guest.regs().await))
14649            .map_or(true, |rsp| rsp <= scratch)
14650        {
14651            info!(
14652                "Guest stack pointer cannot hold the utimensat target lookup; \
14653                 leaving the virtual mtime unchanged."
14654            );
14655            return self.utimensat_without_lookup(guest, call, staged).await;
14656        }
14657
14658        // The staged times and the buffer for the target lookups share one
14659        // scratch stack, and its guard is held until the last injected syscall
14660        // has run, so that neither the kernel's read of the times nor a lookup
14661        // finds the other's bytes, or a restored stack, at its address.
14662        let mut stack = guest.stack().await;
14663        let staged_call = match staged {
14664            Some(times) => call.with_times(Some(stack.push(times))),
14665            None => call,
14666        };
14667        let statptr: StatPtr = StatPtr(stack.reserve());
14668        // The scratch stack lies below the guest's red zone, where a raw
14669        // syscall may keep its own path or times. Writing the lookup buffer
14670        // over either would change the call before Linux reads it.
14671        if utimensat_input_overlaps(&guest.memory(), &call, staged.is_none(), statptr) {
14672            info!(
14673                "utimensat inputs overlap the target lookup buffer; \
14674                 leaving the virtual mtime unchanged."
14675            );
14676            drop(stack);
14677            return self.utimensat_without_lookup(guest, call, staged).await;
14678        }
14679        // The buffer's address range can still share its pages with guest data
14680        // through a second shared mapping, which no address comparison sees.
14681        // Its bytes are saved here and put back after the commit and after
14682        // each lookup, so the lookups and the call read their inputs unchanged
14683        // and the guest keeps its memory.
14684        let mut saved = [0u8; std::mem::size_of::<libc::stat>()];
14685        if guest
14686            .memory()
14687            .read_exact(statptr.0.cast(), &mut saved)
14688            .is_err()
14689        {
14690            info!(
14691                "Guest stack scratch cannot hold the utimensat target lookup; \
14692                 leaving the virtual mtime unchanged."
14693            );
14694            drop(stack);
14695            return self.utimensat_without_lookup(guest, call, staged).await;
14696        }
14697        let _guard = match stack.commit() {
14698            Ok(guard) => guard,
14699            // The lookup buffer only serves the virtual update, so a scratch
14700            // stack that cannot hold it, for example one next to the guard
14701            // page, must not keep the guest's own call from reaching Linux.
14702            // The commit writes page by page, so it can fail after writing a
14703            // lower writable page; those bytes are put back first.
14704            Err(_) => {
14705                info!(
14706                    "Guest stack scratch cannot hold the utimensat target lookup; \
14707                     leaving the virtual mtime unchanged."
14708                );
14709                restore_lookup_buffer(&mut guest.memory(), statptr, &saved);
14710                return self.utimensat_without_lookup(guest, call, staged).await;
14711            }
14712        };
14713        restore_lookup_buffer(&mut guest.memory(), statptr, &saved);
14714        let call = staged_call;
14715
14716        // The kernel applies the new times to the real file, but the guest
14717        // observes the virtual mtime, which otherwise only moves on writes. Copy
14718        // the requested mtime into it so that `tar` extraction, `cp -p` and
14719        // `touch -r` restore a file's mtime and `make` compares the times the
14720        // build asked for rather than the order in which files were unpacked.
14721        //
14722        // Nothing below may change the syscall's result: an unreadable `times`
14723        // or a failed lookup only skips the virtual update, and the kernel
14724        // reports its own error for the call itself.
14725        let mtime = match (staged, call.times()) {
14726            (Some([_, mtime]), _) => Some(mtime),
14727            (None, None) => Some(Timespec {
14728                tv_sec: 0,
14729                tv_nsec: libc::UTIME_NOW,
14730            }),
14731            (None, Some(times)) => guest
14732                .memory()
14733                .read_value(times)
14734                .ok()
14735                .map(|[_, mtime]| mtime),
14736        }
14737        .filter(|mtime| mtime.tv_nsec != libc::UTIME_OMIT);
14738        let before = match mtime {
14739            Some(_) => self.utimensat_target(guest, &call, statptr).await,
14740            None => None,
14741        };
14742        restore_lookup_buffer(&mut guest.memory(), statptr, &saved);
14743
14744        let res = self.record_or_replay(guest, call).await?;
14745
14746        // Update only the inode the kernel modified. No other guest thread
14747        // runs during the call, but a process outside the container can still
14748        // rename, unlink or replace the target; the target is resolved before
14749        // and after the call, and the update is skipped unless both name the
14750        // same file. Equal lookups alone do not show that the kernel updated
14751        // that file: the name may have been swapped away and back during the
14752        // call, or an inode number reused. So an explicit mtime is copied only
14753        // when the file holds it, truncated to the filesystem's granularity,
14754        // and the virtual mtime takes the value the kernel stored, which is
14755        // what stat reports on Linux. `UTIME_NOW` has no such witness.
14756        let (Some(mtime), Some(before)) = (mtime, before) else {
14757            return Ok(res);
14758        };
14759        let after = self.utimensat_target(guest, &call, statptr).await;
14760        restore_lookup_buffer(&mut guest.memory(), statptr, &saved);
14761        let Some(after) = after else {
14762            return Ok(res);
14763        };
14764        if (after.st_dev, after.st_ino) != (before.st_dev, before.st_ino) {
14765            return Ok(res);
14766        }
14767        if mtime.tv_nsec == libc::UTIME_NOW {
14768            touch_file(guest, RawInode::new(after.st_dev, after.st_ino)).await;
14769            return Ok(res);
14770        }
14771        const NANOS_PER_SEC: i128 = 1_000_000_000;
14772        let requested = i128::from(mtime.tv_sec) * NANOS_PER_SEC + i128::from(mtime.tv_nsec);
14773        let stored = i128::from(after.st_mtime) * NANOS_PER_SEC + i128::from(after.st_mtime_nsec);
14774        // Linux truncates the requested time down to the filesystem's
14775        // granularity, at most a second on the filesystems builds use.
14776        if (0..NANOS_PER_SEC).contains(&(requested - stored)) {
14777            let nanos = u64::try_from(stored.max(0)).unwrap_or(u64::MAX);
14778            set_file_mtime(
14779                guest,
14780                RawInode::new(after.st_dev, after.st_ino),
14781                LogicalTime::from_nanos(nanos),
14782            )
14783            .await;
14784        }
14785        Ok(res)
14786    }
14787
14788    /// Performs a utimensat call without the virtual mtime update. Staged
14789    /// times are then the only scratch-stack allocation, as they were before
14790    /// the update existed.
14791    async fn utimensat_without_lookup<G: Guest<Self>>(
14792        &self,
14793        guest: &mut G,
14794        call: syscalls::Utimensat,
14795        staged: Option<[Timespec; 2]>,
14796    ) -> Result<i64, Errno> {
14797        let Some(times) = staged else {
14798            return self.record_or_replay(guest, call).await;
14799        };
14800        let mut stack = guest.stack().await;
14801        let call = call.with_times(Some(stack.push(times)));
14802        let _guard = stack.commit()?;
14803        self.record_or_replay(guest, call).await
14804    }
14805
14806    /// The metadata of the file a utimensat call targets, with the same target
14807    /// selection: the descriptor itself for `futimens` (a NULL path), else a
14808    /// path walk honoring the flags utimensat accepts. `None` if the lookup
14809    /// fails.
14810    async fn utimensat_target<G: Guest<Self>>(
14811        &self,
14812        guest: &mut G,
14813        call: &syscalls::Utimensat,
14814        statptr: StatPtr<'_>,
14815    ) -> Option<libc::stat> {
14816        let lookup = match call.path() {
14817            None => Syscall::Fstat(
14818                syscalls::Fstat::new()
14819                    .with_fd(call.dirfd())
14820                    .with_stat(Some(statptr)),
14821            ),
14822            Some(path) => {
14823                let allowed = libc::AT_SYMLINK_NOFOLLOW | libc::AT_EMPTY_PATH;
14824                crate::arch::fstatat_syscall(
14825                    syscalls::Fstatat::new()
14826                        .with_dirfd(call.dirfd())
14827                        .with_path(Some(path))
14828                        .with_stat(Some(statptr))
14829                        .with_flags(AtFlags::from_bits_truncate(call.flags() & allowed)),
14830                )
14831            }
14832        };
14833        self.record_or_replay(guest, lookup).await.ok()?;
14834        statptr.read(&guest.memory()).ok()
14835    }
14836
14837    /// socket system call.
14838    pub async fn handle_socket<G: Guest<Self>>(
14839        &self,
14840        guest: &mut G,
14841        call: syscalls::Socket,
14842    ) -> Result<i64, Error> {
14843        // The socket syscall itself is not blocking, but we must decide whether to make the socket
14844        // returned physically nonblocking.
14845        if !self.cfg.sequentialize_threads || self.cfg.recordreplay_modes {
14846            // Allow possibly blocking syscall in record mode
14847            let fd = self.record_or_replay(guest, call).await? as RawFd;
14848            self.add_fd(
14849                guest,
14850                fd,
14851                OFlag::from_bits_truncate(call.r#type()),
14852                FdType::Socket,
14853            )
14854            .await?;
14855            self.mark_sock_diag_fd(guest, fd, &call);
14856            self.note_unix_socket(guest, fd, call.family(), call.r#type());
14857            self.mark_error_queue_reads(guest, fd, &call)?;
14858            // Linux's SOCK_TYPE_MASK: the type without SOCK_NONBLOCK and SOCK_CLOEXEC.
14859            const SOCK_TYPE_MASK: i32 = 0xf;
14860            if self.accept_in_turn_enabled() && call.r#type() & SOCK_TYPE_MASK == libc::SOCK_STREAM
14861            {
14862                guest
14863                    .thread_state()
14864                    .with_detfd(fd, |detfd| detfd.set_accept_in_turn_eligible())?;
14865            }
14866            Ok(fd as i64)
14867        } else {
14868            // Under run mode, force all sockets to be registered to be nonblocking in the OS:
14869            let call2 = if self.cfg.use_nonblocking_sockets() {
14870                call.with_type(call.r#type() | libc::SOCK_NONBLOCK)
14871            } else {
14872                call
14873            };
14874            let fd = self.record_or_replay(guest, call2).await? as RawFd; // Cannot hang.
14875            self.add_fd(
14876                guest,
14877                fd,
14878                OFlag::from_bits_truncate(
14879                    call.r#type() & (libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC),
14880                ),
14881                FdType::Socket,
14882            )
14883            .await?;
14884            self.maybe_set_nonblocking_fd(guest, fd);
14885            self.mark_sock_diag_fd(guest, fd, &call);
14886            self.note_unix_socket(guest, fd, call.family(), call.r#type());
14887            self.mark_error_queue_reads(guest, fd, &call)?;
14888            // A guest made it (`DetFd::is_guest_created`).
14889            guest
14890                .thread_state()
14891                .with_detfd(fd, |detfd| detfd.set_guest_created())?;
14892
14893            Ok(fd as i64)
14894        }
14895    }
14896
14897    // TODO-HUMAN-REVIEW(PR-3908)
14898    /// Whether record/replay runs a blocking `accept(2)` on a stream socket
14899    /// the container created in the caller's turn
14900    /// (https://github.com/rrnewton/hermit/issues/3880). The temporary
14901    /// `O_NONBLOCK` around each attempt goes through Detcore's own copy of the
14902    /// listener (`AcceptBracket`), which needs a tool outside the guest's
14903    /// descriptor table, as under ptrace.
14904    pub(crate) fn accept_in_turn_enabled(&self) -> bool {
14905        self.cfg.recordreplay_modes
14906            && self.cfg.use_nonblocking_sockets()
14907            && !self.cfg.backend.tool_shares_guest_descriptor_table
14908    }
14909
14910    /// Record whether a receive with `MSG_ERRQUEUE` on the socket `call`
14911    /// created never waits (`crate::fd::error_queue_read_never_waits`), so that
14912    /// such a receive runs once even on a socket the guest left blocking.
14913    fn mark_error_queue_reads<G: Guest<Self>>(
14914        &self,
14915        guest: &mut G,
14916        fd: RawFd,
14917        call: &syscalls::Socket,
14918    ) -> Result<(), Error> {
14919        let never_waits =
14920            crate::fd::error_queue_read_never_waits(call.family(), call.r#type(), call.protocol());
14921        guest
14922            .thread_state()
14923            .with_detfd(fd, |detfd| detfd.set_error_queue_never_waits(never_waits))?;
14924        Ok(())
14925    }
14926
14927    // AUTONOMOUS-BOT-IMPLEMENTED
14928    // TODO-HUMAN-REVIEW(PR-1064)
14929    /// Flag `AF_NETLINK`/`NETLINK_SOCK_DIAG` sockets so `handle_recvmsg`
14930    /// determinizes the socket inode numbers carried by their binary dump
14931    /// replies (see `crate::sock_diag`). Best-effort: if the descriptor lookup
14932    /// fails the reply is simply left unsanitized.
14933    fn mark_sock_diag_fd<G: Guest<Self>>(&self, guest: &mut G, fd: RawFd, call: &syscalls::Socket) {
14934        if call.family() != libc::AF_NETLINK {
14935            return;
14936        }
14937        if call.protocol() == libc::NETLINK_SOCK_DIAG {
14938            let _ = guest
14939                .thread_state()
14940                .with_detfd(fd, |detfd| detfd.set_sock_diag());
14941        }
14942        // TODO-HUMAN-REVIEW(PR-2478)
14943        // NETLINK_ROUTE link dumps carry live interface counters. They were
14944        // invisible until IO-buffer hashing went on by default, because the
14945        // reply's LENGTH and return value are identical between runs and only
14946        // the payload bytes move.
14947        if call.protocol() == libc::NETLINK_ROUTE {
14948            let _ = guest
14949                .thread_state()
14950                .with_detfd(fd, |detfd| detfd.set_netlink_route());
14951        }
14952    }
14953
14954    /// socketpair system call.
14955    pub async fn handle_socketpair<G: Guest<Self>>(
14956        &self,
14957        guest: &mut G,
14958        call: syscalls::Socketpair,
14959    ) -> Result<i64, Error> {
14960        let call2 = if self.cfg.sequentialize_threads && !self.cfg.debug_externalize_sockets {
14961            call.with_type(call.r#type() | libc::SOCK_NONBLOCK)
14962        } else {
14963            call
14964        };
14965        let res = self.record_or_replay(guest, call2).await?;
14966        if let Some(usockvec) = call.usockvec() {
14967            let memory = guest.memory();
14968            let fds: [i32; 2] = memory.read_value(usockvec)?;
14969
14970            // Logical flags are as requested:
14971            self.add_fd(
14972                guest,
14973                fds[0],
14974                OFlag::from_bits_truncate(
14975                    call.r#type() & (libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC),
14976                ),
14977                FdType::Socket,
14978            )
14979            .await?;
14980            self.add_fd(
14981                guest,
14982                fds[1],
14983                OFlag::from_bits_truncate(
14984                    call.r#type() & (libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC),
14985                ),
14986                FdType::Socket,
14987            )
14988            .await?;
14989
14990            // Both endpoints are container-internal by construction: socketpair(2)
14991            // creates the pair itself, so no peer outside the container can ever
14992            // hold one. Record that here, at the only point where it is known,
14993            // so potentially-blocking I/O on them takes the same
14994            // nonblockize-and-retry path as a pipe rather than being treated as
14995            // external blocking I/O against an fd Detcore just made SOCK_NONBLOCK.
14996            // See `syscall_targets_internal_fd`. Only when the endpoints really
14997            // are physically nonblocking, which is the same condition as `call2`
14998            // above: under `debug_externalize_sockets` they stay external.
14999            if self.cfg.use_nonblocking_sockets() {
15000                // The guest holds both ends of a stream or seqpacket pair
15001                // (`DetFd::is_guest_held`). A datagram end is not marked: it can
15002                // gain an address, from `bind` or autobind (`SO_PASSCRED`), and
15003                // then receive from any process once its peer is gone.
15004                let kind = call.r#type() & !(libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC);
15005                let connection = kind == libc::SOCK_STREAM || kind == libc::SOCK_SEQPACKET;
15006                for fd in fds {
15007                    let _ = guest.thread_state().with_detfd(fd, |detfd| {
15008                        detfd.set_socketpair_endpoint();
15009                        if connection {
15010                            detfd.set_guest_held();
15011                        }
15012                    });
15013                }
15014            }
15015            // A guest made both ends (`DetFd::is_guest_created`).
15016            for fd in fds {
15017                let _ = guest
15018                    .thread_state()
15019                    .with_detfd(fd, |detfd| detfd.set_guest_created());
15020            }
15021            self.note_socketpair(guest, fds, call.family(), call.r#type());
15022
15023            self.maybe_set_nonblocking_fd(guest, fds[0]);
15024            self.maybe_set_nonblocking_fd(guest, fds[1]);
15025        }
15026        Ok(res)
15027    }
15028
15029    // AUTONOMOUS-BOT-IMPLEMENTED
15030    // TODO-HUMAN-REVIEW(#663)
15031    /// Apply a socket option to an already tracked socket. Record mode captures
15032    /// the result; replay re-applies a successful option before later socket I/O,
15033    /// which remains mediated by Detcore's nonblocking scheduler paths.
15034    ///
15035    /// Under record and replay a successful `SO_RCVTIMEO` also sets the
15036    /// socket's modeled receive timeout (`DetFd::receive_timeout`), which a
15037    /// blocking accept in turn waits by. The value is read before the call,
15038    /// from the same guest memory in both phases.
15039    pub async fn handle_setsockopt<G: Guest<Self>>(
15040        &self,
15041        guest: &mut G,
15042        call: syscalls::Setsockopt,
15043    ) -> Result<i64, Error> {
15044        // TODO-HUMAN-REVIEW(PR-3908)
15045        const SO_RCVTIMEO_NEW: i32 = 66;
15046        let receive_timeout = (self.accept_in_turn_enabled()
15047            && call.level() == libc::SOL_SOCKET
15048            && matches!(call.optname(), libc::SO_RCVTIMEO | SO_RCVTIMEO_NEW))
15049        .then(|| {
15050            // On x86_64 both forms take 16 bytes: seconds, then microseconds.
15051            call.optval()
15052                .and_then(|value| {
15053                    guest
15054                        .memory()
15055                        .read_value::<_, libc::timeval>(value.cast())
15056                        .ok()
15057                })
15058                .map_or(ReceiveTimeout::Unknown, ReceiveTimeout::set_by)
15059        });
15060        let result = self.record_or_replay(guest, call).await?;
15061        if let Some(timeout) = receive_timeout {
15062            let _ = guest
15063                .thread_state()
15064                .with_detfd(call.fd(), |detfd| detfd.set_receive_timeout(timeout));
15065        }
15066        Ok(result)
15067    }
15068
15069    // AUTONOMOUS-BOT-IMPLEMENTED
15070    // TODO-HUMAN-REVIEW(#663)
15071    /// Transition an already tracked socket into listening state.
15072    pub async fn handle_listen<G: Guest<Self>>(
15073        &self,
15074        guest: &mut G,
15075        call: syscalls::Listen,
15076    ) -> Result<i64, Error> {
15077        Ok(self.record_or_replay(guest, call).await?)
15078    }
15079
15080    // AUTONOMOUS-BOT-IMPLEMENTED
15081    // TODO-HUMAN-REVIEW(#663)
15082    /// Return the local address of a tracked socket.
15083    pub async fn handle_getsockname<G: Guest<Self>>(
15084        &self,
15085        guest: &mut G,
15086        call: syscalls::Getsockname,
15087    ) -> Result<i64, Error> {
15088        Ok(self.record_or_replay(guest, call).await?)
15089    }
15090
15091    // AUTONOMOUS-BOT-IMPLEMENTED
15092    // TODO-HUMAN-REVIEW(#663)
15093    /// Return the peer address of a tracked socket.
15094    pub async fn handle_getpeername<G: Guest<Self>>(
15095        &self,
15096        guest: &mut G,
15097        call: syscalls::Getpeername,
15098    ) -> Result<i64, Error> {
15099        Ok(self.record_or_replay(guest, call).await?)
15100    }
15101
15102    // AUTONOMOUS-BOT-IMPLEMENTED
15103    // TODO-HUMAN-REVIEW(#663)
15104    /// Return an option value from a tracked socket. Hermit only promises normal
15105    /// run determinism for isolated guest networking; record/replay captures the
15106    /// result when external socket state is part of the recording boundary.
15107    pub async fn handle_getsockopt<G: Guest<Self>>(
15108        &self,
15109        guest: &mut G,
15110        call: syscalls::Getsockopt,
15111    ) -> Result<i64, Error> {
15112        // TODO-HUMAN-REVIEW(PR-894): Review deterministic network-namespace identity.
15113        let requested_length =
15114            if call.level() == libc::SOL_SOCKET && call.optname() == libc::SO_NETNS_COOKIE {
15115                let fd_type = guest
15116                    .thread_state()
15117                    .with_detfd(call.fd(), |detfd| detfd.ty())?;
15118                if fd_type == FdType::Socket {
15119                    call.optlen()
15120                        .map(|length| guest.memory().read_value(length))
15121                        .transpose()?
15122                } else {
15123                    None
15124                }
15125            } else {
15126                None
15127            };
15128
15129        // TODO-HUMAN-REVIEW(PR-886): Review deterministic SO_COOKIE identities.
15130        let deterministic_cookie =
15131            if call.level() == libc::SOL_SOCKET && call.optname() == libc::SO_COOKIE {
15132                let requested_length = call
15133                    .optlen()
15134                    .map(|length| guest.memory().read_value(length))
15135                    .transpose()?;
15136                let open_file_id = guest
15137                    .thread_state()
15138                    .with_detfd(call.fd(), |detfd| detfd.open_file_id())?;
15139                Some((open_file_id.deterministic_socket_cookie(), requested_length))
15140            } else {
15141                None
15142            };
15143
15144        let result = self.record_or_replay(guest, call).await?;
15145
15146        // TODO-HUMAN-REVIEW(PR-898): Hermit exposes one virtual CPU, so do not
15147        // leak the host CPU that processed a socket's most recent packet.
15148        if result == 0
15149            && call.level() == libc::SOL_SOCKET
15150            && call.optname() == libc::SO_INCOMING_CPU
15151            && let (Some(optval), Some(optlen)) = (call.optval(), call.optlen())
15152        {
15153            let returned_len: libc::socklen_t = guest.memory().read_value(optlen)?;
15154            let zero_cpu = 0_i32.to_ne_bytes();
15155            let returned_len = (returned_len as usize).min(zero_cpu.len());
15156            guest
15157                .memory()
15158                .write_exact(optval.cast::<u8>(), &zero_cpu[..returned_len])?;
15159        }
15160        if result == 0
15161            && call.level() == libc::IPPROTO_TCP
15162            && call.optname() == libc::TCP_INFO
15163            && let (Some(optval), Some(optlen)) = (call.optval(), call.optlen())
15164        {
15165            let returned_len: libc::socklen_t = guest.memory().read_value(optlen)?;
15166            let mut info = vec![0; returned_len as usize];
15167            let optval = optval.cast::<u8>();
15168            guest.memory().read_exact(optval, info.as_mut_slice())?;
15169            canonicalize_tcp_info(&mut info);
15170            guest.memory().write_exact(optval, info.as_slice())?;
15171        }
15172        if let Some(requested_length) = requested_length
15173            && let Some(value) = call.optval()
15174        {
15175            let bytes = DETERMINISTIC_NETNS_COOKIE.to_ne_bytes();
15176            let write_length = (requested_length as usize).min(bytes.len());
15177            guest
15178                .memory()
15179                .write_exact(value.cast(), &bytes[..write_length])?;
15180        }
15181        if let Some((cookie, Some(requested_length))) = deterministic_cookie
15182            && let Some(value) = call.optval()
15183        {
15184            let bytes = cookie.to_ne_bytes();
15185            let write_length = (requested_length as usize).min(bytes.len());
15186            guest
15187                .memory()
15188                .write_exact(value.cast(), &bytes[..write_length])?;
15189        }
15190        Ok(result)
15191    }
15192    // AUTONOMOUS-BOT-IMPLEMENTED
15193    // TODO-HUMAN-REVIEW(#818)
15194    /// Half-close the read and/or write direction of an already tracked socket.
15195    /// shutdown never blocks and returns no data; its effect is deterministic
15196    /// given the container's socket state, so it forwards via record_or_replay
15197    /// exactly like the rest of the socket family (KVM ratchet round 12).
15198    ///
15199    /// Shutting the read side marks the socket when Linux shuts it: on success,
15200    /// and on an unconnected socket, whose read side Linux shuts even as it
15201    /// reports `ENOTCONN` (`inet_shutdown`). A polled receive asks the kernel
15202    /// before it relies on the mark (`finish_after_read_shutdown`), since a TCP
15203    /// disconnect opens the read side again (`tcp_disconnect`).
15204    pub async fn handle_shutdown<G: Guest<Self>>(
15205        &self,
15206        guest: &mut G,
15207        call: syscalls::Shutdown,
15208    ) -> Result<i64, Error> {
15209        let result = self.record_or_replay(guest, call).await;
15210        if matches!(call.how(), libc::SHUT_RD | libc::SHUT_RDWR)
15211            && matches!(result, Ok(_) | Err(Errno::ENOTCONN))
15212        {
15213            // An untracked descriptor has no model to mark.
15214            let _ = guest
15215                .thread_state()
15216                .with_detfd(call.fd(), |detfd| detfd.set_read_shutdown_requested());
15217        }
15218        Ok(result?)
15219    }
15220
15221    /// bind system call.
15222    pub async fn handle_bind<G: Guest<Self>>(
15223        &self,
15224        guest: &mut G,
15225        call: syscalls::Bind,
15226    ) -> Result<i64, Error> {
15227        // WIP!
15228        if guest.config().sched_heuristic == SchedHeuristic::ConnectBind {
15229            trace!("Scheduling heuristic: reprioritizing bind");
15230            let resource = ResourceID::PriorityChangePoint(
15231                LAST_PRIORITY,
15232                guest.thread_state().thread_logical_time.as_nanos(),
15233                guest.thread_state().committed_clock_value,
15234                Vec::new(),
15235            );
15236            let req = guest.thread_state().mk_request(resource, Permission::W);
15237            resource_request(guest, req).await;
15238        }
15239        let addr = call.umyaddr().ok_or(Errno::EFAULT)?;
15240        let sock_fd = call.fd();
15241        // An address lets any process reach the socket, a host process
15242        // included: a datagram socketpair end whose peer has gone accepts
15243        // datagrams from anyone who names it, so it is no longer guest-held
15244        // (`DetFd::is_guest_held`). Cleared whatever the bind returns.
15245        let _ = guest.thread_state().with_detfd(sock_fd, |detfd| {
15246            if detfd.is_guest_held() {
15247                detfd.clear_guest_held();
15248            }
15249        });
15250        let open_file_id = guest
15251            .thread_state()
15252            .with_detfd(sock_fd, |detfd| detfd.open_file_id())?;
15253
15254        let sockaddr_family = guest.memory().read_value(addr.cast::<u16>())?;
15255        // The abstract name to notice once the bind returns. Read only to word
15256        // the notice: the bind itself is unchanged, and an unreadable address
15257        // is left to fail as Linux fails it.
15258        let notice_abstract_name = if sockaddr_family == libc::AF_UNIX as u16
15259            && guest.config().notice_host_network_abstract_binds
15260        {
15261            let mut bytes = [0u8; std::mem::size_of::<libc::sockaddr_un>()];
15262            let length = usize::try_from(call.addrlen())
15263                .unwrap_or(0)
15264                .min(bytes.len());
15265            guest
15266                .memory()
15267                .read_exact(addr.cast::<u8>(), &mut bytes[..length])
15268                .ok()
15269                .and_then(|_| abstract_unix_name(&bytes[..length]).map(<[u8]>::to_vec))
15270        } else {
15271            None
15272        };
15273        // AUTONOMOUS-BOT-IMPLEMENTED
15274        // TODO-HUMAN-REVIEW(PR-872): Review deterministic AF_UNIX autobind identity.
15275        if sockaddr_family == libc::AF_UNIX as u16
15276            && call.addrlen() == std::mem::offset_of!(libc::sockaddr_un, sun_path) as i32
15277        {
15278            let resp = send_and_update_time(guest, GlobalRequest::RequestPort(open_file_id)).await;
15279            let port = match resp.1 {
15280                GlobalResponse::RequestPort(port) => port,
15281                GlobalResponse::PortFull => {
15282                    return Err(reverie::Error::from(nix::errno::Errno::EADDRINUSE));
15283                }
15284                _ => unreachable!(),
15285            };
15286
15287            let autobind_address = unix_autobind_address(port);
15288            let mut stack = guest.stack().await;
15289            let autobind_addr: AddrMut<libc::sockaddr_un> = stack.reserve();
15290            let _stack_guard = stack.commit()?;
15291            guest
15292                .memory()
15293                .write_value(autobind_addr, &autobind_address)?;
15294            let deterministic_bind = call
15295                .with_umyaddr(Some(autobind_addr.cast()))
15296                .with_addrlen(unix_autobind_addrlen());
15297            let result = self.record_or_replay(guest, deterministic_bind).await;
15298            if guest.config().notice_host_network_abstract_binds {
15299                let mut name = [0u8; UNIX_AUTOBIND_NAME_LEN - 1];
15300                for (byte, source) in name.iter_mut().zip(&autobind_address.sun_path[1..]) {
15301                    // `c_char` is `i8` on x86_64 and `u8` on aarch64; take its bits.
15302                    [*byte] = source.to_ne_bytes();
15303                }
15304                notice_host_network_abstract_bind(&name, &result);
15305            }
15306            return Ok(result?);
15307        // AUTONOMOUS-BOT-IMPLEMENTED
15308        // TODO-HUMAN-REVIEW(PR-880): Review deterministic Netlink autobind identities.
15309        } else if sockaddr_family == libc::AF_NETLINK as u16
15310            && call.addrlen() >= std::mem::size_of::<libc::sockaddr_nl>() as i32
15311        {
15312            let mut sockaddr_nl: libc::sockaddr_nl = guest
15313                .memory()
15314                .read_value(addr.cast::<libc::sockaddr_nl>())?;
15315            if sockaddr_nl.nl_pid == 0 {
15316                let resp =
15317                    send_and_update_time(guest, GlobalRequest::RequestPort(open_file_id)).await;
15318                match resp.1 {
15319                    GlobalResponse::RequestPort(port) => {
15320                        sockaddr_nl.nl_pid = DETERMINISTIC_NETLINK_PORT_ID_BASE | u32::from(port);
15321                        let mut stack = guest.stack().await;
15322                        let deterministic_addr: AddrMut<libc::sockaddr_nl> = stack.reserve();
15323                        let _stack_guard = stack.commit()?;
15324                        guest
15325                            .memory()
15326                            .write_value(deterministic_addr, &sockaddr_nl)?;
15327                        let deterministic_bind = call.with_umyaddr(Some(deterministic_addr.cast()));
15328                        return Ok(self.record_or_replay(guest, deterministic_bind).await?);
15329                    }
15330                    GlobalResponse::PortFull => {
15331                        return Err(reverie::Error::from(nix::errno::Errno::EADDRINUSE));
15332                    }
15333                    _ => unreachable!(),
15334                }
15335            }
15336        } else if sockaddr_family == libc::AF_INET as u16 {
15337            // For IPv4
15338            let mut sockaddr_in: libc::sockaddr_in = guest
15339                .memory()
15340                .read_value(addr.cast::<libc::sockaddr_in>())?;
15341
15342            let port = sockaddr_in.sin_port.to_be();
15343            let ipaddr = Ipv4Addr::from(sockaddr_in.sin_addr.s_addr);
15344            if port != 0 {
15345                if guest.config().warn_non_zero_binds {
15346                    warn!(
15347                        "Analyze Networking: Non-zero port detected: {:?}:{:?}",
15348                        ipaddr, port
15349                    );
15350                }
15351                // Send RPC to make sure already used ports are not used.
15352                let resp =
15353                    send_and_update_time(guest, GlobalRequest::AddUsedPort(port, open_file_id))
15354                        .await;
15355                match resp.1 {
15356                    GlobalResponse::AddUsedPort => {
15357                        trace!("Added to used port {}", port);
15358                    }
15359                    _ => unreachable!(),
15360                }
15361            } else {
15362                // Request a determinzed port
15363                let resp =
15364                    send_and_update_time(guest, GlobalRequest::RequestPort(open_file_id)).await;
15365                match resp.1 {
15366                    GlobalResponse::RequestPort(port_assigned) => {
15367                        sockaddr_in.sin_port = port_assigned.to_be();
15368                        guest
15369                            .memory()
15370                            .write_value(addr.cast::<libc::sockaddr_in>(), &sockaddr_in)?;
15371                    }
15372                    GlobalResponse::PortFull => {
15373                        return Err(reverie::Error::from(nix::errno::Errno::EADDRINUSE));
15374                    }
15375                    _ => unreachable!(),
15376                }
15377            }
15378        } else if sockaddr_family == libc::AF_INET6 as u16 {
15379            // For IPv6
15380            let mut sockfaddr_in: libc::sockaddr_in6 = guest
15381                .memory()
15382                .read_value(addr.cast::<libc::sockaddr_in6>())?;
15383            let port = sockfaddr_in.sin6_port.to_be();
15384            let ipaddr = Ipv6Addr::from(sockfaddr_in.sin6_addr.s6_addr);
15385            if port != 0 {
15386                if guest.config().warn_non_zero_binds {
15387                    warn!(
15388                        "Analyze Networking: Non-zero port detected: {:?}:{:?}",
15389                        ipaddr, port
15390                    );
15391                }
15392                let resp =
15393                    send_and_update_time(guest, GlobalRequest::AddUsedPort(port, open_file_id))
15394                        .await;
15395                match resp.1 {
15396                    GlobalResponse::AddUsedPort => {
15397                        trace!("Added to used port {}", port);
15398                    }
15399                    _ => unreachable!(),
15400                }
15401            } else {
15402                let resp =
15403                    send_and_update_time(guest, GlobalRequest::RequestPort(open_file_id)).await;
15404                match resp.1 {
15405                    GlobalResponse::RequestPort(port_assigned) => {
15406                        sockfaddr_in.sin6_port = port_assigned.to_be();
15407                        guest
15408                            .memory()
15409                            .write_value(addr.cast::<libc::sockaddr_in6>(), &sockfaddr_in)?;
15410                        trace!("Port assigned {}", port_assigned)
15411                    }
15412                    GlobalResponse::PortFull => {
15413                        return Err(reverie::Error::from(nix::errno::Errno::EADDRINUSE));
15414                    }
15415                    _ => unreachable!(),
15416                }
15417            }
15418        }
15419        // The name a guest names this socket by, read before the call: Linux
15420        // reads the address once, and another thread could rewrite it.
15421        let unix_name = (sockaddr_family == libc::AF_UNIX as u16)
15422            .then(|| {
15423                crate::syscalls::scm_rights::unix_socket_name(
15424                    &guest.memory(),
15425                    addr.as_raw(),
15426                    usize::try_from(call.addrlen()).unwrap_or(0),
15427                )
15428            })
15429            .flatten();
15430        let res = self.record_or_replay(guest, call).await;
15431        // Named in any form, the socket can be sent to by other sockets.
15432        if res.is_ok() && sockaddr_family == libc::AF_UNIX as u16 {
15433            self.note_unix_socket_reachable(guest, sock_fd);
15434        }
15435        if res.is_ok()
15436            && let Some(name) = unix_name
15437        {
15438            self.note_unix_bind(guest, sock_fd, &name);
15439        }
15440        if let Some(name) = &notice_abstract_name {
15441            notice_host_network_abstract_bind(name, &res);
15442        }
15443
15444        Ok(res?)
15445    }
15446
15447    /// Create and register an event notification counter.
15448    ///
15449    /// Determinism: strict execution serializes creation, so the initial counter, guest-visible
15450    /// flags, and descriptor number depend only on syscall arguments and the reconstructed file
15451    /// table. Any internally added nonblocking flag remains hidden from the guest.
15452    pub async fn handle_eventfd2<G: Guest<Self>>(
15453        &self,
15454        guest: &mut G,
15455        call: syscalls::Eventfd2,
15456    ) -> Result<i64, Error> {
15457        let internally_nonblocking =
15458            self.cfg.use_nonblocking_sockets() && !self.cfg.recordreplay_modes;
15459        let injected = if internally_nonblocking {
15460            call.with_flags(call.flags() | syscalls::EfdFlags::EFD_NONBLOCK)
15461        } else {
15462            call
15463        };
15464        let fd = self.record_or_replay(guest, injected).await? as RawFd;
15465        self.add_fd(
15466            guest,
15467            fd,
15468            OFlag::from_bits_truncate(
15469                call.flags().bits() & (libc::EFD_CLOEXEC | libc::EFD_NONBLOCK),
15470            ),
15471            FdType::Eventfd,
15472        )
15473        .await?;
15474        // Only a holder of this descriptor can signal it (`DetFd::is_guest_held`).
15475        guest
15476            .thread_state()
15477            .with_detfd(fd, |detfd| detfd.set_guest_held())?;
15478        // Its fdinfo's `eventfd-semaphore`, which no call reads back.
15479        guest.thread_state().with_detfd(fd, |detfd| {
15480            detfd.set_fdinfo_facts(FdinfoFacts::Eventfd {
15481                semaphore: call.flags().bits() & libc::EFD_SEMAPHORE != 0,
15482            })
15483        })?;
15484        if internally_nonblocking {
15485            self.maybe_set_nonblocking_fd(guest, fd);
15486        }
15487        Ok(fd as i64)
15488    }
15489
15490    /// signalfd4 system call.
15491    pub async fn handle_signalfd4<G: Guest<Self>>(
15492        &self,
15493        guest: &mut G,
15494        call: syscalls::Signalfd4,
15495    ) -> Result<i64, Error> {
15496        // A signalfd of a pending real-time timer expiration that every thread
15497        // blocked when it expired would be readable at once
15498        // (https://github.com/rrnewton/hermit/issues/4089). A mask the kernel
15499        // cannot read fails the call below.
15500        let mask = match call.mask().and_then(|mask| Addr::from_raw(mask.as_raw())) {
15501            Some(mask) => {
15502                // The call's first injection (`restart_if_first_probe_not_run`).
15503                match crate::syscalls::signal::read_kernel_sigset(guest, mask).await {
15504                    Err(error @ Error::Errno(Errno::ERESTARTSYS)) => {
15505                        return crate::syscalls::signal::restart_if_first_probe_not_run(Err(error));
15506                    }
15507                    read => read.ok(),
15508                }
15509            }
15510            None => None,
15511        };
15512        if let Some(mask) = mask {
15513            let pending = crate::syscalls::helpers::pending_real_time_among(guest, mask).await;
15514            crate::syscalls::helpers::act_on_pending_real_time(guest, pending, mask, 0).await;
15515        }
15516        let signalfd = self.record_or_replay(guest, call).await? as RawFd;
15517        // A real-time timer expiration this descriptor, or a copy a fork
15518        // inherits, could read is never kept pending
15519        // (`Scheduler::real_time_expiration_fate`), a `SIGEV_THREAD_ID`
15520        // timer's copy of a signal it covers may be taken unobserved
15521        // (`Scheduler::thread_timer_copy_is_observable`), and a process one
15522        // of whose threads blocks in an I/O call while it holds one for
15523        // SIGCHLD does not attribute SIGCHLD through the send window
15524        // (`Scheduler::note_outside_blocker`), since that call may read it
15525        // outside the schedule.
15526        if let Some(mask) = mask
15527            && mask != 0
15528            && let Some(detpid) = guest.thread_state().detpid
15529        {
15530            crate::tool_global::register_signalfd(guest, detpid, mask).await;
15531        }
15532        // A call on an existing signalfd (`ufd` not -1) changes that one
15533        // context's mask (`do_signalfd4`, fs/signalfd.c): every descriptor of
15534        // the description reads the new mask, and the call's flags, which
15535        // Linux only validates, change nothing. Only a new signalfd is a new
15536        // description (https://github.com/rrnewton/hermit/issues/4914). An
15537        // update of a descriptor Detcore does not model as a signalfd (one it
15538        // classified otherwise) is modelled as one from now on, with the
15539        // status flags it had. A named residual: an update of a descriptor
15540        // Detcore keeps no model of at all (one received over `SCM_RIGHTS`
15541        // that the receipt did not record; Hermit closes every inherited
15542        // descriptor above 2 at entry) is modelled from the call's flags, as
15543        // every update was before, since its own flags would take injected
15544        // `F_GETFL` and `F_GETFD` calls that no recording holds.
15545        let modelled = if call.fd() == -1 {
15546            None
15547        } else {
15548            guest
15549                .thread_state()
15550                .with_detfd(signalfd, |detfd| {
15551                    (
15552                        detfd.ty() == FdType::Signalfd,
15553                        detfd.status_flags(),
15554                        detfd.is_cloexec(),
15555                    )
15556                })
15557                .ok()
15558        };
15559        let creation_flags = OFlag::from_bits_truncate(
15560            call.flags().bits() & (libc::SFD_CLOEXEC | libc::SFD_NONBLOCK),
15561        );
15562        match modelled {
15563            Some((true, _, _)) => {}
15564            Some((false, status_flags, cloexec)) => {
15565                let kept = OFlag::from_bits_truncate(status_flags & libc::O_NONBLOCK)
15566                    | if cloexec {
15567                        OFlag::O_CLOEXEC
15568                    } else {
15569                        OFlag::empty()
15570                    };
15571                self.add_fd(guest, signalfd, kept, FdType::Signalfd).await?;
15572            }
15573            None => {
15574                self.add_fd(guest, signalfd, creation_flags, FdType::Signalfd)
15575                    .await?;
15576            }
15577        }
15578        // Its fdinfo's `sigmask:`, which no call reads back: the mask this
15579        // call set, which Linux keeps without SIGKILL and SIGSTOP
15580        // (`signalfd4`, fs/signalfd.c), on the description every alias
15581        // shares. A mask Detcore could not read leaves none, and that fdinfo
15582        // is not rendered.
15583        if let Some(mask) = mask {
15584            let unblockable = (1u64 << (libc::SIGKILL - 1)) | (1u64 << (libc::SIGSTOP - 1));
15585            guest.thread_state().with_detfd(signalfd, |detfd| {
15586                detfd.set_fdinfo_facts(FdinfoFacts::Signalfd {
15587                    mask: mask & !unblockable,
15588                })
15589            })?;
15590        }
15591        Ok(signalfd as i64)
15592    }
15593
15594    /// Create and register a timer notification descriptor.
15595    ///
15596    /// Determinism: strict execution serializes creation, which exposes only kernel validation,
15597    /// guest-visible flags, and a descriptor number; this operation does not read the clock.
15598    pub async fn handle_timerfd_create<G: Guest<Self>>(
15599        &self,
15600        guest: &mut G,
15601        call: syscalls::TimerfdCreate,
15602    ) -> Result<i64, Error> {
15603        // A timerfd on virtual time is physically nonblocking, as an eventfd
15604        // is, so a logically blocking read retries in scheduler turns
15605        // (internal IO polling) instead of blocking in the host until a
15606        // host-timed moment (`crate::syscalls::timerfd`).
15607        crate::syscalls::timerfd::refuse_unshared_model(guest);
15608        let internally_nonblocking = crate::syscalls::timerfd::enabled(guest);
15609        let injected = if internally_nonblocking {
15610            call.with_flags(call.flags() | syscalls::TimerFlags::TFD_NONBLOCK)
15611        } else {
15612            call
15613        };
15614        let fd = self.record_or_replay(guest, injected).await? as RawFd;
15615        self.add_fd(
15616            guest,
15617            fd,
15618            OFlag::from_bits_truncate(
15619                call.flags().bits() & (libc::TFD_CLOEXEC | libc::TFD_NONBLOCK),
15620            ),
15621            FdType::Timerfd,
15622        )
15623        .await?;
15624        // Its fdinfo's `clockid:`, which no call reads back.
15625        {
15626            use reverie::syscalls::FromToRaw;
15627            let clockid = call.clockid().into_raw() as i32;
15628            guest.thread_state().with_detfd(fd, |detfd| {
15629                detfd.set_fdinfo_facts(FdinfoFacts::Timerfd {
15630                    clockid,
15631                    settime_flags: 0,
15632                    linux: crate::fd::LinuxTimerfd::default(),
15633                })
15634            })?;
15635        }
15636        if internally_nonblocking {
15637            self.maybe_set_nonblocking_fd(guest, fd);
15638        }
15639        Ok(fd as i64)
15640    }
15641
15642    /// Serialize a notification descriptor control operation.
15643    async fn notification_fd_control<G: Guest<Self>>(
15644        &self,
15645        guest: &mut G,
15646        call: Syscall,
15647    ) -> Result<i64, Error> {
15648        let dettid = guest.thread_state().dettid;
15649        resource_request(guest, Resources::new(dettid)).await;
15650        Ok(self.record_or_replay(guest, call).await?)
15651    }
15652
15653    /// timerfd_settime system call. Under virtual time the setting is kept on
15654    /// Detcore's clock and the host timerfd stays disarmed
15655    /// (`crate::syscalls::timerfd`).
15656    pub async fn handle_timerfd_settime<G: Guest<Self>>(
15657        &self,
15658        guest: &mut G,
15659        call: syscalls::TimerfdSettime,
15660    ) -> Result<i64, Error> {
15661        let dettid = guest.thread_state().dettid;
15662        resource_request(guest, Resources::new(dettid)).await;
15663        if let Some(result) = crate::syscalls::timerfd::settime(guest, call).await? {
15664            return Ok(result);
15665        }
15666        Ok(self.record_or_replay(guest, call).await?)
15667    }
15668
15669    /// timerfd_gettime system call, answered from the virtual clock for a
15670    /// timerfd Detcore keeps there.
15671    pub async fn handle_timerfd_gettime<G: Guest<Self>>(
15672        &self,
15673        guest: &mut G,
15674        call: syscalls::TimerfdGettime,
15675    ) -> Result<i64, Error> {
15676        let dettid = guest.thread_state().dettid;
15677        resource_request(guest, Resources::new(dettid)).await;
15678        if let Some(result) = crate::syscalls::timerfd::gettime(guest, call).await? {
15679            return Ok(result);
15680        }
15681        Ok(self.record_or_replay(guest, call).await?)
15682    }
15683
15684    /// inotify_init1 system call (and inotify_init, which is inotify_init1
15685    /// with no flags).
15686    ///
15687    /// As for an eventfd, the instance is made physically nonblocking
15688    /// whenever Detcore sequentializes threads outside record and replay, and
15689    /// the guest's blocking mode is the logical one Detcore keeps on the open
15690    /// file description, which `F_GETFL`, `F_SETFL` and `FIONBIO` show and
15691    /// change (`keeps_physically_nonblocking`). A blocking read with nothing
15692    /// queued then waits in Detcore's turns (`IOAction::NonblockizeRetry`)
15693    /// instead of in the kernel, where nothing orders the other guest tasks
15694    /// whose filesystem operations would end it
15695    /// (<https://github.com/rrnewton/hermit/issues/4768>).
15696    pub async fn handle_inotify_init1<G: Guest<Self>>(
15697        &self,
15698        guest: &mut G,
15699        call: syscalls::InotifyInit1,
15700    ) -> Result<i64, Error> {
15701        let internally_nonblocking =
15702            self.cfg.use_nonblocking_sockets() && !self.cfg.recordreplay_modes;
15703        // Only IN_NONBLOCK is added: flags Linux refuses stay, so the call
15704        // fails with EINVAL as the guest's own would.
15705        let injected = if internally_nonblocking {
15706            call.with_flags(call.flags() | syscalls::InitFlags::IN_NONBLOCK)
15707        } else {
15708            call
15709        };
15710        let fd = self.record_or_replay(guest, injected).await? as RawFd;
15711        self.add_fd(
15712            guest,
15713            fd,
15714            OFlag::from_bits_truncate(call.flags().bits() & (libc::IN_CLOEXEC | libc::IN_NONBLOCK)),
15715            FdType::Inotify,
15716        )
15717        .await?;
15718        if internally_nonblocking {
15719            self.maybe_set_nonblocking_fd(guest, fd);
15720        }
15721        Ok(fd as i64)
15722    }
15723
15724    /// inotify_add_watch system call.
15725    pub async fn handle_inotify_add_watch<G: Guest<Self>>(
15726        &self,
15727        guest: &mut G,
15728        call: syscalls::InotifyAddWatch,
15729    ) -> Result<i64, Error> {
15730        self.notification_fd_control(guest, call.into()).await
15731    }
15732
15733    /// inotify_rm_watch system call.
15734    pub async fn handle_inotify_rm_watch<G: Guest<Self>>(
15735        &self,
15736        guest: &mut G,
15737        call: syscalls::InotifyRmWatch,
15738    ) -> Result<i64, Error> {
15739        self.notification_fd_control(guest, call.into()).await
15740    }
15741
15742    /// memfd_create system call.
15743    pub async fn handle_memfd_create<G: Guest<Self>>(
15744        &self,
15745        guest: &mut G,
15746        call: syscalls::MemfdCreate,
15747    ) -> Result<i64, Error> {
15748        let fd = self.record_or_replay(guest, call).await? as RawFd;
15749        self.add_fd(
15750            guest,
15751            fd,
15752            OFlag::from_bits_truncate((call.flags() & libc::MFD_CLOEXEC) as i32),
15753            FdType::Memfd,
15754        )
15755        .await?;
15756        Ok(fd as i64)
15757    }
15758
15759    // AUTONOMOUS-BOT-IMPLEMENTED
15760    // TODO-HUMAN-REVIEW(PR-862): pidfd creation and Detcore FD registration.
15761    /// Create a pidfd through record/replay and synchronize the descriptor with
15762    /// Detcore's metadata before fcntl, poll, close, or waitid can observe it.
15763    pub async fn handle_pidfd_open<G: Guest<Self>>(
15764        &self,
15765        guest: &mut G,
15766        call: syscalls::PidfdOpen,
15767    ) -> Result<i64, Error> {
15768        let allowed_flags = libc::O_NONBLOCK as u32;
15769        if call.flags() & !allowed_flags != 0 {
15770            return Err(Errno::EINVAL.into());
15771        }
15772        // A pidfd for Hermit's own process, PID 1 of the guest's namespace,
15773        // would let `pidfd_send_signal` signal it and `pidfd_getfd` take its
15774        // descriptors (https://github.com/rrnewton/hermit/issues/4066). Linux
15775        // would open it; refusing it with EPERM is a policy, the answer the
15776        // guest already gets for `ptrace` and `process_vm_readv` on that process.
15777        let pid = call.pid() as i32;
15778        // A nested caller names its own namespace's pid, where none of
15779        // Hermit's tasks is visible; the pidfd records the task it names
15780        // (https://github.com/rrnewton/hermit/issues/4155). A pid no task of
15781        // that namespace has goes to the kernel, which answers ESRCH.
15782        if pid > 0 && guest.thread_state().nested_pid_namespace {
15783            let target = match crate::tool_global::guest_pid(guest, pid).await {
15784                crate::scheduler::pid_namespaces::GuestPidAnswer::Task { task, .. } => Some(task),
15785                crate::scheduler::pid_namespaces::GuestPidAnswer::NoSuchPid => None,
15786                crate::scheduler::pid_namespaces::GuestPidAnswer::Untranslatable(reason) => {
15787                    tracing::error!(
15788                        "[detcore, dtid {}] pidfd_open of pid {} from a nested PID namespace is not supported: {} (https://github.com/rrnewton/hermit/issues/4155)",
15789                        guest.thread_state().dettid,
15790                        pid,
15791                        reason,
15792                    );
15793                    return self
15794                        .refuse_unserviceable_operation(guest, Sysno::pidfd_open, Errno::ENOSYS)
15795                        .await;
15796                }
15797            };
15798            let fd = self.record_or_replay(guest, call).await? as RawFd;
15799            let flags = OFlag::O_CLOEXEC | OFlag::from_bits_truncate(call.flags() as libc::c_int);
15800            self.add_fd(guest, fd, flags, FdType::Pidfd).await?;
15801            if let Some(target) = target {
15802                guest
15803                    .thread_state()
15804                    .with_detfd(fd, |detfd| detfd.set_pidfd_target(target))?;
15805            }
15806            return Ok(fd as i64);
15807        }
15808        if pid > 0 && crate::tool_global::names_hermit_task(guest, Some(pid), pid).await {
15809            tracing::info!(
15810                "[detcore, dtid {}] pidfd_open of {}, Hermit's own process: refused with EPERM \
15811                 (https://github.com/rrnewton/hermit/issues/4066)",
15812                guest.thread_state().dettid,
15813                pid,
15814            );
15815            return Err(Errno::EPERM.into());
15816        }
15817
15818        let fd = self.record_or_replay(guest, call).await? as RawFd;
15819        let flags = OFlag::O_CLOEXEC | OFlag::from_bits_truncate(call.flags() as libc::c_int);
15820        self.add_fd(guest, fd, flags, FdType::Pidfd).await?;
15821        let target = DetPid::from_raw(call.pid() as i32);
15822        guest
15823            .thread_state()
15824            .with_detfd(fd, |detfd| detfd.set_pidfd_target(target))?;
15825        Ok(fd as i64)
15826    }
15827
15828    // AUTONOMOUS-BOT-IMPLEMENTED
15829    // TODO-HUMAN-REVIEW(PR-1175): pidfd_send_signal(2) determinization.
15830    /// Deliver a signal to the process referred to by a pidfd.
15831    ///
15832    /// `pidfd_send_signal(pidfd, sig, info, flags)` names its target by an open
15833    /// kernel descriptor rather than a numeric PID. Unlike `kill(2)`, there is
15834    /// therefore no host-PID/virtual-PID ambiguity for Detcore to resolve: the
15835    /// pidfd was bound to one specific process at `pidfd_open` time. Signal
15836    /// generation runs inside this thread's serialized scheduler turn, as for
15837    /// `tgkill`/`tkill`/`rt_tgsigqueueinfo`, which forward through record/replay
15838    /// and then tell the scheduler that the signal is pending for its receiver
15839    /// (`notify_cross_task_signal`), so that a receiver parked in a blocking call
15840    /// is woken. This handler does the same for a process with one live thread
15841    /// (<https://github.com/rrnewton/hermit/issues/4099>).
15842    ///
15843    /// The flags and the descriptor are decided as Linux decides them, ahead of
15844    /// the forward (<https://github.com/rrnewton/hermit/issues/4108>):
15845    /// - an unknown flag bit, or more than one of the three scope flags, is
15846    ///   EINVAL; `PIDFD_SIGNAL_THREAD_GROUP` is the scope a process pidfd has
15847    ///   anyway, and `PIDFD_SIGNAL_THREAD` sends to the thread whose id is the
15848    ///   pid, as `tgkill(pid, pid)` does; `PIDFD_SIGNAL_PROCESS_GROUP` is
15849    ///   refused by name, even with signal 0, whose answer would come from the
15850    ///   process group the guest inherits from Hermit;
15851    /// - a descriptor names a process when it is a pidfd from `pidfd_open` or
15852    ///   a `/proc/<pid>` directory of the guest's procfs
15853    ///   (`process_directory_descriptor`); any other file is EBADF, and a
15854    ///   descriptor Detcore cannot classify is refused by name.
15855    ///
15856    /// A process-directed signal to a process with several live threads is
15857    /// refused by name, as `kill` refuses it (`Detcore::process_signal_target`).
15858    ///
15859    /// `call` is the raw `Syscall::Other`; `pidfd`/`flags` are pre-extracted from
15860    /// its arguments by the dispatcher.
15861    pub async fn handle_pidfd_send_signal<G: Guest<Self>>(
15862        &self,
15863        guest: &mut G,
15864        call: Syscall,
15865        pidfd: RawFd,
15866        signal: i32,
15867        flags: u32,
15868    ) -> Result<i64, Error> {
15869        // Linux checks the flags before it looks at the descriptor.
15870        let mut scope = pidfd_signal_scope(flags)?;
15871        let info = AddrMut::<GuestSiginfo>::from_raw(call.into_parts().1.arg2);
15872        if let Some(refused) = self
15873            .refuse_forged_own_sigchld(guest, Sysno::pidfd_send_signal, signal, info)
15874            .await
15875        {
15876            return refused;
15877        }
15878        // The two sentinels name the caller without a descriptor: its thread
15879        // (`PIDFD_SELF_THREAD`, thread-directed unless a flag says otherwise)
15880        // or its thread group (`PIDFD_SELF_THREAD_GROUP`)
15881        // (https://github.com/rrnewton/hermit/issues/4147).
15882        let named = if let Some((target, default)) = pidfd_self_target(guest, pidfd) {
15883            if flags == 0 {
15884                scope = default;
15885            }
15886            Some(target)
15887        } else {
15888            // An unknown or closed descriptor is EBADF, and so is any file that is
15889            // neither a pidfd nor an opened path (a pipe, a socket, an eventfd...).
15890            // Linux's fdget skips an O_PATH descriptor, so it is EBADF too.
15891            let kind = guest
15892                .thread_state()
15893                .with_detfd(pidfd, |detfd| match detfd.ty() {
15894                    FdType::Pidfd => Some(Some(detfd.pidfd_target())),
15895                    FdType::Regular if detfd.status_flags() & libc::O_PATH == 0 => Some(None),
15896                    _ => None,
15897                })?;
15898            let Some(kind) = kind else {
15899                return Err(Errno::EBADF.into());
15900            };
15901            // `named` is the pid the descriptor names. A pidfd from `pidfd_open`
15902            // names a process leader; a `/proc/<tid>` directory may name any thread,
15903            // whose process is its leader's (https://github.com/rrnewton/hermit/issues/4094).
15904            match kind {
15905                Some(target) => target,
15906                None => {
15907                    let descriptor = if self.cfg.recordreplay_modes {
15908                        // Replay holds a placeholder for a reopened file, so the
15909                        // descriptor cannot be read the same way in both.
15910                        ProcessDirectoryDescriptor::Unknown(
15911                            "record and replay do not read what a descriptor names",
15912                        )
15913                    } else if !guest.config().backend.host_task_reads_reach_guest_threads {
15914                        // Only on ptrace are the guest's thread ids the host's, so
15915                        // that its descriptors can be read through /proc/<tid>.
15916                        ProcessDirectoryDescriptor::Unknown(
15917                            "this backend's guest thread ids are not host thread ids",
15918                        )
15919                    } else {
15920                        process_directory_descriptor(guest.tid().as_raw(), pidfd)
15921                    };
15922                    match descriptor {
15923                        ProcessDirectoryDescriptor::Process(task)
15924                            if crate::tool_global::names_hermit_task(
15925                                guest,
15926                                None,
15927                                task.as_raw(),
15928                            )
15929                            .await =>
15930                        {
15931                            // Any of Hermit's threads, as a `kill` naming one is
15932                            // dropped and `pidfd_open` of Hermit's process is refused
15933                            // (https://github.com/rrnewton/hermit/issues/4066).
15934                            tracing::info!(
15935                                "[detcore, dtid {}] pidfd_send_signal through /proc/{}, one of \
15936                                 Hermit's own threads: refused with EPERM",
15937                                guest.thread_state().dettid,
15938                                task,
15939                            );
15940                            return Err(Errno::EPERM.into());
15941                        }
15942                        ProcessDirectoryDescriptor::Process(task) => Some(task),
15943                        ProcessDirectoryDescriptor::NotAProcess => return Err(Errno::EBADF.into()),
15944                        ProcessDirectoryDescriptor::Unknown(reason) => {
15945                            tracing::error!(
15946                                "[detcore, dtid {}] pidfd_send_signal through descriptor {} is not \
15947                                 supported: {} (https://github.com/rrnewton/hermit/issues/4108)",
15948                                guest.thread_state().dettid,
15949                                pidfd,
15950                                reason,
15951                            );
15952                            return self
15953                                .refuse_unserviceable_operation(
15954                                    guest,
15955                                    Sysno::pidfd_send_signal,
15956                                    Errno::ENOSYS,
15957                                )
15958                                .await;
15959                        }
15960                    }
15961                }
15962            }
15963        };
15964        if scope == PidfdSignalScope::ProcessGroup {
15965            tracing::error!(
15966                "[detcore, dtid {}] signal {} by pidfd_send_signal with PIDFD_SIGNAL_PROCESS_GROUP \
15967                 is not supported: Detcore does not model process-group membership, and the \
15968                 guest's process group is inherited from Hermit's \
15969                 (https://github.com/rrnewton/hermit/issues/4108)",
15970                guest.thread_state().dettid,
15971                signal,
15972            );
15973            return self
15974                .refuse_unserviceable_operation(guest, Sysno::pidfd_send_signal, Errno::ENOSYS)
15975                .await;
15976        }
15977        // A reserved in-guest signal to another live process is refused here,
15978        // after the group scope's refusal above and before any path below
15979        // forwards the call; a target with no live thread (a zombie, or gone)
15980        // keeps its answer below.
15981        if let Some(refused) = self
15982            .refuse_reserved_signal_send(
15983                guest,
15984                Sysno::pidfd_send_signal,
15985                signal,
15986                crate::syscalls::signal::ReservedSendTarget::Pidfd {
15987                    named,
15988                    thread: scope == PidfdSignalScope::Thread,
15989                },
15990                crate::syscalls::signal::ReservedSendSiginfo::Pidfd(info.map(|info| info.as_raw())),
15991            )
15992            .await
15993        {
15994            return refused;
15995        }
15996        // D48 (A5 step 3c.1c): a stop signal under the in-guest delivery
15997        // gate, the caller's own process included.
15998        if let Some(refused) = self
15999            .refuse_stop_signal_send(
16000                guest,
16001                Sysno::pidfd_send_signal,
16002                signal,
16003                crate::syscalls::signal::ReservedSendTarget::Pidfd {
16004                    named,
16005                    thread: scope == PidfdSignalScope::Thread,
16006                },
16007                crate::syscalls::signal::ReservedSendSiginfo::Pidfd(info.map(|info| info.as_raw())),
16008            )
16009            .await
16010        {
16011            return refused;
16012        }
16013        let process = match named {
16014            Some(named) => Some(signal_target_process(guest, named).await.unwrap_or(named)),
16015            None => None,
16016        };
16017        // A nested caller's signal to its own PID namespace's init is dropped
16018        // by the kernel unless the init catches it, SIGKILL always
16019        // (`Detcore::own_namespace_init_drops`): forward it unchanged, with
16020        // nothing to retire or record
16021        // (https://github.com/rrnewton/hermit/issues/4155).
16022        if guest.thread_state().nested_pid_namespace
16023            && let Some(process) = process
16024            && let Ok(Some(local)) = crate::tool_global::local_pid(guest, process).await
16025            && let crate::scheduler::pid_namespaces::GuestPidAnswer::Task {
16026                own_init_caught, ..
16027            } = crate::tool_global::guest_pid(guest, local).await
16028            && Self::own_namespace_init_drops(own_init_caught, signal)
16029        {
16030            return Ok(self.record_or_replay(guest, call).await?);
16031        }
16032        // A process Detcore saw exit and that is gone is ESRCH, as Linux
16033        // answers once it is reaped, whatever the host has reaped by now
16034        // (https://github.com/rrnewton/hermit/issues/4091).
16035        let mut terminal_no_effect = false;
16036        if guest.config().sequentialize_threads
16037            && let Some(process) = process
16038        {
16039            let (targets, zombie) = resolve_kill_targets_and_exit(guest, process).await;
16040            if targets.is_empty() && zombie == Some(false) {
16041                return self.gone_pidfd_signal_answer(guest, call, signal).await;
16042            }
16043            terminal_no_effect = targets.is_empty() && zombie == Some(true);
16044        }
16045        // The thread scope sends to the thread whose id is the pid, like
16046        // `tgkill(pid, pid)`; the process scope to its process.
16047        let thread_scope = scope == PidfdSignalScope::Thread;
16048        let receiver_id = if thread_scope { named } else { process };
16049        self.record_orphaned_group_signals(guest, signal).await;
16050        if signal == libc::SIGALRM {
16051            // `pidfd_open` records the process a pidfd names; a pidfd with no
16052            // recorded target is refused while any process handles SIGALRM.
16053            let control = match receiver_id {
16054                Some(target) => SigalrmControl::SendTo(DetTid::from_raw(target.as_raw())),
16055                None => SigalrmControl::SendToUnnamed,
16056            };
16057            refuse_sigalrm(guest, control).await?;
16058        }
16059        // A thread-scoped SIGKILL to a leader that exited while its group
16060        // lives on changes nothing, as `tgkill`'s does
16061        // (https://github.com/rrnewton/hermit/issues/4445).
16062        let exited_leader = signal == libc::SIGKILL
16063            && guest.config().sequentialize_threads
16064            && thread_scope
16065            && match named {
16066                Some(thread) => {
16067                    crate::syscalls::signal::sigkill_names_an_exited_leader(
16068                        guest,
16069                        DetTid::from_raw(thread.as_raw()),
16070                    )
16071                    .await
16072                }
16073                None => false,
16074            };
16075        if signal == libc::SIGKILL
16076            && guest.config().sequentialize_threads
16077            && !exited_leader
16078            && let (Some(process), Some(receiver)) = (process, receiver_id)
16079        {
16080            // A SIGKILL ends the whole process whichever thread it names, and
16081            // is retired at this turn as `kill`'s and `tgkill`'s are
16082            // (https://github.com/rrnewton/hermit/issues/3994).
16083            if self
16084                .reserve_controlled_self_sigkill_exit(
16085                    guest,
16086                    signal,
16087                    Some(process),
16088                    thread_scope.then(|| DetTid::from_raw(receiver.as_raw())),
16089                )
16090                .await
16091            {
16092                return Ok(self.record_or_replay(guest, call).await?);
16093            }
16094            if let Some(reservation) =
16095                reserve_sigkill(guest, DetTid::from_raw(receiver.as_raw())).await
16096            {
16097                return self.send_reserved_sigkill(guest, reservation, call).await;
16098            }
16099        }
16100        if thread_scope {
16101            // A thread-scoped send queues the copy in the named thread's
16102            // private queue (the Claude re-check of
16103            // https://github.com/rrnewton/hermit/pull/4131).
16104            let value = match (named, process) {
16105                (Some(thread), Some(process)) => {
16106                    self.send_guest_signal(
16107                        guest,
16108                        call,
16109                        DetTid::from_raw(thread.as_raw()),
16110                        signal,
16111                        Some(process),
16112                        true,
16113                        DetTid::from_raw(thread.as_raw()),
16114                        exited_leader || terminal_no_effect,
16115                    )
16116                    .await?
16117                }
16118                _ => {
16119                    crate::reserved_input::before_signal_send(guest, signal, terminal_no_effect)?;
16120                    self.record_or_replay(guest, call).await?
16121                }
16122            };
16123            if exited_leader {
16124                return Ok(value);
16125            }
16126            if let (Some(thread), Some(process)) = (named, process) {
16127                let thread = DetTid::from_raw(thread.as_raw());
16128                if signal == libc::SIGKILL {
16129                    crate::tool_global::notify_signal_pending(
16130                        guest,
16131                        thread,
16132                        SigWrapper(libc::SIGKILL),
16133                        Some(process),
16134                        crate::tool_global::SignalScope::Thread,
16135                    )
16136                    .await;
16137                } else {
16138                    self.notify_cross_task_signal(
16139                        guest,
16140                        thread,
16141                        signal,
16142                        Some(process),
16143                        crate::tool_global::SignalScope::Thread,
16144                    )
16145                    .await;
16146                }
16147            }
16148            return Ok(value);
16149        }
16150        // A process-directed signal. To a process with two or more live threads
16151        // it goes to the thread Linux's complete_signal picks, as Detcore
16152        // decides it, or waits in the shared queue when none wants it, as for
16153        // `kill` (https://github.com/rrnewton/hermit/issues/4034). Everything
16154        // else is forwarded and the kernel answers: signal 0, which only
16155        // checks that the process exists; a SIGKILL that could not be
16156        // reserved, which kills the whole group whichever thread it reaches
16157        // (the scheduler is told below); a process with one live thread, whose
16158        // receiver the scheduler is then told about, as `kill` does; and a
16159        // process with none (a zombie, which Linux still signals
16160        // successfully).
16161        let mut receiver = None;
16162        // Whether the scheduler chose `receiver` among several threads, so a
16163        // successful send moves the process's `curr_target`.
16164        let mut chosen = false;
16165        if signal != 0
16166            && signal != libc::SIGKILL
16167            && guest.config().sequentialize_threads
16168            && let Some(target) = process
16169        {
16170            // A process-scoped pidfd names the process's leader; a
16171            // `/proc/<tid>` descriptor names its thread.
16172            let named_task = DetTid::from_raw(named.unwrap_or(target).as_raw());
16173            match self
16174                .process_signal_target(
16175                    guest,
16176                    reverie::syscalls::Sysno::pidfd_send_signal,
16177                    target,
16178                    named_task,
16179                    signal,
16180                )
16181                .await
16182            {
16183                Ok(ProcessSignalTarget::Thread(thread)) => receiver = Some((thread, target)),
16184                Ok(ProcessSignalTarget::Chosen(thread)) => {
16185                    receiver = Some((thread, target));
16186                    chosen = true;
16187                }
16188                // Forwarded below, waking no thread: into the shared queue,
16189                // to a zombie, or to a process the kernel answers for.
16190                Ok(ProcessSignalTarget::SharedQueue | ProcessSignalTarget::Zombie)
16191                | Err(Error::Errno(Errno::ESRCH)) => {}
16192                Err(error) => return Err(error),
16193            }
16194        }
16195        let value = match receiver {
16196            Some((thread, process)) => {
16197                // A process-scoped pidfd names the process's leader
16198                // (`pidfd_send_signal` with `PIDTYPE_TGID`).
16199                self.send_guest_signal(
16200                    guest,
16201                    call,
16202                    thread,
16203                    signal,
16204                    Some(process),
16205                    false,
16206                    DetTid::from_raw(process.as_raw()),
16207                    false,
16208                )
16209                .await?
16210            }
16211            None => {
16212                crate::reserved_input::before_signal_send(guest, signal, terminal_no_effect)?;
16213                self.record_or_replay(guest, call).await?
16214            }
16215        };
16216        if chosen && let Some((_, process)) = receiver {
16217            crate::tool_global::process_signal_sent(guest, process).await;
16218        }
16219        if let Some((thread, process)) = receiver {
16220            // Tell the scheduler, as `kill` does, so a receiver parked in a
16221            // blocking call (pause(), a futex wait) is woken to take it
16222            // (https://github.com/rrnewton/hermit/issues/4099).
16223            self.notify_cross_task_signal(
16224                guest,
16225                thread,
16226                signal,
16227                Some(process),
16228                crate::tool_global::SignalScope::Process,
16229            )
16230            .await;
16231        }
16232        if signal == libc::SIGKILL {
16233            // A process killed through a pidfd will die; the scheduler must
16234            // not count a wait for it as part of a deadlock
16235            // (https://github.com/rrnewton/hermit/issues/3904).
16236            match process {
16237                Some(target) => {
16238                    crate::tool_global::notify_signal_pending(
16239                        guest,
16240                        DetTid::from_raw(target.as_raw()),
16241                        SigWrapper(libc::SIGKILL),
16242                        Some(target),
16243                        crate::tool_global::SignalScope::Process,
16244                    )
16245                    .await
16246                }
16247                None => crate::tool_global::notify_unresolved_sigkill(guest, None).await,
16248            }
16249        }
16250        Ok(value)
16251    }
16252
16253    /// The answer to a `pidfd_send_signal` whose target Detcore saw exit and
16254    /// that is gone, in Linux's order: a non-NULL `info` is copied and its
16255    /// layout checked against its own `si_signo` (EFAULT, E2BIG), then that
16256    /// `si_signo` against the signal (EINVAL), then a forged `si_code`
16257    /// (EPERM), and only then is the task missing (ESRCH). The copy, the layout
16258    /// check and the `si_code` check are the kernel's: the same `info` goes to
16259    /// `rt_sigqueueinfo` naming a pid no task can have, which makes those
16260    /// checks and sends nothing (as
16261    /// <https://github.com/rrnewton/hermit/issues/4124> does for that call).
16262    /// `rt_sigqueueinfo` checks the layout against the signal it is given, so
16263    /// it is given the info's own `si_signo`.
16264    async fn gone_pidfd_signal_answer<G: Guest<Self>>(
16265        &self,
16266        guest: &mut G,
16267        call: Syscall,
16268        signal: i32,
16269    ) -> Result<i64, Error> {
16270        let info = match call {
16271            Syscall::Other(_, args) => args.arg2,
16272            _ => 0,
16273        };
16274        let Some(info) = AddrMut::<GuestSiginfo>::from_raw(info) else {
16275            return Err(Errno::ESRCH.into());
16276        };
16277        let signo = guest.memory().read_value(info.cast::<libc::c_int>()).ok();
16278        let probe = syscalls::RtSigqueueinfo::new()
16279            .with_tgid(super::signal::NO_SUCH_PID)
16280            .with_sig(signo.unwrap_or(signal))
16281            .with_siginfo(Some(info.cast()));
16282        let checked = guest.inject(probe).await;
16283        let answer = match checked {
16284            Err(errno @ (Errno::EPERM | Errno::ESRCH)) => errno,
16285            Err(errno) => return Err(errno.into()),
16286            Ok(_) => Errno::ESRCH,
16287        };
16288        // The copy succeeded, so the kernel could read `si_signo`; a page
16289        // only the kernel reaches (`[vvar]`) cannot be decided here.
16290        let Some(signo) = signo else {
16291            tracing::error!(
16292                "[detcore, dtid {}] pidfd_send_signal to a gone process with a siginfo_t at \
16293                 {:#x} is not supported: Hermit cannot read its si_signo \
16294                 (https://github.com/rrnewton/hermit/issues/4108)",
16295                guest.thread_state().dettid,
16296                info.as_raw(),
16297            );
16298            return self
16299                .refuse_unserviceable_operation(guest, Sysno::pidfd_send_signal, Errno::ENOSYS)
16300                .await;
16301        };
16302        if signo != signal {
16303            return Err(Errno::EINVAL.into());
16304        }
16305        Err(answer.into())
16306    }
16307
16308    // AUTONOMOUS-BOT-IMPLEMENTED
16309    // TODO-HUMAN-REVIEW(PR-1175): pidfd_getfd(2) determinization and the
16310    // FdType of the duplicated descriptor.
16311    /// Duplicate a descriptor from the process referred to by a pidfd.
16312    ///
16313    /// `pidfd_getfd(pidfd, targetfd, flags)` returns a fresh descriptor in the
16314    /// caller that aliases `targetfd` in the target process. The source pidfd
16315    /// names one specific process fixed at `pidfd_open` time, the returned
16316    /// descriptor number is chosen through record/replay (so it is stable across
16317    /// runs), and a successful modeled operation executes inside this thread's
16318    /// serialized turn, so the result is deterministic. For zero flags, Detcore
16319    /// fails closed with `EBADF` unless it models the descriptor as a pidfd.
16320    /// Linux checks the kernel-reserved `flags` first, however, so a nonzero
16321    /// value takes the raw record/replay path and preserves the kernel's exact
16322    /// `EINVAL` across valid and invalid descriptor combinations.
16323    ///
16324    /// The modeled path is narrower than "same process": the caller must be the
16325    /// thread-group leader named by the pidfd. `CLONE_THREAD` does not imply
16326    /// `CLONE_FILES`, so a nonleader can share the target's TGID while using a
16327    /// different descriptor table. Requiring `target == getpid() == gettid()`
16328    /// proves that `targetfd` is resolved in the caller's exact table. The
16329    /// returned descriptor is then modeled as a real alias of the source open
16330    /// file description. Broader support needs a cross-task OFD channel that
16331    /// Detcore does not have today, so every other target is refused with
16332    /// `EOPNOTSUPP`. Failed calls leave descriptor state unchanged.
16333    pub async fn handle_pidfd_getfd<G: Guest<Self>>(
16334        &self,
16335        guest: &mut G,
16336        call: Syscall,
16337        pidfd: RawFd,
16338        targetfd: RawFd,
16339        flags: u32,
16340    ) -> Result<i64, Error> {
16341        if flags != 0 {
16342            return match self.record_or_replay(guest, call).await {
16343                Err(error) => Err(error.into()),
16344                Ok(fd) => {
16345                    let fd = fd as RawFd;
16346                    let close_result = guest.inject(syscalls::Close::new().with_fd(fd)).await;
16347                    Err(Error::Tool(anyhow::anyhow!(
16348                        "pidfd_getfd unexpectedly accepted reserved flags and returned fd {fd}; cleanup close result: {close_result:?}"
16349                    )))
16350                }
16351            };
16352        }
16353
16354        if !guest.config().sequentialize_threads {
16355            return self
16356                .refuse_unserviceable_operation(guest, Sysno::pidfd_getfd, Errno::EOPNOTSUPP)
16357                .await;
16358        }
16359
16360        // A nested caller's own getpid and gettid are its namespace's ids,
16361        // while a pidfd records the task in the container's: compare the
16362        // caller's own DetPid and DetTid then
16363        // (https://github.com/rrnewton/hermit/issues/4155).
16364        let (current_tgid, current_tid) = match guest.thread_state().detpid {
16365            Some(detpid) if guest.thread_state().nested_pid_namespace => {
16366                (detpid, guest.thread_state().dettid)
16367            }
16368            _ => (
16369                DetPid::from_raw(guest.inject(syscalls::Getpid::new()).await? as i32),
16370                DetTid::from_raw(guest.inject(syscalls::Gettid::new()).await? as i32),
16371            ),
16372        };
16373        let source = guest.thread_state().capture_pidfd_getfd_source(
16374            pidfd,
16375            targetfd,
16376            current_tgid,
16377            current_tid,
16378        )?;
16379        let fd = match self.record_or_replay(guest, call).await {
16380            Ok(fd) => fd as RawFd,
16381            Err(error) => {
16382                if let Some(open_file_id) = guest.thread_state().abandon_captured_fd(source) {
16383                    self.release_port_for_open_file(guest, open_file_id).await;
16384                }
16385                return Err(error.into());
16386            }
16387        };
16388        // pidfd_getfd always sets FD_CLOEXEC on the returned descriptor.
16389        let replaced = match guest.thread_state_mut().install_captured_fd(
16390            source,
16391            fd,
16392            OFlag::O_CLOEXEC,
16393        ) {
16394            Ok(replaced) => replaced,
16395            Err(error @ CapturedDetFdInstallError { .. }) => {
16396                let expected_files_id = error.expected_files_id;
16397                let actual_files_id = error.actual_files_id;
16398                let mut cleanup = error.into_cleanup();
16399                guest
16400                    .thread_state()
16401                    .defer_released_rights(std::mem::take(&mut cleanup.released_rights));
16402                let close_result = guest
16403                    .inject(syscalls::Close::new().with_fd(cleanup.close_fd))
16404                    .await;
16405                if let Some(open_file_id) = cleanup.release_open_file {
16406                    self.release_port_for_open_file(guest, open_file_id).await;
16407                }
16408                if let Err(close_error) = close_result {
16409                    return Err(Error::Tool(anyhow::anyhow!(
16410                        "pidfd_getfd returned fd {fd}, but its captured source table changed from {expected_files_id:?} to {actual_files_id:?}; cleanup close failed with {close_error}"
16411                    )));
16412                }
16413                warn!(
16414                    "pidfd_getfd returned fd {fd}, but its captured source table changed from {expected_files_id:?} to {actual_files_id:?}; closed the result and refusing with EOPNOTSUPP"
16415                );
16416                return Err(Errno::EOPNOTSUPP.into());
16417            }
16418        };
16419        if let Some(open_file_id) = replaced {
16420            self.release_port_for_open_file(guest, open_file_id).await;
16421        }
16422        // This process now holds the description too, with any OFD or flock
16423        // lock it holds (`procfs::locks`).
16424        self.refresh_description_locks(guest, fd, LockCall::PidfdGetfd, None)
16425            .await?;
16426        Ok(fd as i64)
16427    }
16428
16429    /// userfaultfd system call. A backend that does not implement it declines
16430    /// it with a typed refusal, which is named rather than handed to the
16431    /// guest as its errno (`Detcore::report_backend_refusal`).
16432    pub async fn handle_userfaultfd<G: Guest<Self>>(
16433        &self,
16434        guest: &mut G,
16435        call: syscalls::Userfaultfd,
16436    ) -> Result<i64, Error> {
16437        let result = self
16438            .record_or_replay(guest, call)
16439            .await
16440            .map_err(Error::from);
16441        let fd = self
16442            .report_backend_refusal(
16443                guest,
16444                Sysno::userfaultfd,
16445                reverie::UnsupportedOperation::Userfaultfd,
16446                result,
16447            )
16448            .await? as RawFd;
16449        self.add_fd(
16450            guest,
16451            fd,
16452            OFlag::from_bits_truncate(call.flags()),
16453            FdType::Userfaultfd,
16454        )
16455        .await?;
16456        Ok(fd as i64)
16457    }
16458
16459    /// accept4 system call (MAYHANG).
16460    ///
16461    /// Category: External OR Internal IO
16462    /// ---------------------------------
16463    /// When do we know?  We only know if an accept4 did an extra-container IO AFTER it returns.
16464    /// I.e. we could accept a connection from another endpoint in the container, or from the outside,
16465    /// and we don't know which at the point where `accept4` is called.
16466    pub async fn handle_accept4<G: Guest<Self>>(
16467        &self,
16468        guest: &mut G,
16469        call: syscalls::Accept4,
16470    ) -> Result<i64, Error> {
16471        // This option applies both to the socket we're doing the accept call on, and the connection
16472        // that we return. We don't have any smart detection yet to separate internal/external, so
16473        // applies to everything.
16474        //
16475        // Record and replay leave the connection as the guest asked, as `handle_socket` does.
16476        // Their reads and writes on it run as backgrounded blocking calls, and on a physically
16477        // nonblocking connection such a call returns EAGAIN to a guest that asked to block
16478        // (https://github.com/rrnewton/hermit/issues/3882). The rejoin log orders their results.
16479        // TODO-HUMAN-REVIEW(PR-3908)
16480        let physically_nonblocking =
16481            self.cfg.use_nonblocking_sockets() && !self.cfg.recordreplay_modes;
16482        let call2 = if physically_nonblocking {
16483            // Let the socket returned from accept4 be physically nonblocking:
16484            call.with_flags(call.flags() | SockFlag::SOCK_NONBLOCK)
16485        } else {
16486            call
16487        };
16488        let in_turn = self.accept_in_turn_enabled()
16489            && guest
16490                .thread_state()
16491                .with_detfd(call.sockfd(), |detfd| detfd.is_accept_in_turn_listener())
16492                .unwrap_or(false);
16493        let completes_in_turn = in_turn || self.completes_in_turn(guest, call2);
16494        // A connection reads its error queue as its listener does. Read before
16495        // the accept, which may wait while other threads change the table.
16496        let error_queue_never_waits = guest
16497            .thread_state()
16498            .with_detfd(call.sockfd(), |detfd| detfd.error_queue_never_waits())
16499            .unwrap_or(false);
16500        let fd = if in_turn {
16501            // Record and replay run the accept in the caller's turn, so the new descriptor's
16502            // number is allocated deterministically (https://github.com/rrnewton/hermit/issues/3880).
16503            self.accept_in_turn(guest, call2).await? as RawFd
16504        } else {
16505            // This will do blocking/polling as appropriate based on the fd status:
16506            self.execute_nonblockable_fd_syscall(guest, call2).await? as RawFd
16507        };
16508
16509        self.add_fd(
16510            guest,
16511            fd,
16512            // This will specify whether the socket returned is logically non-blocking:
16513            oflag_from_sock_bits(call.flags().bits()),
16514            FdType::Socket,
16515        )
16516        .await?;
16517        guest
16518            .thread_state()
16519            .with_detfd(fd, |detfd| detfd.set_accepted_connection())?;
16520        let peer = self.note_unix_accept(guest, call.sockfd(), fd, completes_in_turn);
16521        guest.thread_state().with_detfd(fd, |detfd| {
16522            detfd.set_accepted_connection();
16523            detfd.set_error_queue_never_waits(error_queue_never_waits);
16524        })?;
16525        // Guests alone hold both ends only where the listener and the socket
16526        // whose connection this took are sockets guests made and never passed
16527        // out (`Detcore::classify_guest_held_peer`).
16528        let listener = guest
16529            .thread_state()
16530            .with_detfd(call.sockfd(), |listener| listener.open_file_handle())
16531            .ok();
16532        let guest_created = crate::syscalls::scm_rights::both_ends_guest_made(
16533            completes_in_turn,
16534            peer.as_ref(),
16535            listener.as_ref(),
16536        );
16537        self.classify_guest_held_peer(guest, fd, guest_created)
16538            .await;
16539
16540        if physically_nonblocking {
16541            self.maybe_set_nonblocking_fd(guest, fd);
16542        }
16543        self.classify_socket_peer(guest, fd, SocketRole::Connected)
16544            .await;
16545
16546        Ok(fd as i64)
16547    }
16548
16549    /// getdents system call.
16550    #[cfg(target_arch = "x86_64")]
16551    pub async fn handle_getdents<G: Guest<Self>>(
16552        &self,
16553        guest: &mut G,
16554        call: syscalls::Getdents,
16555    ) -> Result<i64, Error> {
16556        let virtualize = guest.config().virtualize_metadata;
16557        if !virtualize
16558            && (call.dirent().is_none()
16559                || !self.is_caller_descriptor_table(guest, call.fd() as i32))
16560        {
16561            return Ok(self.record_or_replay(guest, call).await?);
16562        }
16563        let buf = call.dirent().ok_or(Errno::EFAULT)?.cast::<u8>();
16564        let call = GetdentsCall {
16565            call: Syscall::from(call),
16566            empty: Syscall::from(call.with_count(0)),
16567            fd: call.fd() as i32,
16568            buf,
16569            capacity: call.count() as usize,
16570            format: DirentFormat::Legacy,
16571        };
16572        if virtualize {
16573            self.serve_directory_stream(guest, call).await
16574        } else {
16575            self.project_one_buffer(guest, call).await
16576        }
16577    }
16578
16579    /// getdents64 system call.
16580    pub async fn handle_getdents64<G: Guest<Self>>(
16581        &self,
16582        guest: &mut G,
16583        call: syscalls::Getdents64,
16584    ) -> Result<i64, Error> {
16585        let virtualize = guest.config().virtualize_metadata;
16586        if !virtualize
16587            && (call.dirent().is_none()
16588                || !self.is_caller_descriptor_table(guest, call.fd() as i32))
16589        {
16590            return Ok(self.record_or_replay(guest, call).await?);
16591        }
16592        let buf = call.dirent().ok_or(Errno::EFAULT)?.cast::<u8>();
16593        let call = GetdentsCall {
16594            call: Syscall::from(call),
16595            empty: Syscall::from(call.with_count(0)),
16596            fd: call.fd() as i32,
16597            buf,
16598            capacity: call.count() as usize,
16599            format: DirentFormat::Dirent64,
16600        };
16601        if virtualize {
16602            self.serve_directory_stream(guest, call).await
16603        } else {
16604            self.project_one_buffer(guest, call).await
16605        }
16606    }
16607
16608    /// Answer a `getdents` call from the open file's sorted directory stream
16609    /// (see [`DirectoryStream`]), reading the host directory first if the
16610    /// stream has no snapshot.
16611    ///
16612    /// Sorting each kernel buffer on its own is not enough: a directory larger
16613    /// than one buffer (about a thousand short names for glibc's 32KiB) would
16614    /// come back as sorted runs whose boundaries, and the host `d_off` cookies
16615    /// inside them, depend on the filesystem's on-disk layout.
16616    async fn serve_directory_stream<G: Guest<Self>>(
16617        &self,
16618        guest: &mut G,
16619        call: GetdentsCall<'_>,
16620    ) -> Result<i64, Error> {
16621        let negative = i32::try_from(call.capacity).is_err();
16622        let lock = match guest
16623            .thread_state()
16624            .with_detfd(call.fd, |detfd| detfd.directory_lock())
16625        {
16626            Ok(lock) => lock,
16627            // Detcore has no open file description for this descriptor (one
16628            // received through SCM_RIGHTS and not recorded, for example), so there is nowhere
16629            // to keep a stream. Sort the one kernel buffer instead, as before
16630            // directory streams existed; the kernel reports a descriptor that
16631            // is not open at all.
16632            Err(Errno::EBADF) if negative => {
16633                return self.serve_negative_count(guest, call, None).await;
16634            }
16635            Err(Errno::EBADF) => return self.project_one_buffer(guest, call).await,
16636            Err(errno) => return Err(errno.into()),
16637        };
16638        let _serving = lock.lock().await;
16639        let (host_order, needs_snapshot, position) =
16640            guest.thread_state().with_detfd(call.fd, |detfd| {
16641                (
16642                    detfd.directory_in_host_order(),
16643                    detfd.directory_needs_snapshot(),
16644                    detfd.with_directory_stream(|stream| stream.position()).ok(),
16645                )
16646            })?;
16647        if negative {
16648            let stream = if host_order {
16649                None
16650            } else {
16651                position.map(|position| (position, needs_snapshot))
16652            };
16653            return self.serve_negative_count(guest, call, stream).await;
16654        }
16655        if host_order {
16656            return self.project_one_buffer(guest, call).await;
16657        }
16658        if !needs_snapshot {
16659            return self.serve_next_batch(guest, call).await;
16660        }
16661        let mark = directory_snapshot_mark(guest).await;
16662        if let Some((entries, inodes, order)) = self.modeled_directory_snapshot(guest, call).await?
16663        {
16664            let mut entries = Some(entries);
16665            guest.thread_state().with_detfd(call.fd, |detfd| {
16666                detfd.install_directory_snapshot(
16667                    entries.take().unwrap_or_default(),
16668                    mark,
16669                    inodes.clone(),
16670                    order,
16671                )
16672            })?;
16673            return self.serve_next_batch(guest, call).await;
16674        }
16675        // How to key the entries' numbers is decided with the snapshot, as a
16676        // procfs directory loses its name once its task exits (see
16677        // `DirectoryStream`).
16678        let inodes = self.directory_inodes(guest, call.fd).await?;
16679        match self.snapshot_directory_privately(guest, call).await {
16680            Ok(Some(entries)) => {
16681                let mut entries = Some(entries);
16682                guest.thread_state().with_detfd(call.fd, |detfd| {
16683                    detfd.install_directory_snapshot(
16684                        entries.take().unwrap_or_default(),
16685                        mark,
16686                        inodes.clone(),
16687                        DirectoryOrder::NameBytes,
16688                    )
16689                })?;
16690                self.serve_next_batch(guest, call).await
16691            }
16692            Ok(None) => {
16693                guest
16694                    .thread_state()
16695                    .with_detfd(call.fd, |detfd| detfd.use_host_directory_order())?;
16696                self.project_one_buffer(guest, call).await
16697            }
16698            Err(error) => Err(error),
16699        }
16700    }
16701
16702    /// Answer a `getdents` whose count does not fit in an `int`. Linux keeps
16703    /// the count in one, where it is negative, so no entry fits: the call
16704    /// fails with `EINVAL`, or returns 0 at the end of the directory, and
16705    /// moves nothing. It does not look at the buffer, so a count of 0 gets
16706    /// the same answer for any pointer, and the same `EBADF`, `ENOTDIR` or
16707    /// `ENOENT`; that is the call issued, since a backend that does not
16708    /// truncate the guest's count to an `int` would copy host records into
16709    /// the guest's buffer.
16710    ///
16711    /// `stream` is the directory stream's position and whether it needs a
16712    /// snapshot, or `None` when the open file has no stream (it has not been
16713    /// read, is read in host order, or is not tracked). Then the guest's
16714    /// position is the kernel's, so the kernel's answer is Linux's. So it is
16715    /// for a stream rewound to 0, whose kernel position is the start. A
16716    /// stream that has a snapshot decides between `EINVAL` and 0 itself. The
16717    /// kernel position of a stream without a snapshot is moved to the start
16718    /// first, and then to where the snapshot, if made, puts it. A descriptor
16719    /// Detcore does not track that shares the open file, and had moved that
16720    /// position, reads on from there, where on Linux the call moves nothing. A
16721    /// stream rewound and then seeked past 0 needs to know how many entries
16722    /// the directory has, so a snapshot is taken (see
16723    /// [`Self::snapshot_directory_privately`]). If the directory must be
16724    /// read in host order instead, the kernel's answer from the start is
16725    /// returned, which is `EINVAL` even at or past the end; if no mapping can
16726    /// be made for the snapshot, the call fails with `ENOMEM`
16727    /// (https://github.com/rrnewton/hermit/issues/3723).
16728    async fn serve_negative_count<G: Guest<Self>>(
16729        &self,
16730        guest: &mut G,
16731        call: GetdentsCall<'_>,
16732        stream: Option<(u64, bool)>,
16733    ) -> Result<i64, Error> {
16734        if let Some((_, true)) = stream {
16735            // Without a snapshot, the kernel position of a stream is the
16736            // start; a descriptor Detcore does not track may have moved it.
16737            self.move_directory_kernel_position(guest, call.fd, 0).await;
16738        }
16739        let kernel = self.record_or_replay(guest, call.empty).await;
16740        match kernel {
16741            Ok(_) | Err(Errno::EINVAL) => {}
16742            Err(errno) => return Err(errno.into()),
16743        }
16744        match stream {
16745            None | Some((0, true)) => return Ok(kernel.map(|_| 0)?),
16746            Some((_, true)) => {
16747                let mark = directory_snapshot_mark(guest).await;
16748                let modeled = self.modeled_directory_snapshot(guest, call).await?;
16749                let (snapshot, inodes, order) = match modeled {
16750                    Some((entries, inodes, order)) => (Ok(Some(entries)), inodes, order),
16751                    None => {
16752                        let inodes = self.directory_inodes(guest, call.fd).await?;
16753                        (
16754                            self.snapshot_directory_privately(guest, call).await,
16755                            inodes,
16756                            DirectoryOrder::NameBytes,
16757                        )
16758                    }
16759                };
16760                match snapshot {
16761                    Ok(Some(entries)) => {
16762                        let mut entries = Some(entries);
16763                        let target = guest.thread_state().with_detfd(call.fd, |detfd| {
16764                            detfd.install_directory_snapshot(
16765                                entries.take().unwrap_or_default(),
16766                                mark,
16767                                inodes.clone(),
16768                                order,
16769                            );
16770                            detfd.with_directory_stream(|stream| stream.kernel_target())
16771                        })??;
16772                        self.move_directory_kernel_position(guest, call.fd, target)
16773                            .await;
16774                    }
16775                    Ok(None) => return Ok(kernel.map(|_| 0)?),
16776                    Err(error) => return Err(error),
16777                }
16778            }
16779            Some((_, false)) => {}
16780        }
16781        let batch = guest.thread_state().with_detfd(call.fd, |detfd| {
16782            detfd.with_directory_stream(|stream| stream.next_batch(call.format, 0))
16783        })??;
16784        batch?;
16785        Ok(0)
16786    }
16787
16788    /// Return the entries at the stream's position that fit in the guest's
16789    /// buffer, with determinized inodes, keyed as decided when the snapshot
16790    /// was taken, and each `d_off` naming the position after its entry,
16791    /// counting from 1.
16792    ///
16793    /// Whatever the outcome, the kernel position then follows the stream (see
16794    /// [`DirectoryStream::kernel_target`]).
16795    async fn serve_next_batch<G: Guest<Self>>(
16796        &self,
16797        guest: &mut G,
16798        call: GetdentsCall<'_>,
16799    ) -> Result<i64, Error> {
16800        let (start, batch, mark, inodes) =
16801            guest.thread_state().with_detfd(call.fd, |detfd| {
16802                detfd.with_directory_stream(|stream| {
16803                    (
16804                        stream.position(),
16805                        stream.next_batch(call.format, call.capacity),
16806                        stream.snapshot_mark(),
16807                        stream.snapshot_inodes(),
16808                    )
16809                })
16810            })??;
16811        let (passed, copied) = match batch {
16812            Ok(batch) => {
16813                // Every snapshot is installed with its keys; a stream without
16814                // them is keyed now.
16815                let inodes = match inodes {
16816                    Some(inodes) => inodes,
16817                    None => self.directory_inodes(guest, call.fd).await?,
16818                };
16819                let mut records = Vec::new();
16820                let mut names = Vec::with_capacity(batch.len());
16821                for (index, entry) in batch.iter().enumerate() {
16822                    let (host, path) = inodes.entry_inode(&entry.name, entry.ino);
16823                    let files = self.rendered_files(guest);
16824                    let d_ino = match self
16825                        .rendered_listed_inode(guest, &inodes, &entry.name, entry.ino, files)
16826                        .await?
16827                    {
16828                        Some(inode) => DetInode::mint(inode),
16829                        None => determinize_listed_inode(guest, host, path, mark).await.0,
16830                    };
16831                    let d_off = i64::try_from(start + index as u64 + 1).unwrap_or(i64::MAX);
16832                    call.format
16833                        .encode(entry, d_ino.as_raw(), d_off, &mut records);
16834                    names.push(entry.name.len());
16835                }
16836                copy_records(
16837                    &mut guest.memory(),
16838                    call.buf,
16839                    call.format,
16840                    &records,
16841                    &names,
16842                    start,
16843                )
16844            }
16845            Err(errno) => (0, Err(errno)),
16846        };
16847        // With --no-sequentialize-threads another thread can close the
16848        // descriptor during the copy; like Linux, the call's result stands.
16849        let target = guest.thread_state().with_detfd(call.fd, |detfd| {
16850            detfd.with_directory_stream(|stream| {
16851                stream.advance(passed);
16852                stream.kernel_target()
16853            })
16854        });
16855        if let Ok(Ok(target)) = target {
16856            self.move_directory_kernel_position(guest, call.fd, target)
16857                .await;
16858        }
16859        Ok(copied.map(|len| len as i64)?)
16860    }
16861
16862    /// The entries of the `/proc` directory open as `fd`, and how to key their
16863    /// inode numbers, if Detcore lists it from its own model (see
16864    /// [`crate::procfs::listing`]): the root, the level table's other
16865    /// directories, a process's or a thread's directory, or a process's
16866    /// `task`, with processes and threads as the guest numbers them, so that
16867    /// every backend lists the same.
16868    ///
16869    /// The directory is the one the descriptor was opened on: where guest
16870    /// thread IDs name host tasks (ptrace), the kernel's canonical name for
16871    /// it (`procfs_entry`), which bound `self` and `thread-self` to the
16872    /// opener, so an inherited or passed descriptor keeps listing the
16873    /// opener's; on a backend that serves its own `/proc` (KVM), the guest's
16874    /// spelling, and only a directory with no process in its path (the root
16875    /// and the table's system directories), as its process directories cannot
16876    /// be opened yet (slice B3a-4).
16877    ///
16878    /// Each entry is looked up by its spelling below the directory, as the
16879    /// guest's own `lstat` of it would be: on the kernel's procfs an entry the
16880    /// lookup does not find (a name the level lists but this kernel lacks,
16881    /// such as `children` without `CONFIG_PROC_CHILDREN`) is left out, unless
16882    /// Detcore renders it; on a backend's own /proc (KVM), whose lookups
16883    /// follow its executor's process table at host-timed moments, a lookup
16884    /// decides no entry. One the lookup finds is
16885    /// numbered by the inode it reports, which a stat of it pools under too
16886    /// (with the entry's name and its task's incarnation), so the two agree. A
16887    /// file Detcore renders is numbered by its rendered inode
16888    /// ([`Self::serve_next_batch`]), and an entry the backend refuses to look
16889    /// up (KVM) by its path's key.
16890    ///
16891    /// The kernel's directory is still read once, as for any other directory
16892    /// ([`Self::snapshot_directory_privately`]), for its `d_off` cookies: each
16893    /// entry carries the cookie of the host entry of its name, so that the
16894    /// stream leaves the kernel position where a descriptor Detcore does not
16895    /// track reads on from (at the end, the kernel's own end). A descriptor
16896    /// whose position the guest moved before its first read is then read in
16897    /// host order, as any other is.
16898    ///
16899    /// `None`, so that the kernel's listing is served as before: for any other
16900    /// directory; an `O_PATH` descriptor, which lists nothing (the kernel's
16901    /// `EBADF`); under record and replay, where the replayer's descriptor
16902    /// does not name the recorded directory; in a nested PID namespace, whose
16903    /// numbers are not Detcore's; after a guest `chroot`, where `/proc` need
16904    /// not be procfs; for a descriptor not on the guest's procfs (a user bind
16905    /// over part of it); on the in-guest backends whose kernel numbers
16906    /// processes otherwise than Detcore (DBT,
16907    /// <https://github.com/rrnewton/hermit/issues/4009>), where a listed guest
16908    /// pid would open the kernel's process of that number until `/proc/<pid>`
16909    /// is translated (slice B3a-4); and for a process or thread the scheduler
16910    /// does not list, which the kernel's directory then answers for.
16911    async fn modeled_directory_snapshot<G: Guest<Self>>(
16912        &self,
16913        guest: &mut G,
16914        call: GetdentsCall<'_>,
16915    ) -> Result<Option<(Vec<DirEntry>, DirectoryInodes, DirectoryOrder)>, Error> {
16916        let fd = call.fd;
16917        use std::os::unix::ffi::OsStrExt;
16918
16919        use crate::procfs::listing;
16920        use crate::procfs::listing::ModeledDirectory;
16921        let state = guest.thread_state();
16922        if self.cfg.recordreplay_modes
16923            || state.nested_pid_namespace
16924            || state.changed_root.load(Ordering::SeqCst)
16925        {
16926            return Ok(None);
16927        }
16928        let Ok((spelled, status_flags)) =
16929            state.with_detfd(fd, |detfd| (detfd.path(), detfd.status_flags()))
16930        else {
16931            return Ok(None);
16932        };
16933        // Only a descriptor the guest opened by a spelling of such a
16934        // directory is one, which spares every other directory's listing the
16935        // naming below.
16936        if status_flags & libc::O_PATH != 0 {
16937            return Ok(None);
16938        }
16939        // Where numbers are translated (DBT), the kernel's name for the
16940        // descriptor says which directory it is, whatever spelling opened it
16941        // (`/proc/self/fd/N` of a descriptor of `/proc`).
16942        let kernel_named = || {
16943            std::fs::read_link(format!("/proc/thread-self/fd/{fd}"))
16944                .is_ok_and(|kernel| listing::classify(&kernel, 0, 0).is_some())
16945        };
16946        if spelled
16947            .as_deref()
16948            .and_then(|spelled| listing::classify(spelled, 0, 0))
16949            .is_none()
16950            && !(Self::translates_procfs_numbers(guest)
16951                && guest.config().backend.tool_shares_guest_descriptor_table
16952                && kernel_named())
16953        {
16954            return Ok(None);
16955        }
16956        let state = guest.thread_state();
16957        let tid = state.dettid.as_raw();
16958        let names_tasks =
16959            procfs_inode::guest_tids_name_host_tasks(&self.cfg.backend, state.physical_tid);
16960        let pid = self.own_detpid(guest).as_raw();
16961        let cached = guest
16962            .thread_state()
16963            .with_detfd(fd, |detfd| detfd.stat())
16964            .ok()
16965            .flatten();
16966        let stat = match cached {
16967            Some(stat) => stat,
16968            None => DetStat::from(self.inject_fstat(guest, fd).await?),
16969        };
16970        if stat.mode & libc::S_IFMT != libc::S_IFDIR {
16971            return Ok(None);
16972        }
16973        // Where the kernel numbers processes otherwise than Detcore (DBT),
16974        // the directory is the one the guest's spelling names, which reached
16975        // the kernel's directory of the same process (`procfs_numbers`).
16976        let translates = Self::translates_procfs_numbers(guest)
16977            && rendered_procfs::proc_device() == Some(stat.dev);
16978        let path = if names_tasks && rendered_procfs::proc_device() == Some(stat.dev) {
16979            match Self::procfs_entry(guest, &stat, StatTarget::Descriptor(fd)).await {
16980                ProcfsEntry::At(within) => {
16981                    Path::new("/proc").join(within.strip_prefix("/").unwrap_or(&within))
16982                }
16983                ProcfsEntry::NotProcfs | ProcfsEntry::Unnamed | ProcfsEntry::Unknown => {
16984                    return Ok(None);
16985                }
16986            }
16987        } else if translates {
16988            match Self::guest_procfs_spelling(guest, fd).await {
16989                Some(path) => path,
16990                None => return Ok(None),
16991            }
16992        } else {
16993            return Ok(None);
16994        };
16995        let Some(directory) = listing::classify(&path, pid, tid) else {
16996            return Ok(None);
16997        };
16998        let process = match directory {
16999            ModeledDirectory::System(_) => None,
17000            ModeledDirectory::Process(process)
17001            | ModeledDirectory::Thread(process, _)
17002            | ModeledDirectory::Tasks(process) => Some(DetPid::from_raw(process)),
17003        };
17004        let facts = crate::tool_global::procfs_listing(guest, process).await;
17005        let listed = match directory {
17006            ModeledDirectory::System(_) => true,
17007            // The namespace's init is Hermit's own process unless a guest
17008            // holds pid 1; its threads are the kernel's to list.
17009            ModeledDirectory::Process(process) => {
17010                process == 1 || facts.processes.contains(&process)
17011            }
17012            ModeledDirectory::Thread(_, thread) => facts.threads.contains(&thread),
17013            ModeledDirectory::Tasks(_) => !facts.threads.is_empty(),
17014        };
17015        if !listed {
17016            return Ok(None);
17017        }
17018        let Some(names) = listing::entries(&directory, self.cfg.proc_level, &facts) else {
17019            return Ok(None);
17020        };
17021        // The host's cookies, where the kernel lists the directory.
17022        let Some(host) = self.snapshot_directory_privately(guest, call).await? else {
17023            return Ok(None);
17024        };
17025        // A process or thread number the kernel lists is the kernel's (DBT):
17026        // its cookie is the cookie of the guest's number for it.
17027        let host_numbers = if translates {
17028            let held = host
17029                .iter()
17030                .filter(|entry| {
17031                    crate::procfs::ids::names_an_id(&path, std::ffi::OsStr::from_bytes(&entry.name))
17032                })
17033                .filter_map(|entry| std::str::from_utf8(&entry.name).ok()?.parse::<i32>().ok())
17034                .collect::<Vec<_>>();
17035            Self::guest_procfs_numbers(guest, &held)
17036                .await
17037                .unwrap_or_default()
17038        } else {
17039            BTreeMap::new()
17040        };
17041        // A kernel number that is no guest's has no cookie to give: its name
17042        // may be another guest number's.
17043        let host_cookies: std::collections::HashMap<Vec<u8>, i64> = host
17044            .iter()
17045            .filter_map(|entry| {
17046                if !translates
17047                    || !crate::procfs::ids::names_an_id(
17048                        &path,
17049                        std::ffi::OsStr::from_bytes(&entry.name),
17050                    )
17051                {
17052                    return Some((entry.name.clone(), entry.off));
17053                }
17054                let kernel = std::str::from_utf8(&entry.name).ok()?.parse::<i32>().ok()?;
17055                let guest = host_numbers.get(&kernel)?;
17056                Some((guest.to_string().into_bytes(), entry.off))
17057            })
17058            .collect();
17059        let host_cookie = |name: &[u8]| host_cookies.get(name).copied().unwrap_or(0);
17060        let session_files = self.rendered_files(guest);
17061        let host_end = host.last().map_or(0, |entry| entry.off);
17062        // Looked up from the guest's own descriptor where it names the
17063        // kernel's directory, and by the guest's spelling on DBT, whose lookup
17064        // translates the guest's numbers to the kernel's as the guest's own
17065        // lookup of each would be (`inject_statat`, `procfs_numbers`).
17066        let (lookup_dir, spellings): (RawFd, Vec<PathBuf>) = if translates {
17067            let spellings = names
17068                .iter()
17069                .map(|(name, _)| path.join(std::ffi::OsStr::from_bytes(name)))
17070                .collect();
17071            (libc::AT_FDCWD, spellings)
17072        } else {
17073            let spellings = names
17074                .iter()
17075                .map(|(name, _)| PathBuf::from(std::ffi::OsStr::from_bytes(name)))
17076                .collect();
17077            (fd, spellings)
17078        };
17079        let looked_up = self.looked_up_inodes(guest, lookup_dir, &spellings).await?;
17080        let within = listing::within(&directory);
17081        let entries = names
17082            .into_iter()
17083            .zip(looked_up)
17084            .filter_map(|((name, ty), looked_up)| {
17085                let entry = match name.as_slice() {
17086                    b"." => within.clone(),
17087                    b".." => within.parent().unwrap_or(&within).to_path_buf(),
17088                    name => within.join(std::ffi::OsStr::from_bytes(name)),
17089                };
17090                // A file Detcore renders exists whatever the kernel has. (Not
17091                // a task's `comm` where the kernel serves every one.)
17092                let rendered = crate::procfs::ProcfsFile::from_path(
17093                    &Path::new("/proc").join(entry.strip_prefix("/").unwrap_or(&entry)),
17094                )
17095                .is_some_and(|file| {
17096                    !self.kernel_serves_every_comm(&file)
17097                        && file.rendered_path(session_files).is_some()
17098                });
17099                let ino = match looked_up {
17100                    Ok(ino) => ino,
17101                    Err(Errno::ENOENT) if name != b"." && name != b".." && !rendered => {
17102                        return None;
17103                    }
17104                    Err(_) => procfs_inode::path_inode(stat.dev, &entry).ino,
17105                };
17106                Some(DirEntry {
17107                    ino,
17108                    off: host_cookie(&name),
17109                    ty,
17110                    name,
17111                })
17112            })
17113            .collect::<Vec<_>>();
17114        // In host order, which `DirectoryStream::install` takes them in, and
17115        // ending where the host's listing ends, so the stream's end is the
17116        // kernel's.
17117        let mut entries = entries;
17118        entries.sort_by_key(|entry| entry.off);
17119        if let Some(last) = entries.last_mut() {
17120            last.off = last.off.max(host_end);
17121        }
17122        let inodes = DirectoryInodes::new(stat.dev, Some(ProcfsDirectory::new(within)));
17123        let order = match &directory {
17124            ModeledDirectory::System(names) if names.is_empty() => DirectoryOrder::ProcfsIds,
17125            ModeledDirectory::Tasks(_) => DirectoryOrder::ProcfsIds,
17126            _ => DirectoryOrder::NameBytes,
17127        };
17128        Ok(Some((entries, inodes, order)))
17129    }
17130
17131    /// The inode number a lookup of each of `paths` from the directory open as
17132    /// `dir` (or absolute, with `AT_FDCWD`), without following a final link,
17133    /// gives in the guest, as the guest's own `fstatat` of it would, or the
17134    /// lookup's error. The paths are staged in an
17135    /// anonymous mapping injected for the purpose and unmapped before the guest
17136    /// runs again, as a directory snapshot's reads are (see
17137    /// [`Self::snapshot_directory_privately`]); without one, every lookup is
17138    /// `ENOMEM`. A mapping that cannot be unmapped would be visible to the
17139    /// guest, so that is an error.
17140    async fn looked_up_inodes<G: Guest<Self>>(
17141        &self,
17142        guest: &mut G,
17143        dir: RawFd,
17144        paths: &[PathBuf],
17145    ) -> Result<Vec<Result<u64, Errno>>, Error> {
17146        use std::os::unix::ffi::OsStrExt;
17147
17148        let mapped = guest
17149            .inject_with_retry(Syscall::Mmap(
17150                syscalls::Mmap::new()
17151                    .with_addr(None)
17152                    .with_len(DIRECTORY_PAGE)
17153                    .with_prot(ProtFlags::PROT_READ | ProtFlags::PROT_WRITE)
17154                    .with_flags(MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS)
17155                    .with_fd(-1)
17156                    .with_offset(0),
17157            ))
17158            .await;
17159        let Some(page) = mapped
17160            .ok()
17161            .and_then(|addr| usize::try_from(addr).ok())
17162            .and_then(AddrMut::<u8>::from_raw)
17163        else {
17164            return Ok(vec![Err(Errno::ENOMEM); paths.len()]);
17165        };
17166        let mut inodes = Vec::with_capacity(paths.len());
17167        for path in paths {
17168            let mut bytes = path.as_os_str().as_bytes().to_vec();
17169            bytes.push(0);
17170            let staged =
17171                bytes.len() <= DIRECTORY_PAGE && guest.memory().write_exact(page, &bytes).is_ok();
17172            let staged_path = PathPtr::from_ptr(page.as_raw() as *const libc::c_char);
17173            let inode = match (staged, staged_path) {
17174                (true, Some(staged_path)) => Self::inject_statat(
17175                    guest,
17176                    dir,
17177                    staged_path,
17178                    [Some(staged_path), None],
17179                    AtFlags::AT_SYMLINK_NOFOLLOW,
17180                )
17181                .await
17182                .map(|stat| stat.st_ino),
17183                _ => Err(Errno::ENAMETOOLONG),
17184            };
17185            inodes.push(inode);
17186        }
17187        guest
17188            .inject_with_retry(Syscall::Munmap(
17189                syscalls::Munmap::new()
17190                    .with_addr(Some(Addr::from(page).cast()))
17191                    .with_len(DIRECTORY_PAGE),
17192            ))
17193            .await?;
17194        Ok(inodes)
17195    }
17196
17197    /// How to key the inode numbers of the entries of the directory open as
17198    /// `fd`. Every entry is on the directory's host device (a mount point's
17199    /// entry names the directory beneath the mount, on the same device), and
17200    /// a procfs directory's entries also name their paths beneath its
17201    /// canonical path (see [`procfs_inode`]). The directory's stat is the
17202    /// cached one when Detcore tracks `fd`, and an injected `fstat` otherwise.
17203    async fn directory_inodes<G: Guest<Self>>(
17204        &self,
17205        guest: &mut G,
17206        fd: RawFd,
17207    ) -> Result<DirectoryInodes, Errno> {
17208        let cached = guest
17209            .thread_state()
17210            .with_detfd(fd, |detfd| detfd.stat())
17211            .ok()
17212            .flatten();
17213        let stat = match cached {
17214            Some(stat) => stat,
17215            None => DetStat::from(self.inject_fstat(guest, fd).await?),
17216        };
17217        let procfs = match Self::procfs_entry(guest, &stat, StatTarget::Descriptor(fd)).await {
17218            ProcfsEntry::At(within) => Some(ProcfsDirectory::new(within)),
17219            ProcfsEntry::NotProcfs | ProcfsEntry::Unnamed | ProcfsEntry::Unknown => None,
17220        };
17221        Ok(DirectoryInodes::new(stat.dev, procfs))
17222    }
17223
17224    /// Move the kernel position of the open file behind `fd` to `target`, a
17225    /// [`DirectoryStream::kernel_target`], so that a descriptor Detcore does
17226    /// not track that aliases it reads every entry the stream has not
17227    /// returned. Without this, the snapshot leaves the kernel at the end of
17228    /// the directory, and such a descriptor would read nothing.
17229    ///
17230    /// The guest's own call has already been decided, so a failed seek is not
17231    /// reported to it; the next move of the stream seeks again.
17232    async fn move_directory_kernel_position<G: Guest<Self>>(
17233        &self,
17234        guest: &mut G,
17235        fd: RawFd,
17236        target: i64,
17237    ) {
17238        // A placeholder's kernel position is its tag, which nothing moves:
17239        // Detcore answers its listing and its seeks from its model.
17240        if Self::placeholder_of(guest, fd).is_some() {
17241            return;
17242        }
17243        let seek = syscalls::Lseek::new()
17244            .with_fd(fd)
17245            .with_offset(target)
17246            .with_whence(Whence::SEEK_SET);
17247        let _ = self.record_or_replay(guest, seek).await;
17248    }
17249
17250    /// Drop from `entries`, read from the directory open as `fd`, the CPUs and
17251    /// nodes that the virtual CPUs and single node leave out (see
17252    /// [`is_absent_cpu_entry`]), so that a guest counting
17253    /// `/sys/devices/system/cpu/cpuN` finds the virtual CPUs, as `online` and
17254    /// `sched_getaffinity` report
17255    /// (<https://github.com/rrnewton/hermit/issues/4160>); and the `/proc`
17256    /// names outside the run's `--proc` level, which lookups refuse (see
17257    /// [`levels::absent_entry`]).
17258    ///
17259    /// The directory is named by the kernel's path for the descriptor, so a
17260    /// relative or symlinked spelling is recognized too. Not under record and
17261    /// replay, where the replayer's descriptor does not name the recorded
17262    /// directory, so the two runs would not drop alike; Detcore names no
17263    /// procfs entry there either (see [`Self::procfs_entry`]).
17264    async fn drop_absent_entries<G: Guest<Self>>(
17265        &self,
17266        guest: &mut G,
17267        fd: RawFd,
17268        entries: &mut Vec<DirEntry>,
17269    ) -> Result<(), Error> {
17270        if self.cfg.recordreplay_modes {
17271            return Ok(());
17272        }
17273        let link = format!("{}/fd/{fd}", self.caller_proc_directory(guest));
17274        // Names are dropped as outside the level only from a directory the
17275        // tool sees is on procfs: not from a user bind over part of `/proc`,
17276        // and not where the tool cannot tell (its `/proc` entry for the
17277        // caller is not the caller's where the guest's threads are no host
17278        // tasks, KVM, or is unreadable after a `chroot`).
17279        let tool_sees_caller =
17280            crate::procfs_inode::tool_can_name_guest_descriptors(&self.cfg.backend);
17281        let directory_metadata = tool_sees_caller
17282            .then(|| std::fs::metadata(&link).ok())
17283            .flatten();
17284        let on_procfs = match (&directory_metadata, rendered_procfs::proc_device()) {
17285            (Some(metadata), Some(device)) => metadata.dev() == device,
17286            _ => false,
17287        };
17288        if self.is_caller_descriptor_table(guest, fd) {
17289            entries.retain(|entry| {
17290                !runtime_owned_descriptor_entry(&entry.name, |fd| guest.is_runtime_owned_fd(fd))
17291            });
17292        }
17293        let directory = listed_directory(
17294            tool_sees_caller,
17295            || std::fs::read_link(&link).ok(),
17296            || {
17297                guest
17298                    .thread_state()
17299                    .with_detfd(fd, |detfd| detfd.path())
17300                    .ok()
17301                    .flatten()
17302            },
17303        );
17304        // After a `chroot` into another root, the kernel names the directory
17305        // in the tool's view, below that root.
17306        let directory = match (directory, self.guest_root_in_view(guest)) {
17307            (Some(directory), Some(Some(root))) if !directory.starts_with("/proc") => directory
17308                .strip_prefix(&root)
17309                .ok()
17310                .map(|below| Path::new("/").join(below))
17311                .or(Some(directory)),
17312            (directory, _) => directory,
17313        };
17314        if let Some(directory) = directory {
17315            let level = self.cfg.proc_level;
17316            let cpus = self.cfg.virtual_cpu_count();
17317            // A user bind over a sysfs path keeps its own names. Ask the
17318            // opened descriptor, not its path in the tool's mount namespace.
17319            let has_absent_cpu = entries
17320                .iter()
17321                .any(|entry| is_absent_cpu_entry(&directory, &entry.name, cpus));
17322            let on_sysfs = has_absent_cpu
17323                && self
17324                    .descriptor_is_on_filesystem(guest, fd, libc::SYSFS_MAGIC)
17325                    .await?;
17326            entries.retain(|entry| {
17327                !(on_sysfs && is_absent_cpu_entry(&directory, &entry.name, cpus))
17328                    && !(on_procfs && levels::absent_entry(&directory, &entry.name, level))
17329            });
17330        }
17331        Ok(())
17332    }
17333
17334    /// Whether the opened procfs object names the caller's own descriptor
17335    /// table. Authenticate kernel object identity, not the guest spelling:
17336    /// an inherited directory may belong to a different process. Process and
17337    /// thread aliases have distinct inodes; the process alias requires the
17338    /// existing shared-table proof. Independent of metadata virtualization.
17339    fn is_caller_descriptor_table<G: Guest<Self>>(&self, guest: &G, fd: RawFd) -> bool {
17340        if self.cfg.recordreplay_modes
17341            || !crate::procfs_inode::tool_can_name_guest_descriptors(&self.cfg.backend)
17342        {
17343            return false;
17344        }
17345        let link = format!("{}/fd/{fd}", self.caller_proc_directory(guest));
17346        std::fs::metadata(link).is_ok_and(|metadata| {
17347            self.is_caller_descriptor_identity(
17348                guest,
17349                (metadata.dev(), metadata.ino()),
17350                &["fd", "fdinfo"],
17351            )
17352        })
17353    }
17354
17355    /// The same object test for an opened directory or a pathname lookup.
17356    /// Readlink accepts only `fd`; listings also accept `fdinfo`.
17357    fn is_caller_descriptor_identity<G: Guest<Self>>(
17358        &self,
17359        guest: &G,
17360        identity: (u64, u64),
17361        kinds: &[&str],
17362    ) -> bool {
17363        if rendered_procfs::proc_device() != Some(identity.0) {
17364            return false;
17365        }
17366        let state = guest.thread_state();
17367        let holds_process_table =
17368            state.dettid == self.own_detpid(guest) || state.holds_process_files();
17369        let process = if self.cfg.backend.tool_shares_guest_descriptor_table {
17370            String::from("/proc/self")
17371        } else {
17372            format!("/proc/{}", guest.pid().as_raw())
17373        };
17374        caller_descriptor_identity_matches(
17375            identity,
17376            &self.caller_proc_directory(guest),
17377            holds_process_table.then_some(process.as_str()),
17378            kinds,
17379        )
17380    }
17381
17382    /// Hide an owner-reported descriptor only after the original pathname's
17383    /// parent resolves to this caller's actual FD directory. A recorded
17384    /// `/proc/self/fd` spelling alone cannot authenticate an inherited,
17385    /// replaced, or invalid directory descriptor.
17386    pub(crate) async fn readlink_names_runtime_fd<G: Guest<Self>>(
17387        &self,
17388        guest: &mut G,
17389        dirfd: RawFd,
17390        path: &Path,
17391    ) -> bool {
17392        use std::os::unix::ffi::OsStrExt;
17393
17394        use super::namespace::decimal_fd;
17395
17396        let bytes = path.as_os_str().as_bytes();
17397        if self.cfg.recordreplay_modes
17398            || !crate::procfs_inode::tool_can_name_guest_descriptors(&self.cfg.backend)
17399            || guest.thread_state().changed_root.load(Ordering::SeqCst)
17400            || bytes.is_empty()
17401            || bytes.len() >= libc::PATH_MAX as usize
17402            || bytes.ends_with(b"/")
17403        {
17404            return false;
17405        }
17406        // Only parse the final raw component. The actual parent object,
17407        // rather than an absolute or recognized spelling, proves whose
17408        // descriptor table this is. In particular, retain interior `..`
17409        // and symlinks for Linux to resolve in order.
17410        let name = bytes.rsplit(|byte| *byte == b'/').next().unwrap();
17411        let Some(fd) = decimal_fd(std::ffi::OsStr::from_bytes(name)) else {
17412            return false;
17413        };
17414        // Keep the original relative lookup and its dirfd, including errors
17415        // such as EBADF and ENOTDIR. The translated injection handles guest
17416        // PID spellings and placeholder directories just as the original
17417        // readlink does. Do not inspect the protected descriptor itself.
17418        let parent = match bytes.iter().rposition(|byte| *byte == b'/') {
17419            Some(0) => &b"/"[..],
17420            Some(end) => &bytes[..end],
17421            None => &b"."[..],
17422        };
17423        let stat_size = std::mem::size_of::<libc::stat>();
17424        let length = stat_size + parent.len() + 1;
17425        let Some(page) = procfs_levels::map_transient(guest, length).await else {
17426            return false;
17427        };
17428        // SAFETY: the mapping contains stat_size + parent.len() + 1 bytes,
17429        // so this u8 offset remains within the successfully mapped region.
17430        let parent_address = unsafe { page.add(stat_size) };
17431        let mut staged = parent.to_vec();
17432        staged.push(0);
17433        let stat = StatPtr(page.cast());
17434        let observed = if guest.memory().write_exact(parent_address, &staged).is_ok() {
17435            let call = syscalls::Newfstatat::new()
17436                .with_dirfd(dirfd)
17437                .with_path(PathPtr::from_ptr(
17438                    parent_address.as_raw() as *const libc::c_char
17439                ))
17440                .with_stat(Some(stat))
17441                .with_flags(AtFlags::empty());
17442            match Self::inject_translated_in_page(guest, Syscall::from(call)).await {
17443                Ok(_) => stat.read(&guest.memory()).ok(),
17444                Err(_) => None,
17445            }
17446        } else {
17447            None
17448        };
17449        procfs_levels::unmap_transient(guest, page, length).await;
17450        observed.is_some_and(|stat| {
17451            stat.st_mode & libc::S_IFMT == libc::S_IFDIR
17452                && self.is_caller_descriptor_identity(guest, (stat.st_dev, stat.st_ino), &["fd"])
17453                && guest.is_runtime_owned_fd(fd)
17454        })
17455    }
17456
17457    /// Answer `getdents` without a directory stream by issuing the guest's own
17458    /// call. With metadata virtualization, sort that kernel buffer and
17459    /// determinize its inodes. Without it, only omit runtime-owned entries
17460    /// from an authenticated caller descriptor table; keep host order and
17461    /// inodes. The host's `d_off` cookies are kept, so the kernel can
17462    /// still interpret them. A directory larger than one buffer comes back as
17463    /// sorted runs whose boundaries depend on the host.
17464    ///
17465    /// A buffer whose every entry is dropped (see
17466    /// [`Self::drop_absent_entries`]) is not returned empty, which would
17467    /// end the directory: the call is issued again from where the kernel
17468    /// left off. The bytes the kernel wrote past the entries returned, by this
17469    /// call or one it reissued, are zeroed, as they may name the entries
17470    /// dropped; so is a buffer every dropped entry was written to when the
17471    /// directory then ends.
17472    async fn project_one_buffer<G: Guest<Self>>(
17473        &self,
17474        guest: &mut G,
17475        call: GetdentsCall<'_>,
17476    ) -> Result<i64, Error> {
17477        // The longest any call wrote: a reissued call can write less than the
17478        // one before it, whose dropped names lie past it.
17479        let virtualize = guest.config().virtualize_metadata;
17480        let mut written = 0;
17481        let mut dropped = false;
17482        let (len, mut entries) = loop {
17483            let len = match self.record_or_replay(guest, call.call).await {
17484                Ok(len) => len as usize,
17485                Err(error) => {
17486                    if written > 0 {
17487                        guest.memory().write_exact(call.buf, &vec![0; written])?;
17488                    }
17489                    return Err(error.into());
17490                }
17491            };
17492            if len == 0 {
17493                if written > 0 {
17494                    guest.memory().write_exact(call.buf, &vec![0; written])?;
17495                }
17496                return Ok(0);
17497            }
17498            written = written.max(len);
17499            let mut entries = match read_records(&guest.memory(), call.buf, len, call.format) {
17500                Ok(entries) => entries,
17501                Err(error) => {
17502                    guest.memory().write_exact(call.buf, &vec![0; written])?;
17503                    return Err(error.into());
17504                }
17505            };
17506            if virtualize {
17507                if let Err(error) = self.drop_absent_entries(guest, call.fd, &mut entries).await {
17508                    // The kernel already wrote these names. A failed identity
17509                    // probe must not leave omitted CPUs visible in the buffer.
17510                    guest.memory().write_exact(call.buf, &vec![0; written])?;
17511                    return Err(error);
17512                }
17513            } else {
17514                let original = entries.len();
17515                entries.retain(|entry| {
17516                    !runtime_owned_descriptor_entry(&entry.name, |fd| guest.is_runtime_owned_fd(fd))
17517                });
17518                if entries.len() == original && !dropped {
17519                    return Ok(len as i64);
17520                }
17521                dropped = true;
17522            }
17523            if !entries.is_empty() {
17524                break (len, entries);
17525            }
17526        };
17527        let inodes = if virtualize {
17528            sort_dir_entries(&mut entries);
17529            Some(self.directory_inodes(guest, call.fd).await?)
17530        } else {
17531            None
17532        };
17533        let mut records = Vec::with_capacity(len);
17534        for entry in &entries {
17535            let Some(inodes) = &inodes else {
17536                call.format
17537                    .encode(entry, entry.ino, entry.off, &mut records);
17538                continue;
17539            };
17540            let (host, path) = inodes.entry_inode(&entry.name, entry.ino);
17541            let files = self.rendered_files(guest);
17542            let d_ino = match self
17543                .rendered_listed_inode(guest, inodes, &entry.name, entry.ino, files)
17544                .await?
17545            {
17546                Some(inode) => DetInode::mint(inode),
17547                None => determinize_named_inode(guest, host, path).await.0,
17548            };
17549            call.format
17550                .encode(entry, d_ino.as_raw(), entry.off, &mut records);
17551        }
17552        let returned = records.len();
17553        // Zero what the kernel wrote past the entries returned.
17554        records.resize(returned.max(written), 0);
17555        if guest.memory().write_exact(call.buf, &records).is_err() {
17556            // The padding after the last name may not be writable, as Linux
17557            // does not write it.
17558            let written = entries.last().map_or(0, |last| {
17559                returned - call.format.record_len(last.name.len())
17560                    + call.format.written_len(last.name.len())
17561            });
17562            guest.memory().write_exact(call.buf, &records[..written])?;
17563        }
17564        Ok(returned as i64)
17565    }
17566
17567    /// Read the whole host directory behind `call.fd` into a private mapping
17568    /// (see [`Self::snapshot_directory`]), and return its entries in host
17569    /// order, less the CPUs and nodes Hermit's single virtual CPU and node
17570    /// leave out and the `/proc` names outside the run's level (see
17571    /// [`Self::drop_absent_entries`]). Every directory
17572    /// snapshot a stream stores comes from here, so none holds them.
17573    ///
17574    /// Linux writes into guest memory only the records a call returns and the
17575    /// fields it stored of one it could not finish, so the reads do not go to
17576    /// the guest's buffer, which may be too small
17577    /// for any entry, unreadable, or partly unmapped, nor to its stack. They
17578    /// go to an anonymous mapping injected for the purpose and unmapped
17579    /// before the guest runs again, as the atomic `writev` does for its
17580    /// iovecs. The mapping is fresh, so the reads leave nothing that the
17581    /// guest could see in memory. On the kvm backend, whose guest allocator
17582    /// fills the lowest hole below a cursor first and moves the cursor past
17583    /// each mapping it makes, the mapping moves that cursor, so a later
17584    /// mapping that fits no hole below it lands elsewhere than on Linux. That
17585    /// follows from the guest's own calls, so it is the same on every run.
17586    ///
17587    /// If a mapping of [`DIRECTORY_DRAIN_COUNT`] bytes cannot be made, one
17588    /// page is used, which still holds any entry. If that cannot be made
17589    /// either (the guest's address space is at its limit), the call fails
17590    /// with `ENOMEM` before anything is read, and the stream stays without a
17591    /// snapshot, so a later call tries again. Linux's `getdents` does not
17592    /// fail so; serving the directory in host order would make the order
17593    /// depend on the host. This divergence is tracked in
17594    /// https://github.com/rrnewton/hermit/issues/3723.
17595    async fn snapshot_directory_privately<G: Guest<Self>>(
17596        &self,
17597        guest: &mut G,
17598        call: GetdentsCall<'_>,
17599    ) -> Result<Option<Vec<DirEntry>>, Error> {
17600        let mut mapping = None;
17601        for len in [DIRECTORY_DRAIN_COUNT as usize, DIRECTORY_PAGE] {
17602            let mapped = guest
17603                .inject_with_retry(Syscall::Mmap(
17604                    syscalls::Mmap::new()
17605                        .with_addr(None)
17606                        .with_len(len)
17607                        .with_prot(ProtFlags::PROT_READ | ProtFlags::PROT_WRITE)
17608                        .with_flags(MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS)
17609                        .with_fd(-1)
17610                        .with_offset(0),
17611                ))
17612                .await;
17613            if let Some(scratch) = mapped
17614                .ok()
17615                .and_then(|addr| usize::try_from(addr).ok())
17616                .and_then(AddrMut::<u8>::from_raw)
17617            {
17618                mapping = Some((scratch, len));
17619                break;
17620            }
17621        }
17622        let Some((scratch, len)) = mapping else {
17623            return Err(Errno::ENOMEM.into());
17624        };
17625        let snapshot = self
17626            .snapshot_directory(guest, call.into_scratch(scratch, len as u32))
17627            .await;
17628        let unmapped = guest
17629            .inject_with_retry(Syscall::Munmap(
17630                syscalls::Munmap::new()
17631                    .with_addr(Some(Addr::from(scratch).cast()))
17632                    .with_len(len),
17633            ))
17634            .await;
17635        // A mapping left behind would be visible to the guest.
17636        unmapped?;
17637        let mut snapshot = snapshot?;
17638        if let Some(entries) = &mut snapshot {
17639            self.drop_absent_entries(guest, call.fd, entries).await?;
17640        }
17641        Ok(snapshot)
17642    }
17643
17644    /// Read the whole host directory by issuing `call.call` until it returns
17645    /// 0, and return its entries in host order. `call` reads into a private
17646    /// mapping that holds any entry.
17647    ///
17648    /// Every step goes through `record_or_replay`, so a replay reads the same
17649    /// entries from the log. The reads must start at the beginning of the
17650    /// directory: a known directory is rewound there, and before the first
17651    /// snapshot the kernel position must already be 0.
17652    ///
17653    /// The first read fails as the guest's call would at any position: the
17654    /// descriptor is not a directory, or the directory has been removed.
17655    ///
17656    /// Returns `None` when this open file must be read in host order instead,
17657    /// with the kernel position where the guest left it. Either the position
17658    /// was moved before the first `getdents`, to a host cookie that no entry
17659    /// index stands for, or a read after the first failed, so the directory
17660    /// cannot be read to its end. A regular file, pipe or `O_PATH` descriptor
17661    /// whose position is not 0 also lands here, and reading it in host order
17662    /// reports the kernel's error without moving its offset.
17663    async fn snapshot_directory<G: Guest<Self>>(
17664        &self,
17665        guest: &mut G,
17666        call: GetdentsCall<'_>,
17667    ) -> Result<Option<Vec<DirEntry>>, Error> {
17668        let known_directory = guest
17669            .thread_state()
17670            .with_detfd(call.fd, |detfd| detfd.has_directory_stream())?;
17671        let lseek = |offset, whence| {
17672            syscalls::Lseek::new()
17673                .with_fd(call.fd)
17674                .with_offset(offset)
17675                .with_whence(whence)
17676        };
17677        if known_directory {
17678            self.record_or_replay(guest, lseek(0, Whence::SEEK_SET))
17679                .await?;
17680        } else if self
17681            .record_or_replay(guest, lseek(0, Whence::SEEK_CUR))
17682            .await
17683            != Ok(0)
17684        {
17685            return Ok(None);
17686        }
17687
17688        let mut entries = Vec::new();
17689        let mut read_any = false;
17690        let error: Error = loop {
17691            let len = match self.record_or_replay(guest, call.call).await {
17692                Ok(0) => return Ok(Some(entries)),
17693                Ok(len) => len as usize,
17694                Err(error) => break error.into(),
17695            };
17696            read_any = true;
17697            match read_records(&guest.memory(), call.buf, len, call.format) {
17698                Ok(parsed) => entries.extend(parsed),
17699                Err(error) => break error.into(),
17700            }
17701        };
17702        if !read_any {
17703            return Err(error);
17704        }
17705        // The earlier reads moved the position, which the guest's call must
17706        // find where the guest left it: at the start.
17707        self.record_or_replay(guest, lseek(0, Whence::SEEK_SET))
17708            .await?;
17709        Ok(None)
17710    }
17711}
17712
17713/// The size of the private mapping that a snapshot reads the host directory
17714/// into, and so the most bytes one read of it asks for. It holds any entry.
17715const DIRECTORY_DRAIN_COUNT: u32 = 64 * 1024;
17716
17717/// The granularity at which the guest's buffer can stop being writable.
17718const DIRECTORY_PAGE: usize = 4096;
17719
17720/// The most pages one vectored copy names: Linux's `IOV_MAX`.
17721const PAGES_PER_COPY: usize = 1024;
17722
17723/// The pieces of the `len` bytes at guest address `addr`, split where they
17724/// cross a page: the offset and length of each.
17725fn guest_pages(addr: usize, len: usize) -> Vec<(usize, usize)> {
17726    let mut pieces = Vec::with_capacity(len / DIRECTORY_PAGE + 2);
17727    let mut at = 0;
17728    while at < len {
17729        let piece = (DIRECTORY_PAGE - (addr + at) % DIRECTORY_PAGE).min(len - at);
17730        pieces.push((at, piece));
17731        at += piece;
17732    }
17733    pieces
17734}
17735
17736/// Read and decode the `len` bytes of records a `getdents` returned in the
17737/// guest's buffer. Linux does not write the padding after the last record's
17738/// name, which can lie in a page the guest cannot read.
17739fn read_records(
17740    memory: &impl MemoryAccess,
17741    buf: AddrMut<u8>,
17742    len: usize,
17743    format: DirentFormat,
17744) -> Result<Vec<DirEntry>, Errno> {
17745    let mut bytes = vec![0; len];
17746    let readable = match memory.read_exact(buf, &mut bytes) {
17747        Ok(()) => len,
17748        Err(_) => read_guest_prefix(memory, buf, &mut bytes)?,
17749    };
17750    format.parse_written(&mut bytes, readable)
17751}
17752
17753/// Read the start of the guest's buffer into `bytes`, up to the first page the
17754/// guest cannot read, and return how many bytes were read. Only a fault
17755/// (`EFAULT`) ends the copy short; any other error of the copy, such as
17756/// `ESRCH` for a thread that is gone or `EIO` from the backend, is returned.
17757///
17758/// This and [`write_guest_pieces`] use only vectored copies, one page per
17759/// piece, which respect the guest's page protection. Reverie's `read` and
17760/// `write` do not always: on the ptrace backend, a copy of at most 8 bytes goes
17761/// through `PTRACE_PEEKDATA` or `PTRACE_POKEDATA`, which reach a page the guest
17762/// has made inaccessible.
17763pub(crate) fn read_guest_prefix(
17764    memory: &impl MemoryAccess,
17765    buf: AddrMut<u8>,
17766    bytes: &mut [u8],
17767) -> Result<usize, Errno> {
17768    let mut done = 0;
17769    for pages in guest_pages(buf.as_raw(), bytes.len()).chunks(PAGES_PER_COPY) {
17770        let from = pages[0].0;
17771        let len: usize = pages.iter().map(|&(_, len)| len).sum();
17772        let remote: Vec<AddrSlice<u8>> = pages
17773            .iter()
17774            .map(|&(at, len)| unsafe { AddrSlice::from_raw_parts(buf.add(at).into(), len) })
17775            .collect();
17776        let remote: Vec<std::io::IoSlice> = remote
17777            .iter()
17778            .map(|piece| unsafe { piece.as_ioslice() })
17779            .collect();
17780        let mut local = [std::io::IoSliceMut::new(&mut bytes[from..from + len])];
17781        let copied = match memory.read_vectored(&remote, &mut local) {
17782            Err(Errno::EFAULT) => 0,
17783            copied => copied?,
17784        };
17785        done += copied;
17786        if copied < len {
17787            break;
17788        }
17789    }
17790    Ok(done)
17791}
17792
17793/// Write each of `pieces`, an offset in the guest's buffer and the bytes to
17794/// store there, within one page, in the order given, up to the first the
17795/// guest cannot write. Return how many bytes of the pieces were written,
17796/// counted in that order. As in [`read_guest_prefix`], only a fault ends the
17797/// copy short, and any other error is returned.
17798fn write_guest_pieces(
17799    memory: &mut impl MemoryAccess,
17800    buf: AddrMut<u8>,
17801    pieces: &[(usize, &[u8])],
17802) -> Result<usize, Errno> {
17803    let mut done = 0;
17804    for pieces in pieces.chunks(PAGES_PER_COPY) {
17805        let len: usize = pieces.iter().map(|(_, bytes)| bytes.len()).sum();
17806        let mut remote: Vec<AddrSliceMut<u8>> = pieces
17807            .iter()
17808            .map(|&(at, bytes)| unsafe { AddrSliceMut::from_raw_parts(buf.add(at), bytes.len()) })
17809            .collect();
17810        let mut remote: Vec<std::io::IoSliceMut> = remote
17811            .iter_mut()
17812            .map(|piece| unsafe { piece.as_ioslice_mut() })
17813            .collect();
17814        let local: Vec<std::io::IoSlice> = pieces
17815            .iter()
17816            .map(|&(_, bytes)| std::io::IoSlice::new(bytes))
17817            .collect();
17818        let copied = match memory.write_vectored(&local, &mut remote) {
17819            Err(Errno::EFAULT) => 0,
17820            copied => copied?,
17821        };
17822        done += copied;
17823        if copied < len {
17824            break;
17825        }
17826    }
17827    Ok(done)
17828}
17829
17830/// One store that Linux makes into the guest's buffer to copy directory
17831/// records: where in the buffer, the bytes stored (a range of the records,
17832/// or of the first record's placeholder `d_off` after them), and whether the
17833/// CPU makes it as one store, which writes nothing if any byte of it faults.
17834struct DirentStore {
17835    at: usize,
17836    from: std::ops::Range<usize>,
17837    single: bool,
17838}
17839
17840/// The stores Linux makes, in its order, to copy records whose names are
17841/// `names` bytes long, and for each record, how many of the stores precede
17842/// the end of its `filldir`. `placeholder` is where the first record's
17843/// placeholder `d_off` follows the records.
17844///
17845/// For each record, `filldir` (fs/readdir.c) first stores the `d_off` of the
17846/// record before it, which is its own entry's position. The first record of
17847/// a call has none before it, so its own `d_off` gets that position, to be
17848/// overwritten. Then come `d_ino`, `d_reclen`, `d_type`, the NUL after the
17849/// name, and the name. Once no more records are copied, the call stores the
17850/// last one's `d_off`. No padding is written.
17851///
17852/// Linux copies a name in words from its start, after the NUL that follows
17853/// it. A name spans at most two pages, and the NUL's page could be written,
17854/// so if a word of the name faults, the first does, and none of the name is
17855/// written. So it is here, where the name is written page by page.
17856fn dirent_stores(
17857    format: DirentFormat,
17858    names: &[usize],
17859    placeholder: usize,
17860) -> (Vec<DirentStore>, Vec<usize>) {
17861    let field = |at: usize, len: usize| DirentStore {
17862        at,
17863        from: at..at + len,
17864        single: true,
17865    };
17866    let mut stores = Vec::with_capacity(names.len() * 6 + 1);
17867    let mut filled = Vec::with_capacity(names.len());
17868    let mut previous = None;
17869    let mut at = 0;
17870    for &name_len in names {
17871        let reclen = format.record_len(name_len);
17872        let name = at + format.name_offset();
17873        stores.push(match previous {
17874            Some(previous) => field(previous + 8, 8),
17875            None => DirentStore {
17876                at: at + 8,
17877                from: placeholder..placeholder + 8,
17878                single: true,
17879            },
17880        });
17881        stores.push(field(at, 8));
17882        stores.push(field(at + 16, 2));
17883        stores.push(field(at + format.type_offset(reclen), 1));
17884        stores.push(field(name + name_len, 1));
17885        stores.push(DirentStore {
17886            at: name,
17887            from: name..name + name_len,
17888            single: false,
17889        });
17890        filled.push(stores.len());
17891        previous = Some(at);
17892        at += reclen;
17893    }
17894    if let Some(previous) = previous {
17895        stores.push(field(previous + 8, 8));
17896    }
17897    (stores, filled)
17898}
17899
17900/// Copy directory records into the guest's buffer as Linux does: store by
17901/// store (see [`dirent_stores`]), up to the first store the guest cannot
17902/// write. `records` holds the records of entries whose names are `names`
17903/// bytes long, the first at position `start`.
17904///
17905/// Return how many entries the call passes, and its result: the bytes of the
17906/// records whose `filldir` completed. It is `EFAULT` if none did, or if the
17907/// fault hit a `d_off`, which the call's last store would then write again.
17908/// A copy that fails with any error other than a fault passes no entry and
17909/// returns that error, as the call did before directory streams.
17910///
17911/// Each store within one page is written whole or not at all, as Linux's
17912/// are. A single store that crosses into another page, a field of a buffer
17913/// not aligned to 8 bytes, the CPU writes whole or not at all, so its two
17914/// parts are written one after the other and the first is put back if the
17915/// second cannot be written. What is put back is read just before the first
17916/// part is written, after every store before it, which may have changed those
17917/// bytes through another mapping of the same memory. The part written first
17918/// is the one whose bytes Detcore can read to put back: the part in the
17919/// second page, unless only the part in the first page can be read. Where
17920/// neither can be read with a vectored copy, the part in the second page is
17921/// written first, so that a write-only buffer ending at a page the guest
17922/// cannot touch is left as Linux leaves it, and its bytes are read to put
17923/// back with Reverie's `read`, which for at most 8 bytes is `PTRACE_PEEKDATA`
17924/// on the ptrace backend and reads a write-only page. While no other thread
17925/// writes the buffer, only a store from a page the guest can neither read nor
17926/// write into a write-only page that this read cannot reach either leaves
17927/// bytes Linux does not write: up to 7, in the write-only page.
17928fn copy_records(
17929    memory: &mut impl MemoryAccess,
17930    buf: AddrMut<u8>,
17931    format: DirentFormat,
17932    records: &[u8],
17933    names: &[usize],
17934    start: u64,
17935) -> (usize, Result<usize, Errno>) {
17936    if names.is_empty() {
17937        return (0, Ok(0));
17938    }
17939    let mut source = records.to_vec();
17940    let placeholder = source.len();
17941    source.extend_from_slice(&i64::try_from(start).unwrap_or(i64::MAX).to_ne_bytes());
17942    let (stores, filled) = dirent_stores(format, names, placeholder);
17943    // Each piece, within one page: where in the buffer and its bytes, and,
17944    // in `owner`, its store.
17945    let mut pieces: Vec<(usize, &[u8])> = Vec::with_capacity(stores.len());
17946    let mut owner = Vec::with_capacity(stores.len());
17947    // For each single store that crosses pages: its part written first, as an
17948    // index into `pieces`, and whether the guest can read that part's bytes.
17949    let mut crossing = Vec::new();
17950    for (index, store) in stores.iter().enumerate() {
17951        let mut split = guest_pages(buf.as_raw() + store.at, store.from.len());
17952        if store.single && split.len() > 1 {
17953            let readable: Result<Vec<bool>, Errno> = split
17954                .iter()
17955                .map(|&(at, len)| {
17956                    let mut bytes = vec![0; len];
17957                    let addr = unsafe { buf.add(store.at + at) };
17958                    Ok(read_guest_prefix(memory, addr, &mut bytes)? == len)
17959                })
17960                .collect();
17961            let mut readable = match readable {
17962                Ok(readable) => readable,
17963                Err(errno) => return (0, Err(errno)),
17964            };
17965            if readable[1] || !readable[0] {
17966                split.reverse();
17967                readable.reverse();
17968            }
17969            crossing.push((pieces.len(), readable[0]));
17970        }
17971        for (at, len) in split {
17972            let from = store.from.start + at;
17973            pieces.push((store.at + at, &source[from..from + len]));
17974            owner.push(index);
17975        }
17976    }
17977    // Write the pieces in order, stopping before the first part of each
17978    // crossing store to save what the guest has there. The stores before it
17979    // can have changed those bytes through another mapping of the same memory,
17980    // so they are read only once those stores are made.
17981    let mut next = 0;
17982    let mut saved: Option<(usize, Vec<u8>)> = None;
17983    let mut failed = None;
17984    for &(stop, readable) in crossing.iter().chain([(pieces.len(), false)].iter()) {
17985        let segment = &pieces[next..stop];
17986        let written = match write_guest_pieces(memory, buf, segment) {
17987            Ok(written) => written,
17988            Err(errno) => return (0, Err(errno)),
17989        };
17990        let mut end = 0;
17991        if let Some(at) = segment.iter().position(|(_, bytes)| {
17992            end += bytes.len();
17993            end > written
17994        }) {
17995            failed = Some(next + at);
17996            break;
17997        }
17998        if stop == pieces.len() {
17999            break;
18000        }
18001        let (at, bytes) = pieces[stop];
18002        let addr = unsafe { buf.add(at) };
18003        let mut before = vec![0; bytes.len()];
18004        let read = if readable {
18005            match read_guest_prefix(memory, addr, &mut before) {
18006                Ok(read) => read == before.len(),
18007                Err(errno) => return (0, Err(errno)),
18008            }
18009        } else {
18010            // The part in the second page, at its start, which no vectored copy
18011            // can read. Reverie's `read` of at most 8 bytes can on the ptrace
18012            // backend, through `PTRACE_PEEKDATA`, and a read changes nothing
18013            // the guest sees.
18014            memory.read_exact(addr, &mut before).is_ok()
18015        };
18016        saved = read.then_some((stop, before));
18017        next = stop;
18018    }
18019    let Some(failed) = failed else {
18020        return (names.len(), Ok(records.len()));
18021    };
18022    let store = owner[failed];
18023    if failed > 0
18024        && owner[failed - 1] == store
18025        && let Some((first, before)) = &saved
18026        && *first == failed - 1
18027        && let Err(errno) =
18028            write_guest_pieces(memory, buf, &[(pieces[*first].0, before.as_slice())])
18029    {
18030        return (0, Err(errno));
18031    }
18032    // The entry whose `filldir` made the store, or all of them for the call's
18033    // last store, and the first store of that `filldir`.
18034    let entry = filled.partition_point(|&end| end <= store);
18035    let first = entry.checked_sub(1).map_or(0, |before| filled[before]);
18036    let result = if entry == 0 || store == first {
18037        Err(Errno::EFAULT)
18038    } else {
18039        Ok(names[..entry]
18040            .iter()
18041            .map(|&name_len| format.record_len(name_len))
18042            .sum())
18043    };
18044    (entry, result)
18045}
18046
18047/// A guest `getdents` or `getdents64` call.
18048#[derive(Clone, Copy)]
18049struct GetdentsCall<'a> {
18050    /// The call as the guest made it.
18051    call: Syscall,
18052    /// The same call asking for no bytes.
18053    empty: Syscall,
18054    fd: RawFd,
18055    buf: AddrMut<'a, u8>,
18056    /// The guest's buffer size.
18057    capacity: usize,
18058    format: DirentFormat,
18059}
18060
18061impl<'a> GetdentsCall<'a> {
18062    /// The same call reading at most `count` bytes into `buf` instead.
18063    fn into_scratch<'b>(self, buf: AddrMut<'b, u8>, count: u32) -> GetdentsCall<'b> {
18064        let call = match self.call {
18065            #[cfg(target_arch = "x86_64")]
18066            Syscall::Getdents(call) => {
18067                Syscall::from(call.with_dirent(Some(buf.cast())).with_count(count))
18068            }
18069            Syscall::Getdents64(call) => {
18070                Syscall::from(call.with_dirent(Some(buf.cast())).with_count(count))
18071            }
18072            other => other,
18073        };
18074        GetdentsCall {
18075            call,
18076            empty: self.empty,
18077            fd: self.fd,
18078            buf,
18079            capacity: count as usize,
18080            format: self.format,
18081        }
18082    }
18083}
18084
18085/// Whether every byte of `[address, address + len)` lies in some mapping of a
18086/// `/proc/<pid>/maps` listing (`Detcore::vectored_signalfd_read`).
18087fn signalfd_range_is_mapped(maps: &str, address: usize, len: usize) -> bool {
18088    signalfd_range_is_covered(maps, address, len, false)
18089}
18090
18091/// Whether every byte of `[address, address + len)` lies in some writable
18092/// mapping of a `/proc/<pid>/maps` listing
18093/// (`Detcore::settle_faulted_signalfd_record`).
18094fn signalfd_range_is_writable(maps: &str, address: usize, len: usize) -> bool {
18095    signalfd_range_is_covered(maps, address, len, true)
18096}
18097
18098/// The pieces of the guest buffers `extents` (address, length) that the
18099/// signalfd record after the first `read` bytes would fill, or `None` if
18100/// they have no room for a whole record there (Linux reads only whole ones).
18101fn signalfd_next_record(extents: &[(usize, usize)], read: usize) -> Option<Vec<(usize, usize)>> {
18102    const RECORD: usize = std::mem::size_of::<libc::signalfd_siginfo>();
18103    let capacity = extents
18104        .iter()
18105        .try_fold(0usize, |total, (_, len)| total.checked_add(*len))?;
18106    if capacity / RECORD <= read / RECORD {
18107        return None;
18108    }
18109    let mut skip = read - read % RECORD;
18110    let mut left = RECORD;
18111    let mut pieces = Vec::new();
18112    for (address, len) in extents {
18113        if left == 0 {
18114            break;
18115        }
18116        if skip >= *len {
18117            skip -= len;
18118            continue;
18119        }
18120        let take = (len - skip).min(left);
18121        pieces.push((address + skip, take));
18122        left -= take;
18123        skip = 0;
18124    }
18125    Some(pieces)
18126}
18127
18128/// The end of the user address space, x86-64's `TASK_SIZE_MAX`, above which
18129/// `access_ok` fails a buffer before a call does anything: 2^56 - 4096 where
18130/// the kernel runs five-level page tables, 2^47 - 4096 otherwise. Measured
18131/// once, by asking for a mapping above 2^47, which the kernel grants there
18132/// only with five-level page tables.
18133fn user_address_end() -> usize {
18134    static END: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18135    *END.get_or_init(|| {
18136        const HINT: usize = 1 << 48;
18137        // SAFETY: a fresh anonymous mapping, unmapped at once; the hint is
18138        // only a hint (no MAP_FIXED).
18139        let at = unsafe {
18140            libc::mmap(
18141                HINT as *mut libc::c_void,
18142                4096,
18143                libc::PROT_NONE,
18144                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
18145                -1,
18146                0,
18147            )
18148        };
18149        if at == libc::MAP_FAILED {
18150            return (1 << 47) - 4096;
18151        }
18152        // SAFETY: the mapping made above.
18153        unsafe { libc::munmap(at, 4096) };
18154        if at as usize >= 1 << 47 {
18155            (1 << 56) - 4096
18156        } else {
18157            (1 << 47) - 4096
18158        }
18159    })
18160}
18161
18162/// Authenticate the current task's FD/fdinfo directory by kernel object
18163/// identity, including symlink/bind aliases, without interpreting guest PIDs.
18164#[cfg(test)]
18165fn is_caller_descriptor_directory(
18166    observed: &std::fs::Metadata,
18167    caller: &str,
18168    process: Option<&str>,
18169) -> bool {
18170    caller_descriptor_identity_matches(
18171        (observed.dev(), observed.ino()),
18172        caller,
18173        process,
18174        &["fd", "fdinfo"],
18175    )
18176}
18177
18178fn caller_descriptor_identity_matches(
18179    observed: (u64, u64),
18180    caller: &str,
18181    process: Option<&str>,
18182    kinds: &[&str],
18183) -> bool {
18184    [Some(caller), process].into_iter().flatten().any(|owner| {
18185        kinds.iter().any(|kind| {
18186            std::fs::metadata(format!("{owner}/{kind}"))
18187                .is_ok_and(|expected| observed == (expected.dev(), expected.ino()))
18188        })
18189    })
18190}
18191
18192fn runtime_owned_descriptor_entry(name: &[u8], owns: impl FnOnce(RawFd) -> bool) -> bool {
18193    !name.is_empty()
18194        && name.iter().all(u8::is_ascii_digit)
18195        && std::str::from_utf8(name)
18196            .ok()
18197            .and_then(|name| name.parse::<RawFd>().ok())
18198            .is_some_and(owns)
18199}
18200
18201/// The signals a signalfd reads, from its `/proc/<pid>/fdinfo` entry.
18202fn signalfd_sigmask(pid: i32, fd: i32) -> Option<u64> {
18203    let info = std::fs::read_to_string(format!("/proc/{pid}/fdinfo/{fd}")).ok()?;
18204    let mask = info
18205        .lines()
18206        .find_map(|line| line.strip_prefix("sigmask:"))?;
18207    u64::from_str_radix(mask.trim(), 16).ok()
18208}
18209
18210fn signalfd_range_is_covered(maps: &str, address: usize, len: usize, writable: bool) -> bool {
18211    let mut ranges: Vec<(usize, usize)> = maps
18212        .lines()
18213        .filter_map(|line| {
18214            let mut fields = line.split_whitespace();
18215            let (start, end) = fields.next()?.split_once('-')?;
18216            if writable && !fields.next()?.contains('w') {
18217                return None;
18218            }
18219            Some((
18220                usize::from_str_radix(start, 16).ok()?,
18221                usize::from_str_radix(end, 16).ok()?,
18222            ))
18223        })
18224        .collect();
18225    ranges.sort();
18226    let Some(end) = address.checked_add(len) else {
18227        return false;
18228    };
18229    let mut next = address;
18230    for (start, stop) in ranges {
18231        if start <= next && next < stop {
18232            next = stop;
18233            if next >= end {
18234                return true;
18235            }
18236        }
18237    }
18238    false
18239}
18240
18241#[cfg(test)]
18242mod guest_proc_visibility_tests {
18243    use std::os::fd::AsRawFd;
18244
18245    use reverie::BackendCapabilities;
18246
18247    use super::reopens_scheduler_managed_pipe;
18248    use super::resolved_open_path;
18249
18250    /// The open-path helpers read `/proc/<pid>/fd/<fd>` wherever guest
18251    /// threads are host tasks: ptrace (whose facts a DBT run without its
18252    /// configuration keeps), in-guest LiteInst, DBT and SaBRe.
18253    /// With this process's own pipe and file, those read the links. KVM, whose
18254    /// guest is not a host task, never consults them and gives the helpers'
18255    /// "unknown" answers.
18256    #[tokio::test]
18257    async fn host_proc_links_are_read_except_under_kvm() {
18258        let pid = std::process::id() as i32;
18259        let (reader, _writer) = nix::unistd::pipe().unwrap();
18260        let alias = nix::unistd::dup(&reader).unwrap();
18261        let file = std::fs::File::open("/proc/self/exe").unwrap();
18262        let (pipe_fd, alias_fd) = (reader.as_raw_fd(), alias.as_raw_fd());
18263        let file_fd = file.as_raw_fd();
18264
18265        for backend in [
18266            BackendCapabilities::PTRACE,
18267            BackendCapabilities::LITEINST_IN_GUEST,
18268            BackendCapabilities::DBT,
18269            BackendCapabilities::SABRE,
18270        ] {
18271            assert!(
18272                reopens_scheduler_managed_pipe(&backend, pid, alias_fd, &[pipe_fd]),
18273                "{backend:?}"
18274            );
18275            assert!(
18276                resolved_open_path(&backend, pid, file_fd).is_some(),
18277                "{backend:?}"
18278            );
18279        }
18280        let kvm = BackendCapabilities::KVM;
18281        assert!(!reopens_scheduler_managed_pipe(
18282            &kvm,
18283            pid,
18284            alias_fd,
18285            &[pipe_fd]
18286        ));
18287        assert_eq!(resolved_open_path(&kvm, pid, file_fd), None);
18288
18289        // Runtime ownership is exact: ordinary high application descriptors
18290        // remain visible, and a directory merely named "fd" is not procfs.
18291        for name in [
18292            b"1023".as_slice(),
18293            b"1025",
18294            b"4096",
18295            b".",
18296            b"..",
18297            b"+1024",
18298            b"2147483648",
18299        ] {
18300            assert!(!super::runtime_owned_descriptor_entry(name, |fd| fd == 1024));
18301        }
18302        assert!(super::runtime_owned_descriptor_entry(b"1024", |fd| fd == 1024));
18303        assert!(!super::runtime_owned_descriptor_entry(b"1024", |_| false));
18304        for kind in ["fd", "fdinfo"] {
18305            let thread = std::fs::metadata(format!("/proc/thread-self/{kind}")).unwrap();
18306            let process = std::fs::metadata(format!("/proc/self/{kind}")).unwrap();
18307            assert!(super::is_caller_descriptor_directory(
18308                &thread,
18309                "/proc/thread-self",
18310                None
18311            ));
18312            assert!(!super::is_caller_descriptor_directory(
18313                &process,
18314                "/proc/thread-self",
18315                None
18316            ));
18317            assert!(super::is_caller_descriptor_directory(
18318                &process,
18319                "/proc/thread-self",
18320                Some("/proc/self")
18321            ));
18322        }
18323        let ordinary = tempfile::tempdir().unwrap();
18324        let metadata = std::fs::metadata(ordinary.path()).unwrap();
18325        assert!(!super::is_caller_descriptor_directory(
18326            &metadata,
18327            "/proc/thread-self",
18328            Some("/proc/self")
18329        ));
18330        super::inject_fstat_scratch::raw_directory_projection().await;
18331    }
18332}
18333
18334#[cfg(test)]
18335mod signalfd_geometry_tests {
18336    use super::signalfd_next_record;
18337    use super::signalfd_range_is_mapped;
18338    use super::signalfd_range_is_writable;
18339
18340    /// A range is mapped only when contiguous mappings cover every byte.
18341    #[test]
18342    fn an_iovec_array_is_mapped_only_when_mappings_cover_every_byte() {
18343        let maps =
18344            "1000-2000 r--p 0 0:0 0\n2000-3000 ---p 0 0:0 0 [vvar]\n5000-6000 rw-p 0 0:0 0\n";
18345        assert!(signalfd_range_is_mapped(maps, 0x1ff0, 32));
18346        assert!(
18347            !signalfd_range_is_mapped(maps, 0x2ff0, 32),
18348            "runs into a hole"
18349        );
18350        assert!(!signalfd_range_is_mapped(maps, 0x4000, 16));
18351        assert!(!signalfd_range_is_mapped(maps, usize::MAX - 4, 16));
18352        assert!(!signalfd_range_is_mapped("", 0x1000, 16));
18353    }
18354
18355    /// The record a signalfd read could have lost after the bytes it
18356    /// returned: only where the buffers hold a whole further record, which
18357    /// may span iovecs, and lost only if some byte of it is not writable.
18358    #[test]
18359    fn the_record_after_a_read_is_where_the_buffers_have_room_for_it() {
18360        assert_eq!(
18361            signalfd_next_record(&[(0x1000, 256)], 128),
18362            Some(vec![(0x1080, 128)])
18363        );
18364        assert_eq!(signalfd_next_record(&[(0x1000, 255)], 128), None);
18365        assert_eq!(signalfd_next_record(&[(0x1000, 128)], 128), None);
18366        assert_eq!(
18367            signalfd_next_record(&[(0x1000, 100), (0x3000, 60), (0x5000, 300)], 128),
18368            Some(vec![(0x3000 + 28, 32), (0x5000, 96)])
18369        );
18370        assert_eq!(
18371            signalfd_next_record(&[(0x1000, usize::MAX), (0x2000, 2)], 0),
18372            None
18373        );
18374        let maps = "1000-2000 rw-p 0 0:0 0\n2000-3000 ---p 0 0:0 0\n";
18375        assert!(signalfd_range_is_writable(maps, 0x1f80, 128));
18376        assert!(!signalfd_range_is_writable(maps, 0x1fc0, 128));
18377        assert!(signalfd_range_is_mapped(maps, 0x1fc0, 128));
18378    }
18379}
18380
18381#[cfg(test)]
18382mod procfs_wiring_guard {
18383    //! The procfs snapshot WIRING, guarded where the wiring lives.
18384    //!
18385    //! WHY THIS IS A SOURCE-LEVEL GUARD AND NOT A BEHAVIOURAL TEST. The thing
18386    //! at risk is not `ProcfsFile`'s logic -- that is exercised elsewhere. It is
18387    //! the CALL from each read handler into the snapshot initialiser. Those
18388    //! handlers are `async fn`s on the `Tool` trait taking a live `Guest`, so a
18389    //! unit test cannot invoke one without standing up a traced guest process;
18390    //! that is exactly why the only thing guarding this today is one heavyweight
18391    //! integration test that compiles a C probe and runs hermit.
18392    //!
18393    //! MEASURED GAP (2026-08-07, hermit 75506005d): deleting the pread64
18394    //! snapshot-initialisation block leaves ALL 386 detcore lib tests green.
18395    //! A mechanism whose only proof of life is one fixture is one deletion away
18396    //! from vanishing unnoticed -- the positioned-read determinism bug this
18397    //! wiring fixes would silently return.
18398    //!
18399    //! These assertions are deliberately narrow: they bind to the CALL, name the
18400    //! mechanism when they fail, and cost nothing to run. They do not claim to
18401    //! verify that the snapshot is correct.
18402
18403    /// The production source only. `include_str!` pulls in THIS module too, so a
18404    /// naive scan counts the guard's own string literals and reports phantom
18405    /// duplicates -- it did exactly that on first run. Truncating at the guard's
18406    /// own header makes every assertion below immune to self-reference.
18407    fn production_source() -> &'static str {
18408        const WHOLE: &str = include_str!("files.rs");
18409        const GUARD: &str = "#[cfg(test)]\nmod procfs_wiring_guard {";
18410        match WHOLE.find(GUARD) {
18411            Some(cut) => &WHOLE[..cut],
18412            None => WHOLE,
18413        }
18414    }
18415
18416    /// The body of `fn <name>` up to the next top-level `    }` at fn indent.
18417    fn handler_body(name: &str) -> &'static str {
18418        let start = production_source()
18419            .find(&format!("fn {}<G: Guest<Self>>", name))
18420            .unwrap_or_else(|| {
18421                panic!(
18422                    "procfs wiring guard: handler `{name}` not found in files.rs.\n\
18423                     TWO VERY DIFFERENT CAUSES, and the guard cannot tell them apart:\n\
18424                       (a) the handler was RENAMED or its signature changed -- the \
18425                     mechanism is fine, update the name in this guard; or\n\
18426                       (b) the handler was DELETED -- the procfs snapshot wiring is gone.\n\
18427                     Check which before editing. This guard binds to source text on \
18428                     purpose: the handlers are async `Tool` methods taking a live Guest, \
18429                     so nothing cheaper can observe the call. It is deliberately loud \
18430                     when it cannot see the code, because silently passing is the \
18431                     failure it exists to prevent."
18432                )
18433            });
18434        let rest = &production_source()[start..];
18435        let end = rest
18436            .find(
18437                "
18438    }
18439",
18440            )
18441            .map(|e| e + 6)
18442            .unwrap_or(rest.len());
18443        &rest[..end]
18444    }
18445
18446    #[test]
18447    fn pread64_initializes_the_procfs_snapshot() {
18448        let body = handler_body("handle_pread64");
18449        assert!(
18450            body.contains("procfs_needs_snapshot") && body.contains("initialize_procfs_snapshot"),
18451            "MISSING MECHANISM: the pread64 handler no longer initialises the procfs \
18452             snapshot. Positioned reads will fall through to LIVE KERNEL BYTES instead of \
18453             the sanitized ProcfsFile snapshot, reintroducing the positioned-read \
18454             nondeterminism that hermit-cli/tests/procfs_positioned_determinism.rs exists \
18455             to catch. Restore the `procfs_needs_snapshot` -> `initialize_procfs_snapshot` \
18456             call in handle_pread64."
18457        );
18458    }
18459
18460    #[test]
18461    fn read_initializes_the_procfs_snapshot() {
18462        let body = handler_body("handle_read");
18463        assert!(
18464            body.contains("procfs_needs_snapshot") && body.contains("initialize_procfs_snapshot"),
18465            "MISSING MECHANISM: the sequential read handler no longer initialises the \
18466             procfs snapshot. Reads of /proc will observe live kernel bytes. Restore the \
18467             `procfs_needs_snapshot` -> `initialize_procfs_snapshot` call in handle_read."
18468        );
18469    }
18470
18471    #[test]
18472    fn both_read_paths_share_one_snapshot_initializer() {
18473        // The original defect was exactly this asymmetry: `read` consumed the
18474        // sanitized snapshot while `pread64` did not. Forking the logic into two
18475        // initialisers is how that asymmetry comes back.
18476        let n = production_source()
18477            .matches("async fn initialize_procfs_snapshot")
18478            .count();
18479        assert_eq!(
18480            n, 1,
18481            "MISSING MECHANISM: expected exactly ONE `initialize_procfs_snapshot` \
18482             definition so every read path shares it; found {n}. Two initialisers is how \
18483             read/pread64 drifted apart in the first place."
18484        );
18485        for handler in ["handle_read", "handle_pread64"] {
18486            assert!(
18487                handler_body(handler).contains("self.initialize_procfs_snapshot("),
18488                "MISSING MECHANISM: `{handler}` does not call the shared \
18489                 initialize_procfs_snapshot."
18490            );
18491        }
18492    }
18493
18494    /// Both mountinfo captures in the snapshot initializer must drop ephemeral
18495    /// host seed rows (https://github.com/rrnewton/hermit/pull/3219): the guest
18496    /// read's capture, for the files that carry mountinfo identities, and the
18497    /// tracer-side capture that validates fdinfo `mnt_id`. Without either, host
18498    /// seed churn reaches guest-visible mountinfo or shifts mount-ID assignment,
18499    /// and no unit test of the filter itself notices.
18500    #[test]
18501    fn snapshot_initializer_excludes_host_seed_mounts_at_both_captures() {
18502        let body = handler_body("initialize_procfs_snapshot");
18503        let calls = body.matches("exclude_ephemeral_host_seed_mounts(").count();
18504        assert_eq!(
18505            calls, 2,
18506            "MISSING MECHANISM: initialize_procfs_snapshot must call \
18507             exclude_ephemeral_host_seed_mounts exactly twice (the guest mountinfo \
18508             capture and the fdinfo mnt_id capture); found {calls}. Without it, \
18509             unrelated host squashfuse seed mounts make /proc/<pid>/mountinfo and \
18510             fdinfo mount IDs depend on host timing."
18511        );
18512        let guest_capture = body
18513            .find("exclude_ephemeral_host_seed_mounts(&raw_contents)")
18514            .expect(
18515                "MISSING MECHANISM: the guest read's procfs capture (`raw_contents`) is \
18516                 no longer filtered for host seed mounts",
18517            );
18518        let gate = body[..guest_capture]
18519            .rfind("let contents = if")
18520            .map(|start| &body[start..guest_capture])
18521            .unwrap_or_default();
18522        assert!(
18523            gate.contains("procfs_needs_mountinfo_identities()"),
18524            "MISSING MECHANISM: the guest capture's seed filter must apply exactly to \
18525             the files that carry mountinfo identities (`let contents = if ... \
18526             procfs_needs_mountinfo_identities()`); found {gate:?}"
18527        );
18528        let fdinfo_capture = body
18529            .find("exclude_ephemeral_host_seed_mounts(&mountinfo_contents)")
18530            .expect(
18531                "MISSING MECHANISM: the fdinfo mnt_id capture (`mountinfo_contents`) is \
18532                 no longer filtered for host seed mounts",
18533            );
18534        let parse = body
18535            .find("parse_mountinfo(&mountinfo_contents)")
18536            .expect("the fdinfo mnt_id capture no longer parses mountinfo_contents");
18537        assert!(
18538            fdinfo_capture < parse,
18539            "MISSING MECHANISM: the fdinfo mnt_id capture parses mountinfo before \
18540             dropping host seed rows"
18541        );
18542    }
18543
18544    #[test]
18545    fn the_guard_can_actually_see_the_handlers() {
18546        // Positive control: if the extractor silently returned empty bodies the
18547        // three assertions above would be vacuous rather than protective.
18548        for handler in ["handle_read", "handle_pread64"] {
18549            let body = handler_body(handler);
18550            assert!(
18551                body.len() > 200 && body.contains("call.fd()"),
18552                "guard extractor did not find a real body for `{handler}` \
18553                 (len {}), so the wiring assertions would be vacuous",
18554                body.len()
18555            );
18556        }
18557    }
18558}
18559
18560#[cfg(test)]
18561mod test {
18562    use nix::fcntl::OFlag;
18563    use reverie::syscalls::FromToRaw;
18564    use reverie::syscalls::LocalMemory;
18565    use reverie::syscalls::Whence;
18566
18567    use super::deterministic_pipe_capacity_bytes;
18568    use super::fcntl_host_timed_signals;
18569    use super::ioctl_host_timed_signals;
18570    use super::may_have_acquired_controlling_terminal;
18571    use super::open_can_acquire_controlling_terminal;
18572    use super::opened_controlling_terminal;
18573    use super::pipe_capacity_request_exceeds_ceiling;
18574    use super::terminal_can_become_controlling;
18575    use crate::test_pages::PAGE;
18576    use crate::test_pages::Pages;
18577
18578    /// Run the test named `case` in an isolated child process, returning
18579    /// true when this invocation is the parent and the caller must return
18580    /// at once. Tests that close this process's real descriptors and then
18581    /// assert the numbers are dead race sibling test threads under plain
18582    /// `cargo test`: a sibling can be handed a just-closed number before
18583    /// the assertion reads it. A child process has no sibling threads
18584    /// allocating descriptors.
18585    fn isolated_child(case: &str) -> bool {
18586        const CHILD: &str = "HERMIT_FILES_TEST_CHILD";
18587        if std::env::var_os(CHILD).as_deref() == Some(std::ffi::OsStr::new(case)) {
18588            return false;
18589        }
18590        let output = std::process::Command::new(std::env::current_exe().unwrap())
18591            .args(["--exact", case, "--nocapture", "--test-threads=1"])
18592            .env(CHILD, case)
18593            .output()
18594            .unwrap();
18595        assert!(output.status.success(), "{output:?}");
18596        assert!(
18597            String::from_utf8_lossy(&output.stdout).contains("1 passed"),
18598            "{output:?}"
18599        );
18600        true
18601    }
18602
18603    /// A `/proc/self` or `/proc/thread-self` spelling leads to the opener's
18604    /// task, which a name relative to the descriptor is then looked up below,
18605    /// whoever looks it up.
18606    #[test]
18607    fn a_proc_selector_leads_to_the_openers_task() {
18608        use std::path::Path;
18609
18610        use super::procfs_selector_reached;
18611
18612        let own = Some((3, 5));
18613        for (spelling, reached) in [
18614            ("/proc/self", Some("/proc/3")),
18615            ("/proc/self/", Some("/proc/3")),
18616            ("/proc/self/task", Some("/proc/3/task")),
18617            ("/proc/self/task/5", Some("/proc/3/task/5")),
18618            ("/proc/thread-self", Some("/proc/3/task/5")),
18619            ("/proc/self/fd/7", None),
18620            ("/proc/self/task/05", None),
18621            ("/proc/thread-self/fd/7", None),
18622            ("/proc/self/cwd", None),
18623            ("/proc/3", Some("/proc/3")),
18624            ("/proc/7/", Some("/proc/7")),
18625            ("/proc/7/task", Some("/proc/7/task")),
18626            ("/proc/7/task/9", Some("/proc/7/task/9")),
18627            ("/proc/07", None),
18628            ("/proc/7/task/09", None),
18629            ("/proc/7/fd", None),
18630            ("/proc/7/task/9/fd/1", None),
18631            ("/proc/sys", None),
18632            ("/proc", None),
18633            ("/proc/selfish", None),
18634            ("/dev/fd", None),
18635            ("self", None),
18636        ] {
18637            assert_eq!(
18638                procfs_selector_reached(Path::new(spelling), own).as_deref(),
18639                reached.map(Path::new),
18640                "{spelling}"
18641            );
18642        }
18643        // In a nested PID namespace Detcore does not know the caller's numbers.
18644        assert_eq!(procfs_selector_reached(Path::new("/proc/self"), None), None);
18645        assert_eq!(procfs_selector_reached(Path::new("/proc/3"), None), None);
18646        // The selector links themselves, which an `O_PATH | O_NOFOLLOW` open
18647        // opens rather than the directory they lead to.
18648        for (spelling, link) in [
18649            ("/proc/self", true),
18650            ("/proc/thread-self", true),
18651            // A trailing slash follows the link (`opens_the_link_itself`).
18652            ("/proc/self/", true),
18653            ("/proc/self/task", false),
18654            ("/proc/3", false),
18655            ("/proc/selfish", false),
18656        ] {
18657            assert_eq!(
18658                super::names_a_selector_link(Path::new(spelling)),
18659                link,
18660                "{spelling}"
18661            );
18662        }
18663        // The task each reached directory is, or is below.
18664        let task = |process, thread| crate::procfs::BoundTask { process, thread };
18665        for (reached, expected) in [
18666            ("/proc/3", Some(task(3, None))),
18667            ("/proc/3/task", Some(task(3, None))),
18668            ("/proc/3/task/5", Some(task(3, Some(5)))),
18669            ("/proc/3/task/5/ns", Some(task(3, Some(5)))),
18670            ("/proc/3/ns", Some(task(3, None))),
18671            ("/proc", None),
18672            ("/proc/sys", None),
18673            ("/proc/0", None),
18674        ] {
18675            assert_eq!(
18676                super::procfs_reached_task(Path::new(reached)),
18677                expected,
18678                "{reached}"
18679            );
18680        }
18681    }
18682
18683    /// A descriptor opened through one of the caller's descriptor links
18684    /// records the path of the descriptor the link names, so a name relative
18685    /// to it is the file Linux finds below that file: `/proc/stat` through a
18686    /// reopened `/proc`, and through a reopened reopening. It keeps its
18687    /// spelling wherever the recorded path could name another file: a link
18688    /// opened itself (`O_NOFOLLOW` without a trailing slash), the process's
18689    /// table where the caller holds a table of its own, a target without an
18690    /// absolute recorded path, and a target that names a per-caller
18691    /// selector. Every other open records its spelling.
18692    #[test]
18693    fn an_open_through_a_descriptor_link_records_the_targets_path() {
18694        use std::path::Path;
18695        use std::path::PathBuf;
18696
18697        use super::DescriptorLinkOpener;
18698        use super::path_to_record_for_open;
18699
18700        let leader = DescriptorLinkOpener {
18701            own: Some((3, 3)),
18702            holds_process_table: true,
18703        };
18704        let read = OFlag::O_RDONLY | OFlag::O_DIRECTORY;
18705        let table = |fd: i32| match fd {
18706            4 => Some(PathBuf::from("/proc")),
18707            6 => Some(PathBuf::from("relative/dir")),
18708            8 => Some(PathBuf::from("/proc/self")),
18709            9 => Some(PathBuf::from("/proc/thread-self")),
18710            10 => Some(PathBuf::from("/dev/fd")),
18711            11 => Some(PathBuf::from("/proc/12")),
18712            _ => None,
18713        };
18714        for spelling in [
18715            "/proc/self/fd/4",
18716            "/dev/fd/4",
18717            "/proc/thread-self/fd/4",
18718            "/proc/3/fd/4",
18719            "/proc/3/task/3/fd/4",
18720        ] {
18721            let recorded = path_to_record_for_open(Path::new(spelling), read, leader, table, None);
18722            assert_eq!(recorded, Path::new("/proc"), "{spelling}");
18723            // A dirfd-relative name below the new descriptor is then the
18724            // rendered file's own path (`Detcore::observed_path`).
18725            assert_eq!(recorded.join("stat"), Path::new("/proc/stat"));
18726            assert_eq!(
18727                recorded.join("sys/kernel/random/uuid"),
18728                Path::new("/proc/sys/kernel/random/uuid")
18729            );
18730        }
18731        // An alias of an alias: the second open finds the first's recorded
18732        // path, already the target's.
18733        let first =
18734            path_to_record_for_open(Path::new("/proc/self/fd/4"), read, leader, table, None);
18735        let second = path_to_record_for_open(
18736            Path::new("/dev/fd/9"),
18737            read,
18738            leader,
18739            |fd| (fd == 9).then(|| first.clone()),
18740            None,
18741        );
18742        assert_eq!(second, Path::new("/proc"));
18743        // O_NOFOLLOW opens the link itself (O_PATH) unless a trailing slash
18744        // or `/.` follows it.
18745        let nofollow = OFlag::O_PATH | OFlag::O_NOFOLLOW;
18746        assert_eq!(
18747            path_to_record_for_open(Path::new("/proc/self/fd/4"), nofollow, leader, table, None),
18748            Path::new("/proc/self/fd/4")
18749        );
18750        for spelling in ["/proc/self/fd/4/", "/proc/self/fd/4/.", "/dev/fd/4/"] {
18751            assert_eq!(
18752                path_to_record_for_open(Path::new(spelling), nofollow, leader, table, None),
18753                Path::new("/proc"),
18754                "{spelling}"
18755            );
18756        }
18757        // A thread with a table of its own: the process's links name the
18758        // leader's table, not the caller's, so they keep the spelling; its
18759        // own thread links still name its table.
18760        let split = DescriptorLinkOpener {
18761            own: Some((3, 5)),
18762            holds_process_table: false,
18763        };
18764        for spelling in ["/proc/self/fd/4", "/dev/fd/4", "/proc/3/fd/4"] {
18765            assert_eq!(
18766                path_to_record_for_open(
18767                    Path::new(spelling),
18768                    read,
18769                    split,
18770                    |_| { panic!("{spelling} names the leader's table, not the caller's") },
18771                    None
18772                ),
18773                Path::new(spelling)
18774            );
18775        }
18776        for spelling in ["/proc/thread-self/fd/4", "/proc/3/task/5/fd/4"] {
18777            assert_eq!(
18778                path_to_record_for_open(Path::new(spelling), read, split, table, None),
18779                Path::new("/proc"),
18780                "{spelling}"
18781            );
18782        }
18783        // A target without a recorded path, with a relative one, or with one
18784        // procfs resolves per caller, and another process's link, keep the
18785        // spelling. A numeric /proc/<pid> target is not per caller.
18786        for spelling in [
18787            "/proc/self/fd/5",
18788            "/proc/self/fd/6",
18789            "/proc/self/fd/8",
18790            "/proc/self/fd/9",
18791            "/dev/fd/10",
18792            "/proc/7/fd/4",
18793        ] {
18794            assert_eq!(
18795                path_to_record_for_open(Path::new(spelling), read, leader, table, None),
18796                Path::new(spelling)
18797            );
18798        }
18799        assert_eq!(
18800            path_to_record_for_open(Path::new("/dev/fd/11"), read, leader, table, None),
18801            Path::new("/proc/12")
18802        );
18803        // So does every open that names no descriptor link.
18804        for spelling in ["/proc/stat", "/proc/self/fd/4/stat", "/tmp/x"] {
18805            assert_eq!(
18806                path_to_record_for_open(
18807                    Path::new(spelling),
18808                    read,
18809                    leader,
18810                    |_| { panic!("{spelling} names no descriptor") },
18811                    None
18812                ),
18813                Path::new(spelling)
18814            );
18815        }
18816        // The path is N's as the open's lookup found it, and only while N
18817        // holds the same open file description: an open of N's link that
18818        // waited (a FIFO) while a thread sharing the table replaced N with a
18819        // /proc descriptor opened the FIFO, so it is not recorded as /proc.
18820        use super::LinkSource;
18821        let creator = crate::types::DetTid::from_raw(3);
18822        let fifo = LinkSource {
18823            fd: 5,
18824            path: Some(PathBuf::from("/tmp/dir/fifo")),
18825            open_file: crate::types::OpenFileId::new(creator, 7),
18826            reached: None,
18827        };
18828        let replaced = Some(crate::types::OpenFileId::new(creator, 8));
18829        let unchanged = Some(fifo.open_file);
18830        let spelling = Path::new("/proc/self/fd/5");
18831        assert_eq!(
18832            path_to_record_for_open(
18833                spelling,
18834                read,
18835                leader,
18836                |fd| fifo.path_if_unchanged(fd, unchanged),
18837                None
18838            ),
18839            Path::new("/tmp/dir/fifo")
18840        );
18841        assert_eq!(
18842            path_to_record_for_open(
18843                spelling,
18844                read,
18845                leader,
18846                |fd| fifo.path_if_unchanged(fd, replaced),
18847                None
18848            ),
18849            spelling
18850        );
18851        assert_eq!(
18852            path_to_record_for_open(
18853                spelling,
18854                read,
18855                leader,
18856                |fd| fifo.path_if_unchanged(fd, None),
18857                None
18858            ),
18859            spelling
18860        );
18861        assert_eq!(fifo.path_if_unchanged(4, unchanged), None);
18862    }
18863
18864    /// Where the descriptor a link names has no recorded path (one received
18865    /// through `SCM_RIGHTS`, which has none), an open through
18866    /// the link records the procfs directory the kernel names for the opened
18867    /// descriptor (`Detcore::procfs_directory_named_for`): `/proc`, below
18868    /// which `stat` is `/proc/stat`. Without that name it keeps the
18869    /// spelling, as before. A recorded path of `N` still comes first, and the
18870    /// name is never taken for a link opened itself, a spelling that names no
18871    /// descriptor link, or a link to a table the caller does not hold. Only
18872    /// `/proc` and the pid-free paths below it are names the kernel's link
18873    /// may give (`procfs_directory_link`).
18874    #[test]
18875    fn an_open_through_an_unrecorded_descriptors_link_records_its_identity() {
18876        use std::path::Path;
18877        use std::path::PathBuf;
18878
18879        use super::DescriptorLinkOpener;
18880        use super::path_to_record_for_open;
18881
18882        let leader = DescriptorLinkOpener {
18883            own: Some((3, 3)),
18884            holds_process_table: true,
18885        };
18886        let read = OFlag::O_RDONLY | OFlag::O_DIRECTORY;
18887        let proc = || Some(PathBuf::from("/proc"));
18888        for spelling in [
18889            "/proc/self/fd/10",
18890            "/dev/fd/10",
18891            "/proc/thread-self/fd/10",
18892            "/proc/3/fd/10",
18893            "/proc/3/task/3/fd/10",
18894        ] {
18895            let recorded =
18896                path_to_record_for_open(Path::new(spelling), read, leader, |_| None, proc());
18897            assert_eq!(recorded, Path::new("/proc"), "{spelling}");
18898            assert_eq!(recorded.join("stat"), Path::new("/proc/stat"));
18899            assert_eq!(
18900                path_to_record_for_open(Path::new(spelling), read, leader, |_| None, None),
18901                Path::new(spelling)
18902            );
18903        }
18904        assert_eq!(
18905            path_to_record_for_open(
18906                Path::new("/proc/self/fd/4"),
18907                read,
18908                leader,
18909                |fd| (fd == 4).then(|| PathBuf::from("/proc/12")),
18910                proc(),
18911            ),
18912            Path::new("/proc/12")
18913        );
18914        let nofollow = OFlag::O_PATH | OFlag::O_NOFOLLOW;
18915        assert_eq!(
18916            path_to_record_for_open(
18917                Path::new("/proc/self/fd/10"),
18918                nofollow,
18919                leader,
18920                |_| None,
18921                proc()
18922            ),
18923            Path::new("/proc/self/fd/10")
18924        );
18925        for spelling in ["/proc/self/fd/10/stat", "/tmp/x"] {
18926            assert_eq!(
18927                path_to_record_for_open(Path::new(spelling), read, leader, |_| None, proc()),
18928                Path::new(spelling)
18929            );
18930        }
18931        // A recorded path refused as per caller is not replaced by the name.
18932        assert_eq!(
18933            path_to_record_for_open(
18934                Path::new("/proc/self/fd/8"),
18935                read,
18936                leader,
18937                |_| Some(PathBuf::from("/proc/self")),
18938                proc(),
18939            ),
18940            Path::new("/proc/self/fd/8")
18941        );
18942        // A thread with a table of its own: the process's links name the
18943        // leader's table, whose N the caller's table does not describe, so
18944        // they keep the spelling; its own thread links take the name.
18945        let split = DescriptorLinkOpener {
18946            own: Some((3, 5)),
18947            holds_process_table: false,
18948        };
18949        for spelling in ["/proc/self/fd/10", "/dev/fd/10", "/proc/3/fd/10"] {
18950            assert_eq!(
18951                path_to_record_for_open(Path::new(spelling), read, split, |_| None, proc()),
18952                Path::new(spelling)
18953            );
18954        }
18955        for spelling in ["/proc/thread-self/fd/10", "/proc/3/task/5/fd/10"] {
18956            assert_eq!(
18957                path_to_record_for_open(Path::new(spelling), read, split, |_| None, proc()),
18958                Path::new("/proc"),
18959                "{spelling}"
18960            );
18961        }
18962        use super::procfs_directory_link;
18963        for accepted in ["/proc", "/proc/sys", "/proc/sys/kernel", "/proc/sys/net"] {
18964            assert!(procfs_directory_link(Path::new(accepted)), "{accepted}");
18965        }
18966        for refused in [
18967            "/tmp/dir",
18968            "/procfs",
18969            "/proc (deleted)",
18970            "/proc/sys (deleted)",
18971            "/proc/12",
18972            "/proc/12/task/12",
18973            "/proc/self",
18974            "/proc/thread-self",
18975            // A symbolic link to the caller's own /proc/self/net.
18976            "/proc/net",
18977            "/proc/sys/../12",
18978            "proc",
18979            "anon_inode:[eventfd]",
18980            "/memfd:x (deleted)",
18981        ] {
18982            assert!(!procfs_directory_link(Path::new(refused)), "{refused}");
18983        }
18984    }
18985
18986    /// The kernel's name for a received procfs directory gives its components
18987    /// below `/proc` and the positions of the task numbers among them, which
18988    /// DBT translates to the guest's: a process's directory and a thread's
18989    /// below its `task`, nothing for the system directories, and no name
18990    /// procfs would not give.
18991    #[test]
18992    fn a_received_procfs_directory_names_its_task_positions() {
18993        use super::procfs_directory_task_positions;
18994        let parsed = |link: &str| procfs_directory_task_positions(link);
18995        let owned = |names: &[&str]| names.iter().map(|name| (*name).to_owned()).collect();
18996        assert_eq!(parsed("/proc"), Some((Vec::new(), Vec::new())));
18997        assert_eq!(
18998            parsed("/proc/sys/kernel"),
18999            Some((owned(&["sys", "kernel"]), Vec::new()))
19000        );
19001        assert_eq!(parsed("/proc/12"), Some((owned(&["12"]), vec![0])));
19002        assert_eq!(parsed("/proc/12/ns"), Some((owned(&["12", "ns"]), vec![0])));
19003        assert_eq!(parsed("/proc/12/fd"), Some((owned(&["12", "fd"]), vec![0])));
19004        assert_eq!(
19005            parsed("/proc/12/task/14"),
19006            Some((owned(&["12", "task", "14"]), vec![0, 2]))
19007        );
19008        assert_eq!(
19009            parsed("/proc/12/task/14/ns"),
19010            Some((owned(&["12", "task", "14", "ns"]), vec![0, 2]))
19011        );
19012        assert_eq!(
19013            parsed("/proc/irq/5"),
19014            Some((owned(&["irq", "5"]), Vec::new()))
19015        );
19016        for refused in [
19017            "/proc/012",
19018            "/proc/12/task/014",
19019            "/procfs",
19020            "/tmp/12",
19021            "proc/12",
19022            "/proc/12 (deleted)",
19023            "socket:[1]",
19024            "/proc/12/../13",
19025        ] {
19026            assert_eq!(parsed(refused), None, "{refused}");
19027        }
19028    }
19029
19030    #[test]
19031    fn inherited_input_status_flags_remain_kernel_owned() {
19032        for recordreplay_modes in [false, true] {
19033            let tool = crate::Detcore {
19034                detpid: crate::types::DetPid::from_raw(3),
19035                cfg: crate::Config {
19036                    sequentialize_threads: true,
19037                    recordreplay_modes,
19038                    ..Default::default()
19039                },
19040                record_or_replay: crate::record_or_replay::NoopTool,
19041            };
19042            for physically_nonblocking in [false, true] {
19043                assert!(!tool.keeps_physically_nonblocking(
19044                    crate::fd::FdType::Pipe,
19045                    false,
19046                    physically_nonblocking,
19047                    true
19048                ));
19049                assert_eq!(
19050                    tool.keeps_physically_nonblocking(
19051                        crate::fd::FdType::Pipe,
19052                        false,
19053                        physically_nonblocking,
19054                        false
19055                    ),
19056                    !recordreplay_modes || physically_nonblocking
19057                );
19058                assert_eq!(
19059                    tool.keeps_physically_nonblocking(
19060                        crate::fd::FdType::Socket,
19061                        true,
19062                        physically_nonblocking,
19063                        false
19064                    ),
19065                    !recordreplay_modes || physically_nonblocking
19066                );
19067            }
19068        }
19069    }
19070
19071    /// An inotify instance stays physically nonblocking through the guest's
19072    /// F_SETFL and FIONBIO wherever Detcore made it so at creation, as an
19073    /// eventfd does: with sequentialized threads, outside record and replay
19074    /// (<https://github.com/rrnewton/hermit/issues/4768>). Elsewhere the
19075    /// guest's request reaches the kernel.
19076    #[test]
19077    fn inotify_instances_stay_physically_nonblocking_where_detcore_waits() {
19078        for sequentialize_threads in [false, true] {
19079            for recordreplay_modes in [false, true] {
19080                let tool = crate::Detcore {
19081                    detpid: crate::types::DetPid::from_raw(3),
19082                    cfg: crate::Config {
19083                        sequentialize_threads,
19084                        recordreplay_modes,
19085                        ..Default::default()
19086                    },
19087                    record_or_replay: crate::record_or_replay::NoopTool,
19088                };
19089                for physically_nonblocking in [false, true] {
19090                    assert_eq!(
19091                        tool.keeps_physically_nonblocking(
19092                            crate::fd::FdType::Inotify,
19093                            false,
19094                            physically_nonblocking,
19095                            false
19096                        ),
19097                        sequentialize_threads && !recordreplay_modes,
19098                        "sequentialize_threads {sequentialize_threads}, record/replay \
19099                         {recordreplay_modes}, physically nonblocking {physically_nonblocking}"
19100                    );
19101                }
19102            }
19103        }
19104    }
19105
19106    /// Every `fcntl` and `ioctl` that can make Linux signal a descriptor's
19107    /// owner at a moment set by host timing names SIGIO and SIGURG, plus the
19108    /// signal `F_SETSIG` chooses; every other command names nothing
19109    /// (https://github.com/rrnewton/hermit/issues/3146).
19110    #[test]
19111    fn async_io_arming_calls_name_their_host_timed_signals() {
19112        use reverie::syscalls::FcntlCmd;
19113        use reverie::syscalls::ioctl::Request;
19114        let bit = |signal: i32| 1_u64 << (signal - 1);
19115        let async_io = bit(libc::SIGIO) | bit(libc::SIGURG);
19116        for (cmd, expected) in [
19117            (
19118                FcntlCmd::F_SETFL(libc::O_ASYNC | libc::O_NONBLOCK),
19119                async_io,
19120            ),
19121            (FcntlCmd::F_SETFL(libc::O_NONBLOCK), 0),
19122            (FcntlCmd::F_SETOWN, async_io),
19123            (FcntlCmd::F_SETOWN_EX(None), async_io),
19124            (FcntlCmd::F_SETLEASE(libc::F_RDLCK), async_io),
19125            // DN_MODIFY, which the libc crate does not export.
19126            (FcntlCmd::F_NOTIFY(0x2), async_io),
19127            (
19128                FcntlCmd::F_SETSIG(libc::SIGUSR1),
19129                async_io | bit(libc::SIGUSR1),
19130            ),
19131            (FcntlCmd::F_SETSIG(0), async_io),
19132            (FcntlCmd::F_GETFL, 0),
19133            (FcntlCmd::F_SETFD(libc::FD_CLOEXEC), 0),
19134        ] {
19135            assert_eq!(fcntl_host_timed_signals(cmd), expected, "{cmd:?}");
19136        }
19137        for (request, expected) in [
19138            (Request::FIOASYNC(None), async_io),
19139            (Request::FIOSETOWN(None), async_io),
19140            (Request::SIOCSPGRP(None), async_io),
19141            (Request::FIOCLEX, 0),
19142        ] {
19143            assert_eq!(ioctl_host_timed_signals(request), expected, "{request:?}");
19144        }
19145    }
19146
19147    /// The calls that give a session its controlling terminal (TIOCSCTTY, and
19148    /// TIOCGPTPEER without O_NOCTTY, which opens a pseudoterminal's other end
19149    /// as `open` does) or choose the terminal's foreground process group
19150    /// (TIOCSPGRP) name every signal Linux sends because of that terminal, at
19151    /// a moment set by host timing: SIGHUP and SIGCONT when it hangs up or the
19152    /// leader exits, SIGINT, SIGQUIT and SIGTSTP for its interrupt, quit and
19153    /// suspend characters, SIGWINCH when its size changes, and SIGTTIN and
19154    /// SIGTTOU when a background process group reads or writes it. Reading the
19155    /// foreground group (TIOCGPGRP), giving up the controlling terminal
19156    /// (TIOCNOTTY), opening the other end with O_NOCTTY, and setting the size
19157    /// (TIOCSWINSZ, whose SIGWINCH is sent inside the caller's call) name
19158    /// nothing (round-9 High 2 and the round-11 finding "Terminal resize
19159    /// remains an admitted host-timed interruption" of
19160    /// https://github.com/rrnewton/hermit/pull/3361).
19161    #[test]
19162    fn terminal_control_calls_name_the_terminal_signals() {
19163        use reverie::syscalls::ioctl::Request;
19164        let bit = |signal: i32| 1_u64 << (signal - 1);
19165        let terminal = [
19166            libc::SIGHUP,
19167            libc::SIGCONT,
19168            libc::SIGINT,
19169            libc::SIGQUIT,
19170            libc::SIGTSTP,
19171            libc::SIGWINCH,
19172            libc::SIGTTIN,
19173            libc::SIGTTOU,
19174        ]
19175        .into_iter()
19176        .fold(0, |set, signal| set | bit(signal));
19177        for (request, expected) in [
19178            (Request::TIOCSCTTY(0), terminal),
19179            (Request::TIOCSCTTY(1), terminal),
19180            (Request::TIOCSPGRP(None), terminal),
19181            (Request::TIOCGPTPEER(libc::O_RDWR), terminal),
19182            (
19183                Request::TIOCGPTPEER(libc::O_RDWR | libc::O_CLOEXEC),
19184                terminal,
19185            ),
19186            (Request::TIOCGPTPEER(libc::O_RDWR | libc::O_NOCTTY), 0),
19187            (Request::TIOCGPGRP(None), 0),
19188            (Request::TIOCNOTTY, 0),
19189            (Request::TIOCSWINSZ(None), 0),
19190        ] {
19191            assert_eq!(ioctl_host_timed_signals(request), expected, "{request:?}");
19192        }
19193    }
19194
19195    /// An `open` can give its caller a controlling terminal only without
19196    /// O_NOCTTY and O_PATH, only in a session leader, and only of a terminal
19197    /// Linux can make controlling. None of the inputs is the controlling
19198    /// terminal procfs lists, which a hangup clears
19199    /// (https://github.com/rrnewton/hermit/issues/3146,
19200    /// https://github.com/rrnewton/hermit/pull/3361).
19201    #[test]
19202    fn an_open_acquires_a_controlling_terminal_only_as_a_session_leader() {
19203        assert!(open_can_acquire_controlling_terminal(OFlag::O_RDWR));
19204        assert!(!open_can_acquire_controlling_terminal(
19205            OFlag::O_RDWR | OFlag::O_NOCTTY
19206        ));
19207        assert!(!open_can_acquire_controlling_terminal(OFlag::O_PATH));
19208        // A host's /proc/tty/drivers, plus a driver whose name has a space.
19209        let table = Some(
19210            "/dev/tty             /dev/tty        5       0 system:/dev/tty\n\
19211             /dev/console         /dev/console    5       1 system:console\n\
19212             /dev/ptmx            /dev/ptmx       5       2 system\n\
19213             /dev/vc/0            /dev/vc/0       4       0 system:vtmaster\n\
19214             serial_8250          /dev/ttyS       4 64-95 serial\n\
19215             acm serial           /dev/ttyACM   166 0-255 serial\n\
19216             pty_slave            /dev/pts      136 0-1048575 pty:slave\n\
19217             pty_master           /dev/ptm      128 0-1048575 pty:master\n\
19218             unknown              /dev/tty        4 1-63 console\n",
19219        );
19220        // /dev/pts/3 is character device 136:3; minor 300 sets bits above 0xff.
19221        let pts3 = libc::makedev(136, 3);
19222        let pts300 = libc::makedev(136, 300);
19223        assert!(may_have_acquired_controlling_terminal(42, 42, pts3, table));
19224        assert!(may_have_acquired_controlling_terminal(
19225            42, 42, pts300, table
19226        ));
19227        assert!(!may_have_acquired_controlling_terminal(43, 42, pts3, table));
19228        // Serial lines, virtual consoles and /dev/tty can become controlling;
19229        // /dev/console, /dev/ptmx, /dev/vc/0, pseudoterminal masters, minors
19230        // outside every range and devices that are not terminals cannot.
19231        for (major, minor) in [(4, 64), (4, 95), (166, 7), (4, 1), (4, 63), (5, 0)] {
19232            let rdev = libc::makedev(major, minor);
19233            assert!(
19234                terminal_can_become_controlling(rdev, table),
19235                "{major}:{minor}"
19236            );
19237        }
19238        for (major, minor) in [(5, 1), (5, 2), (4, 0), (128, 3), (4, 96), (188, 0), (1, 3)] {
19239            let rdev = libc::makedev(major, minor);
19240            assert!(
19241                !terminal_can_become_controlling(rdev, table),
19242                "{major}:{minor}"
19243            );
19244        }
19245        // A table that cannot be read answers true, except for the devices
19246        // Linux never makes controlling. A line that does not parse answers
19247        // true, unless another line names the device.
19248        let null = libc::makedev(1, 3);
19249        assert!(terminal_can_become_controlling(pts3, None));
19250        assert!(terminal_can_become_controlling(null, None));
19251        assert!(!terminal_can_become_controlling(libc::makedev(5, 1), None));
19252        let garbled = Some("pty_slave /dev/pts 136 0-1048575 pty:slave\nnot a driver line\n");
19253        assert!(terminal_can_become_controlling(null, garbled));
19254        assert!(terminal_can_become_controlling(pts3, garbled));
19255        let master = Some("pty_master /dev/ptm 128 0-1048575 pty:master\nnot a driver line\n");
19256        assert!(!terminal_can_become_controlling(
19257            libc::makedev(128, 3),
19258            master
19259        ));
19260    }
19261
19262    /// A session leader that opens a pseudoterminal's slave without O_NOCTTY
19263    /// gains it as its controlling terminal. Closing the master then hangs the
19264    /// slave up, which clears the controlling terminal procfs lists but changes
19265    /// neither the session nor the descriptor's device. A hangup that lands
19266    /// after the `open`, whether before or after the descriptor's stat, must
19267    /// not hide that the `open` may have acquired the terminal
19268    /// (https://github.com/rrnewton/hermit/pull/3361).
19269    #[test]
19270    fn a_hangup_after_the_open_does_not_hide_the_acquired_terminal() {
19271        use std::time::Duration;
19272        use std::time::Instant;
19273
19274        struct Child(libc::pid_t);
19275        impl Drop for Child {
19276            fn drop(&mut self) {
19277                // SAFETY: the pid is this test's own child, not yet reaped.
19278                unsafe {
19279                    libc::kill(self.0, libc::SIGKILL);
19280                    libc::waitpid(self.0, std::ptr::null_mut(), 0);
19281                }
19282            }
19283        }
19284
19285        // SAFETY (every block below): libc calls on descriptors, buffers and
19286        // a child process this test owns.
19287        let flags = libc::O_RDWR | libc::O_NOCTTY | libc::O_CLOEXEC;
19288        let master = unsafe { libc::posix_openpt(flags) };
19289        assert!(
19290            master >= 0,
19291            "posix_openpt: {}",
19292            std::io::Error::last_os_error()
19293        );
19294        assert_eq!(unsafe { libc::grantpt(master) }, 0);
19295        assert_eq!(unsafe { libc::unlockpt(master) }, 0);
19296        let mut slave_path = [0 as libc::c_char; 64];
19297        let path_len = slave_path.len();
19298        assert_eq!(
19299            unsafe { libc::ptsname_r(master, slave_path.as_mut_ptr(), path_len) },
19300            0
19301        );
19302        // `ready` carries the slave's descriptor number to this process;
19303        // nothing writes `release`, so this process's exit ends the child.
19304        let (mut ready, mut release) = ([0; 2], [0; 2]);
19305        assert_eq!(
19306            unsafe { libc::pipe2(ready.as_mut_ptr(), libc::O_CLOEXEC) },
19307            0
19308        );
19309        assert_eq!(
19310            unsafe { libc::pipe2(release.as_mut_ptr(), libc::O_CLOEXEC) },
19311            0
19312        );
19313        let pid = unsafe { libc::fork() };
19314        assert!(pid >= 0, "fork: {}", std::io::Error::last_os_error());
19315        if pid == 0 {
19316            // The child makes only async-signal-safe calls and allocates
19317            // nothing, so forking from a test harness thread is safe.
19318            unsafe {
19319                libc::alarm(60);
19320                libc::signal(libc::SIGHUP, libc::SIG_IGN);
19321                libc::close(master);
19322                libc::close(ready[0]);
19323                libc::close(release[1]);
19324                if libc::setsid() < 0 {
19325                    libc::_exit(2);
19326                }
19327                let slave = libc::open(slave_path.as_ptr(), libc::O_RDWR);
19328                if slave < 0 {
19329                    libc::_exit(3);
19330                }
19331                let bytes = slave.to_ne_bytes();
19332                if libc::write(ready[1], bytes.as_ptr().cast(), bytes.len()) != 4 {
19333                    libc::_exit(4);
19334                }
19335                let mut byte = [0_u8; 1];
19336                libc::read(release[0], byte.as_mut_ptr().cast(), 1);
19337                libc::_exit(0);
19338            }
19339        }
19340        let child = Child(pid);
19341        unsafe {
19342            libc::close(ready[1]);
19343            libc::close(release[0]);
19344        }
19345        let mut bytes = [0_u8; 4];
19346        let got = unsafe { libc::read(ready[0], bytes.as_mut_ptr().cast(), bytes.len()) };
19347        assert_eq!(got, 4, "the child did not open the slave");
19348        let slave = i32::from_ne_bytes(bytes);
19349        // The descriptor's stat, read before the hangup, as `handle_openat`
19350        // reads it before it classifies the open.
19351        let link = std::ffi::CString::new(format!("/proc/{pid}/fd/{slave}")).unwrap();
19352        let mut host_stat: libc::stat = unsafe { std::mem::zeroed() };
19353        assert_eq!(unsafe { libc::stat(link.as_ptr(), &mut host_stat) }, 0);
19354        let terminal = (
19355            libc::major(host_stat.st_rdev) as i32,
19356            libc::minor(host_stat.st_rdev) as i32,
19357        );
19358        let process = procfs::process::Process::new(pid).unwrap();
19359        let before = process.stat().unwrap();
19360        assert_eq!(
19361            (before.session, before.tty_nr()),
19362            (pid, terminal),
19363            "the child's open did not acquire the terminal"
19364        );
19365        assert!(opened_controlling_terminal(pid, slave, Some(&host_stat)));
19366        // Hang the slave up: Linux clears the session's controlling terminal.
19367        assert_eq!(unsafe { libc::close(master) }, 0);
19368        let deadline = Instant::now() + Duration::from_secs(10);
19369        while process.stat().unwrap().tty_nr() != (0, 0) {
19370            assert!(
19371                Instant::now() < deadline,
19372                "the hangup did not clear the controlling terminal"
19373            );
19374            std::thread::sleep(Duration::from_millis(10));
19375        }
19376        assert_eq!(process.stat().unwrap().session, pid);
19377        assert!(
19378            opened_controlling_terminal(pid, slave, Some(&host_stat)),
19379            "a hangup after the descriptor's stat hid the acquisition"
19380        );
19381        assert!(
19382            opened_controlling_terminal(pid, slave, None),
19383            "a hangup before the descriptor's stat hid the acquisition"
19384        );
19385        drop(child);
19386    }
19387
19388    /// The ceiling is inclusive. A guest that reads the advertised
19389    /// `pipe-max-size` and asks for exactly that must be allowed to have it;
19390    /// refusing at the boundary would advertise a size that cannot be set.
19391    /// The pin is two of the host's pages, which Linux applies exactly at any
19392    /// page size; on x86-64 that is the 8 KiB it always was.
19393    #[test]
19394    fn the_pipe_capacity_is_two_host_pages() {
19395        // SAFETY: sysconf has no preconditions.
19396        let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
19397        assert_eq!(i64::from(deterministic_pipe_capacity_bytes()), 2 * page);
19398        #[cfg(target_arch = "x86_64")]
19399        assert_eq!(deterministic_pipe_capacity_bytes(), 8192);
19400    }
19401
19402    #[test]
19403    fn the_pipe_ceiling_admits_exactly_the_pinned_capacity() {
19404        assert!(!pipe_capacity_request_exceeds_ceiling(
19405            deterministic_pipe_capacity_bytes()
19406        ));
19407        assert!(!pipe_capacity_request_exceeds_ceiling(
19408            deterministic_pipe_capacity_bytes() - 1
19409        ));
19410        assert!(pipe_capacity_request_exceeds_ceiling(
19411            deterministic_pipe_capacity_bytes() + 1
19412        ));
19413    }
19414
19415    /// A host pipe's capacity above the ceiling reads as the ceiling, so a
19416    /// default 16-page pipe and one the host shrank to two pages under
19417    /// `pipe-user-pages-soft` read the same. A capacity at or below the ceiling
19418    /// (Hermit's pin, or a guest's own shrink to one page) reads as it is.
19419    #[test]
19420    fn a_host_pipe_capacity_reads_no_larger_than_the_ceiling() {
19421        let ceiling = i64::from(deterministic_pipe_capacity_bytes());
19422        let page = ceiling / 2;
19423        assert_eq!(super::reported_pipe_capacity(16 * page), ceiling);
19424        assert_eq!(super::reported_pipe_capacity(1 << 30), ceiling);
19425        assert_eq!(super::reported_pipe_capacity(ceiling), ceiling);
19426        assert_eq!(super::reported_pipe_capacity(page), page);
19427    }
19428
19429    /// Two pages is load-independent only where `pipe-max-size` is at least
19430    /// two pages. On a host whose `pipe-max-size` is one page, a new pipe has
19431    /// one page below the quota and two above it, so the decision rests on the
19432    /// setting alone: both allocation states are refused alike. An unreadable
19433    /// setting is decided by the probe alone, which runs only then.
19434    #[test]
19435    fn two_pages_is_trusted_only_with_a_two_page_pipe_max_size() {
19436        let ceiling = i64::from(deterministic_pipe_capacity_bytes());
19437        let page = ceiling / 2;
19438        let unused = || panic!("the probe runs only for an unreadable setting");
19439        assert!(super::host_pipe_floor_is_two_pages(Some(ceiling), unused));
19440        assert!(super::host_pipe_floor_is_two_pages(Some(1 << 20), unused));
19441        assert!(!super::host_pipe_floor_is_two_pages(Some(page), unused));
19442        assert!(super::host_pipe_floor_is_two_pages(None, || true));
19443        assert!(!super::host_pipe_floor_is_two_pages(None, || false));
19444        // This host's probe runs; its answer depends on the host's quota and
19445        // capabilities, so it is not asserted.
19446        let _ = super::probe_two_page_pipe_max_size();
19447    }
19448
19449    /// Shrinking stays legal. `tests/c/pipe_capacity.c`
19450    /// shrinks to one page and requires the value to round-trip, so a blanket
19451    /// clamp to the pinned capacity would break a guest-visible contract this
19452    /// repository already locked.
19453    #[test]
19454    fn shrinking_is_never_refused_by_the_ceiling() {
19455        for requested in [1, 4096, deterministic_pipe_capacity_bytes() / 2] {
19456            assert!(
19457                !pipe_capacity_request_exceeds_ceiling(requested),
19458                "shrink to {requested} must remain permitted"
19459            );
19460        }
19461    }
19462
19463    /// The host's own ceiling is the value this change exists to stop
19464    /// consulting: 1048576 on a default host, 65536 on a hardened one. Both are
19465    /// refused now, so the guest-visible answer no longer depends on which host
19466    /// it is.
19467    #[test]
19468    fn host_ceilings_are_refused_identically_on_any_host() {
19469        let ceiling = deterministic_pipe_capacity_bytes();
19470        // Above the two-page ceiling at any page size: twice it, eight times
19471        // it, and the default 1 MiB.
19472        for host_ceiling in [2 * ceiling, 8 * ceiling, 1 << 20] {
19473            assert!(
19474                pipe_capacity_request_exceeds_ceiling(host_ceiling),
19475                "{host_ceiling} must be refused regardless of the host sysctl"
19476            );
19477        }
19478    }
19479
19480    use super::Errno;
19481    use super::TimerSlackBinding;
19482    use super::UNIX_AUTOBIND_NAME_LEN;
19483    use super::abstract_unix_name;
19484    use super::canonicalize_tcp_info;
19485    use super::classify_timer_slack_binding;
19486    use super::host_network_abstract_bind_notice;
19487    use super::is_inherited_container_output;
19488    use super::parse_timer_slack_write;
19489    use super::pipe_capacity_failure;
19490    use super::random_device_lseek_result;
19491    use super::unix_autobind_address;
19492    use super::unix_autobind_addrlen;
19493    use super::vectored_offset;
19494    use crate::resources::Device;
19495    use crate::resources::ResourceID;
19496
19497    /// This is an assumption we're making about flags.  Probably these flags can never be
19498    /// changed, but let's check just in case.
19499    #[test]
19500    fn linux_flags_assumptions() {
19501        assert_eq!(libc::SOCK_NONBLOCK, OFlag::O_NONBLOCK.bits());
19502        assert_eq!(libc::SOCK_CLOEXEC, OFlag::O_CLOEXEC.bits());
19503    }
19504
19505    #[test]
19506    fn pipe_capacity_failure_classifies_the_errno() {
19507        let created = [17, 18];
19508
19509        // The only success shape: Linux applied EXACTLY the capacity we asked for.
19510        assert_eq!(
19511            pipe_capacity_failure(created, Ok(i64::from(deterministic_pipe_capacity_bytes()))),
19512            None
19513        );
19514
19515        // Successful-but-wrong capacity is a pipe whose size we did not choose. That is not a
19516        // kernel errno, so it is reported as EIO rather than dressed up as one.
19517        let mismatch = pipe_capacity_failure(
19518            created,
19519            Ok(i64::from(deterministic_pipe_capacity_bytes()) * 2),
19520        )
19521        .expect("a capacity Linux rounded away from the pin must not read as success");
19522        assert_eq!(mismatch.created_fds, created);
19523        assert_eq!(mismatch.error, Errno::EIO);
19524
19525        // A real kernel errno is preserved rather than rewritten.
19526        let denied = pipe_capacity_failure(created, Err(Errno::EPERM))
19527            .expect("a kernel refusal must not read as success");
19528        assert_eq!(denied.created_fds, created);
19529        assert_eq!(denied.error, Errno::EPERM);
19530
19531        // The descriptors are carried through every failure shape, because they are what the
19532        // caller has to close; losing them here is how they would leak.
19533        assert_eq!(denied.created_fds, created);
19534    }
19535
19536    #[test]
19537    fn pipe_capacity_failure_closes_both_created_descriptors() {
19538        if isolated_child(
19539            "syscalls::files::test::pipe_capacity_failure_closes_both_created_descriptors",
19540        ) {
19541            return;
19542        }
19543        let mut created = [-1; 2];
19544        assert_eq!(
19545            unsafe { libc::pipe2(created.as_mut_ptr(), libc::O_CLOEXEC) },
19546            0
19547        );
19548
19549        let pin_result = unsafe { libc::fcntl(created[0], libc::F_SETPIPE_SZ, -1) };
19550        assert_eq!(pin_result, -1);
19551        let pin_error = Errno::last();
19552        // Pin the errno, not just the failure. Without this the test asserts only that
19553        // `fcntl` returned -1, so it would still pass if the call failed for a reason we
19554        // did not engineer -- an invalid `created[0]` fails EBADF, every assertion below
19555        // still holds, and the capacity-pin path is never exercised at all.
19556        //
19557        // EINVAL is structural here, not a property of this host. `fcntl`'s argument is an
19558        // `unsigned long`, so -1 arrives as ULONG_MAX; `round_pipe_size` returns 0 for any
19559        // size above 2^31, and `pipe_set_size` maps that 0 to -EINVAL BEFORE it consults
19560        // `CAP_SYS_RESOURCE`. So the result does not depend on privilege or on
19561        // `/proc/sys/fs/pipe-max-size`.
19562        assert_eq!(pin_error, Errno::EINVAL);
19563
19564        let failure = pipe_capacity_failure(created, Err(pin_error))
19565            .expect("the forced capacity-pin failure must enter the cleanup path");
19566        for close in failure.close_syscalls() {
19567            assert_eq!(unsafe { libc::close(close.fd()) }, 0);
19568        }
19569
19570        for fd in created {
19571            assert_eq!(unsafe { libc::fcntl(fd, libc::F_GETFD) }, -1);
19572            assert_eq!(Errno::last(), Errno::EBADF);
19573        }
19574    }
19575
19576    #[test]
19577    fn rendered_procfs_seek_follows_each_llseek() {
19578        use super::RenderedSeek::*;
19579        // generic_file_llseek (auxv, cmdline): SEEK_DATA and SEEK_HOLE at any
19580        // offset of a file of size 0, a negative one included, are ENXIO.
19581        for whence in [Whence::SEEK_DATA, Whence::SEEK_HOLE] {
19582            for offset in [i64::MIN, -1, 0, 1, i64::MAX] {
19583                assert_eq!(
19584                    super::rendered_procfs_seek(Generic, whence, offset, 0),
19585                    Err(Errno::ENXIO)
19586                );
19587            }
19588            assert_eq!(
19589                super::rendered_procfs_seek(Default, whence, 0, 0),
19590                Err(Errno::ENXIO)
19591            );
19592            assert_eq!(
19593                super::rendered_procfs_seek(SeqFile, whence, -1, 0),
19594                Err(Errno::EINVAL)
19595            );
19596        }
19597        // default_llseek compares signed: a negative SEEK_DATA offset stays
19598        // negative (EINVAL); a negative SEEK_HOLE offset becomes the size, 0.
19599        assert_eq!(
19600            super::rendered_procfs_seek(Default, Whence::SEEK_DATA, -1, 0),
19601            Err(Errno::EINVAL)
19602        );
19603        assert_eq!(
19604            super::rendered_procfs_seek(Default, Whence::SEEK_HOLE, -1, 5),
19605            Ok(0)
19606        );
19607        // vfs_setpos: s_maxbytes (MAX_NON_LFS) bounds generic_file_llseek only.
19608        let max = (1_i64 << 31) - 1;
19609        assert_eq!(
19610            super::rendered_procfs_seek(Generic, Whence::SEEK_SET, max, 0),
19611            Ok(max as usize)
19612        );
19613        assert_eq!(
19614            super::rendered_procfs_seek(Generic, Whence::SEEK_SET, max + 1, 0),
19615            Err(Errno::EINVAL)
19616        );
19617        assert_eq!(
19618            super::rendered_procfs_seek(Generic, Whence::SEEK_CUR, 1, max as usize),
19619            Err(Errno::EINVAL)
19620        );
19621        assert_eq!(
19622            super::rendered_procfs_seek(Default, Whence::SEEK_SET, max + 1, 0),
19623            Ok(max as usize + 1)
19624        );
19625        assert_eq!(
19626            super::rendered_procfs_seek(Generic, Whence::SEEK_SET, -1, 0),
19627            Err(Errno::EINVAL)
19628        );
19629        assert_eq!(
19630            super::rendered_procfs_seek(Generic, Whence::SEEK_END, 3, 0),
19631            Ok(3)
19632        );
19633        assert_eq!(
19634            super::rendered_procfs_seek(SeqFile, Whence::SEEK_END, 0, 0),
19635            Err(Errno::EINVAL)
19636        );
19637        assert_eq!(
19638            super::rendered_procfs_seek(Generic, Whence::from_raw(99), 0, 0),
19639            Err(Errno::EINVAL)
19640        );
19641        // A sum past LLONG_MAX wraps negative in `loff_t`: EINVAL for a
19642        // seq_file and a sysctl file alike, never EOVERFLOW. LLONG_MAX itself
19643        // is a position.
19644        let end = i64::MAX as usize;
19645        assert_eq!(
19646            super::rendered_procfs_seek(SeqFile, Whence::SEEK_SET, i64::MAX, 4),
19647            Ok(end)
19648        );
19649        for seek in [SeqFile, Default] {
19650            assert_eq!(
19651                super::rendered_procfs_seek(seek, Whence::SEEK_CUR, i64::MAX, 4),
19652                Err(Errno::EINVAL)
19653            );
19654            assert_eq!(
19655                super::rendered_procfs_seek(seek, Whence::SEEK_CUR, 1, end),
19656                Err(Errno::EINVAL)
19657            );
19658            assert_eq!(
19659                super::rendered_procfs_seek(seek, Whence::SEEK_CUR, 0, end),
19660                Ok(end)
19661            );
19662        }
19663    }
19664
19665    /// The reason Detcore gives where it refuses a read or a seek of a
19666    /// per-process file whose record sequence it cannot know here
19667    /// (`Detcore::refuse_unknown_record_sequence`) names the descriptor, the
19668    /// file where Detcore knows it, and why.
19669    #[test]
19670    fn an_unknown_record_sequence_is_refused_by_name() {
19671        let reason =
19672            super::unknown_record_sequence_reason(5, Some(std::path::Path::new("/proc/7/statm")));
19673        for part in [
19674            "descriptor 5, the per-process file /proc/7/statm,",
19675            "over SCM_RIGHTS",
19676            "the one open file every process holding it shares",
19677            "neither reads nor seeks it",
19678        ] {
19679            assert!(reason.contains(part), "{part}: {reason}");
19680        }
19681        assert!(
19682            super::unknown_record_sequence_reason(5, None)
19683                .contains("descriptor 5, a per-process /proc file,")
19684        );
19685        // The refusal ends the run wherever that ends it: not where the
19686        // fail-closed exit would end only a guest process from inside its
19687        // tool (SaBRe's capabilities), where the call gets EOPNOTSUPP instead.
19688        let config = |backend, shutdown| crate::Config {
19689            backend,
19690            shutdown_on_unsupported_syscall: shutdown,
19691            ..crate::Config::default()
19692        };
19693        assert!(super::refusal_would_strand_the_run(&config(
19694            reverie::BackendCapabilities::SABRE,
19695            true
19696        )));
19697        for (backend, shutdown) in [
19698            (reverie::BackendCapabilities::SABRE, false),
19699            (reverie::BackendCapabilities::DBT, false),
19700            (reverie::BackendCapabilities::PTRACE, true),
19701            (reverie::BackendCapabilities::KVM, true),
19702        ] {
19703            assert!(!super::refusal_would_strand_the_run(&config(
19704                backend, shutdown
19705            )));
19706        }
19707    }
19708
19709    #[test]
19710    fn random_device_lseek_matches_linux_noop_llseek() {
19711        for whence in [
19712            Whence::SEEK_SET,
19713            Whence::SEEK_CUR,
19714            Whence::SEEK_END,
19715            Whence::SEEK_DATA,
19716            Whence::SEEK_HOLE,
19717        ] {
19718            for status_flags in [
19719                OFlag::empty().bits(),
19720                OFlag::O_WRONLY.bits(),
19721                OFlag::O_RDWR.bits(),
19722            ] {
19723                assert_eq!(random_device_lseek_result(status_flags, whence), Ok(0));
19724            }
19725            assert_eq!(
19726                random_device_lseek_result(OFlag::O_PATH.bits(), whence),
19727                Err(Errno::EBADF)
19728            );
19729        }
19730        assert_eq!(
19731            random_device_lseek_result(OFlag::empty().bits(), Whence::from_raw(99)),
19732            Err(Errno::EINVAL)
19733        );
19734        assert_eq!(
19735            random_device_lseek_result(OFlag::O_PATH.bits(), Whence::from_raw(99)),
19736            Err(Errno::EBADF)
19737        );
19738    }
19739
19740    #[test]
19741    fn timer_slack_write_parser_matches_decimal_procfs_contract() {
19742        assert_eq!(parse_timer_slack_write(b"0"), Ok(0));
19743        assert_eq!(parse_timer_slack_write(b"+123\n"), Ok(123));
19744        assert_eq!(parse_timer_slack_write(b"456\0ignored"), Ok(456));
19745        assert_eq!(
19746            parse_timer_slack_write(u64::MAX.to_string().as_bytes()),
19747            Ok(u64::MAX)
19748        );
19749
19750        for invalid in [b"".as_slice(), b"+", b"-1", b" 1", b"1 ", b"1\n2", b"0x10"] {
19751            assert_eq!(parse_timer_slack_write(invalid), Err(Errno::EINVAL));
19752        }
19753        assert_eq!(
19754            parse_timer_slack_write(b"18446744073709551616"),
19755            Err(Errno::ERANGE)
19756        );
19757    }
19758
19759    #[test]
19760    fn timer_slack_vectored_offset_preserves_minus_one_sentinel() {
19761        assert_eq!(vectored_offset(u64::MAX, u64::MAX), -1);
19762        assert_eq!(vectored_offset(0, 0), 0);
19763        assert_eq!(vectored_offset(7, 0), 7);
19764    }
19765
19766    #[test]
19767    fn timer_slack_binding_rejects_exit_reuse_and_other_tasks() {
19768        let binding = TimerSlackBinding {
19769            target: 202,
19770            device: 11,
19771            inode: 22,
19772        };
19773        assert_eq!(
19774            classify_timer_slack_binding(binding, Some((11, 22)), 202),
19775            Ok(())
19776        );
19777        assert_eq!(
19778            classify_timer_slack_binding(binding, Some((11, 22)), 303),
19779            Err(Errno::EPERM)
19780        );
19781        assert_eq!(
19782            classify_timer_slack_binding(binding, None, 202),
19783            Err(Errno::ESRCH)
19784        );
19785        assert_eq!(
19786            classify_timer_slack_binding(binding, Some((11, 23)), 202),
19787            Err(Errno::ESRCH),
19788            "a recycled numeric TID must not revive an old proc inode"
19789        );
19790    }
19791
19792    #[test]
19793    fn only_inherited_container_output_is_nonseekable() {
19794        assert!(is_inherited_container_output(Some(ResourceID::Device(
19795            Device::ContainerStdout
19796        ))));
19797        assert!(is_inherited_container_output(Some(ResourceID::Device(
19798            Device::ContainerStderr
19799        ))));
19800        assert!(!is_inherited_container_output(Some(ResourceID::Device(
19801            Device::ContainerStdin
19802        ))));
19803        assert!(!is_inherited_container_output(None));
19804    }
19805
19806    #[test]
19807    fn host_network_notice_names_only_abstract_unix_addresses() {
19808        let path = std::mem::offset_of!(libc::sockaddr_un, sun_path);
19809        let family = (libc::AF_UNIX as u16).to_ne_bytes();
19810        let address = |sun_path: &[u8]| [&family[..], sun_path].concat();
19811        assert_eq!(
19812            abstract_unix_name(&address(b"\0myapp:3")),
19813            Some(&b"myapp:3"[..])
19814        );
19815        // A leading NUL alone is an abstract name of length zero.
19816        assert_eq!(abstract_unix_name(&address(b"\0")), Some(&b""[..]));
19817        // A path name, and a bare family (an autobind, which Hermit names
19818        // itself), are not.
19819        assert_eq!(abstract_unix_name(&address(b"/tmp/socket")), None);
19820        assert_eq!(abstract_unix_name(&family), None);
19821        assert_eq!(address(b"").len(), path);
19822
19823        let bound = host_network_abstract_bind_notice(b"x\0y\"z", false);
19824        assert!(
19825            bound.starts_with("hermit: NOTICE: the guest bound "),
19826            "{bound}"
19827        );
19828        assert!(
19829            bound.contains("\"@x\\x00y\\\"z\" under --network=host."),
19830            "{bound}"
19831        );
19832        assert!(bound.contains("--network=local"), "{bound}");
19833        let in_use = host_network_abstract_bind_notice(b"myapp:3", true);
19834        assert!(
19835            in_use.contains("\"@myapp:3\" under --network=host failed with EADDRINUSE"),
19836            "{in_use}"
19837        );
19838    }
19839
19840    #[test]
19841    fn unix_autobind_address_matches_linux_shape() {
19842        let address = unix_autobind_address(0x2af);
19843        assert_eq!(address.sun_family, libc::AF_UNIX as libc::sa_family_t);
19844        assert_eq!(address.sun_path[0], 0);
19845        let name = address.sun_path[1..UNIX_AUTOBIND_NAME_LEN]
19846            .iter()
19847            .map(|byte| *byte as u8)
19848            .collect::<Vec<_>>();
19849        assert_eq!(name, b"002af");
19850        assert_eq!(
19851            unix_autobind_addrlen() as usize,
19852            std::mem::offset_of!(libc::sockaddr_un, sun_path) + UNIX_AUTOBIND_NAME_LEN
19853        );
19854    }
19855
19856    #[test]
19857    fn tcp_info_retains_only_logical_connection_header() {
19858        let mut info = [0xff; 16];
19859        canonicalize_tcp_info(&mut info);
19860
19861        for (offset, byte) in info.into_iter().enumerate() {
19862            let expected = if matches!(offset, 0 | 1 | 5 | 6) {
19863                0xff
19864            } else {
19865                0
19866            };
19867            assert_eq!(byte, expected, "unexpected byte at offset {offset}");
19868        }
19869
19870        for len in 0..8 {
19871            canonicalize_tcp_info(&mut [0xff; 8][..len]);
19872        }
19873    }
19874
19875    /// Records with the names `names`, as `serve_next_batch` encodes them
19876    /// from position 0, and the length of each name.
19877    fn directory_records(format: super::DirentFormat, names: &[&[u8]]) -> (Vec<u8>, Vec<usize>) {
19878        let mut records = Vec::new();
19879        for (index, name) in names.iter().enumerate() {
19880            let entry = super::DirEntry {
19881                name: name.to_vec(),
19882                ino: 0x1111_1111_1111_1111,
19883                off: 0,
19884                ty: 4,
19885            };
19886            format.encode(&entry, entry.ino, index as i64 + 1, &mut records);
19887        }
19888        (records, names.iter().map(|name| name.len()).collect())
19889    }
19890
19891    /// `records` as Linux leaves them in a buffer that held `Pages::FILL`:
19892    /// the padding after each name is not written.
19893    fn as_linux_writes(format: super::DirentFormat, records: &[u8], names: &[usize]) -> Vec<u8> {
19894        let mut bytes = records.to_vec();
19895        let mut at = 0;
19896        for &name_len in names {
19897            let reclen = format.record_len(name_len);
19898            let padding = at + format.name_offset() + name_len + 1;
19899            let end = match format {
19900                super::DirentFormat::Dirent64 => at + reclen,
19901                super::DirentFormat::Legacy => at + reclen - 1,
19902            };
19903            bytes[padding..end].fill(Pages::FILL);
19904            at += reclen;
19905        }
19906        bytes
19907    }
19908
19909    const FORMATS: [super::DirentFormat; 2] =
19910        [super::DirentFormat::Dirent64, super::DirentFormat::Legacy];
19911
19912    const TEN: [&[u8]; 10] = [b"a", b"b", b"c", b"d", b"e", b"f", b"g", b"h", b"i", b"j"];
19913
19914    /// A buffer starting 16 bytes before a writable page, in a page the guest
19915    /// cannot write: Linux's first store, to the first record's `d_off`,
19916    /// fails, so the call fails and the writable page is left as it was.
19917    #[test]
19918    fn a_first_record_that_cannot_be_written_leaves_the_next_page_untouched() {
19919        for protection in [libc::PROT_NONE, libc::PROT_READ] {
19920            for format in FORMATS {
19921                let pages = Pages::new(&[protection, libc::PROT_READ | libc::PROT_WRITE]);
19922                let (records, names) = directory_records(format, &TEN);
19923                let copied = super::copy_records(
19924                    &mut LocalMemory::new(),
19925                    pages.address(PAGE - 16),
19926                    format,
19927                    &records,
19928                    &names,
19929                    0,
19930                );
19931                assert_eq!(copied, (0, Err(Errno::EFAULT)), "{format:?} {protection}");
19932                assert!(
19933                    pages.contents().iter().all(|&byte| byte == Pages::FILL),
19934                    "{format:?} after a page with protection {protection}: bytes changed"
19935                );
19936            }
19937        }
19938    }
19939
19940    /// Guest memory whose every vectored copy fails with one error that is
19941    /// not a fault, as when the guest's thread is gone or the backend fails.
19942    struct FailingMemory(Errno);
19943
19944    impl reverie::syscalls::MemoryAccess for FailingMemory {
19945        fn read_vectored(
19946            &self,
19947            _: &[std::io::IoSlice],
19948            _: &mut [std::io::IoSliceMut],
19949        ) -> Result<usize, Errno> {
19950            Err(self.0)
19951        }
19952
19953        fn write_vectored(
19954            &mut self,
19955            _: &[std::io::IoSlice],
19956            _: &mut [std::io::IoSliceMut],
19957        ) -> Result<usize, Errno> {
19958            Err(self.0)
19959        }
19960    }
19961
19962    /// Only a fault ends a copy of directory records short. Any other error
19963    /// of the copy is the call's error, as it was before directory streams,
19964    /// and not `EFAULT`: reading the records back, copying them out (through
19965    /// a buffer whose first store crosses a page, and through one where none
19966    /// does), and reading the prefix the `getdents64` observer hashes.
19967    #[test]
19968    fn a_copy_error_other_than_a_fault_is_returned() {
19969        let buf = |offset| reverie::syscalls::AddrMut::<u8>::from_raw(16 * PAGE + offset).unwrap();
19970        for errno in [Errno::ESRCH, Errno::EIO] {
19971            let mut bytes = [0; 32];
19972            assert_eq!(
19973                super::read_guest_prefix(&FailingMemory(errno), buf(0), &mut bytes),
19974                Err(errno)
19975            );
19976            for format in FORMATS {
19977                let (records, names) = directory_records(format, &TEN);
19978                for offset in [0, PAGE - 4] {
19979                    let copied = super::copy_records(
19980                        &mut FailingMemory(errno),
19981                        buf(offset),
19982                        format,
19983                        &records,
19984                        &names,
19985                        0,
19986                    );
19987                    assert_eq!(copied, (0, Err(errno)), "{format:?} at {offset}");
19988                }
19989                let read =
19990                    super::read_records(&FailingMemory(errno), buf(0), records.len(), format);
19991                assert_eq!(read.err(), Some(errno), "{format:?}");
19992            }
19993        }
19994    }
19995
19996    /// A vectored copy that fails with `EFAULT`, which `process_vm_readv` and
19997    /// the KVM backend both return when the first byte is out of the guest's
19998    /// reach, copied nothing rather than failing: the caller decides what an
19999    /// empty copy means, as it does for a short one.
20000    #[test]
20001    fn a_fault_of_the_whole_copy_copies_nothing() {
20002        let buf = reverie::syscalls::AddrMut::<u8>::from_raw(16 * PAGE).unwrap();
20003        let mut bytes = [0; 32];
20004        assert_eq!(
20005            super::read_guest_prefix(&FailingMemory(Errno::EFAULT), buf, &mut bytes),
20006            Ok(0)
20007        );
20008        assert_eq!(
20009            super::write_guest_pieces(&mut FailingMemory(Errno::EFAULT), buf, &[(0, &bytes)]),
20010            Ok(0)
20011        );
20012    }
20013
20014    /// A buffer starting 8 bytes before a writable page, in a page the guest
20015    /// cannot write. Linux's first store, the first record's position into
20016    /// its own `d_off`, lands in the writable page; its next, `d_ino`, fails.
20017    /// The call fails and leaves the position there.
20018    #[test]
20019    fn a_first_record_leaves_its_position_in_its_d_off() {
20020        for format in FORMATS {
20021            let pages = Pages::new(&[libc::PROT_READ, libc::PROT_READ | libc::PROT_WRITE]);
20022            let (records, names) = directory_records(format, &TEN);
20023            let copied = super::copy_records(
20024                &mut LocalMemory::new(),
20025                pages.address(PAGE - 8),
20026                format,
20027                &records,
20028                &names,
20029                7,
20030            );
20031            assert_eq!(copied, (0, Err(Errno::EFAULT)), "{format:?}");
20032            let contents = pages.contents();
20033            assert_eq!(&contents[PAGE..PAGE + 8], &7i64.to_ne_bytes(), "{format:?}");
20034            assert!(
20035                contents[..PAGE]
20036                    .iter()
20037                    .chain(&contents[PAGE + 8..])
20038                    .all(|&byte| byte == Pages::FILL),
20039                "{format:?}: bytes other than the first d_off changed"
20040            );
20041        }
20042    }
20043
20044    /// A buffer starting 12 bytes before a writable page, in a page the guest
20045    /// can read but not write. The first record's `d_off` crosses into the
20046    /// writable page, and the CPU stores it at once, so Linux writes none of
20047    /// it: the part written in the writable page is put back.
20048    #[test]
20049    fn a_store_crossing_out_of_an_unwritable_page_writes_nothing() {
20050        for format in FORMATS {
20051            let pages = Pages::new(&[libc::PROT_READ, libc::PROT_READ | libc::PROT_WRITE]);
20052            let (records, names) = directory_records(format, &TEN);
20053            let copied = super::copy_records(
20054                &mut LocalMemory::new(),
20055                pages.address(PAGE - 12),
20056                format,
20057                &records,
20058                &names,
20059                0,
20060            );
20061            assert_eq!(copied, (0, Err(Errno::EFAULT)), "{format:?}");
20062            assert!(
20063                pages.contents().iter().all(|&byte| byte == Pages::FILL),
20064                "{format:?}: bytes changed"
20065            );
20066        }
20067    }
20068
20069    /// The same store between two pages one of which the guest cannot write,
20070    /// for each other pair Detcore can leave as Linux does: one page it can
20071    /// read, or a write-only page before one it cannot touch. Whichever part
20072    /// is written first must be one Detcore can put back.
20073    #[test]
20074    fn a_store_crossing_pages_is_put_back_from_the_page_that_can_be_read() {
20075        let (none, read, write) = (libc::PROT_NONE, libc::PROT_READ, libc::PROT_WRITE);
20076        let both = read | write;
20077        for protections in [
20078            [read, write],
20079            [write, read],
20080            [write, none],
20081            [both, none],
20082            [both, read],
20083            [none, both],
20084        ] {
20085            for format in FORMATS {
20086                let pages = Pages::new(&protections);
20087                let (records, names) = directory_records(format, &TEN);
20088                let copied = super::copy_records(
20089                    &mut LocalMemory::new(),
20090                    pages.address(PAGE - 12),
20091                    format,
20092                    &records,
20093                    &names,
20094                    0,
20095                );
20096                assert_eq!(
20097                    copied,
20098                    (0, Err(Errno::EFAULT)),
20099                    "{protections:?} {format:?}"
20100                );
20101                assert!(
20102                    pages.contents().iter().all(|&byte| byte == Pages::FILL),
20103                    "{protections:?} {format:?}: bytes changed"
20104                );
20105            }
20106        }
20107    }
20108
20109    /// Records of 24 bytes starting 100 bytes before a page the guest cannot
20110    /// write. Four fit. The fifth's `d_ino` crosses into that page, so Linux
20111    /// writes none of it: four records are returned, the fifth's position
20112    /// (the fourth's `d_off`) is written, and the four bytes before the page
20113    /// are left as they were.
20114    #[test]
20115    fn a_record_crossing_into_an_inaccessible_page_leaves_nothing_before_it() {
20116        for format in FORMATS {
20117            let pages = Pages::new(&[libc::PROT_READ | libc::PROT_WRITE, libc::PROT_NONE]);
20118            let (records, names) = directory_records(format, &TEN);
20119            let copied = super::copy_records(
20120                &mut LocalMemory::new(),
20121                pages.address(PAGE - 100),
20122                format,
20123                &records,
20124                &names,
20125                0,
20126            );
20127            assert_eq!(copied, (4, Ok(96)), "{format:?}");
20128            let contents = pages.contents();
20129            assert_eq!(
20130                &contents[PAGE - 100..PAGE - 4],
20131                &as_linux_writes(format, &records[..96], &names[..4])[..],
20132                "{format:?}"
20133            );
20134            assert!(
20135                contents[..PAGE - 100]
20136                    .iter()
20137                    .chain(&contents[PAGE - 4..])
20138                    .all(|&byte| byte == Pages::FILL),
20139                "{format:?}: bytes outside the records copied changed"
20140            );
20141        }
20142    }
20143
20144    /// A read of a procfs file whose buffer faults two bytes in: a
20145    /// `seq_file` returns the two bytes, a sysctl fails with `EFAULT`, as
20146    /// measured on Linux 7.1; a file that fits copies whole either way.
20147    #[test]
20148    fn a_torn_procfs_read_is_partial_for_a_seq_file_and_a_fault_for_a_sysctl() {
20149        let pages = Pages::new(&[libc::PROT_READ | libc::PROT_WRITE, libc::PROT_NONE]);
20150        let edge = || Some(pages.address(PAGE - 2));
20151        let read = |bytes: &[u8], sysctl| {
20152            super::copy_procfs_read(&mut LocalMemory::new(), edge(), bytes, sysctl)
20153                .map_err(|error| error.into_errno().unwrap())
20154        };
20155        assert_eq!(read(b"Linux version\n", false), Ok(2));
20156        assert_eq!(
20157            read(b"65530\n", true),
20158            Err(reverie::syscalls::Errno::EFAULT)
20159        );
20160        assert_eq!(read(b"0\n", true), Ok(2));
20161        assert_eq!(read(b"", true), Ok(0));
20162    }
20163
20164    /// A procfs read is EBADF unless the descriptor was opened for reading.
20165    #[test]
20166    fn only_a_descriptor_opened_for_reading_reads_procfs() {
20167        assert!(super::can_read(libc::O_RDONLY));
20168        assert!(super::can_read(libc::O_RDWR | libc::O_CLOEXEC));
20169        assert!(!super::can_read(libc::O_WRONLY));
20170        assert!(!super::can_read(libc::O_ACCMODE));
20171        assert!(!super::can_read(libc::O_PATH));
20172    }
20173
20174    /// `kiocb_set_rw_flags` for a procfs file, in Linux 7.1's order.
20175    #[test]
20176    fn procfs_vector_flags_are_refused_in_linux_order() {
20177        use reverie::syscalls::Errno;
20178        const NOSIGNAL: i32 = 0x100;
20179        const UNKNOWN: i32 = 0x200;
20180        for accepted in [
20181            0,
20182            libc::RWF_HIPRI,
20183            libc::RWF_DSYNC,
20184            libc::RWF_SYNC,
20185            libc::RWF_APPEND,
20186            libc::RWF_NOAPPEND,
20187            NOSIGNAL,
20188        ] {
20189            assert_eq!(super::validate_procfs_vector_flags(accepted, true), Ok(()));
20190        }
20191        for (flags, errno) in [
20192            (libc::RWF_NOWAIT, Errno::EOPNOTSUPP),
20193            (libc::RWF_ATOMIC, Errno::EOPNOTSUPP),
20194            (libc::RWF_DONTCACHE, Errno::EOPNOTSUPP),
20195            (UNKNOWN, Errno::EOPNOTSUPP),
20196            (libc::RWF_APPEND | libc::RWF_NOAPPEND, Errno::EINVAL),
20197            (libc::RWF_NOWAIT | UNKNOWN, Errno::EOPNOTSUPP),
20198            (
20199                libc::RWF_APPEND | libc::RWF_NOAPPEND | UNKNOWN,
20200                Errno::EOPNOTSUPP,
20201            ),
20202            (
20203                libc::RWF_ATOMIC | libc::RWF_APPEND | libc::RWF_NOAPPEND,
20204                Errno::EINVAL,
20205            ),
20206            (
20207                libc::RWF_NOWAIT | libc::RWF_APPEND | libc::RWF_NOAPPEND,
20208                Errno::EINVAL,
20209            ),
20210        ] {
20211            assert_eq!(
20212                super::validate_procfs_vector_flags(flags, true),
20213                Err(errno),
20214                "{flags:#x}"
20215            );
20216        }
20217        // A file without `read_iter` (`/proc/modules`, `/proc/swaps`) takes
20218        // `RWF_HIPRI` alone, and refuses even the conflicting pair with
20219        // EOPNOTSUPP.
20220        assert_eq!(
20221            super::validate_procfs_vector_flags(libc::RWF_HIPRI, false),
20222            Ok(())
20223        );
20224        for flags in [
20225            libc::RWF_DSYNC,
20226            libc::RWF_APPEND,
20227            NOSIGNAL,
20228            libc::RWF_APPEND | libc::RWF_NOAPPEND,
20229        ] {
20230            assert_eq!(
20231                super::validate_procfs_vector_flags(flags, false),
20232                Err(Errno::EOPNOTSUPP),
20233                "{flags:#x}"
20234            );
20235        }
20236    }
20237
20238    /// A buffer starting 4 bytes before a page the guest cannot write, in a
20239    /// page it can write but not read. Linux first writes the first record's
20240    /// `d_off`, 8 bytes in, which fails; it never writes the 4 bytes before
20241    /// the page.
20242    #[test]
20243    fn a_first_record_is_written_from_its_d_off() {
20244        for format in FORMATS {
20245            let pages = Pages::new(&[libc::PROT_WRITE, libc::PROT_NONE]);
20246            let (records, names) = directory_records(format, &TEN);
20247            let copied = super::copy_records(
20248                &mut LocalMemory::new(),
20249                pages.address(PAGE - 4),
20250                format,
20251                &records,
20252                &names,
20253                0,
20254            );
20255            assert_eq!(copied, (0, Err(Errno::EFAULT)), "{format:?}");
20256            assert!(
20257                pages.contents().iter().all(|&byte| byte == Pages::FILL),
20258                "{format:?}: bytes before the inaccessible page changed"
20259            );
20260        }
20261    }
20262
20263    /// Records across two writable pages and into a third the guest cannot
20264    /// write, starting 40 bytes before the second page, so the third starts
20265    /// 4136 bytes in. Record 172 starts at 4128: Linux stores its position
20266    /// into record 171's `d_off` and its `d_ino`, which ends at the third
20267    /// page, and then fails at its `d_reclen`. The call returns 172 records
20268    /// and leaves that `d_ino` written.
20269    #[test]
20270    fn a_record_not_copied_is_left_as_linux_leaves_it() {
20271        let names: Vec<[u8; 1]> = (0..200u8).map(|i| [b'a' + i % 26]).collect();
20272        let names: Vec<&[u8]> = names.iter().map(|name| &name[..]).collect();
20273        for format in FORMATS {
20274            let rw = libc::PROT_READ | libc::PROT_WRITE;
20275            let pages = Pages::new(&[rw, rw, libc::PROT_NONE]);
20276            let start = PAGE - 40;
20277            let (records, lens) = directory_records(format, &names);
20278            let copied = super::copy_records(
20279                &mut LocalMemory::new(),
20280                pages.address(start),
20281                format,
20282                &records,
20283                &lens,
20284                0,
20285            );
20286            assert_eq!(copied, (172, Ok(172 * 24)), "{format:?}");
20287            let contents = pages.contents();
20288            let mut expected = as_linux_writes(format, &records[..4128], &lens[..172]);
20289            expected.extend_from_slice(&records[4128..4136]);
20290            assert_eq!(&contents[start..start + 4136], &expected[..], "{format:?}");
20291            assert!(
20292                contents[..start]
20293                    .iter()
20294                    .chain(&contents[start + 4136..])
20295                    .all(|&byte| byte == Pages::FILL),
20296                "{format:?}: bytes outside what Linux writes changed"
20297            );
20298        }
20299    }
20300
20301    /// A second record with a 20-byte name, whose record ends past the end of
20302    /// what the guest can write. For `getdents64`, the NUL after the name is
20303    /// stored before the name and fails, so none of the name is written; for
20304    /// `getdents`, `d_type`, at the record's end, is stored before the name
20305    /// and fails. Either way the record's own `d_off` is not written: Linux
20306    /// stores it only with the next record or at the end of the call.
20307    #[test]
20308    fn a_name_is_stored_after_the_bytes_that_follow_it() {
20309        for format in FORMATS {
20310            let pages = Pages::new(&[libc::PROT_READ | libc::PROT_WRITE, libc::PROT_NONE]);
20311            let (records, names) = directory_records(format, &[b"a", b"abcdefghijklmnopqrst"]);
20312            let copied = super::copy_records(
20313                &mut LocalMemory::new(),
20314                pages.address(PAGE - 50),
20315                format,
20316                &records,
20317                &names,
20318                0,
20319            );
20320            assert_eq!(copied, (1, Ok(24)), "{format:?}");
20321            let contents = &pages.contents()[PAGE - 50..PAGE];
20322            let mut expected = as_linux_writes(format, &records[..24], &names[..1]);
20323            expected.extend_from_slice(&records[24..32]);
20324            expected.extend_from_slice(&[Pages::FILL; 8]);
20325            expected.extend_from_slice(&records[40..42]);
20326            match format {
20327                super::DirentFormat::Dirent64 => expected.push(records[42]),
20328                super::DirentFormat::Legacy => expected.push(Pages::FILL),
20329            }
20330            expected.resize(50, Pages::FILL);
20331            assert_eq!(contents, &expected[..], "{format:?}");
20332        }
20333    }
20334
20335    /// Records that all fit are all copied, apart from the padding after each
20336    /// name, which Linux does not write, and the bytes after them are left as
20337    /// they were.
20338    #[test]
20339    fn records_that_fit_are_copied_whole() {
20340        for format in FORMATS {
20341            let rw = libc::PROT_READ | libc::PROT_WRITE;
20342            let pages = Pages::new(&[rw, rw]);
20343            let (records, names) = directory_records(format, &TEN);
20344            let copied = super::copy_records(
20345                &mut LocalMemory::new(),
20346                pages.address(PAGE - 50),
20347                format,
20348                &records,
20349                &names,
20350                0,
20351            );
20352            assert_eq!(copied, (10, Ok(240)), "{format:?}");
20353            let contents = pages.contents();
20354            assert_eq!(
20355                &contents[PAGE - 50..PAGE - 50 + records.len()],
20356                &as_linux_writes(format, &records, &names)[..],
20357                "{format:?}"
20358            );
20359            assert!(
20360                contents[..PAGE - 50]
20361                    .iter()
20362                    .chain(&contents[PAGE - 50 + records.len()..])
20363                    .all(|&byte| byte == Pages::FILL),
20364                "{format:?}: bytes outside the records changed"
20365            );
20366        }
20367    }
20368}
20369
20370/// `inject_fstat` and `add_fd` against a scripted guest whose "address space"
20371/// is this test process, so every injected syscall runs for real on host
20372/// memory and a real descriptor. Regression coverage for
20373/// <https://github.com/rrnewton/hermit/issues/3328>; the traced end-to-end case
20374/// is `tests_misc::tight_stack_openat`.
20375#[cfg(test)]
20376mod inject_fstat_scratch {
20377    use std::os::fd::IntoRawFd;
20378    use std::os::fd::RawFd;
20379    use std::os::unix::ffi::OsStrExt;
20380    use std::os::unix::ffi::OsStringExt;
20381    use std::os::unix::fs::MetadataExt;
20382    use std::sync::Arc;
20383    use std::sync::atomic::AtomicBool;
20384    use std::sync::atomic::Ordering;
20385
20386    use reverie::GlobalRPC;
20387    use reverie::GlobalTool;
20388    use reverie::Pid;
20389    use reverie::Tool;
20390    use reverie::syscalls::LocalMemory;
20391    use reverie::syscalls::ProtFlags;
20392
20393    use super::*;
20394    use crate::Config;
20395    use crate::GlobalState;
20396    use crate::ThreadState;
20397    use crate::types::DetPid;
20398
20399    /// Room for one `libc::stat`, 8-byte aligned like a real stack slot.
20400    const ARENA_WORDS: usize = 32;
20401
20402    /// A guest stack scratch that is either writable or faults on commit,
20403    /// like the ptrace scratch below an `rsp` with no writable memory under
20404    /// it. The writable arena belongs to the guest and outlives every guard,
20405    /// so an early guard drop is reported by `guard_live` rather than by a
20406    /// write into freed memory.
20407    struct ScriptedStack {
20408        writable: bool,
20409        /// Match ptrace reserve/commit's actual zero write in the new controls.
20410        zero_on_commit: bool,
20411        arena: usize,
20412        capacity: usize,
20413        guard_live: Arc<AtomicBool>,
20414        /// A failing commit first overwrites the scratch's first word, as a
20415        /// commit that faults part way does.
20416        partial: bool,
20417        /// A page that a failing commit makes read-only after its partial
20418        /// write, so the borrower cannot put the bytes back.
20419        protect_on_failure: Option<usize>,
20420    }
20421
20422    struct ScriptedStackGuard {
20423        guard_live: Arc<AtomicBool>,
20424    }
20425
20426    impl Drop for ScriptedStackGuard {
20427        fn drop(&mut self) {
20428            self.guard_live.store(false, Ordering::SeqCst);
20429        }
20430    }
20431
20432    impl reverie::Stack for ScriptedStack {
20433        type StackGuard = ScriptedStackGuard;
20434
20435        fn size(&self) -> usize {
20436            panic!("inject_fstat must not query the scratch size")
20437        }
20438        fn capacity(&self) -> usize {
20439            panic!("inject_fstat must not query the scratch capacity")
20440        }
20441        fn push<'stack, T>(&mut self, value: T) -> Addr<'stack, T> {
20442            assert!(std::mem::size_of::<T>() <= self.capacity);
20443            assert!(std::mem::align_of::<T>() <= std::mem::align_of::<u64>());
20444            // SAFETY: this mock's arena is aligned and large enough for the
20445            // one pathname the readlink handler stages before its injection.
20446            unsafe { (self.arena as *mut T).write(value) };
20447            Addr::from_raw(self.arena).unwrap()
20448        }
20449        fn reserve<'stack, T>(&mut self) -> AddrMut<'stack, T> {
20450            assert!(std::mem::size_of::<T>() <= self.capacity);
20451            AddrMut::from_raw(self.arena).unwrap()
20452        }
20453        fn commit(self) -> Result<Self::StackGuard, Errno> {
20454            if !self.writable {
20455                if self.partial {
20456                    // SAFETY: the arena (or scratch page) is this guest's,
20457                    // writable until protected below.
20458                    unsafe { (self.arena as *mut u64).write(0) };
20459                }
20460                if let Some(page) = self.protect_on_failure {
20461                    // SAFETY: the page is the test's own mapping.
20462                    assert_eq!(
20463                        unsafe { libc::mprotect(page as *mut _, 4096, libc::PROT_READ) },
20464                        0
20465                    );
20466                }
20467                return Err(Errno::EFAULT);
20468            }
20469            if self.zero_on_commit {
20470                // SAFETY: reserve checked the arena capacity and the owned
20471                // arena/page is writable for the duration of this stack.
20472                unsafe {
20473                    std::ptr::write_bytes(
20474                        self.arena as *mut u8,
20475                        0,
20476                        std::mem::size_of::<libc::stat>(),
20477                    )
20478                };
20479            }
20480            self.guard_live.store(true, Ordering::SeqCst);
20481            Ok(ScriptedStackGuard {
20482                guard_live: self.guard_live,
20483            })
20484        }
20485    }
20486
20487    struct ScriptedGuest {
20488        config: Config,
20489        thread: ThreadState<()>,
20490        stack_writable: bool,
20491        zero_on_commit: bool,
20492        mmap_fails: bool,
20493        arena: Box<[u64; ARENA_WORDS]>,
20494        guard_live: Arc<AtomicBool>,
20495        injected: Vec<Sysno>,
20496        /// Whether a stack guard was live when each fstat was injected.
20497        fstat_guard_live: Vec<bool>,
20498        /// Buffer address of each injected fstat.
20499        fstat_buffers: Vec<usize>,
20500        /// (address, length) of each page the guest mapped.
20501        mapped: Vec<(usize, usize)>,
20502        /// (address, length) of each successful munmap.
20503        unmapped: Vec<(usize, usize)>,
20504        /// Descriptors closed through injection.
20505        closed: Vec<RawFd>,
20506        runtime_fd: Option<RawFd>,
20507        backend_ids: Option<(Pid, Pid)>,
20508        /// Explicit guest-to-kernel process/thread mapping for proc translation.
20509        procfs_ids: Option<[(i32, i32); 2]>,
20510        rsp: Option<usize>,
20511        /// Paths actually handed to the kernel for parent authentication.
20512        statat_paths: Vec<Vec<u8>>,
20513        directory_batches: std::collections::VecDeque<Result<Vec<u8>, Errno>>,
20514        /// Scripted refusals of injected opens, in order; an open with none
20515        /// left opens a real file.
20516        open_errors: std::collections::VecDeque<Errno>,
20517        /// The paths of injected opens, as the guest's memory held them.
20518        opened_paths: Vec<Vec<u8>>,
20519        /// The real descriptors injected opens returned.
20520        opened_fds: Vec<RawFd>,
20521        /// Scripted errors of injected closes, in order, before any real
20522        /// close.
20523        close_errors: std::collections::VecDeque<Errno>,
20524        /// Every injected close's descriptor, a scripted failure included.
20525        close_attempts: Vec<RawFd>,
20526        /// Scripted errors of injected fstats, in order, before any real
20527        /// fstat.
20528        fstat_errors: std::collections::VecDeque<Errno>,
20529        /// The effective set an injected capget reports (the permitted set is
20530        /// every capability).
20531        capget_effective: u64,
20532        /// A page of its own for the stack scratch, in place of the arena.
20533        scratch_page: Option<usize>,
20534        /// A failing commit's partial write, and whether it then makes the
20535        /// scratch page read-only.
20536        commit_partial: bool,
20537        protect_page_on_commit_failure: bool,
20538        /// The injected syscall at which the scratch page becomes read-only,
20539        /// so that putting its bytes back fails.
20540        protect_page_on: Option<Sysno>,
20541        private_memory: Option<reverie::RuntimePrivateMemorySnapshot>,
20542        private_retired: bool,
20543        private_validations: std::sync::atomic::AtomicUsize,
20544        maps_reads: std::collections::VecDeque<Vec<u8>>,
20545        /// A successful Binder read publishes this many command bytes.
20546        binder_read_consumed: Option<u64>,
20547    }
20548
20549    impl ScriptedGuest {
20550        fn new(stack_writable: bool, mmap_fails: bool) -> (Detcore, Self) {
20551            let config = Config {
20552                virtualize_metadata: true,
20553                ..Config::default()
20554            };
20555            let pid = DetPid::from_raw(1);
20556            let mut thread = ThreadState::new(pid, &config, ());
20557            thread.detpid = Some(pid);
20558            let tool = <Detcore as Tool>::new(Pid::from_raw(1), &config);
20559            let guest = Self {
20560                config,
20561                thread,
20562                stack_writable,
20563                zero_on_commit: false,
20564                mmap_fails,
20565                arena: Box::new([u64::MAX; ARENA_WORDS]),
20566                guard_live: Arc::new(AtomicBool::new(false)),
20567                injected: Vec::new(),
20568                fstat_guard_live: Vec::new(),
20569                fstat_buffers: Vec::new(),
20570                mapped: Vec::new(),
20571                unmapped: Vec::new(),
20572                closed: Vec::new(),
20573                runtime_fd: None,
20574                backend_ids: None,
20575                procfs_ids: None,
20576                rsp: None,
20577                statat_paths: Vec::new(),
20578                directory_batches: Default::default(),
20579                open_errors: Default::default(),
20580                opened_paths: Vec::new(),
20581                opened_fds: Vec::new(),
20582                close_errors: Default::default(),
20583                close_attempts: Vec::new(),
20584                fstat_errors: Default::default(),
20585                capget_effective: (1 << 41) - 1,
20586                scratch_page: None,
20587                commit_partial: false,
20588                protect_page_on_commit_failure: false,
20589                protect_page_on: None,
20590                private_memory: None,
20591                private_retired: false,
20592                private_validations: std::sync::atomic::AtomicUsize::new(0),
20593                maps_reads: Default::default(),
20594                binder_read_consumed: None,
20595            };
20596            (tool, guest)
20597        }
20598
20599        /// A guest whose stack scratch is a page of its own, holding a
20600        /// pattern, and whose failure policy returns EOPNOTSUPP rather than
20601        /// stopping (as with --allow-unsupported-syscalls).
20602        fn with_scratch_page(stack_writable: bool) -> (Detcore, Self) {
20603            let (tool, mut guest) = Self::new(stack_writable, false);
20604            guest.config.panic_on_unsupported_syscalls = false;
20605            // SAFETY: a fresh private anonymous page, unmapped by the test.
20606            let page = unsafe {
20607                libc::mmap(
20608                    std::ptr::null_mut(),
20609                    4096,
20610                    libc::PROT_READ | libc::PROT_WRITE,
20611                    libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
20612                    -1,
20613                    0,
20614                )
20615            };
20616            assert_ne!(page, libc::MAP_FAILED);
20617            // SAFETY: the page is 4096 writable bytes.
20618            unsafe { std::ptr::write_bytes(page as *mut u8, 0xa5, 4096) };
20619            guest.scratch_page = Some(page as usize);
20620            (tool, guest)
20621        }
20622
20623        /// The scratch page's bytes, and the page unmapped.
20624        fn take_scratch_page(&mut self) -> Vec<u8> {
20625            let page = self.scratch_page.take().unwrap();
20626            // SAFETY: the page is the test's mapping, readable throughout.
20627            let bytes = unsafe { std::slice::from_raw_parts(page as *const u8, 4096) }.to_vec();
20628            assert_eq!(unsafe { libc::munmap(page as *mut _, 4096) }, 0);
20629            bytes
20630        }
20631    }
20632
20633    #[reverie::tool]
20634    impl GlobalRPC<GlobalState> for ScriptedGuest {
20635        async fn send_rpc(
20636            &self,
20637            message: <GlobalState as GlobalTool>::Request,
20638        ) -> <GlobalState as GlobalTool>::Response {
20639            if let GlobalRequest::ProcfsIds(to_kernel, ids) = &message.2
20640                && let Some(mapping) = self.procfs_ids
20641            {
20642                let resolved = ids
20643                    .iter()
20644                    .map(|id| {
20645                        mapping.iter().find_map(|&(guest, kernel)| {
20646                            let (from, to) = if *to_kernel {
20647                                (guest, kernel)
20648                            } else {
20649                                (kernel, guest)
20650                            };
20651                            (*id == from).then_some(to)
20652                        })
20653                    })
20654                    .collect();
20655                return (None, GlobalResponse::ProcfsIds(resolved));
20656            }
20657            panic!("fd registration must not send an RPC: {:?}", message.2)
20658        }
20659        fn config(&self) -> &Config {
20660            &self.config
20661        }
20662    }
20663
20664    #[reverie::tool]
20665    impl Guest<Detcore> for ScriptedGuest {
20666        type Memory = LocalMemory;
20667        type Stack = ScriptedStack;
20668
20669        fn tid(&self) -> Pid {
20670            self.backend_ids.map_or(Pid::from_raw(1), |(_, tid)| tid)
20671        }
20672        fn pid(&self) -> Pid {
20673            self.backend_ids.map_or(Pid::from_raw(1), |(pid, _)| pid)
20674        }
20675        fn ppid(&self) -> Option<Pid> {
20676            None
20677        }
20678        fn is_runtime_owned_fd(&self, fd: RawFd) -> bool {
20679            self.runtime_fd == Some(fd)
20680        }
20681        fn runtime_private_memory_regions(&self) -> Option<reverie::RuntimePrivateMemory> {
20682            self.private_memory.clone().map(|facts| {
20683                if self.private_retired {
20684                    reverie::RuntimePrivateMemory::Unavailable
20685                } else {
20686                    reverie::RuntimePrivateMemory::Ready(facts)
20687                }
20688            })
20689        }
20690        fn runtime_private_memory_is_current(
20691            &self,
20692            snapshot: &reverie::RuntimePrivateMemorySnapshot,
20693        ) -> bool {
20694            self.private_validations.fetch_add(1, Ordering::SeqCst);
20695            !self.private_retired && self.private_memory.as_ref() == Some(snapshot)
20696        }
20697        fn memory(&self) -> Self::Memory {
20698            LocalMemory::new()
20699        }
20700        fn thread_state_mut(&mut self) -> &mut ThreadState<()> {
20701            &mut self.thread
20702        }
20703        fn thread_state(&self) -> &ThreadState<()> {
20704            &self.thread
20705        }
20706        async fn regs(&mut self) -> libc::user_regs_struct {
20707            // SAFETY: every register is an integer; only the configured
20708            // stack pointer is needed by these translation controls.
20709            let mut regs: libc::user_regs_struct = unsafe { std::mem::zeroed() };
20710            regs.rsp = self.rsp.expect("fd registration must not read registers") as u64;
20711            regs
20712        }
20713        async fn stack(&mut self) -> Self::Stack {
20714            ScriptedStack {
20715                writable: self.stack_writable,
20716                zero_on_commit: self.zero_on_commit,
20717                arena: self
20718                    .scratch_page
20719                    .unwrap_or(self.arena.as_mut_ptr() as usize),
20720                capacity: if self.scratch_page.is_some() {
20721                    4096
20722                } else {
20723                    ARENA_WORDS * std::mem::size_of::<u64>()
20724                },
20725                guard_live: self.guard_live.clone(),
20726                partial: self.commit_partial,
20727                protect_on_failure: self
20728                    .scratch_page
20729                    .filter(|_| self.protect_page_on_commit_failure),
20730            }
20731        }
20732        async fn daemonize(&mut self) {
20733            panic!("fd registration must not daemonize")
20734        }
20735        async fn inject<S: SyscallInfo>(&mut self, syscall: S) -> Result<i64, Errno> {
20736            let (number, args) = syscall.into_parts();
20737            self.injected.push(number);
20738            if let Some(page) = self
20739                .scratch_page
20740                .filter(|_| self.protect_page_on == Some(number))
20741            {
20742                // SAFETY: the page is the test's own mapping.
20743                assert_eq!(
20744                    unsafe { libc::mprotect(page as *mut _, 4096, libc::PROT_READ) },
20745                    0
20746                );
20747            }
20748            if matches!(number, Sysno::getdents | Sysno::getdents64) {
20749                let bytes = self
20750                    .directory_batches
20751                    .pop_front()
20752                    .expect("scripted getdents")?;
20753                assert!(bytes.len() <= args.arg2);
20754                if !bytes.is_empty() {
20755                    let buffer = AddrMut::from_raw(args.arg1).unwrap();
20756                    self.memory().write_exact(buffer, &bytes)?;
20757                }
20758                return Ok(bytes.len() as i64);
20759            }
20760            if matches!(
20761                number,
20762                Sysno::readlink | Sysno::readlinkat | Sysno::newfstatat
20763            ) {
20764                if number == Sysno::newfstatat {
20765                    let path = PathPtr::from_ptr(args.arg1 as *const libc::c_char).unwrap();
20766                    self.statat_paths
20767                        .push(path.read(&self.memory())?.into_os_string().into_vec());
20768                }
20769                // SAFETY: the tests own every non-null input/output buffer;
20770                // null buffers and invalid descriptors are checked by Linux.
20771                return Errno::result(unsafe {
20772                    libc::syscall(
20773                        number as libc::c_long,
20774                        args.arg0,
20775                        args.arg1,
20776                        args.arg2,
20777                        args.arg3,
20778                        args.arg4,
20779                        args.arg5,
20780                    )
20781                });
20782            }
20783            // SAFETY: each arm runs the syscall Detcore asked for against this
20784            // process, on addresses Detcore obtained from this guest.
20785            let raw = match Syscall::from_raw(number, args) {
20786                Syscall::Ioctl(_) if self.binder_read_consumed.is_some() => {
20787                    assert_eq!(args.arg1 as u64, crate::syscalls::binder::BINDER_WRITE_READ);
20788                    let address = AddrMut::<[u64; 6]>::from_raw(args.arg2).unwrap();
20789                    let mut words: [u64; 6] = self.memory().read_value(address)?;
20790                    words[4] = self.binder_read_consumed.unwrap();
20791                    self.memory().write_value(address, &words)?;
20792                    0
20793                }
20794                Syscall::Lseek(call) if self.private_memory.is_some() => {
20795                    assert_eq!(call.offset(), 0);
20796                    assert_eq!(call.whence(), Whence::SEEK_SET);
20797                    0
20798                }
20799                Syscall::Read(call) if self.private_memory.is_some() => {
20800                    let bytes = self.maps_reads.pop_front().expect("scripted maps drain");
20801                    assert!(bytes.len() <= call.len());
20802                    self.memory().write_exact(call.buf().unwrap(), &bytes)?;
20803                    if bytes.is_empty() {
20804                        // The owner changes generation while the complete
20805                        // drain is in flight, before snapshot publication.
20806                        self.private_retired = true;
20807                    }
20808                    bytes.len() as i64
20809                }
20810                Syscall::Mmap(call) => {
20811                    assert!(call.addr().is_none(), "the kernel must choose the address");
20812                    assert_eq!(call.prot(), ProtFlags::PROT_READ | ProtFlags::PROT_WRITE);
20813                    assert_eq!(
20814                        call.flags(),
20815                        MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS
20816                    );
20817                    if self.mmap_fails {
20818                        return Err(Errno::ENOMEM);
20819                    }
20820                    let address = unsafe {
20821                        libc::mmap(
20822                            std::ptr::null_mut(),
20823                            call.len(),
20824                            libc::PROT_READ | libc::PROT_WRITE,
20825                            libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
20826                            -1,
20827                            0,
20828                        )
20829                    };
20830                    if address == libc::MAP_FAILED {
20831                        -1
20832                    } else {
20833                        self.mapped.push((address as usize, call.len()));
20834                        address as i64
20835                    }
20836                }
20837                Syscall::Fstat(call) => {
20838                    // Model the same owner guard as a real Guest, while
20839                    // keeping readlink a real kernel call. Before the fix,
20840                    // this error arrives after the link wrote its output.
20841                    if self.is_runtime_owned_fd(call.fd()) {
20842                        return Err(Errno::EBADF);
20843                    }
20844                    let buffer = call.stat().expect("fstat without a buffer").0.as_raw();
20845                    self.fstat_buffers.push(buffer);
20846                    self.fstat_guard_live
20847                        .push(self.guard_live.load(Ordering::SeqCst));
20848                    if let Some(errno) = self.fstat_errors.pop_front() {
20849                        return Err(errno);
20850                    }
20851                    i64::from(unsafe { libc::fstat(call.fd(), buffer as *mut libc::stat) })
20852                }
20853                Syscall::Munmap(call) => {
20854                    let address = call.addr().expect("munmap without an address").as_raw();
20855                    let raw = i64::from(unsafe { libc::munmap(address as *mut _, call.len()) });
20856                    if raw == 0 {
20857                        self.unmapped.push((address, call.len()));
20858                    }
20859                    raw
20860                }
20861                Syscall::Close(call) => {
20862                    self.close_attempts.push(call.fd());
20863                    if let Some(errno) = self.close_errors.pop_front() {
20864                        return Err(errno);
20865                    }
20866                    self.closed.push(call.fd());
20867                    i64::from(unsafe { libc::close(call.fd()) })
20868                }
20869                Syscall::Openat(call) => {
20870                    let path = call.path().expect("openat without a path");
20871                    let path = path.read(&self.memory()).unwrap();
20872                    self.opened_paths
20873                        .push(path.as_os_str().as_encoded_bytes().to_vec());
20874                    if let Some(errno) = self.open_errors.pop_front() {
20875                        return Err(errno);
20876                    }
20877                    let (fd, _) = open_file();
20878                    self.opened_fds.push(fd);
20879                    i64::from(fd)
20880                }
20881                Syscall::Capget(call) => {
20882                    // Every capability, in the effective and permitted sets.
20883                    let data = call.data().expect("capget without data").as_raw();
20884                    let effective = self.capget_effective;
20885                    let words: [u32; 6] = [
20886                        effective as u32,
20887                        u32::MAX,
20888                        0,
20889                        (effective >> 32) as u32,
20890                        0x1ff,
20891                        0,
20892                    ];
20893                    let bytes: Vec<u8> = words.iter().flat_map(|word| word.to_ne_bytes()).collect();
20894                    self.memory()
20895                        .write_exact(AddrMut::<u8>::from_raw(data).unwrap(), &bytes)?;
20896                    0
20897                }
20898                other => panic!("unexpected injected syscall {other:?}"),
20899            };
20900            Errno::result(raw)
20901        }
20902        async fn tail_inject<S: SyscallInfo>(&mut self, _: S) -> reverie::Never {
20903            panic!("fd registration must not retire the guest")
20904        }
20905        fn set_timer(&mut self, _: reverie::TimerSchedule) -> Result<(), Error> {
20906            panic!("fd registration must not set a timer")
20907        }
20908        fn set_timer_precise(&mut self, _: reverie::TimerSchedule) -> Result<(), Error> {
20909            panic!("fd registration must not set a timer")
20910        }
20911        fn read_clock(&mut self) -> Result<u64, Error> {
20912            panic!("fd registration must not read a clock")
20913        }
20914    }
20915
20916    /// Binder's ioctl recorder does not restore nested transaction bytes or
20917    /// installed descriptors. Even valid returned bytes must not add receipt
20918    /// probe events in record/replay. Drive the actual handler; ScriptedGuest
20919    /// rejects an unexpected F_GETFD, so removing the guard fails this test.
20920    #[tokio::test]
20921    async fn binder_record_replay_does_not_add_receipt_probe_events() {
20922        let mut object = [0u8; 24];
20923        object[..4].copy_from_slice(&0x6664_2a85u32.to_ne_bytes()); // BINDER_TYPE_FD
20924        object[8..12].copy_from_slice(&123u32.to_ne_bytes());
20925        let offsets = [0u64];
20926        let mut transaction = [0u8; 64];
20927        transaction[40..48].copy_from_slice(&8u64.to_ne_bytes());
20928        transaction[48..56].copy_from_slice(&(object.as_ptr() as u64).to_ne_bytes());
20929        transaction[56..64].copy_from_slice(&(offsets.as_ptr() as u64).to_ne_bytes());
20930        let mut commands = 0x8040_7202u32.to_ne_bytes().to_vec(); // BR_TRANSACTION
20931        commands.extend_from_slice(&transaction);
20932        for replaying in [false, true] {
20933            let (_, mut guest) = ScriptedGuest::new(true, false);
20934            guest.config.recordreplay_modes = true;
20935            guest.config.replaying = replaying;
20936            guest.config.sequentialize_threads = true;
20937            assert!(!guest.config.backend.tool_shares_guest_descriptor_table);
20938            guest.binder_read_consumed = Some(commands.len() as u64);
20939            let tool = <Detcore as Tool>::new(Pid::from_raw(1), &guest.config);
20940            let mut words = [0, 0, 0, commands.len() as u64, 0, commands.as_ptr() as u64];
20941            let call =
20942                syscalls::Ioctl::new()
20943                    .with_fd(99)
20944                    .with_request(syscalls::ioctl::Request::Other(
20945                        crate::syscalls::binder::BINDER_WRITE_READ as usize,
20946                        words.as_mut_ptr() as usize,
20947                    ));
20948            assert_eq!(tool.handle_ioctl(&mut guest, call).await.unwrap(), 0);
20949            assert_eq!(words[4], commands.len() as u64);
20950            assert_eq!(guest.injected, vec![Sysno::ioctl]);
20951            assert!(guest.thread.with_detfd(123, |_| ()).is_err());
20952            // Positive payload control: this is a real parseable FD result,
20953            // not an empty buffer that would hide an accidentally open gate.
20954            assert_eq!(
20955                crate::syscalls::binder::delivered(&guest.memory(), words.as_ptr() as usize, 0).fds,
20956                vec![123]
20957            );
20958        }
20959    }
20960
20961    mod runtime_readlink_tests {
20962        use std::ffi::CStr;
20963        use std::ffi::CString;
20964        use std::os::fd::AsRawFd;
20965        use std::os::unix::net::UnixStream;
20966
20967        use super::*;
20968
20969        fn local_guest() -> (Detcore, ScriptedGuest) {
20970            let (_, mut guest) = ScriptedGuest::new(true, false);
20971            let pid = std::process::id() as i32;
20972            // SAFETY: gettid has no arguments or memory operands.
20973            let tid = unsafe { libc::syscall(libc::SYS_gettid) } as i32;
20974            guest.backend_ids = Some((Pid::from_raw(pid), Pid::from_raw(tid)));
20975            guest.thread.detpid = Some(DetPid::from_raw(pid));
20976            guest.thread.dettid = DetTid::from_raw(tid);
20977            guest.config.backend = reverie::BackendCapabilities::DBT;
20978            guest.config.virtualize_metadata = false;
20979            let tool = <Detcore as Tool>::new(Pid::from_raw(pid), &guest.config);
20980            (tool, guest)
20981        }
20982
20983        fn record_directory(guest: &ScriptedGuest, fd: RawFd, path: &str) {
20984            guest
20985                .thread
20986                .add_fd(
20987                    fd,
20988                    OFlag::O_RDONLY | OFlag::O_DIRECTORY,
20989                    FdType::Regular,
20990                    None,
20991                )
20992                .unwrap();
20993            guest
20994                .thread
20995                .with_detfd(fd, |detfd| detfd.set_path(path))
20996                .unwrap();
20997        }
20998
20999        async fn read_link(
21000            tool: &Detcore,
21001            guest: &mut ScriptedGuest,
21002            dirfd: Option<RawFd>,
21003            path: Option<&CStr>,
21004            buffer: Option<AddrMut<'_, libc::c_char>>,
21005            size: usize,
21006        ) -> Result<i64, Error> {
21007            let path = path.and_then(|path| PathPtr::from_ptr(path.as_ptr()));
21008            match dirfd {
21009                None => {
21010                    tool.handle_readlink(
21011                        guest,
21012                        syscalls::Readlink::new()
21013                            .with_path(path)
21014                            .with_buf(buffer)
21015                            .with_bufsize(size),
21016                    )
21017                    .await
21018                }
21019                Some(dirfd) => {
21020                    tool.handle_readlinkat(
21021                        guest,
21022                        syscalls::Readlinkat::new()
21023                            .with_dirfd(dirfd)
21024                            .with_path(path)
21025                            .with_buf(buffer)
21026                            .with_buf_len(size),
21027                    )
21028                    .await
21029                }
21030            }
21031        }
21032
21033        fn errno(result: Result<i64, Error>, expected: Errno) {
21034            assert!(
21035                matches!(result, Err(Error::Errno(actual)) if actual == expected),
21036                "{result:?}"
21037            );
21038        }
21039
21040        /// Exercise real cwd-relative lookup without changing the process's
21041        /// cwd while unrelated unit tests may be running.
21042        fn relative_to_cwd(path: &Path) -> String {
21043            let mut relative = PathBuf::new();
21044            for _ in std::env::current_dir().unwrap().components().skip(1) {
21045                relative.push("..");
21046            }
21047            relative.push(path.strip_prefix("/").unwrap());
21048            relative.to_str().unwrap().to_owned()
21049        }
21050
21051        /// A live socket, rather than an absent magic descriptor, supplies
21052        /// the Guest's ownership fact. Exercise the public shared handlers,
21053        /// including their real kernel readlink and protected-fstat boundary.
21054        #[tokio::test]
21055        async fn runtime_fd_links_are_absent_before_any_target_read_or_write() {
21056            let (tool, mut guest) = local_guest();
21057            let (socket, _peer) = UnixStream::pair().unwrap();
21058            let ordinary = std::fs::File::open("/dev/null").unwrap();
21059            let process = std::fs::File::open("/proc/self/fd").unwrap();
21060            let thread = std::fs::File::open("/proc/thread-self/fd").unwrap();
21061            let aliases = tempfile::tempdir().unwrap();
21062            let alias = aliases.path().join("descriptors");
21063            std::os::unix::fs::symlink("/proc/thread-self/fd", &alias).unwrap();
21064            record_directory(&guest, process.as_raw_fd(), "/proc/self/fd");
21065            record_directory(&guest, thread.as_raw_fd(), "/proc/thread-self/fd");
21066            guest.runtime_fd = Some(socket.as_raw_fd());
21067            assert!(
21068                std::fs::read_link(format!("/proc/thread-self/fd/{}", socket.as_raw_fd()))
21069                    .unwrap()
21070                    .as_os_str()
21071                    .as_bytes()
21072                    .starts_with(b"socket:[")
21073            );
21074
21075            for fd in [socket.as_raw_fd(), ordinary.as_raw_fd()] {
21076                for (dirfd, path) in [
21077                    (None, format!("/proc/self/fd/{fd}")),
21078                    (None, format!("/proc/thread-self/fd/{fd}")),
21079                    (None, format!("/dev/fd/{fd}")),
21080                    (None, format!("/proc/./self/fd/{fd}")),
21081                    (None, format!("/proc/self/../self/fd/{fd}")),
21082                    (
21083                        None,
21084                        relative_to_cwd(Path::new(&format!("/proc/self/fd/{fd}"))),
21085                    ),
21086                    (None, relative_to_cwd(&alias.join(fd.to_string()))),
21087                    (Some(-1), format!("/proc/self/fd/{fd}")),
21088                    (Some(libc::AT_FDCWD), format!("/proc/thread-self/fd/{fd}")),
21089                    (
21090                        Some(libc::AT_FDCWD),
21091                        relative_to_cwd(&alias.join(fd.to_string())),
21092                    ),
21093                    (Some(process.as_raw_fd()), fd.to_string()),
21094                    (Some(thread.as_raw_fd()), fd.to_string()),
21095                    (Some(thread.as_raw_fd()), format!("./{fd}")),
21096                ] {
21097                    let path = CString::new(path).unwrap();
21098                    let mut bytes = [0xa5; 128];
21099                    guest.injected.clear();
21100                    let result = read_link(
21101                        &tool,
21102                        &mut guest,
21103                        dirfd,
21104                        Some(&path),
21105                        AddrMut::from_raw(bytes.as_mut_ptr() as usize),
21106                        bytes.len(),
21107                    )
21108                    .await;
21109                    if fd == socket.as_raw_fd() {
21110                        errno(result, Errno::ENOENT);
21111                        assert_eq!(bytes, [0xa5; 128], "{path:?}");
21112                        assert!(
21113                            !guest.injected.iter().any(|call| matches!(
21114                                call,
21115                                Sysno::readlink | Sysno::readlinkat | Sysno::fstat
21116                            )),
21117                            "{path:?}: {:?}",
21118                            guest.injected
21119                        );
21120                    } else {
21121                        assert_eq!(result.unwrap(), 9, "{path:?}");
21122                        assert_eq!(&bytes[..9], b"/dev/null");
21123                        assert_eq!(&bytes[9..], &[0xa5; 119]);
21124                        assert!(
21125                            guest
21126                                .injected
21127                                .iter()
21128                                .any(|call| matches!(call, Sysno::readlink | Sysno::readlinkat))
21129                        );
21130                    }
21131                }
21132            }
21133            assert_eq!(guest.mapped, guest.unmapped);
21134
21135            // The owner withdraws its fact and the same live slot is reused
21136            // by a guest file. Neither the number nor a past query hides it.
21137            assert_eq!(
21138                unsafe { libc::dup2(ordinary.as_raw_fd(), socket.as_raw_fd()) },
21139                socket.as_raw_fd()
21140            );
21141            guest.runtime_fd = None;
21142            let path = CString::new(format!("/proc/self/fd/{}", socket.as_raw_fd())).unwrap();
21143            let mut bytes = [0xa5; 128];
21144            assert_eq!(
21145                read_link(
21146                    &tool,
21147                    &mut guest,
21148                    None,
21149                    Some(&path),
21150                    AddrMut::from_raw(bytes.as_mut_ptr() as usize),
21151                    bytes.len()
21152                )
21153                .await
21154                .unwrap(),
21155                9
21156            );
21157            assert_eq!(&bytes[..9], b"/dev/null");
21158            assert_eq!(&bytes[9..], &[0xa5; 119]);
21159        }
21160
21161        #[tokio::test]
21162        async fn runtime_fd_parent_translation_preserves_overlapping_output() {
21163            let (tool, mut guest) = local_guest();
21164            let (physical_pid, physical_tid) = guest.backend_ids.unwrap();
21165            // These are this modeled task's guest IDs; the RPC maps them to
21166            // the real process/thread executing every injected lookup. Choose
21167            // two IDs that cannot accidentally equal either physical ID.
21168            let mut modeled_ids = [37, 41, 43, 47]
21169                .into_iter()
21170                .filter(|id| ![physical_pid.as_raw(), physical_tid.as_raw()].contains(id));
21171            let (pid, tid) = (modeled_ids.next().unwrap(), modeled_ids.next().unwrap());
21172            assert_ne!(physical_pid.as_raw(), pid);
21173            assert_ne!(physical_tid.as_raw(), tid);
21174            guest.backend_ids = Some((Pid::from_raw(pid), Pid::from_raw(tid)));
21175            guest.thread.detpid = Some(DetPid::from_raw(pid));
21176            guest.thread.dettid = DetTid::from_raw(tid);
21177            guest.thread.physical_tid = Some(physical_tid.as_raw());
21178            guest.procfs_ids = Some([(pid, physical_pid.as_raw()), (tid, physical_tid.as_raw())]);
21179            let (socket, _peer) = UnixStream::pair().unwrap();
21180            let ordinary = std::fs::File::open("/dev/null").unwrap();
21181            guest.runtime_fd = Some(socket.as_raw_fd());
21182            assert!(Detcore::translates_procfs_numbers(&guest));
21183
21184            // The original output is a legitimate live buffer exactly where
21185            // the synthetic lookup used to stage its translated parent.
21186            // This arena is distinct from ScriptedStack's stat/path scratch.
21187            let mut arena = Box::new([0xa5u8; 4096]);
21188            let start = arena.as_mut_ptr() as usize;
21189            let rsp = start + 2048;
21190            guest.rsp = Some(rsp);
21191            for (parent, physical_parent) in [
21192                (
21193                    format!("/proc/{pid}/fd"),
21194                    format!("/proc/{}/fd", physical_pid.as_raw()),
21195                ),
21196                (
21197                    format!("/proc/{pid}/task/{tid}/fd"),
21198                    format!(
21199                        "/proc/{}/task/{}/fd",
21200                        physical_pid.as_raw(),
21201                        physical_tid.as_raw()
21202                    ),
21203                ),
21204            ] {
21205                // Match stage_in_scratch: 1088 bytes below rsp, NUL included,
21206                // rounded down to 16-byte alignment. Old parent translation
21207                // sees only its own stat/path operands, not this output.
21208                let output = (rsp - 1088 - physical_parent.len() - 1) & !15;
21209                let offset = output.checked_sub(start).unwrap();
21210                assert!(offset + 128 <= arena.len());
21211                for fd in [socket.as_raw_fd(), ordinary.as_raw_fd()] {
21212                    for dirfd in [None, Some(libc::AT_FDCWD)] {
21213                        arena.fill(0xa5);
21214                        guest.injected.clear();
21215                        guest.statat_paths.clear();
21216                        let path = CString::new(format!("{parent}/{fd}")).unwrap();
21217                        let result = read_link(
21218                            &tool,
21219                            &mut guest,
21220                            dirfd,
21221                            Some(&path),
21222                            AddrMut::from_raw(output),
21223                            128,
21224                        )
21225                        .await;
21226                        assert_eq!(
21227                            guest.statat_paths,
21228                            vec![physical_parent.as_bytes().to_vec()]
21229                        );
21230                        let bytes = &arena[offset..offset + 128];
21231                        if fd == socket.as_raw_fd() {
21232                            errno(result, Errno::ENOENT);
21233                            assert_eq!(bytes, &[0xa5; 128]);
21234                            assert!(!guest.injected.iter().any(|call| matches!(
21235                                call,
21236                                Sysno::readlink | Sysno::readlinkat | Sysno::fstat
21237                            )));
21238                        } else {
21239                            assert_eq!(result.unwrap(), 9);
21240                            assert_eq!(&bytes[..9], b"/dev/null");
21241                            assert_eq!(&bytes[9..], &[0xa5; 119]);
21242                        }
21243                    }
21244                }
21245            }
21246            // Translation pages nest inside parent-stat pages, so release
21247            // order is the reverse of allocation order.
21248            guest.mapped.sort_unstable();
21249            guest.unmapped.sort_unstable();
21250            assert_eq!(guest.mapped, guest.unmapped);
21251        }
21252
21253        #[tokio::test]
21254        async fn runtime_fd_links_keep_size_path_buffer_and_dirfd_precedence() {
21255            let (tool, mut guest) = local_guest();
21256            let (socket, _peer) = UnixStream::pair().unwrap();
21257            let ordinary = std::fs::File::open("/dev/null").unwrap();
21258            guest.runtime_fd = Some(socket.as_raw_fd());
21259            let hidden = CString::new(format!("/proc/self/fd/{}", socket.as_raw_fd())).unwrap();
21260            let visible = CString::new(format!("/proc/self/fd/{}", ordinary.as_raw_fd())).unwrap();
21261            for dirfd in [None, Some(libc::AT_FDCWD)] {
21262                for size in [0, 1 << 31, 1 << 32] {
21263                    guest.injected.clear();
21264                    errno(
21265                        read_link(&tool, &mut guest, dirfd, None, None, size).await,
21266                        Errno::EINVAL,
21267                    );
21268                    assert!(guest.injected.is_empty());
21269                }
21270                errno(
21271                    read_link(&tool, &mut guest, dirfd, None, None, 128).await,
21272                    Errno::EFAULT,
21273                );
21274                errno(
21275                    read_link(&tool, &mut guest, dirfd, Some(&hidden), None, 128).await,
21276                    Errno::ENOENT,
21277                );
21278                errno(
21279                    read_link(&tool, &mut guest, dirfd, Some(&visible), None, 128).await,
21280                    Errno::EFAULT,
21281                );
21282                let mut bytes = [0xa5; 128];
21283                assert_eq!(
21284                    read_link(
21285                        &tool,
21286                        &mut guest,
21287                        dirfd,
21288                        Some(&visible),
21289                        AddrMut::from_raw(bytes.as_mut_ptr() as usize),
21290                        (1 << 32) + 5
21291                    )
21292                    .await
21293                    .unwrap(),
21294                    5
21295                );
21296                assert_eq!(&bytes[..5], b"/dev/");
21297                assert_eq!(&bytes[5..], &[0xa5; 123]);
21298            }
21299            let relative = CString::new(socket.as_raw_fd().to_string()).unwrap();
21300            for (dirfd, expected) in [(-1, Errno::EBADF), (ordinary.as_raw_fd(), Errno::ENOTDIR)] {
21301                let mut bytes = [0xa5; 128];
21302                errno(
21303                    read_link(
21304                        &tool,
21305                        &mut guest,
21306                        Some(dirfd),
21307                        Some(&relative),
21308                        AddrMut::from_raw(bytes.as_mut_ptr() as usize),
21309                        bytes.len(),
21310                    )
21311                    .await,
21312                    expected,
21313                );
21314                assert_eq!(bytes, [0xa5; 128]);
21315            }
21316            assert_eq!(guest.mapped, guest.unmapped);
21317        }
21318
21319        #[tokio::test]
21320        async fn runtime_fd_links_require_the_live_parent_and_callers_table() {
21321            let (tool, mut guest) = local_guest();
21322            let (socket, _peer) = UnixStream::pair().unwrap();
21323            guest.runtime_fd = Some(socket.as_raw_fd());
21324            let directory = tempfile::tempdir().unwrap();
21325            let name = socket.as_raw_fd().to_string();
21326            std::os::unix::fs::symlink("/dev/null", directory.path().join(&name)).unwrap();
21327            let wrong_parent = std::fs::File::open(directory.path()).unwrap();
21328            // A recorded spelling is deliberately stale. Its actual object
21329            // must win; no kernel read of the private socket is involved.
21330            record_directory(&guest, wrong_parent.as_raw_fd(), "/proc/self/fd");
21331            let path = CString::new(name).unwrap();
21332            let mut bytes = [0xa5; 128];
21333            let result = read_link(
21334                &tool,
21335                &mut guest,
21336                Some(wrong_parent.as_raw_fd()),
21337                Some(&path),
21338                AddrMut::from_raw(bytes.as_mut_ptr() as usize),
21339                bytes.len(),
21340            )
21341            .await;
21342            // This guard must decline the different object. Do not pin
21343            // the old canonicalizer's errno or any bytes it may leak.
21344            assert!(
21345                !matches!(&result, Err(Error::Errno(error)) if *error == Errno::ENOENT),
21346                "{result:?}"
21347            );
21348            assert!(guest.injected.contains(&Sysno::readlinkat));
21349            assert_eq!(guest.mapped, guest.unmapped);
21350
21351            // No proof that this nonleader shares the process's table:
21352            // /proc/self/fd must not borrow its current-thread owner fact.
21353            assert_ne!(guest.thread.dettid, guest.thread.detpid.unwrap());
21354            guest.thread.process_files = None;
21355            guest.injected.clear();
21356            let path = CString::new(format!("/proc/self/fd/{}", socket.as_raw_fd())).unwrap();
21357            bytes.fill(0xa5);
21358            let result = read_link(
21359                &tool,
21360                &mut guest,
21361                None,
21362                Some(&path),
21363                AddrMut::from_raw(bytes.as_mut_ptr() as usize),
21364                bytes.len(),
21365            )
21366            .await;
21367            assert!(
21368                !matches!(&result, Err(Error::Errno(error)) if *error == Errno::ENOENT),
21369                "{result:?}"
21370            );
21371            assert!(guest.injected.contains(&Sysno::readlink));
21372            assert_eq!(guest.mapped, guest.unmapped);
21373        }
21374
21375        #[tokio::test]
21376        async fn runtime_fd_links_leave_empty_and_trailing_lookups_to_linux() {
21377            let (tool, mut guest) = local_guest();
21378            let (socket, _peer) = UnixStream::pair().unwrap();
21379            guest.runtime_fd = Some(socket.as_raw_fd());
21380            let path = CString::new(format!("/proc/self/fd/{}", socket.as_raw_fd())).unwrap();
21381            // SAFETY: a valid path and no creation-mode argument is needed.
21382            let link = unsafe {
21383                libc::open(
21384                    path.as_ptr(),
21385                    libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC,
21386                )
21387            };
21388            assert!(link >= 0);
21389            use std::os::fd::FromRawFd;
21390            let link = unsafe { std::os::fd::OwnedFd::from_raw_fd(link) };
21391            let mut bytes = [0xa5; 128];
21392            let result = read_link(
21393                &tool,
21394                &mut guest,
21395                Some(link.as_raw_fd()),
21396                Some(c""),
21397                AddrMut::from_raw(bytes.as_mut_ptr() as usize),
21398                bytes.len(),
21399            )
21400            .await;
21401            // Empty-path O_PATH lookup stays on the original route. Do not
21402            // require the socket identity or a particular result from it.
21403            assert!(guest.injected.contains(&Sysno::readlinkat));
21404            assert!(guest.mapped.is_empty());
21405            match result {
21406                Ok(size) => {
21407                    let size = usize::try_from(size).unwrap();
21408                    assert!(size <= bytes.len());
21409                    assert!(bytes[size..].iter().all(|byte| *byte == 0xa5));
21410                }
21411                Err(_) => assert_eq!(bytes, [0xa5; 128]),
21412            }
21413            for suffix in ["/", "/.", "/.."] {
21414                let path =
21415                    CString::new(format!("/proc/self/fd/{}{suffix}", socket.as_raw_fd())).unwrap();
21416                bytes.fill(0xa5);
21417                guest.injected.clear();
21418                let _result = read_link(
21419                    &tool,
21420                    &mut guest,
21421                    None,
21422                    Some(&path),
21423                    AddrMut::from_raw(bytes.as_mut_ptr() as usize),
21424                    bytes.len(),
21425                )
21426                .await;
21427                // The guard declines these raw trailing forms. Preserve the
21428                // no-write check without pinning an existence-revealing errno.
21429                assert!(guest.injected.contains(&Sysno::readlink));
21430                assert!(guest.mapped.is_empty());
21431                assert_eq!(bytes, [0xa5; 128]);
21432            }
21433            assert_eq!(guest.mapped, guest.unmapped);
21434        }
21435    }
21436
21437    pub(super) async fn raw_directory_projection() {
21438        use std::os::fd::AsRawFd;
21439        let directory = std::fs::File::open("/proc/self/fd").unwrap();
21440        for format in [DirentFormat::Legacy, DirentFormat::Dirent64] {
21441            let entries = [
21442                DirEntry {
21443                    ino: 91,
21444                    off: 29,
21445                    name: b"40960".to_vec(),
21446                    ty: 8,
21447                },
21448                DirEntry {
21449                    ino: 92,
21450                    off: 17,
21451                    name: b"1024".to_vec(),
21452                    ty: 10,
21453                },
21454                DirEntry {
21455                    ino: 93,
21456                    off: 31,
21457                    name: b"1023".to_vec(),
21458                    ty: 8,
21459                },
21460            ];
21461            let encode = |rows: &[DirEntry]| {
21462                let mut bytes = Vec::new();
21463                for entry in rows {
21464                    format.encode(entry, entry.ino, entry.off, &mut bytes);
21465                }
21466                bytes
21467            };
21468            let mut unmodified = encode(&entries[..1]);
21469            for at in format.name_offset() + entries[0].name.len() + 1..unmodified.len() {
21470                if at != format.type_offset(unmodified.len()) {
21471                    unmodified[at] = 0x7c;
21472                }
21473            }
21474            for (batches, expected, calls) in [
21475                (
21476                    vec![Ok(encode(&entries))],
21477                    Ok(encode(&[entries[0].clone(), entries[2].clone()])),
21478                    1,
21479                ),
21480                (
21481                    vec![Ok(encode(&entries[1..2])), Ok(encode(&entries[..1]))],
21482                    Ok(encode(&entries[..1])),
21483                    2,
21484                ),
21485                (
21486                    vec![Ok(encode(&entries[1..2])), Ok(Vec::new())],
21487                    Ok(Vec::new()),
21488                    2,
21489                ),
21490                (
21491                    vec![Ok(encode(&entries[1..2])), Err(Errno::EIO)],
21492                    Err(Errno::EIO),
21493                    2,
21494                ),
21495                (vec![Ok(unmodified.clone())], Ok(unmodified.clone()), 1),
21496            ] {
21497                let (_, mut guest) = ScriptedGuest::new(true, false);
21498                guest.config.virtualize_metadata = false;
21499                guest.config.backend = reverie::BackendCapabilities::DBT;
21500                guest.runtime_fd = Some(1024);
21501                let written = batches
21502                    .iter()
21503                    .filter_map(|batch| batch.as_ref().ok().map(Vec::len))
21504                    .max()
21505                    .unwrap();
21506                let untouched = expected.as_ref().is_ok_and(|bytes| bytes == &unmodified);
21507                guest.directory_batches = batches.into();
21508                let tool = <Detcore as Tool>::new(Pid::from_raw(1), &guest.config);
21509                let mut bytes = vec![0xa5; 512];
21510                let buffer: AddrMut<u8> = AddrMut::from_raw(bytes.as_mut_ptr() as usize).unwrap();
21511                let result = match format {
21512                    DirentFormat::Legacy => {
21513                        tool.handle_getdents(
21514                            &mut guest,
21515                            syscalls::Getdents::new()
21516                                .with_fd(directory.as_raw_fd() as u32)
21517                                .with_dirent(Some(buffer.cast()))
21518                                .with_count(bytes.len() as u32),
21519                        )
21520                        .await
21521                    }
21522                    DirentFormat::Dirent64 => {
21523                        tool.handle_getdents64(
21524                            &mut guest,
21525                            syscalls::Getdents64::new()
21526                                .with_fd(directory.as_raw_fd() as u32)
21527                                .with_dirent(Some(buffer.cast()))
21528                                .with_count(bytes.len() as u32),
21529                        )
21530                        .await
21531                    }
21532                };
21533                assert_eq!(guest.injected.len(), calls, "{format:?}");
21534                assert!(bytes[written..].iter().all(|byte| *byte == 0xa5));
21535                match expected {
21536                    Ok(expected) => {
21537                        assert_eq!(result.unwrap(), expected.len() as i64, "{format:?}");
21538                        assert_eq!(&bytes[..expected.len()], expected, "{format:?}");
21539                        if !untouched {
21540                            assert!(bytes[expected.len()..written].iter().all(|byte| *byte == 0));
21541                        }
21542                    }
21543                    Err(expected) => {
21544                        assert!(
21545                            matches!(result, Err(Error::Errno(actual)) if actual == expected),
21546                            "{format:?}: {result:?}"
21547                        );
21548                        assert!(bytes[..format.record_len(4)].iter().all(|byte| *byte == 0));
21549                    }
21550                }
21551            }
21552        }
21553    }
21554
21555    #[test]
21556    fn private_maps_targets_require_complete_identity_or_translated_numbers() {
21557        use crate::procfs::BoundTask;
21558        let actual = Path::new("/proc/21/task/23/maps");
21559        for (numbers, expected) in [
21560            (
21561                BTreeMap::from([(21, 21), (23, 23)]),
21562                Some(BoundTask {
21563                    process: 21,
21564                    thread: Some(23),
21565                }),
21566            ),
21567            (
21568                BTreeMap::from([(21, 3), (23, 5)]),
21569                Some(BoundTask {
21570                    process: 3,
21571                    thread: Some(5),
21572                }),
21573            ),
21574            (BTreeMap::from([(21, 3)]), None),
21575        ] {
21576            assert_eq!(
21577                Detcore::<crate::record_or_replay::NoopTool>::maps_target_with_numbers(
21578                    actual,
21579                    Some(&numbers),
21580                    1,
21581                    1,
21582                ),
21583                expected
21584            );
21585        }
21586    }
21587
21588    #[tokio::test]
21589    async fn private_maps_authenticates_the_opened_descriptor_in_its_task_view() {
21590        use std::os::fd::AsRawFd;
21591        let (_, mut guest) = ScriptedGuest::new(true, false);
21592        guest.config.backend = reverie::BackendCapabilities::DBT;
21593        guest.config.sequentialize_threads = true;
21594        guest.thread.physical_tid = Some(123456);
21595        let tool = <Detcore as Tool>::new(Pid::from_raw(1), &guest.config);
21596        assert!(!procfs_inode::guest_tids_name_host_tasks(
21597            &guest.config.backend,
21598            guest.thread.physical_tid,
21599        ));
21600        let file = std::fs::File::open("/proc/thread-self/maps").unwrap();
21601        let fd = file.as_raw_fd();
21602        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
21603        assert_eq!(unsafe { libc::fstat(fd, &mut stat) }, 0);
21604        let stat = DetStat::from(stat);
21605        let authenticated = tool.maps_descriptor_entry(&guest, &stat, fd).await;
21606        assert!(
21607            matches!(&authenticated, ProcfsEntry::At(path) if path.ends_with("maps")),
21608            "{authenticated:?}"
21609        );
21610
21611        // The same spelling cannot authorize a different descriptor/object.
21612        let ordinary = tempfile::tempfile().unwrap();
21613        let mut ordinary_stat: libc::stat = unsafe { std::mem::zeroed() };
21614        assert_eq!(
21615            unsafe { libc::fstat(ordinary.as_raw_fd(), &mut ordinary_stat) },
21616            0
21617        );
21618        assert!(matches!(
21619            tool.maps_descriptor_entry(&guest, &DetStat::from(ordinary_stat), ordinary.as_raw_fd())
21620                .await,
21621            ProcfsEntry::NotProcfs
21622        ));
21623        assert!(matches!(
21624            tool.maps_descriptor_entry(&guest, &stat, ordinary.as_raw_fd())
21625                .await,
21626            ProcfsEntry::Unknown
21627        ));
21628        guest.thread.changed_root.store(true, Ordering::SeqCst);
21629        assert!(matches!(
21630            tool.maps_descriptor_entry(&guest, &stat, fd).await,
21631            ProcfsEntry::Unknown
21632        ));
21633    }
21634
21635    #[tokio::test]
21636    async fn private_maps_retired_during_drain_never_publish_a_snapshot() {
21637        let (_, mut guest) = ScriptedGuest::with_scratch_page(true);
21638        guest.config.sequentialize_threads = true;
21639        let tool = <Detcore as Tool>::new(Pid::from_raw(1), &guest.config);
21640        let target = crate::procfs::MapsTarget {
21641            task: crate::procfs::BoundTask {
21642                process: 2,
21643                thread: None,
21644            },
21645            incarnation: crate::procfs::TaskIncarnation {
21646                process: 1,
21647                thread: None,
21648            },
21649            mm: detcore_model::futex::MmId::initial(DetTid::from_raw(2)),
21650            private: reverie::RuntimePrivateMemorySnapshot {
21651                owner: [1, 2, 3, 4],
21652                version: 1,
21653                regions: vec![reverie::RuntimePrivateMemoryRegion {
21654                    start: 0x1000,
21655                    end: 0x2000,
21656                }],
21657            },
21658        };
21659        guest.private_memory = Some(target.private.clone());
21660        guest.maps_reads = [b"1000-2000 rw-p 00000000 00:00 0 \n".to_vec(), Vec::new()].into();
21661        let fd = 9;
21662        guest
21663            .thread
21664            .add_fd(fd, OFlag::O_RDONLY, FdType::Regular, None)
21665            .unwrap();
21666        let mut file = ProcfsFile::from_path(Path::new("/proc/2/maps")).unwrap();
21667        file.bind_maps_target(target.clone());
21668        guest
21669            .thread
21670            .with_detfd(fd, |detfd| detfd.set_procfs(file.clone()))
21671            .unwrap();
21672        let mut output = [0xa5_u8; 512];
21673        let call = syscalls::Read::new()
21674            .with_fd(fd)
21675            .with_buf(AddrMut::from_raw(output.as_mut_ptr() as usize))
21676            .with_len(output.len());
21677
21678        let error = tool
21679            .initialize_procfs_snapshot(&mut guest, call)
21680            .await
21681            .unwrap_err();
21682        assert!(
21683            matches!(error, Error::Tool(ref error) if error.to_string() ==
21684            "runtime-private maps view unavailable: target ownership changed during capture"),
21685            "{error:?}"
21686        );
21687        assert_eq!(guest.private_validations.load(Ordering::SeqCst), 2);
21688        assert!(guest.maps_reads.is_empty());
21689        assert_eq!(guest.injected, [Sysno::lseek, Sysno::read, Sysno::read]);
21690        assert_eq!(output, [0xa5; 512]);
21691        guest
21692            .thread
21693            .with_detfd(fd, |detfd| {
21694                assert!(detfd.procfs_needs_snapshot());
21695                assert_eq!(detfd.procfs_position(), Some((0, None)));
21696                assert_eq!(detfd.preview_procfs(512), None);
21697                assert_eq!(detfd.procfs_maps_target(), Some(target.clone()));
21698            })
21699            .unwrap();
21700        assert_eq!(guest.take_scratch_page(), vec![0xa5; 4096]);
21701    }
21702
21703    /// A real descriptor the test owns by number, and its inode. Ownership is
21704    /// raw so a descriptor that Detcore closes is never closed a second time.
21705    fn open_file() -> (RawFd, u64) {
21706        let file = tempfile::tempfile().unwrap();
21707        let inode = file.metadata().unwrap().ino();
21708        (file.into_raw_fd(), inode)
21709    }
21710
21711    fn close_unless_detcore_did(guest: &ScriptedGuest, fd: RawFd) {
21712        if !guest.closed.contains(&fd) {
21713            assert_eq!(unsafe { libc::close(fd) }, 0);
21714        }
21715    }
21716
21717    fn recorded_inode(guest: &ScriptedGuest, fd: RawFd) -> Option<u64> {
21718        guest
21719            .thread
21720            .with_detfd(fd, |detfd| detfd.stat().map(|stat| stat.inode))
21721            .unwrap()
21722    }
21723
21724    /// The errno of a call the failure policy answered, which a guest run
21725    /// with --allow-unsupported-syscalls sees.
21726    fn policy_errno(error: Error) -> Errno {
21727        error.into_errno().unwrap()
21728    }
21729
21730    /// The task whose `/proc/3/auxv` the access tests open.
21731    const TARGET: crate::procfs::BoundTask = crate::procfs::BoundTask {
21732        process: 3,
21733        thread: None,
21734    };
21735
21736    /// Another process's open of a rendered `/proc/<pid>/auxv`, decided
21737    /// through `ptrace_read_allowed` by the caller's probe opens, as the
21738    /// kernel beneath answers them:
21739    /// - the target's probe opens: allowed, one probe;
21740    /// - it is refused while the caller's `/proc/thread-self/stat` opens: the
21741    ///   refusal's errno;
21742    /// - both are refused with EACCES (the provider's backstop): the caller's
21743    ///   CAP_SYS_PTRACE decides, with and without it;
21744    /// - both fail with EMFILE (a full descriptor table): EMFILE, never the
21745    ///   fallback's EACCES;
21746    /// - an open Reverie reports did not run (ERESTARTSYS) is retried.
21747    #[tokio::test]
21748    async fn the_auxv_access_check_follows_the_kernel_beneaths_answers() {
21749        let paths = |guest: &ScriptedGuest| guest.opened_paths.clone();
21750        let target = b"/proc/3/auxv".to_vec();
21751        let serves = b"/proc/thread-self/stat".to_vec();
21752        let all = (1_u64 << 41) - 1;
21753        let without_ptrace = all & !(1 << 19);
21754        for (errors, effective, answer, probes, capget) in [
21755            (vec![], all, Ok(()), vec![target.clone()], false),
21756            (
21757                vec![Errno::EACCES],
21758                all,
21759                Err(Errno::EACCES),
21760                vec![target.clone(), serves.clone()],
21761                false,
21762            ),
21763            (
21764                vec![Errno::EACCES, Errno::EACCES],
21765                all,
21766                Ok(()),
21767                vec![target.clone(), serves.clone()],
21768                true,
21769            ),
21770            (
21771                vec![Errno::EACCES, Errno::EACCES],
21772                without_ptrace,
21773                Err(Errno::EACCES),
21774                vec![target.clone(), serves.clone()],
21775                true,
21776            ),
21777            (
21778                vec![Errno::EMFILE, Errno::EMFILE],
21779                without_ptrace,
21780                Err(Errno::EMFILE),
21781                vec![target.clone(), serves.clone()],
21782                false,
21783            ),
21784            (
21785                vec![Errno::ERESTARTSYS],
21786                without_ptrace,
21787                Ok(()),
21788                vec![target.clone(), target.clone()],
21789                false,
21790            ),
21791        ] {
21792            let (_tool, mut guest) = ScriptedGuest::with_scratch_page(true);
21793            guest.open_errors.extend(errors.clone());
21794            guest.capget_effective = effective;
21795            let result = Detcore::ptrace_read_allowed(&mut guest, TARGET).await;
21796            assert_eq!(result.unwrap(), answer, "{errors:?}");
21797            assert_eq!(paths(&guest), probes, "{errors:?}");
21798            assert_eq!(
21799                guest.injected.contains(&Sysno::capget),
21800                capget,
21801                "{errors:?}"
21802            );
21803            assert_eq!(guest.closed, guest.opened_fds, "every probe is closed");
21804            assert!(
21805                guest.take_scratch_page().iter().all(|byte| *byte == 0xa5),
21806                "{errors:?}: the scratch is left modified"
21807            );
21808        }
21809    }
21810
21811    /// Guest stack scratch that cannot be used, or cannot be put back, is
21812    /// the failure policy's (EOPNOTSUPP under --allow-unsupported-syscalls,
21813    /// a named stop otherwise), never an access answer:
21814    /// - a commit that fails, its partial write put back: no probe runs,
21815    ///   and the scratch is as it was;
21816    /// - a commit that fails part way and whose partial write cannot be put
21817    ///   back;
21818    /// - a probe, and a capget (K1's), whose scratch becomes read-only
21819    ///   before it is put back.
21820    /// And a capget that succeeds puts its scratch back.
21821    #[tokio::test]
21822    async fn guest_stack_scratch_failures_go_to_the_failure_policy() {
21823        // A commit that fails, put back.
21824        let (_tool, mut guest) = ScriptedGuest::with_scratch_page(false);
21825        guest.commit_partial = true;
21826        let result = Detcore::ptrace_read_allowed(&mut guest, TARGET).await;
21827        assert_eq!(policy_errno(result.unwrap_err()), Errno::EOPNOTSUPP);
21828        assert!(guest.injected.is_empty());
21829        assert!(guest.take_scratch_page().iter().all(|byte| *byte == 0xa5));
21830        // A partial write that cannot be put back.
21831        let (_tool, mut guest) = ScriptedGuest::with_scratch_page(false);
21832        guest.commit_partial = true;
21833        guest.protect_page_on_commit_failure = true;
21834        let result = Detcore::ptrace_read_allowed(&mut guest, TARGET).await;
21835        assert_eq!(policy_errno(result.unwrap_err()), Errno::EOPNOTSUPP);
21836        assert!(guest.injected.is_empty());
21837        guest.take_scratch_page();
21838        let (_tool, mut guest) = ScriptedGuest::with_scratch_page(false);
21839        guest.commit_partial = true;
21840        guest.protect_page_on_commit_failure = true;
21841        let result =
21842            Detcore::inject_capget(&mut guest, rendered_procfs::CapabilitySet::Effective).await;
21843        assert_eq!(policy_errno(result.unwrap_err()), Errno::EOPNOTSUPP);
21844        guest.take_scratch_page();
21845        // A probe whose scratch cannot be put back: its descriptor is still
21846        // closed.
21847        let (_tool, mut guest) = ScriptedGuest::with_scratch_page(true);
21848        guest.protect_page_on = Some(Sysno::openat);
21849        let result = Detcore::ptrace_read_allowed(&mut guest, TARGET).await;
21850        assert_eq!(policy_errno(result.unwrap_err()), Errno::EOPNOTSUPP);
21851        assert_eq!(guest.closed, guest.opened_fds);
21852        guest.take_scratch_page();
21853        // A capget whose scratch cannot be put back.
21854        let (_tool, mut guest) = ScriptedGuest::with_scratch_page(true);
21855        guest.protect_page_on = Some(Sysno::capget);
21856        let result =
21857            Detcore::inject_capget(&mut guest, rendered_procfs::CapabilitySet::Effective).await;
21858        assert_eq!(policy_errno(result.unwrap_err()), Errno::EOPNOTSUPP);
21859        guest.take_scratch_page();
21860        // A capget that succeeds.
21861        let (_tool, mut guest) = ScriptedGuest::with_scratch_page(true);
21862        let result =
21863            Detcore::inject_capget(&mut guest, rendered_procfs::CapabilitySet::Effective).await;
21864        assert_eq!(result.unwrap(), Some((1 << 41) - 1));
21865        assert!(guest.take_scratch_page().iter().all(|byte| *byte == 0xa5));
21866    }
21867
21868    /// The probe's close, as Linux's close allows: a close Reverie reports
21869    /// did not run (ERESTARTSYS) is retried; EINTR is not (Linux released
21870    /// the descriptor before returning it); any other error is the failure
21871    /// policy's, never a silent leak.
21872    #[tokio::test]
21873    async fn the_auxv_access_probe_closes_its_descriptor_as_linux_close_allows() {
21874        let (_tool, mut guest) = ScriptedGuest::with_scratch_page(true);
21875        guest.close_errors.push_back(Errno::ERESTARTSYS);
21876        let result = Detcore::ptrace_read_allowed(&mut guest, TARGET).await;
21877        assert_eq!(result.unwrap(), Ok(()));
21878        let [fd] = guest.opened_fds[..] else { panic!() };
21879        assert_eq!(
21880            guest.close_attempts,
21881            [fd, fd],
21882            "a close that did not run is retried"
21883        );
21884        assert_eq!(guest.closed, [fd]);
21885        guest.take_scratch_page();
21886        for (errno, failed) in [(Errno::EINTR, false), (Errno::EBADF, true)] {
21887            let (_tool, mut guest) = ScriptedGuest::with_scratch_page(true);
21888            guest.close_errors.push_back(errno);
21889            let result = Detcore::ptrace_read_allowed(&mut guest, TARGET).await;
21890            let [fd] = guest.opened_fds[..] else { panic!() };
21891            assert_eq!(guest.close_attempts, [fd], "{errno} is not retried");
21892            match result {
21893                Ok(answer) => {
21894                    assert!(!failed, "{errno}");
21895                    assert_eq!(answer, Ok(()));
21896                }
21897                Err(error) => {
21898                    assert!(failed, "{errno}");
21899                    assert_eq!(policy_errno(error), Errno::EOPNOTSUPP);
21900                }
21901            }
21902            // The scripted error left the real descriptor open.
21903            assert_eq!(unsafe { libc::close(fd) }, 0);
21904            assert!(guest.take_scratch_page().iter().all(|byte| *byte == 0xa5));
21905        }
21906    }
21907
21908    /// Exercise the actual shared helper with nonzero borrowed bytes and
21909    /// real kernel fstat success/error. The selectable zero commit matches
21910    /// reverie-ptrace GuestStack::commit, rather than duplicating Detcore's
21911    /// restoration algorithm in a standalone test helper.
21912    #[tokio::test]
21913    async fn injected_fstat_restores_nonzero_borrowed_bytes_on_success_and_error() {
21914        for success in [true, false] {
21915            let (_, mut guest) = ScriptedGuest::new(true, false);
21916            guest.zero_on_commit = true;
21917            for (index, word) in guest.arena.iter_mut().enumerate() {
21918                *word = 0xa5a5_a5a5_a5a5_a5a5 ^ index as u64;
21919            }
21920            let original = *guest.arena;
21921            let (fd, inode) = if success { open_file() } else { (-1, 0) };
21922            let copied = Detcore::inject_stat_on_stack(
21923                &mut guest,
21924                |statptr| {
21925                    Syscall::Fstat(syscalls::Fstat::new().with_fd(fd).with_stat(Some(statptr)))
21926                },
21927                None,
21928            )
21929            .await;
21930            if success {
21931                assert_eq!(unsafe { libc::close(fd) }, 0);
21932                assert_eq!(copied.unwrap().st_ino, inode);
21933            } else {
21934                assert_eq!(copied.err(), Some(StatFailure::Call(Errno::EBADF)));
21935            }
21936            assert_eq!(
21937                *guest.arena, original,
21938                "success={success}: all borrowed and neighboring bytes"
21939            );
21940            assert_eq!(guest.injected, [Sysno::fstat]);
21941            assert_eq!(guest.fstat_guard_live, [true]);
21942            assert!(!guest.guard_live.load(Ordering::SeqCst));
21943            assert!(guest.mapped.is_empty());
21944        }
21945    }
21946
21947    #[tokio::test]
21948    async fn partial_fstat_commit_is_restored_before_the_real_page_fallback() {
21949        let (fd, inode) = open_file();
21950        let (tool, mut guest) = ScriptedGuest::new(false, false);
21951        guest.commit_partial = true;
21952        let original = *guest.arena;
21953        let result = tool.inject_fstat(&mut guest, fd).await;
21954        assert_eq!(unsafe { libc::close(fd) }, 0);
21955        assert_eq!(result.unwrap().st_ino, inode);
21956        assert_eq!(
21957            *guest.arena, original,
21958            "the failed commit's partial zero write must be put back"
21959        );
21960        assert_eq!(guest.injected, [Sysno::mmap, Sysno::fstat, Sysno::munmap]);
21961        assert_eq!(
21962            guest.fstat_guard_live,
21963            [false],
21964            "the failed stack guard was never constructed"
21965        );
21966        assert_eq!(
21967            guest.mapped, guest.unmapped,
21968            "the actual temporary page was removed"
21969        );
21970        assert!(!guest.guard_live.load(Ordering::SeqCst));
21971    }
21972
21973    #[tokio::test]
21974    async fn writable_stack_scratch_is_used_while_its_guard_is_live() {
21975        let (fd, inode) = open_file();
21976        let (tool, mut guest) = ScriptedGuest::new(true, false);
21977
21978        let result = tool
21979            .add_fd(&mut guest, fd, OFlag::O_RDONLY, FdType::Regular)
21980            .await;
21981        close_unless_detcore_did(&guest, fd);
21982
21983        assert_eq!(result, Ok(()));
21984        assert_eq!(guest.injected, [Sysno::fstat]);
21985        assert_eq!(
21986            guest.fstat_guard_live,
21987            [true],
21988            "the stack guard must outlive the injected fstat: backends whose \
21989             scratch is an arena free it when the guard drops"
21990        );
21991        assert_eq!(
21992            guest.fstat_buffers,
21993            [guest.arena.as_ptr() as usize],
21994            "fstat must write into the stack scratch"
21995        );
21996        assert_eq!(
21997            *guest.arena,
21998            [u64::MAX; ARENA_WORDS],
21999            "the original borrowed bytes, including neighbors, must remain unchanged"
22000        );
22001        assert_eq!(recorded_inode(&guest, fd), Some(inode));
22002    }
22003
22004    #[tokio::test]
22005    async fn faulting_stack_scratch_falls_back_to_a_transient_page() {
22006        let (fd, inode) = open_file();
22007        let (tool, mut guest) = ScriptedGuest::new(false, false);
22008
22009        let result = tool
22010            .add_fd(&mut guest, fd, OFlag::O_RDONLY, FdType::Regular)
22011            .await;
22012        close_unless_detcore_did(&guest, fd);
22013
22014        assert_eq!(
22015            result,
22016            Ok(()),
22017            "a stack that cannot hold the fstat buffer must not fail the open"
22018        );
22019        assert_eq!(guest.injected, [Sysno::mmap, Sysno::fstat, Sysno::munmap]);
22020        let [(page, len)] = guest.mapped[..] else {
22021            panic!(
22022                "expected exactly one transient page, got {:?}",
22023                guest.mapped
22024            );
22025        };
22026        assert_eq!(
22027            guest.fstat_buffers,
22028            [page],
22029            "fstat must write into the transient page"
22030        );
22031        assert_eq!(
22032            guest.unmapped,
22033            [(page, len)],
22034            "the transient page must be unmapped, whole"
22035        );
22036        assert_eq!(recorded_inode(&guest, fd), Some(inode));
22037    }
22038
22039    #[tokio::test]
22040    async fn descriptor_is_closed_when_no_scratch_can_be_found() {
22041        let (fd, _) = open_file();
22042        let (tool, mut guest) = ScriptedGuest::new(false, true);
22043
22044        let result = tool
22045            .add_fd(&mut guest, fd, OFlag::O_RDONLY, FdType::Regular)
22046            .await;
22047        close_unless_detcore_did(&guest, fd);
22048
22049        assert_eq!(result, Err(Errno::ENOMEM));
22050        assert_eq!(guest.injected, [Sysno::mmap, Sysno::close]);
22051        assert_eq!(
22052            guest.closed,
22053            [fd],
22054            "the descriptor must not stay open behind the error"
22055        );
22056        assert_eq!(
22057            guest.thread.with_detfd(fd, |_| ()),
22058            Err(Errno::EBADF),
22059            "a descriptor that failed registration must not be modeled"
22060        );
22061    }
22062
22063    /// A random-device descriptor for the classification tests, and a guest
22064    /// without virtualized metadata, so the classification takes its own
22065    /// fstat (as under record and replay).
22066    fn random_device_fd() -> RawFd {
22067        std::fs::File::open("/dev/urandom").unwrap().into_raw_fd()
22068    }
22069
22070    #[tokio::test]
22071    async fn classification_retries_an_interrupted_fstat() {
22072        let fd = random_device_fd();
22073        let (tool, mut guest) = ScriptedGuest::new(true, false);
22074        guest.config.virtualize_metadata = false;
22075        guest.fstat_errors = [Errno::ERESTARTSYS, Errno::EINTR].into();
22076        let classified = tool.opened_random_device(&mut guest, fd, None).await;
22077        assert_eq!(unsafe { libc::close(fd) }, 0);
22078        assert_eq!(classified, Ok(true));
22079        assert_eq!(
22080            guest.injected,
22081            [Sysno::fstat, Sysno::fstat, Sysno::fstat],
22082            "each interrupted attempt is repeated, and only those"
22083        );
22084    }
22085
22086    #[tokio::test]
22087    async fn classification_fstat_errors_and_regular_files() {
22088        let fd = random_device_fd();
22089        let (tool, mut guest) = ScriptedGuest::new(true, false);
22090        guest.fstat_errors = [Errno::EIO].into();
22091        assert_eq!(
22092            tool.opened_random_device(&mut guest, fd, None).await,
22093            Err(StatFailure::Call(Errno::EIO)),
22094            "a genuine fstat error is not retried and is the call's failure"
22095        );
22096        assert_eq!(unsafe { libc::close(fd) }, 0);
22097        let (regular, _) = open_file();
22098        assert_eq!(
22099            tool.opened_random_device(&mut guest, regular, None).await,
22100            Ok(false)
22101        );
22102        assert_eq!(unsafe { libc::close(regular) }, 0);
22103    }
22104
22105    #[tokio::test]
22106    async fn classification_falls_back_to_a_transient_page() {
22107        let fd = random_device_fd();
22108        let (tool, mut guest) = ScriptedGuest::new(false, false);
22109        let classified = tool.opened_random_device(&mut guest, fd, None).await;
22110        assert_eq!(unsafe { libc::close(fd) }, 0);
22111        assert_eq!(classified, Ok(true));
22112        assert_eq!(guest.injected, [Sysno::mmap, Sysno::fstat, Sysno::munmap]);
22113        assert_eq!(guest.mapped.len(), 1);
22114        assert_eq!(guest.unmapped, guest.mapped, "the page is unmapped whole");
22115    }
22116
22117    #[tokio::test]
22118    async fn classification_without_any_buffer_is_a_scratch_failure() {
22119        let fd = random_device_fd();
22120        let (tool, mut guest) = ScriptedGuest::new(false, true);
22121        let classified = tool.opened_random_device(&mut guest, fd, None).await;
22122        assert_eq!(unsafe { libc::close(fd) }, 0);
22123        assert_eq!(
22124            classified,
22125            Err(StatFailure::Scratch(Errno::ENOMEM)),
22126            "a failed buffer allocation is not an fstat result"
22127        );
22128        assert_eq!(guest.injected, [Sysno::mmap], "no fstat was attempted");
22129    }
22130
22131    #[tokio::test]
22132    async fn unclassified_close_retries_only_a_close_that_never_ran() {
22133        let (fd, _) = open_file();
22134        let (tool, mut guest) = ScriptedGuest::new(true, false);
22135        guest
22136            .thread
22137            .add_fd(fd, OFlag::O_RDONLY, FdType::Regular, None)
22138            .unwrap();
22139        guest.close_errors = [Errno::ERESTARTSYS, Errno::ERESTARTSYS].into();
22140        let closed = tool.close_unclassified(&mut guest, fd).await;
22141        close_unless_detcore_did(&guest, fd);
22142        assert!(closed.is_ok(), "{closed:?}");
22143        assert_eq!(guest.close_attempts, [fd, fd, fd]);
22144        assert_eq!(guest.closed, [fd]);
22145        assert_eq!(guest.thread.with_detfd(fd, |_| ()), Err(Errno::EBADF));
22146    }
22147
22148    #[tokio::test]
22149    async fn unclassified_close_that_ran_is_not_repeated() {
22150        for errno in [Errno::EINTR, Errno::EIO] {
22151            let (fd, _) = open_file();
22152            let (tool, mut guest) = ScriptedGuest::new(true, false);
22153            guest
22154                .thread
22155                .add_fd(fd, OFlag::O_RDONLY, FdType::Regular, None)
22156                .unwrap();
22157            guest.close_errors = [errno].into();
22158            let closed = tool.close_unclassified(&mut guest, fd).await;
22159            close_unless_detcore_did(&guest, fd);
22160            assert!(closed.is_ok(), "{errno}: {closed:?}");
22161            assert_eq!(guest.close_attempts, [fd], "{errno}: Linux closes once");
22162            assert_eq!(guest.thread.with_detfd(fd, |_| ()), Err(Errno::EBADF));
22163        }
22164    }
22165
22166    #[tokio::test]
22167    async fn unclassified_close_refused_ends_the_run() {
22168        for errors in [vec![Errno::EBADF], vec![Errno::ERESTARTSYS; 16]] {
22169            let (fd, _) = open_file();
22170            let (tool, mut guest) = ScriptedGuest::new(true, false);
22171            guest
22172                .thread
22173                .add_fd(fd, OFlag::O_RDONLY, FdType::Regular, None)
22174                .unwrap();
22175            guest.close_errors = errors.clone().into();
22176            let closed = tool.close_unclassified(&mut guest, fd).await;
22177            close_unless_detcore_did(&guest, fd);
22178            assert!(
22179                matches!(closed, Err(Error::Tool(_))),
22180                "{errors:?}: {closed:?}"
22181            );
22182            assert_eq!(
22183                guest.thread.with_detfd(fd, |_| ()),
22184                Ok(()),
22185                "{errors:?}: a descriptor still open stays modeled"
22186            );
22187        }
22188    }
22189
22190    /// The canonical statfs view: fixed caps on the free counts, clamped to
22191    /// the mount's totals, and a zero `f_fsid`, with the totals kept. These
22192    /// fabricated-input cases moved here from the KVM executor's copy of
22193    /// this canonicalization, deleted in favour of this one.
22194    #[test]
22195    fn canonicalize_statfs_buf_caps_free_counts_and_zeroes_the_fsid() {
22196        let (tool, mut guest) = ScriptedGuest::new(false, false);
22197        let mut canonical = |mut statfs: libc::statfs| {
22198            let buffer = AddrMut::from_raw(std::ptr::from_mut(&mut statfs) as usize);
22199            tool.canonicalize_statfs_buf(&mut guest, buffer).unwrap();
22200            statfs
22201        };
22202        // SAFETY: fsid_t is two initialized integer words on x86-64.
22203        let fsid = |statfs: &libc::statfs| unsafe {
22204            std::mem::transmute::<libc::fsid_t, [i32; 2]>(statfs.f_fsid)
22205        };
22206        // SAFETY: zero is a valid statfs.
22207        let zero = unsafe { std::mem::zeroed::<libc::statfs>() };
22208
22209        // Totals above both caps: the free counts take the caps.
22210        let mut large = zero;
22211        large.f_blocks = 10_000_000;
22212        large.f_bfree = 7_123_456;
22213        large.f_bavail = 6_987_654;
22214        large.f_files = 2_000_000;
22215        large.f_ffree = 1_234_567;
22216        // SAFETY: fsid_t is two integer words on x86-64.
22217        large.f_fsid = unsafe { std::mem::transmute::<[i32; 2], libc::fsid_t>([0x1234, 0x5678]) };
22218        let large = canonical(large);
22219        assert_eq!(
22220            (large.f_bfree, large.f_bavail, large.f_ffree),
22221            (1_000_000, 1_000_000, 500_000)
22222        );
22223        assert_eq!(fsid(&large), [0, 0]);
22224        assert_eq!((large.f_blocks, large.f_files), (10_000_000, 2_000_000));
22225
22226        // Totals under the caps: the free counts are the totals, never more.
22227        let mut small = zero;
22228        small.f_blocks = 42;
22229        small.f_bfree = 40;
22230        small.f_files = 300;
22231        small.f_ffree = 7;
22232        let small = canonical(small);
22233        assert_eq!(
22234            (small.f_bfree, small.f_bavail, small.f_ffree),
22235            (42, 42, 300)
22236        );
22237        assert_eq!((small.f_blocks, small.f_files), (42, 300));
22238
22239        // A filesystem that keeps no inode count reports no free inodes, and
22240        // its free blocks are still clamped to its total.
22241        let mut no_inodes = zero;
22242        no_inodes.f_blocks = 42;
22243        no_inodes.f_ffree = 9;
22244        let no_inodes = canonical(no_inodes);
22245        assert_eq!((no_inodes.f_files, no_inodes.f_ffree), (0, 0));
22246        assert_eq!((no_inodes.f_bfree, no_inodes.f_bavail), (42, 42));
22247
22248        // A null buffer is left alone.
22249        tool.canonicalize_statfs_buf(&mut guest, None).unwrap();
22250    }
22251}
22252
22253#[cfg(test)]
22254mod rebound_path_tests {
22255    use std::path::PathBuf;
22256
22257    use super::rebound_path;
22258
22259    /// A namespace change's operand is reported as the path it names, made
22260    /// absolute. One that could not be read (a tracer may not read a
22261    /// non-dumpable guest's memory) or made absolute is reported as `/`, the
22262    /// ancestor of every path, so that no host input change is named in that
22263    /// run instead of one the guest may have made.
22264    #[test]
22265    fn an_operand_that_cannot_be_established_is_reported_as_the_root() {
22266        assert_eq!(rebound_path(Some(PathBuf::from("/data/F"))), "/data/F");
22267        assert_eq!(rebound_path(None), "/");
22268        assert_eq!(rebound_path(Some(PathBuf::from("F"))), "/");
22269    }
22270}
22271
22272#[cfg(test)]
22273mod untraced_code_tests {
22274    use std::path::Path;
22275    use std::path::PathBuf;
22276
22277    use nix::fcntl::OFlag;
22278    use reverie::syscalls::Errno;
22279    use reverie::syscalls::SyscallArgs;
22280    use reverie::syscalls::Sysno;
22281
22282    use super::DescriptorFacts;
22283    use super::DetPid;
22284    use super::PROC_ROOT_INO;
22285    use super::PidfdSignalScope;
22286    use super::ProcessDirectoryDescriptor;
22287    use super::classify_process_directory;
22288    use super::descriptor_identity;
22289    use super::may_change_untraced_code;
22290    use super::may_write_process_memory;
22291    use super::pidfd_signal_scope;
22292
22293    const PAGE: (u64, u64) = (0x7100_0000, 0x7100_1000);
22294
22295    fn args(arg0: u64, arg1: u64, arg2: u64, arg3: u64, arg4: u64) -> SyscallArgs {
22296        SyscallArgs::new(
22297            arg0 as usize,
22298            arg1 as usize,
22299            arg2 as usize,
22300            arg3 as usize,
22301            arg4 as usize,
22302            0,
22303        )
22304    }
22305
22306    /// A thread cloned without CLONE_FILES has its own descriptor table, so
22307    /// a descriptor is read through the thread that opened it. Through its
22308    /// leader the same number names nothing here, which would make an
22309    /// ordinary write-mode open count as unreadable (a false `/` that
22310    /// withholds a real host input change) and would miss the thread's own
22311    /// process-memory file.
22312    #[test]
22313    fn a_descriptor_is_read_through_the_thread_that_opened_it() {
22314        use std::os::fd::AsRawFd;
22315
22316        let directory = tempfile::tempdir().unwrap();
22317        let ordinary = directory.path().join("out");
22318        let named_mem = directory.path().join("mem");
22319        std::thread::spawn(move || {
22320            // SAFETY: gives only this thread a private copy of the table.
22321            assert_eq!(unsafe { libc::unshare(libc::CLONE_FILES) }, 0);
22322            // SAFETY: plain syscalls on this thread's own descriptors.
22323            let tid = unsafe { libc::gettid() };
22324            let leader = std::process::id() as i32;
22325            let files = [
22326                (std::fs::File::create(&ordinary).unwrap(), false),
22327                (std::fs::File::create(&named_mem).unwrap(), false),
22328                (
22329                    std::fs::OpenOptions::new()
22330                        .read(true)
22331                        .write(true)
22332                        .open("/proc/thread-self/mem")
22333                        .unwrap(),
22334                    true,
22335                ),
22336            ];
22337            for (number, (file, is_process_memory)) in (900..).zip(files) {
22338                assert_eq!(unsafe { libc::dup2(file.as_raw_fd(), number) }, number);
22339                drop(file);
22340                let identity = descriptor_identity(tid, number);
22341                assert!(identity.is_some(), "{number}");
22342                assert_eq!(descriptor_identity(leader, number), None, "{number}");
22343                assert_eq!(
22344                    may_write_process_memory(
22345                        OFlag::O_WRONLY,
22346                        identity
22347                            .as_ref()
22348                            .map(|(link, procfs)| (link.as_path(), *procfs)),
22349                    ),
22350                    is_process_memory,
22351                    "{identity:?}"
22352                );
22353                assert_eq!(unsafe { libc::close(number) }, 0);
22354            }
22355        })
22356        .join()
22357        .unwrap();
22358    }
22359
22360    /// Every call that can replace, unprotect, discard or unmap the range
22361    /// counts when its interval reaches any byte of it, and not when it stops
22362    /// short of it or starts after it.
22363    #[test]
22364    fn a_call_counts_exactly_when_it_may_change_the_range() {
22365        let fixed = libc::MAP_FIXED as u64 | libc::MAP_PRIVATE as u64;
22366        let huge = fixed | libc::MAP_HUGETLB as u64;
22367        let huge_1gb = (30 << libc::MAP_HUGE_SHIFT) as u64;
22368        let remap_fixed = (libc::MREMAP_MAYMOVE | libc::MREMAP_FIXED) as u64;
22369        let remap = libc::SHM_REMAP as u64;
22370        let cases = [
22371            (
22372                "mmap fixed over it",
22373                Sysno::mmap,
22374                args(0x7100_0000, 0x1000, 7, fixed, 0),
22375                true,
22376            ),
22377            (
22378                "mmap fixed reaching it",
22379                Sysno::mmap,
22380                args(0x70ff_f000, 0x1001, 7, fixed, 0),
22381                true,
22382            ),
22383            (
22384                "mmap fixed below it",
22385                Sysno::mmap,
22386                args(0x70ff_f000, 0x1000, 7, fixed, 0),
22387                false,
22388            ),
22389            (
22390                "mmap fixed above it",
22391                Sysno::mmap,
22392                args(0x7100_1000, 0x1000, 7, fixed, 0),
22393                false,
22394            ),
22395            (
22396                "mmap hinted at it",
22397                Sysno::mmap,
22398                args(0x7100_0000, 0x1000, 7, 0x22, 0),
22399                false,
22400            ),
22401            // The host default here is 2 MiB; 0x7100_0000 is 2 MiB aligned.
22402            (
22403                "mmap fixed default huge page holding it",
22404                Sysno::mmap,
22405                args(0x7110_0000, 1, 7, huge, 0),
22406                true,
22407            ),
22408            (
22409                "mmap fixed small page there",
22410                Sysno::mmap,
22411                args(0x7110_0000, 1, 7, fixed, 0),
22412                false,
22413            ),
22414            (
22415                "mmap fixed default huge page below it",
22416                Sysno::mmap,
22417                args(0x70ff_f000, 1, 7, huge, 0),
22418                false,
22419            ),
22420            (
22421                "mmap fixed 1 GiB page holding it",
22422                Sysno::mmap,
22423                args(0x4000_0000, 1, 7, huge | huge_1gb, 0),
22424                true,
22425            ),
22426            (
22427                "mmap fixed 1 GiB page elsewhere",
22428                Sysno::mmap,
22429                args(0x8000_0000, 1, 7, huge | huge_1gb, 0),
22430                false,
22431            ),
22432            (
22433                "mprotect of it",
22434                Sysno::mprotect,
22435                args(0x7100_0000, 0x1000, 7, 0, 0),
22436                true,
22437            ),
22438            (
22439                "mprotect covering it",
22440                Sysno::mprotect,
22441                args(0x7000_0000, 0x200_0000, 7, 0, 0),
22442                true,
22443            ),
22444            (
22445                "mprotect of a zero length",
22446                Sysno::mprotect,
22447                args(0x7100_0000, 0, 7, 0, 0),
22448                true,
22449            ),
22450            (
22451                "pkey_mprotect of it",
22452                Sysno::pkey_mprotect,
22453                args(0x7100_0000, 1, 7, 0, 0),
22454                true,
22455            ),
22456            (
22457                "madvise of it",
22458                Sysno::madvise,
22459                args(0x7100_0000, 0x1000, 4, 0, 0),
22460                true,
22461            ),
22462            (
22463                "munmap of it",
22464                Sysno::munmap,
22465                args(0x7100_0000, 0x1000, 0, 0, 0),
22466                true,
22467            ),
22468            (
22469                "munmap past it",
22470                Sysno::munmap,
22471                args(0x7100_1000, 0x1000, 0, 0, 0),
22472                false,
22473            ),
22474            (
22475                "mremap from it",
22476                Sysno::mremap,
22477                args(0x7100_0000, 0x1000, 0x2000, 1, 0),
22478                true,
22479            ),
22480            (
22481                "mremap fixed onto it",
22482                Sysno::mremap,
22483                args(0x6000_0000, 0x1000, 0x1000, remap_fixed, 0x7100_0000),
22484                true,
22485            ),
22486            (
22487                "mremap fixed of a small page into its gigabyte",
22488                Sysno::mremap,
22489                args(0x6000_0000, 0x1000, 0x1000, remap_fixed, 0x4000_0000),
22490                false,
22491            ),
22492            (
22493                "mremap moving elsewhere",
22494                Sysno::mremap,
22495                args(0x6000_0000, 0x1000, 0x2000, 1, 0x7100_0000),
22496                false,
22497            ),
22498            (
22499                "shmat remapping at it",
22500                Sysno::shmat,
22501                args(3, 0x7100_0000, remap, 0, 0),
22502                true,
22503            ),
22504            (
22505                "shmat remapping below it",
22506                Sysno::shmat,
22507                args(3, 0x6000_0000, remap, 0, 0),
22508                true,
22509            ),
22510            (
22511                "shmat without remap",
22512                Sysno::shmat,
22513                args(3, 0x7100_0000, 0, 0, 0),
22514                false,
22515            ),
22516            (
22517                "shmat anywhere",
22518                Sysno::shmat,
22519                args(3, 0, remap, 0, 0),
22520                false,
22521            ),
22522            ("brk", Sysno::brk, args(0x7100_0000, 0, 0, 0, 0), false),
22523            // A scalar argument that equals an address in the range is not one.
22524            (
22525                "lseek to that offset",
22526                Sysno::lseek,
22527                args(3, 0x7100_0000, 0, 0, 0),
22528                false,
22529            ),
22530            (
22531                "an address near the end",
22532                Sysno::munmap,
22533                args(u64::MAX - 0xfff, 0x1000, 0, 0, 0),
22534                false,
22535            ),
22536        ];
22537        for (label, number, args, expected) in cases {
22538            assert_eq!(
22539                may_change_untraced_code(number, &args, PAGE, 2 << 20),
22540                expected,
22541                "{label}"
22542            );
22543        }
22544    }
22545
22546    /// `pidfd_send_signal`'s flags select one scope, as Linux's do; an unknown
22547    /// bit or two scopes are EINVAL (https://github.com/rrnewton/hermit/issues/4108).
22548    #[test]
22549    fn pidfd_signal_flags_select_one_scope() {
22550        assert_eq!(pidfd_signal_scope(0), Ok(PidfdSignalScope::Process));
22551        assert_eq!(pidfd_signal_scope(1), Ok(PidfdSignalScope::Thread));
22552        assert_eq!(pidfd_signal_scope(2), Ok(PidfdSignalScope::Process));
22553        assert_eq!(pidfd_signal_scope(4), Ok(PidfdSignalScope::ProcessGroup));
22554        for flags in [3, 5, 6, 7, 8, 9, 1 << 31, u32::MAX] {
22555            assert_eq!(pidfd_signal_scope(flags), Err(Errno::EINVAL), "{flags:#x}");
22556        }
22557    }
22558
22559    /// A descriptor names a process for `pidfd_send_signal` only when it is a
22560    /// directory whose parent is the root of the guest's procfs and whose name
22561    /// is a pid; another file is EBADF, and a pidfd Detcore did not open, an
22562    /// unreadable descriptor or another procfs instance cannot be classified.
22563    #[test]
22564    fn a_proc_process_directory_names_its_process() {
22565        use ProcessDirectoryDescriptor as D;
22566        let procfs = |link: &str, parent: Option<(u64, u64)>| DescriptorFacts {
22567            link: PathBuf::from(link),
22568            on_procfs: true,
22569            is_dir: parent.is_some(),
22570            dev: 22,
22571            parent,
22572            guest_proc_dev: Some(22),
22573        };
22574        let reaped = |link: &str| DescriptorFacts {
22575            is_dir: true,
22576            ..procfs(link, None)
22577        };
22578        let root = Some((22, PROC_ROOT_INO));
22579        let cases = [
22580            (
22581                "process",
22582                procfs("/proc/3", root),
22583                D::Process(DetPid::from_raw(3)),
22584            ),
22585            (
22586                "reaped process",
22587                reaped("/proc/7 (deleted)"),
22588                D::Process(DetPid::from_raw(7)),
22589            ),
22590            (
22591                "reaped task directory",
22592                reaped("/proc/7/task/8 (deleted)"),
22593                D::NotAProcess,
22594            ),
22595            (
22596                "reaped elsewhere",
22597                reaped("/mnt/p/7 (deleted)"),
22598                D::Unknown("it names a reaped task outside the guest's /proc"),
22599            ),
22600            (
22601                "procfs mounted elsewhere",
22602                procfs("/mnt/p/3", root),
22603                D::Process(DetPid::from_raw(3)),
22604            ),
22605            (
22606                "task directory",
22607                procfs("/proc/3/task/3", Some((22, 4000))),
22608                D::NotAProcess,
22609            ),
22610            (
22611                "procfs file",
22612                procfs("/proc/3/status", None),
22613                D::NotAProcess,
22614            ),
22615            (
22616                "named root entry",
22617                procfs("/proc/sys", root),
22618                D::NotAProcess,
22619            ),
22620            ("pid zero", procfs("/proc/0", root), D::NotAProcess),
22621            ("leading zero", procfs("/proc/03", root), D::NotAProcess),
22622            ("signed", procfs("/proc/+3", root), D::NotAProcess),
22623            (
22624                "another procfs root",
22625                procfs("/proc/3", Some((23, PROC_ROOT_INO))),
22626                D::NotAProcess,
22627            ),
22628            (
22629                "not the guest's procfs",
22630                DescriptorFacts {
22631                    guest_proc_dev: Some(99),
22632                    ..procfs("/proc/3", root)
22633                },
22634                D::Unknown("it is a procfs that is not the guest's /proc"),
22635            ),
22636            (
22637                "ordinary directory",
22638                DescriptorFacts {
22639                    on_procfs: false,
22640                    ..procfs("/tmp/3", root)
22641                },
22642                D::NotAProcess,
22643            ),
22644            (
22645                "pidfd",
22646                DescriptorFacts {
22647                    on_procfs: false,
22648                    ..procfs("anon_inode:[pidfd]", None)
22649                },
22650                D::Unknown("it is a pidfd that Detcore did not open with pidfd_open"),
22651            ),
22652            (
22653                "pidfs pidfd",
22654                DescriptorFacts {
22655                    on_procfs: false,
22656                    ..procfs("pidfd:[1234]", None)
22657                },
22658                D::Unknown("it is a pidfd that Detcore did not open with pidfd_open"),
22659            ),
22660        ];
22661        for (label, facts, expected) in cases {
22662            assert_eq!(
22663                classify_process_directory(Some(&facts)),
22664                expected,
22665                "{label}"
22666            );
22667        }
22668        assert_eq!(
22669            classify_process_directory(None),
22670            D::Unknown("Hermit cannot read what the descriptor names")
22671        );
22672    }
22673
22674    /// A write-mode open counts when its descriptor is a procfs file named
22675    /// `mem`, or cannot be read at all; a read-only or `O_PATH` open, an
22676    /// ordinary file named `mem`, a pipe, and another procfs file do not.
22677    #[test]
22678    fn an_open_counts_when_it_may_write_process_memory() {
22679        let proc_mem = Some((Path::new("/proc/12/mem"), true));
22680        let cases = [
22681            ("process mem read-write", OFlag::O_RDWR, proc_mem, true),
22682            (
22683                "task mem write-only",
22684                OFlag::O_WRONLY,
22685                Some((Path::new("/proc/12/task/13/mem"), true)),
22686                true,
22687            ),
22688            ("an unreadable descriptor", OFlag::O_WRONLY, None, true),
22689            ("process mem read-only", OFlag::O_RDONLY, proc_mem, false),
22690            (
22691                "process mem as a path",
22692                OFlag::O_PATH | OFlag::O_RDWR,
22693                proc_mem,
22694                false,
22695            ),
22696            (
22697                "an ordinary file named mem",
22698                OFlag::O_WRONLY | OFlag::O_CREAT,
22699                Some((Path::new("/tmp/mem"), false)),
22700                false,
22701            ),
22702            (
22703                "a name that leads to a pipe",
22704                OFlag::O_WRONLY,
22705                Some((Path::new("pipe:[4026]"), false)),
22706                false,
22707            ),
22708            (
22709                "another procfs file",
22710                OFlag::O_WRONLY,
22711                Some((Path::new("/proc/12/attr/current"), true)),
22712                false,
22713            ),
22714            (
22715                "an unreadable read-only descriptor",
22716                OFlag::O_RDONLY,
22717                None,
22718                false,
22719            ),
22720        ];
22721        for (label, flags, descriptor, expected) in cases {
22722            assert_eq!(
22723                may_write_process_memory(flags, descriptor),
22724                expected,
22725                "{label}"
22726            );
22727        }
22728    }
22729}
22730
22731/// Closing advice of every flock refusal. It names no backend: LOCK_NB is the
22732/// form Detcore can serve wherever the backend supports flock, and ENOLCK
22733/// reaches the guest only when the run opted in to unsupported syscalls.
22734const FLOCK_REFUSAL_ADVICE: &str = "LOCK_NB is the supported form where the backend \
22735     supports flock at all; ENOLCK is returned instead only under \
22736     --allow-unsupported-syscalls.";
22737
22738/// Report a flock refusal in every disposition, fail-closed (the run ends)
22739/// and opted in (the guest gets ENOLCK): through tracing, and directly on
22740/// stderr so that neither --log=off nor --log-file hides the reason.
22741fn report_flock_refusal(message: &str) {
22742    error!("{message}");
22743    let _ = std::io::Write::write_fmt(
22744        &mut crate::util::RetryingStderr,
22745        format_args!("hermit: {message}\n"),
22746    );
22747}
22748
22749#[cfg(test)]
22750mod linux_uapi_statx_tests {
22751    use super::statx_arguments_valid;
22752
22753    #[test]
22754    fn statx_sync_modes_keep_linux_positive_and_negative_contract() {
22755        for sync in [0, 0x2000, 0x4000] {
22756            assert!(statx_arguments_valid(sync, 0, false));
22757        }
22758        assert!(!statx_arguments_valid(0x6000, 0, false));
22759        assert!(!statx_arguments_valid(0x6000, 0, true));
22760        assert!(!statx_arguments_valid(0, 0x8000_0000, true));
22761        assert!(!statx_arguments_valid(0x4000_0000, 0, false));
22762        // Preserve the existing descriptor/no-path rule for other flag bits.
22763        assert!(statx_arguments_valid(0x4000_0000, 0, true));
22764    }
22765}