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) = ¬ice_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}