Skip to main content

detcore/
fd.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//! Deterministic file descriptor
10
11use std::collections::VecDeque;
12use std::fmt;
13use std::hash::Hash;
14use std::hash::Hasher;
15use std::path::Path;
16use std::path::PathBuf;
17use std::sync::Arc;
18use std::sync::Mutex;
19use std::sync::OnceLock;
20use std::time::Duration;
21
22use nix::fcntl::OFlag;
23use reverie::syscalls::Errno;
24use serde::Deserialize;
25use serde::Serialize;
26
27use crate::dirents::DirEntry;
28use crate::dirents::DirectoryOrder;
29use crate::dirents::DirectoryStream;
30use crate::procfs::ProcfsFile;
31use crate::procfs::ProcfsSnapshotContext;
32use crate::procfs::TimerSlackReadPreview;
33use crate::procfs_inode::DirectoryInodes;
34use crate::resources::ResourceID;
35use crate::stat::*;
36use crate::tool_global::SnapshotMark;
37use crate::types::RawFd;
38use crate::types::*;
39
40/// file descriptor type
41#[derive(
42    PartialEq,
43    Eq,
44    Debug,
45    Default,
46    Clone,
47    Copy,
48    Hash,
49    Serialize,
50    Deserialize
51)]
52pub enum FdType {
53    /// Regular fd, such as from openat
54    #[default]
55    Regular,
56    /// signalfd
57    Signalfd,
58    /// eventfd
59    Eventfd,
60    /// timerfd
61    Timerfd,
62    /// inotify instance
63    Inotify,
64    /// epoll instance (from epoll_create/epoll_create1)
65    Epoll,
66    /// socket fd
67    Socket,
68    /// pipe fd
69    Pipe,
70    /// memfd
71    Memfd,
72    /// pidfd
73    Pidfd,
74    /// userfaultfd
75    Userfaultfd,
76    /// Random-number generator device
77    Rng,
78}
79
80/// Whether a file of mode `mode` and device number `rdev` is one of Linux's
81/// random devices: character device 1:8 (`/dev/random`) or 1:9
82/// (`/dev/urandom`). Linux names the device by this number, whatever path
83/// reached it (a relative or symbolic-link spelling, `/proc/self/fd/N`, a bind
84/// mount or a copy of the node), so Detcore classifies the object, not the
85/// spelling (https://github.com/rrnewton/hermit/issues/4544).
86pub(crate) fn is_random_device(mode: libc::mode_t, rdev: libc::dev_t) -> bool {
87    mode & libc::S_IFMT == libc::S_IFCHR
88        && libc::major(rdev) == 1
89        && matches!(libc::minor(rdev), 8 | 9)
90}
91
92/// Which of Hermit's random-device streams an open file description reads.
93///
94/// Every read of a Linux random device draws fresh bytes, so no two opens read
95/// the same ones. Detcore gives each open file description of a random device
96/// a stream of its own, named by the task that opened it and by how many
97/// random devices its descriptor table had opened before. Aliases of the
98/// description (`dup`, `fork`) share the stream and its cursor. The sequence
99/// counts only the guest's own opens of random devices, which reach Detcore
100/// on every backend, so the name does not depend on what else a backend's
101/// loader opens; [`OpenFileId`]'s sequence counts those opens too. A
102/// description Detcore discovers rather than sees opened is named by its
103/// descriptor number instead ([`RandomDeviceStream::discovered`]), so that a
104/// backend that discovers descriptors does not shift the names of later opens.
105#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
106pub(crate) struct RandomDeviceStream {
107    pub(crate) creator: DetTid,
108    pub(crate) sequence: u64,
109}
110
111impl RandomDeviceStream {
112    /// The sequences of discovered descriptions, apart from the count's.
113    const DISCOVERED: u64 = 1 << 63;
114
115    /// The name of a random device found at `fd` of a table owned by `owner`.
116    pub(crate) fn discovered(owner: DetTid, fd: RawFd) -> Self {
117        Self {
118            creator: owner,
119            sequence: Self::DISCOVERED | fd as u32 as u64,
120        }
121    }
122}
123
124/// How an active network trace mode classifies a socket's family and type
125/// (see `crate::syscalls::network_trace`).
126#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
127pub(crate) enum NetworkSocketKind {
128    /// Not an IPv4 or IPv6 socket, or no network trace mode was active.
129    #[default]
130    NotInet,
131    /// An IPv4 or IPv6 TCP stream socket. `connect` checks the address
132    /// against the socket's family, as Linux does.
133    InetStream {
134        /// Whether the socket is `AF_INET6`.
135        ipv6: bool,
136    },
137    /// Any other IPv4 or IPv6 socket, such as UDP or raw.
138    InetOther,
139}
140
141/// `IPPROTO_L2TP` (include/uapi/linux/in.h), which the libc crate does not
142/// define.
143const IPPROTO_L2TP: i32 = 115;
144
145/// Whether a receive with `MSG_ERRQUEUE` on a socket that `socket(family,
146/// socket_type, protocol)` created never waits on Linux: its protocol reads the
147/// error queue and fails with `EAGAIN` when that is empty, even on a blocking
148/// socket. These do so: IPv4 and IPv6 TCP, UDP, UDP-Lite, ICMP ping and raw
149/// sockets (`tcp_recvmsg`, `udp_recvmsg`, `udpv6_recvmsg`, `ping_recvmsg`,
150/// `raw_recvmsg`, `rawv6_recvmsg`, through `ip_recv_error` and
151/// `ipv6_recv_error`), MPTCP and SCTP (`mptcp_recvmsg`, `sctp_recvmsg`), IPv6
152/// L2TP (`l2tp_ip6_recvmsg`), and every packet socket (`packet_recvmsg`,
153/// through `sock_recv_errqueue`). Every other socket keeps Detcore's polling:
154/// AF_UNIX and AF_NETLINK ignore the flag and wait as for a plain receive, so
155/// does IPv4 L2TP (`l2tp_ip_recvmsg`), and no other protocol or family is
156/// modelled.
157pub(crate) fn error_queue_read_never_waits(family: i32, socket_type: i32, protocol: i32) -> bool {
158    // Linux's SOCK_TYPE_MASK: the type without SOCK_NONBLOCK and SOCK_CLOEXEC.
159    let socket_type = socket_type & 0xf;
160    match family {
161        libc::AF_INET | libc::AF_INET6 => {
162            let ping = if family == libc::AF_INET {
163                libc::IPPROTO_ICMP
164            } else {
165                libc::IPPROTO_ICMPV6
166            };
167            match socket_type {
168                libc::SOCK_STREAM => matches!(
169                    protocol,
170                    0 | libc::IPPROTO_TCP | libc::IPPROTO_MPTCP | libc::IPPROTO_SCTP
171                ),
172                // SCTP is the only seqpacket protocol, so 0 selects it.
173                libc::SOCK_SEQPACKET => matches!(protocol, 0 | libc::IPPROTO_SCTP),
174                libc::SOCK_DGRAM => {
175                    matches!(protocol, 0 | libc::IPPROTO_UDP | libc::IPPROTO_UDPLITE)
176                        || protocol == ping
177                        || (family == libc::AF_INET6 && protocol == IPPROTO_L2TP)
178                }
179                // Every protocol of a raw socket reaches raw_recvmsg or rawv6_recvmsg.
180                libc::SOCK_RAW => true,
181                _ => false,
182            }
183        }
184        libc::AF_PACKET => true,
185        _ => false,
186    }
187}
188
189/// Read provenance authenticated at the container's initial image: a terminal
190/// checked with TCGETS, the launcher's actually created controller input pipe,
191/// or a readable pipe end the initial image holds on fds 0-2
192/// (`FileMetadata::discover_inherited_pipes`).
193///
194/// The original open-file identity distinguishes aliases from separate opens
195/// of the same tty. The raw identity is used only to validate an exec handoff;
196/// it is never a guest-visible inode or a scheduler resource.
197#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
198pub(crate) struct InheritedExternalRead {
199    pub(crate) origin: OpenFileId,
200    pub(crate) dev: u64,
201    pub(crate) inode: u64,
202    pub(crate) rdev: u64,
203    pub(crate) file_type: u32,
204    pub(crate) access_mode: i32,
205    pub(crate) resource: Option<ResourceID>,
206}
207
208impl std::fmt::Debug for InheritedExternalRead {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        // Kernel identity authenticates a private handoff; distinct launches
211        // must not compare their host pipe/terminal inode numbers.
212        f.debug_struct("InheritedExternalRead")
213            .field("origin", &self.origin)
214            .field("file_type", &self.file_type)
215            .field("access_mode", &self.access_mode)
216            .field("resource", &self.resource)
217            .finish_non_exhaustive()
218    }
219}
220
221/// Deterministic file descriptor
222///
223/// Notice `statbuf` can be cached here, this is because
224/// `stat` is valid as long as fd stays open.
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct DetFd {
227    /// underlying file descriptor
228    pub(crate) fd: RawFd,
229    /// Per-slot descriptor flags, currently only `O_CLOEXEC`.
230    fd_flags: i32,
231    /// State shared by every descriptor referring to the same Linux `struct file`.
232    open_file: Arc<Mutex<OpenFileState>>,
233}
234
235/// A handle on an open file description that Detcore models, which does not
236/// keep it alive: once no modeled descriptor refers to the description, the
237/// handle reaches nothing. Where one tool process serves every guest process
238/// (ptrace, KVM), a rendered procfs descriptor that reaches the same Linux
239/// `struct file` over `SCM_RIGHTS` joins the description through it where no
240/// queued message Detcore can trust names it ([`InFlightOpenFile`] covers
241/// those): when it is received outside the turn, or after a receive Detcore
242/// could not match, or first named (`Detcore::adopt_received_placeholders`).
243#[derive(Clone, Debug)]
244pub(crate) struct OpenFileHandle(std::sync::Weak<Mutex<OpenFileState>>);
245
246impl OpenFileHandle {
247    /// A handle that reaches no description, for tests.
248    #[cfg(test)]
249    pub(crate) fn dangling() -> Self {
250        Self(std::sync::Weak::new())
251    }
252
253    /// Descriptor `fd` of the description, if any modeled descriptor still
254    /// refers to it: an alias, sharing its cursor, snapshot and status flags,
255    /// with `FD_CLOEXEC` as `cloexec` says.
256    pub(crate) fn descriptor(&self, fd: RawFd, cloexec: bool) -> Option<DetFd> {
257        let open_file = self.0.upgrade()?;
258        let mut detfd = DetFd {
259            fd,
260            fd_flags: 0,
261            open_file,
262        };
263        detfd.set_cloexec(cloexec);
264        Some(detfd)
265    }
266
267    /// Whether the description is a socket a guest made in this run and
268    /// never passed out of the guest ([`DetFd::is_guest_created`]); false once
269    /// no modeled descriptor refers to it.
270    pub(crate) fn is_guest_created(&self) -> bool {
271        self.with_description(|d| d.guest_created).unwrap_or(false)
272    }
273
274    /// Runs `f` on the description's model, as [`DetFd::with_description`]
275    /// does, if any modeled descriptor still refers to it.
276    fn with_description<R>(&self, f: impl FnOnce(&mut OpenFileDescription) -> R) -> Option<R> {
277        let open_file = self.0.upgrade()?;
278        let mut state = open_file.lock().expect("open file mutex poisoned");
279        Some(match &mut *state {
280            OpenFileState::Local(description) => f(description),
281            OpenFileState::Shared(shared) => with_shared_description(shared.id, f),
282        })
283    }
284}
285
286/// An open file description that a queued `SCM_RIGHTS` message carries
287/// (`crate::syscalls::scm_rights`), made by [`DetFd::in_flight_open_file`].
288#[derive(Clone, Debug)]
289pub(crate) struct InFlightOpenFile(InFlight);
290
291/// How an [`InFlightOpenFile`] refers to its description.
292#[derive(Clone, Debug)]
293enum InFlight {
294    /// Held as Linux holds the `struct file` of each descriptor a message
295    /// carries: from the send until the receive installs it or Linux
296    /// releases the message. It keeps the description alive, so the receipt
297    /// names the description the sender's table modeled even when every
298    /// modeled descriptor of it has been closed by then, or lives only in the
299    /// table of a sender that has exited, whose state the backend releases
300    /// at no particular point of the guest's schedule. Whether it still
301    /// holds the description decides nothing: the description's count of
302    /// copies in flight does ([`OpenFileDescription::in_flight`]), which only
303    /// guest calls change.
304    Held(Arc<Mutex<OpenFileState>>),
305    /// Under record and replay: the ownership existing recordings were made
306    /// with, which keeps nothing alive ([`OpenFileHandle`]). A receipt there
307    /// joins only a description the receiving table holds anyway, and no
308    /// other decision may see a description only a queued message keeps.
309    Legacy(std::sync::Weak<Mutex<OpenFileState>>),
310}
311
312impl InFlightOpenFile {
313    /// Descriptor `fd` of the description: an alias, sharing its cursor,
314    /// snapshot and status flags, with `FD_CLOEXEC` as `cloexec` says. `None`
315    /// only for a legacy reference (`InFlight::Legacy`) whose description is
316    /// gone.
317    pub(crate) fn descriptor(&self, fd: RawFd, cloexec: bool) -> Option<DetFd> {
318        let open_file = match &self.0 {
319            InFlight::Held(open_file) => Arc::clone(open_file),
320            InFlight::Legacy(open_file) => open_file.upgrade()?,
321        };
322        let mut detfd = DetFd {
323            fd,
324            fd_flags: 0,
325            open_file,
326        };
327        detfd.set_cloexec(cloexec);
328        Some(detfd)
329    }
330
331    /// A queue took this copy in flight (`DetFd::queue_rights_to_peer`):
332    /// the description counts it until a guest call receives or releases
333    /// it ([`OpenFileDescription::in_flight`]).
334    fn enter_flight(&self) {
335        if let InFlight::Held(open_file) = &self.0 {
336            with_state_description(open_file, |d| d.in_flight += 1);
337        }
338    }
339
340    /// A receive installed this copy in the receiving table, which now holds
341    /// the description as a descriptor of its own: it is no longer in
342    /// flight. A peek leaves the message, and its copy, queued.
343    pub(crate) fn land(&self) {
344        if let InFlight::Held(open_file) = &self.0 {
345            with_state_description(open_file, |d| d.in_flight = d.in_flight.saturating_sub(1));
346        }
347    }
348
349    /// Release this copy in flight, as a guest call made Linux release it
350    /// (`Detcore::settle_released_rights`): the description no longer counts
351    /// it. If no copy is in flight any more and no modeled descriptor names
352    /// the description (this is its last reference), the description goes
353    /// with it, as Linux's last `fput` releases the `struct file`: its
354    /// identity and type, and the descriptors of the messages queued to it,
355    /// which go with it in turn. A description that still counts a copy in
356    /// flight is never released, whether or not anything still holds that
357    /// copy: one that went with a table the backend released, or past
358    /// [`MAX_QUEUED_RIGHTS_MESSAGES`], stays counted for the rest of the run.
359    pub(crate) fn release(self) -> Option<(OpenFileId, FdType, Vec<SentDescriptor>)> {
360        let InFlight::Held(open_file) = self.0 else {
361            return None;
362        };
363        let in_flight = with_state_description(&open_file, |d| {
364            d.in_flight = d.in_flight.saturating_sub(1);
365            d.in_flight
366        });
367        if in_flight > 0 {
368            return None;
369        }
370        let state = Arc::into_inner(open_file)?;
371        let state = state.into_inner().expect("open file mutex poisoned");
372        match state {
373            OpenFileState::Local(mut description) => {
374                let queued = std::mem::take(&mut description.rights.queued);
375                let descriptors = queued
376                    .into_iter()
377                    .flat_map(|message| message.descriptors)
378                    .collect();
379                Some((description.id, description.ty, descriptors))
380            }
381            OpenFileState::Shared(shared) => {
382                let ty = with_shared_description(shared.id, |description| description.ty);
383                Some((shared.id, ty, Vec::new()))
384            }
385        }
386    }
387
388    /// Let go of this copy without any guest call to say Linux released it:
389    /// its queue went with a table the backend released, or with a
390    /// description that went that way. The description keeps counting the
391    /// copy in flight, so nothing is released in the model; if this was its
392    /// last reference, the messages queued to it, for the caller to take
393    /// apart in turn without recursing.
394    fn into_queue_if_last(self) -> Vec<QueuedMessage> {
395        let InFlight::Held(open_file) = self.0 else {
396            return Vec::new();
397        };
398        let Some(state) = Arc::into_inner(open_file) else {
399            return Vec::new();
400        };
401        match state.into_inner().expect("open file mutex poisoned") {
402            OpenFileState::Local(mut description) => {
403                std::mem::take(&mut description.rights.queued).into()
404            }
405            OpenFileState::Shared(_) => Vec::new(),
406        }
407    }
408}
409
410/// Runs `f` on the model of the description `open_file` names, as
411/// [`DetFd::with_description`] does.
412fn with_state_description<R>(
413    open_file: &Arc<Mutex<OpenFileState>>,
414    f: impl FnOnce(&mut OpenFileDescription) -> R,
415) -> R {
416    let mut state = open_file.lock().expect("open file mutex poisoned");
417    match &mut *state {
418        OpenFileState::Local(description) => f(description),
419        OpenFileState::Shared(shared) => with_shared_description(shared.id, f),
420    }
421}
422
423/// The kernel's name for the file behind a descriptor: `(st_dev, st_ino)`, as
424/// `fstat` reports it. Every descriptor of one open file description has it.
425pub(crate) type FileIdentity = (u64, u64);
426
427/// One descriptor an `SCM_RIGHTS` message carried, as the sender's table knew
428/// it when the message was sent (`crate::syscalls::scm_rights`).
429#[derive(Clone, Debug)]
430pub(crate) struct SentDescriptor {
431    /// The file it names.
432    pub(crate) file: FileIdentity,
433    /// The open file description the sender's table modeled for it, if any
434    /// ([`InFlightOpenFile`]).
435    pub(crate) open_file: Option<InFlightOpenFile>,
436    /// The description, where `procfs::locks::LockLedger` counts this
437    /// descriptor as a copy in flight of a description whose locks it
438    /// follows, which keeps those locks while the message is queued, as
439    /// Linux's in-flight reference keeps them: the receipt
440    /// (`PlaceholderCopies::Received`) or the release of this descriptor
441    /// (`Detcore::settle_released_rights`) uncounts it.
442    pub(crate) lock_copy: Option<OpenFileId>,
443}
444
445/// The most messages kept queued to one socket ([`SocketRights::queued`]):
446/// far more than a receiver leaves unread in practice. Beyond it the oldest
447/// is dropped from the queue, and its receipt names no open file. Linux still
448/// holds that message, so nothing it carries is released: its descriptions
449/// keep counting their copies in flight ([`OpenFileDescription::in_flight`])
450/// for the rest of the run.
451const MAX_QUEUED_RIGHTS_MESSAGES: usize = 4096;
452
453/// One message queued to a socket ([`SocketRights::queued`]).
454#[derive(Debug, Default)]
455pub(crate) struct QueuedMessage {
456    /// The descriptors its `SCM_RIGHTS` carried, in the order sent: none for
457    /// a message of data alone, which only a queue that holds every message
458    /// keeps (`SocketRights::complete`).
459    pub(crate) descriptors: Vec<SentDescriptor>,
460    /// The bytes it carried; for a stream socket's message, those no read has
461    /// taken yet.
462    pub(crate) bytes: usize,
463}
464
465/// The `SCM_RIGHTS` bookkeeping of one `AF_UNIX` socket: its peer, where
466/// Detcore knows it (the other socket of a `socketpair`, or of a connection
467/// it saw made and accepted), and the descriptors queued to it
468/// (`crate::syscalls::scm_rights`).
469#[derive(Debug, Default)]
470struct SocketRights {
471    /// The socket's type (`SOCK_STREAM`, `SOCK_SEQPACKET`, `SOCK_DGRAM`), as
472    /// `socket` or `accept` made it; `None` for one Detcore did not see made.
473    unix_type: Option<i32>,
474    /// The name a `bind` gave it (`crate::syscalls::scm_rights::unix_socket_name`),
475    /// and which bind of that name it was, by which a listener's connections
476    /// are found (`crate::syscalls::scm_rights::UnixConnections`).
477    unix_name: Option<(Box<[u8]>, u64)>,
478    /// The socket at the other end, which this one's messages reach.
479    peer: Option<OpenFileHandle>,
480    /// Whether the pair is `SOCK_STREAM`, where a send of no bytes carries
481    /// no descriptor.
482    stream: bool,
483    /// Each message sent to this socket with `SCM_RIGHTS`, and, where the
484    /// queue holds every message (`Self::complete`), each one of data alone,
485    /// not yet known to be received, oldest first. Each holds the
486    /// descriptions its message carries ([`InFlightOpenFile`]) until a
487    /// receive takes the message or a later one (`DetFd::take_queued_rights`,
488    /// `DetFd::take_head_message`), Linux purges the queue
489    /// (`DetFd::unpair_rights_peer`), or this socket's description goes with
490    /// its last reference (`DetFd::take_all_queued_rights`).
491    queued: VecDeque<QueuedMessage>,
492    /// Set once a message may have reached this socket without being
493    /// queued: one sent to it out of Detcore's sight, out of the queue's
494    /// order, or past its bound (`DetFd::lose_peer_lockstep`), or one a
495    /// receive took that names no queued message, or a later one than Linux
496    /// can have delivered (`DetFd::take_queued_rights`). A call that took
497    /// messages Detcore could not match does not set it: those messages are
498    /// suspect instead (`Self::suspects`), except under record and replay,
499    /// where such a call sets it, as existing recordings were made with
500    /// (`DetFd::lose_lockstep_if_queued`). While it is set, only order could
501    /// tell an anonymous inode's queued descriptor from an unqueued one's, as
502    /// every anonymous inode has one file identity, so none is joined
503    /// (`Detcore::record_received_facts`). Never regained: a message sent
504    /// without being queued may still be in flight however the queue looks.
505    lockstep_lost: bool,
506    /// How many messages at the front of the queue a call may have taken
507    /// without Detcore matching them, so that Linux may have released them
508    /// already ([`DetFd::lose_lockstep_if_queued`]). Each such call takes one
509    /// message, the oldest Linux holds, so only the first of these can be
510    /// gone after one call, the first two after two, and so on: a message
511    /// past them is still queued in Linux. A receipt that matches one of
512    /// them is trusted only where no later one among them names the same
513    /// files ([`DetFd::take_queued_rights`]), so a message Linux released
514    /// is never joined.
515    suspects: usize,
516    /// Whether the queue holds every message Linux queued to this socket,
517    /// those of data alone included, oldest first, so that a call that takes
518    /// one message without naming its descriptors (a `read`) took the
519    /// oldest queued one (`DetFd::take_head_message`), or on a stream socket
520    /// the bytes it reports from the oldest messages on
521    /// (`DetFd::take_stream_bytes`): set for both sockets of a `socketpair`,
522    /// which nothing but each other can reach while neither is bound to a
523    /// name, and only where Detcore notes every send of data alone (not under
524    /// record and replay, `Detcore::note_send_without_control`).
525    /// Cleared for good ([`give_up_completeness`]) when the queue loses
526    /// lockstep, when the socket gets a name another socket can send to (a
527    /// `bind`, or `SO_PASSCRED`, which names it at its next send), and when
528    /// either socket of the pair is passed to another table, which may hold
529    /// it as an open file of its own (`DetFd::give_up_pair_completeness`).
530    complete: bool,
531}
532
533/// Whether `received`, the identities of the descriptors one receive
534/// installed from one message, are those `sent` queued, in order: all of
535/// them, or a prefix where Linux set `MSG_CTRUNC` (`truncated`) because the
536/// control buffer had room for fewer.
537fn names_sent_rights(sent: &[SentDescriptor], received: &[FileIdentity], truncated: bool) -> bool {
538    !received.is_empty()
539        && (received.len() == sent.len() || (truncated && received.len() < sent.len()))
540        && received
541            .iter()
542            .zip(sent)
543            .all(|(received, sent)| *received == sent.file)
544}
545
546/// Where a stream read of some bytes from a queue that holds every message
547/// (`SocketRights::complete`) leaves it ([`stream_walk`]).
548struct StreamWalk {
549    /// How many messages, from the oldest, it takes in full.
550    taken: usize,
551    /// Of the message after those, how many bytes it takes.
552    part: usize,
553    /// The message whose descriptors it detaches, if any: the first it
554    /// reaches that still carries some.
555    detached: Option<usize>,
556}
557
558/// How a stream read of `read` bytes takes from `rights`'s queue, which holds
559/// every message, as Linux takes it (`unix_stream_read_generic`,
560/// <https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/net/unix/af_unix.c?h=v6.17#n3067>):
561/// bytes from the oldest messages on, a message's descriptors detached with
562/// the first of its bytes a read takes, and no byte past a message whose
563/// descriptors it detached. A read of no bytes that reached the socket (a
564/// `recvfrom` of none) detaches the oldest message's descriptors and takes
565/// nothing. `None` where the queue cannot be what Linux read from: it holds
566/// fewer bytes, or the read went past a message whose descriptors it
567/// detached.
568fn stream_walk(rights: &SocketRights, read: usize) -> Option<StreamWalk> {
569    if read == 0 {
570        let detached = rights
571            .queued
572            .front()
573            .filter(|head| !head.descriptors.is_empty())
574            .map(|_| 0);
575        return Some(StreamWalk {
576            taken: 0,
577            part: 0,
578            detached,
579        });
580    }
581    let mut left = read;
582    let mut detached = None;
583    for (index, message) in rights.queued.iter().enumerate() {
584        if detached.is_some() {
585            return None;
586        }
587        if !message.descriptors.is_empty() {
588            detached = Some(index);
589        }
590        if message.bytes > left {
591            return Some(StreamWalk {
592                taken: index,
593                part: left,
594                detached,
595            });
596        }
597        left -= message.bytes;
598        if left == 0 {
599            return Some(StreamWalk {
600                taken: index + 1,
601                part: 0,
602                detached,
603            });
604        }
605    }
606    None
607}
608
609/// How many bytes a stream read that asked for `asked` takes from `rights`'s
610/// queue, which holds every message, as Linux takes them
611/// (`unix_stream_read_generic`,
612/// <https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/net/unix/af_unix.c?h=v6.17#n3067>):
613/// bytes from the oldest messages on, until it has `asked`, or has taken
614/// bytes of a message that carried descriptors (it stops after the message
615/// whose descriptors it detached), or the queue holds no more.
616fn stream_read_count(rights: &SocketRights, asked: usize) -> usize {
617    let mut read = 0;
618    for message in &rights.queued {
619        let part = message.bytes.min(asked - read);
620        read += part;
621        if read == asked || part < message.bytes || !message.descriptors.is_empty() {
622            break;
623        }
624    }
625    read
626}
627
628/// Takes from `rights`'s queue what `walk` says a stream read took, and
629/// returns the descriptors it detached.
630fn apply_stream_walk(rights: &mut SocketRights, walk: StreamWalk) -> Vec<SentDescriptor> {
631    let detached = walk
632        .detached
633        .and_then(|index| rights.queued.get_mut(index))
634        .map(|message| std::mem::take(&mut message.descriptors))
635        .unwrap_or_default();
636    rights.queued.drain(..walk.taken);
637    if let Some(partly) = rights.queued.front_mut() {
638        partly.bytes -= walk.part;
639    }
640    detached
641}
642
643/// The queue of `rights` no longer holds every message Linux queued to its
644/// socket (`SocketRights::complete`): it keeps only the messages that carried
645/// descriptors, the only ones a receipt can name, and the suspects among them.
646fn give_up_completeness(rights: &mut SocketRights) {
647    if !rights.complete {
648        return;
649    }
650    rights.complete = false;
651    let suspects = rights.suspects;
652    let mut position = 0;
653    let mut kept_suspects = 0;
654    rights.queued.retain(|message| {
655        let keep = !message.descriptors.is_empty();
656        if keep && position < suspects {
657            kept_suspects += 1;
658        }
659        position += 1;
660        keep
661    });
662    rights.suspects = kept_suspects;
663}
664
665/// A socket queued on a socket that is itself queued makes a chain of held
666/// descriptions, as long as a guest cares to build. Dropped recursively, a
667/// long one would overflow the tool thread's stack, so the queue is drained
668/// here and each description it held last is taken apart in turn, its own
669/// queue joining the work, as Linux defers each `fput` rather than recursing.
670///
671/// A queue dropped this way goes with a table the backend released, or with a
672/// description that went that way, at no particular point of the guest's
673/// schedule: it releases nothing in the model, so the descriptions it held
674/// keep counting their copies in flight ([`InFlightOpenFile::into_queue_if_last`]).
675impl Drop for SocketRights {
676    fn drop(&mut self) {
677        let mut pending: Vec<QueuedMessage> = self.queued.drain(..).collect();
678        while let Some(message) = pending.pop() {
679            for sent in message.descriptors {
680                if let Some(open_file) = sent.open_file {
681                    pending.extend(open_file.into_queue_if_last());
682                }
683            }
684        }
685    }
686}
687
688/// What a receive took from a socket's queue ([`DetFd::take_queued_rights`]).
689#[derive(Debug)]
690pub(crate) struct TakenRights {
691    /// The descriptors of the message the receive took, in the order sent.
692    pub(crate) sent: Vec<SentDescriptor>,
693    /// Whether the queue still followed the socket's messages one for one
694    /// (`SocketRights::lockstep_lost`).
695    pub(crate) lockstep: bool,
696    /// Whether the receipt may join the descriptions the message holds: it
697    /// is a message Linux can still have held when the receive ran
698    /// (`SocketRights::suspects`). Otherwise the receipt may be of another
699    /// message that names the same files, and joins nothing.
700    pub(crate) joinable: bool,
701    /// The descriptors of the messages queued before it, which calls Detcore
702    /// could not match took, for their descriptions to be released.
703    pub(crate) forgotten: Vec<SentDescriptor>,
704}
705
706/// A socket's receive timeout (`SO_RCVTIMEO`, socket(7)), as the guest set it.
707// TODO-HUMAN-REVIEW(PR-3908)
708#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
709pub(crate) enum ReceiveTimeout {
710    /// No timeout: a blocking call waits indefinitely. Also what a zero
711    /// value sets.
712    #[default]
713    None,
714    /// A negative second count, which Linux stores as a zero timeout, so a
715    /// blocking call fails at once with `EAGAIN`. `getsockopt` reads it back
716    /// as zero, the same as `None`.
717    Immediate,
718    /// Wait at most this long. Never so long that Linux stores no timeout.
719    After(Duration),
720    /// Set by a call whose value Detcore could not read.
721    Unknown,
722}
723
724impl ReceiveTimeout {
725    /// The timeout a successful `setsockopt(SO_RCVTIMEO)` with `value` sets.
726    /// (Linux has already refused a microsecond count out of range.)
727    ///
728    /// Linux stores a second count too large to count in scheduler ticks, at
729    /// least `LONG_MAX / HZ - 1`, as no timeout (`MAX_SCHEDULE_TIMEOUT`), so
730    /// such an accept restarts after a signal like an untimed one. The model
731    /// takes the bound for the largest `HZ`, 1000; on a kernel with a smaller
732    /// `HZ`, record's check against the kernel refuses a value between the two.
733    pub(crate) fn set_by(value: libc::timeval) -> Self {
734        const UNBOUNDED_SECONDS: libc::time_t = libc::time_t::MAX / 1000 - 1;
735        if value.tv_sec < 0 {
736            Self::Immediate
737        } else if (value.tv_sec == 0 && value.tv_usec == 0) || value.tv_sec >= UNBOUNDED_SECONDS {
738            Self::None
739        } else {
740            Self::After(
741                Duration::from_secs(value.tv_sec as u64)
742                    + Duration::from_micros(value.tv_usec.max(0) as u64),
743            )
744        }
745    }
746}
747
748/// Where the model of one open file description lives.
749///
750/// Every `DetFd` method reaches the model through `DetFd::with_description`,
751/// so a handle's state can change from `Local` to `Shared` under every alias
752/// at once, escaped clones included, because they all hold the same `Arc`.
753// The state always lives inside the one `Arc` allocation its aliases share,
754// so boxing `Local` would add a second allocation to every open.
755#[allow(clippy::large_enum_variant)]
756#[derive(Debug, Serialize, Deserialize)]
757enum OpenFileState {
758    /// Only this process can reach the open file description, so the model
759    /// lives here until a copied runtime or new image needs a shared owner.
760    Local(OpenFileDescription),
761    /// Several guest processes share the open file description, so the one
762    /// canonical model lives in Detcore's global state and this handle holds
763    /// none. Each access takes the model through the installed
764    /// [`SharedOpenFileChannel`] and publishes it back. Promotion precedes
765    /// native fork/exec: <https://github.com/rrnewton/hermit/issues/4381>.
766    Shared(SharedOpenFile),
767}
768
769/// The process-local part of a shared open file description: its identity,
770/// and the runtime lock that is never part of the model.
771#[derive(Debug, Serialize, Deserialize)]
772struct SharedOpenFile {
773    id: OpenFileId,
774    /// Serves `DetFd::directory_lock` for this process's aliases, as the
775    /// description's own lock does for a local description.
776    #[serde(skip)]
777    directory_lock: Arc<futures::lock::Mutex<()>>,
778}
779
780/// The canonical model of an open file description that several guest
781/// processes share. Detcore's global state stores it, and a
782/// [`SharedOpenFileChannel`] carries it to and from the process that holds
783/// the lease. Its contents are private to Detcore.
784#[derive(Debug, Serialize, Deserialize)]
785pub struct OpenFileModel(OpenFileDescription);
786
787impl OpenFileModel {
788    /// The open file description this model describes.
789    pub(crate) fn id(&self) -> OpenFileId {
790        self.0.id
791    }
792
793    #[cfg(test)]
794    pub(crate) fn path_for_test(&self) -> Option<PathBuf> {
795        self.0.path.clone()
796    }
797}
798
799/// The canonical model of one shared open file description, taken by one
800/// process for the length of one synchronous `DetFd` method call.
801#[derive(Debug, Serialize, Deserialize)]
802pub struct OpenFileLease {
803    /// The open file description the lease covers.
804    pub id: OpenFileId,
805    /// Names this lease, so that the global state can refuse a late,
806    /// duplicate or foreign publish.
807    pub sequence: u64,
808    /// The canonical model as of the take.
809    pub model: OpenFileModel,
810}
811
812/// A failure to take or publish a shared open file description.
813#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
814pub struct SharedOpenFileError(pub String);
815
816impl fmt::Display for SharedOpenFileError {
817    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
818        f.write_str(&self.0)
819    }
820}
821
822impl std::error::Error for SharedOpenFileError {}
823
824/// Synchronous access to the canonical models of shared open file
825/// descriptions.
826///
827/// A `DetFd` method on a shared handle calls `take`, runs its synchronous
828/// body on the model, and calls `publish`, all while it holds the handle's
829/// local lock and without awaiting. So a lease never outlives one method
830/// call. Detcore names no backend here: the backend that runs Detcore inside
831/// each guest process installs an implementation with
832/// [`install_shared_open_file_channel`].
833pub trait SharedOpenFileChannel: Send + Sync {
834    /// Publishes the initial local model. A duplicate identity is refused.
835    /// The caller exposes shared handles only after this succeeds.
836    fn promote(&self, model: &OpenFileModel) -> Result<(), SharedOpenFileError>;
837
838    /// Takes the canonical model of `id` and starts a lease on it.
839    fn take(&self, id: OpenFileId) -> Result<OpenFileLease, SharedOpenFileError>;
840
841    /// Stores `lease.model` as the canonical model of `lease.id` and ends the
842    /// lease. A publish that does not name the current lease is refused.
843    fn publish(&self, lease: OpenFileLease) -> Result<(), SharedOpenFileError>;
844}
845
846static SHARED_OPEN_FILE_CHANNEL: OnceLock<Box<dyn SharedOpenFileChannel>> = OnceLock::new();
847
848/// Installs this process's channel to shared open file descriptions. Only the
849/// first channel installed in a process is retained; a later one is returned.
850pub fn install_shared_open_file_channel(
851    channel: Box<dyn SharedOpenFileChannel>,
852) -> Result<(), Box<dyn SharedOpenFileChannel>> {
853    SHARED_OPEN_FILE_CHANNEL.set(channel)
854}
855
856/// Runs `f` on the canonical model of the shared open file description `id`.
857///
858/// A missing channel, a failed take or publish, or a lease for another
859/// description is an infrastructure failure, not something the guest's
860/// syscall can report, so it panics. The model is published even when `f`
861/// returns an error value, so the lease always ends; `f` itself decides what
862/// it changed. If `f` panics, the lease is never published and the process
863/// is lost with it.
864fn with_shared_description<R>(id: OpenFileId, f: impl FnOnce(&mut OpenFileDescription) -> R) -> R {
865    let channel = SHARED_OPEN_FILE_CHANNEL.get().unwrap_or_else(|| {
866        panic!(
867            "open file description {id:?} is shared, but no shared open file channel is installed"
868        )
869    });
870    let mut lease = channel.take(id).unwrap_or_else(|error| {
871        panic!("taking shared open file description {id:?} failed: {error}")
872    });
873    assert_eq!(
874        lease.id, id,
875        "the shared open file channel returned a lease for another description"
876    );
877    assert_eq!(
878        lease.model.0.id, id,
879        "the shared open file channel returned the model of another description"
880    );
881    let result = f(&mut lease.model.0);
882    channel.publish(lease).unwrap_or_else(|error| {
883        panic!("publishing shared open file description {id:?} failed: {error}")
884    });
885    result
886}
887
888#[derive(Serialize, Deserialize)]
889struct OpenFileDescription {
890    id: OpenFileId,
891    /// fd type
892    ty: FdType,
893    /// Immutable inherited-read provenance. Neither guest-created PTYs nor
894    /// unknown FIFOs acquire it merely by becoming stdio.
895    #[serde(default)]
896    inherited_external_read: Option<InheritedExternalRead>,
897    /// Process named by a pidfd created through `pidfd_open`.
898    ///
899    /// This is shared by descriptor aliases just like the kernel pidfd object.
900    /// `None` means either that this is not a pidfd or that Detcore did not
901    /// observe enough provenance to identify its target.
902    #[serde(default)]
903    pidfd_target: Option<DetPid>,
904    /// The task directory this open file is, where it was opened as one: a
905    /// `/proc/self` or `/proc/thread-self` spelling with the selector replaced
906    /// by the opener's numbers, or a `/proc/<pid>` or `/proc/<pid>/task/<tid>`
907    /// spelling itself, and the identity of the task it named then. A name
908    /// relative to it is looked up below that task's directory, whoever looks
909    /// it up, while that task lives, as Linux binds a task directory to its
910    /// task at the open (`Detcore::procfs_spelling_at`). The recorded `path`
911    /// keeps the spelling. Shared by descriptor aliases and a fork's copies.
912    #[serde(default)]
913    procfs_reached: Option<(PathBuf, Option<crate::procfs::TaskIncarnation>)>,
914    /// This open file is the `/proc/self` or `/proc/thread-self` link itself
915    /// (an `O_PATH | O_NOFOLLOW` open), not the directory it leads to, so no
916    /// name is below it (ENOTDIR), whatever its recorded `path` spells.
917    #[serde(default)]
918    procfs_link_itself: bool,
919    /// This open file is a `/proc` directory Detcore serves itself, `/proc`
920    /// or a task's descriptor directory, `/proc/<pid>/fd`
921    /// (`procfs::fd_directory::PlaceholderDirectory`): which directory, its
922    /// position, and whether it was opened with `O_PATH`. The kernel's
923    /// descriptor is on the placeholder directory, an empty read-only tmpfs
924    /// (`DetConfig::directory_placeholder`). Shared by descriptor aliases and
925    /// a fork's copies, as Linux shares the position.
926    #[serde(default)]
927    placeholder: Option<PlaceholderState>,
928    /// File status flags shared by dup and fork aliases.
929    status_flags: i32,
930    /// File path associated with fd.
931    /// This cannot be relied upon. Special devices won't have it, for example.
932    path: Option<PathBuf>,
933    /// Cached det/virtual inode.
934    /// This cannot be relied upon. Special devices won't have it, for example.
935    /// However if `ty` indicates a `Regular` file, then there should reliably be an inode.
936    inode: Option<DetInode>,
937    /// inode is dirty
938    dirty: bool,
939
940    /// Irrespective of whether the file descriptor is marked logically blocking by the
941    /// user, this tracks whether Detcore has converted the fd to nonblocking for its own
942    /// purposes.
943    physically_nonblocking: bool,
944
945    /// A FIFO opened by path in `hermit run`, still `Regular` and physically
946    /// blocking: its first blocking read or write that finds a guest holding
947    /// the other end makes it an internally polled `Pipe`
948    /// (`Detcore::fifo_polls_if_a_guest_holds_its_peer`).
949    #[serde(default)]
950    fifo_awaiting_guest_peer: bool,
951
952    /// A FIFO opened by path that joined internal polling
953    /// (`DetFd::fifo_joins_internal_polling`): any process that can reach its
954    /// path, inside the container or not, may also hold an end of it.
955    #[serde(default)]
956    named_fifo: bool,
957    /// An inotify instance received over SCM_RIGHTS without the open file
958    /// description its sender modelled (an unmatched receipt), whose host
959    /// description is nonblocking, in a run where Detcore makes every inotify
960    /// instance it creates physically nonblocking: that O_NONBLOCK may be
961    /// Detcore's rather than the sender's, so the guest's blocking mode is
962    /// unknown (`Detcore::record_received_descriptors`). Cleared once the
963    /// guest sets the mode itself (F_SETFL, FIONBIO).
964    #[serde(default)]
965    blocking_mode_unknown: bool,
966
967    /// cached statbuf
968    ///
969    /// This is the RAW stat from the file system, NOT determinized.
970    ///
971    /// Some of these fields will change at runtime. But the following fields will
972    /// be constant when `virtualize_metadata` is on, over the life of the DetFd:
973    ///  - dev, rdev, blksize
974    ///
975    /// This should always be `Some` for regular files, as we eagerly populate it.
976    stat: Option<DetStat>,
977    // AUTONOMOUS-BOT-IMPLEMENTED
978    // TODO-HUMAN-REVIEW(PR-1096): Review canonical random-device cursor sharing.
979    /// Cursor into Hermit's backend-independent random-device byte stream.
980    #[serde(default)]
981    random_device_offset: u64,
982    /// The stream this random-device open file reads.
983    #[serde(default)]
984    random_device_stream: RandomDeviceStream,
985    /// resource
986    resource: Option<ResourceID>,
987    /// Deterministic snapshot state for selected procfs files.
988    procfs: Option<ProcfsFile>,
989    /// Signal phase 1's kernel account of the file, recorded at its open by a
990    /// process that handles SIGALRM (`sigalrm_phase1::kernel_provenance`).
991    #[serde(default)]
992    sigalrm_phase1: Option<crate::sigalrm_phase1::Phase1Provenance>,
993    /// Sorted directory stream, created by the first `getdents` call that
994    /// succeeds on this open file description.
995    #[serde(default)]
996    directory: Option<DirectoryStream>,
997    /// True while `getdents` on this open file reads the host directory one
998    /// kernel buffer at a time, because no whole-directory snapshot can stand
999    /// in for its position. See `use_host_directory_order`; an `lseek` back
1000    /// to 0 clears it.
1001    #[serde(default)]
1002    directory_host_order: bool,
1003    /// Held across a whole `getdents` or `lseek` on this open file
1004    /// description. Reading the host directory takes several injected
1005    /// syscalls on the shared kernel position, and the kernel's own per-file
1006    /// position lock covers only one of them; without this, two unsequentialized
1007    /// threads could interleave their reads and each keep part of the
1008    /// directory. Sequentialized threads never contend for it.
1009    ///
1010    /// This must stay a runtime-agnostic lock that uses no thread-locals.
1011    /// The DBT backend polls these handlers on DynamoRIO application
1012    /// threads, outside any Tokio runtime. Tokio's own locks charge each
1013    /// poll to a per-thread budget kept in a thread-local that has a
1014    /// destructor, and registering a thread-local destructor on a DynamoRIO
1015    /// application thread crashes DynamoRIO: the run exits with status 255
1016    /// and loses its evidence FINAL frames.
1017    #[serde(skip)]
1018    directory_lock: Arc<futures::lock::Mutex<()>>,
1019    /// Logical timestamp of the last packet delivered through this socket.
1020    socket_receive_timestamp: Option<LogicalTime>,
1021    /// True when this open file is an `AF_NETLINK`/`NETLINK_SOCK_DIAG` socket,
1022    /// whose binary dump replies carry host-assigned socket inode numbers that
1023    /// must be determinized (see `crate::sock_diag`).
1024    sock_diag: bool,
1025    /// Whether this open file is a `NETLINK_ROUTE` socket whose link-dump
1026    /// replies carry live interface counters that must be zeroed (see
1027    /// `crate::netlink_route`).
1028    netlink_route: bool,
1029    /// True when this socket connected to an IPv4 or IPv6 loopback peer.
1030    #[serde(default)]
1031    loopback_peer: bool,
1032    /// True when this socket is a channel of the external network trace, so
1033    /// its traffic is recorded or replayed (see `crate::syscalls::network_trace`).
1034    #[serde(default)]
1035    network_channel: bool,
1036    /// The socket's `SO_RCVLOWAT` while a network trace mode is active, or
1037    /// `None` for the default of one byte.
1038    #[serde(default)]
1039    network_lowat: Option<usize>,
1040    /// The socket's family and type, recorded at creation while a network
1041    /// trace mode is active.
1042    #[serde(default)]
1043    network_socket: NetworkSocketKind,
1044    /// True when this socket originated in `socketpair(2)`, selecting live
1045    /// internal routing. This does not prove guest-only readiness: an endpoint
1046    /// may be exported, or a datagram endpoint rebound. See
1047    /// `syscall_targets_internal_fd` and `is_guest_held`.
1048    #[serde(default)]
1049    socketpair_endpoint: bool,
1050    /// True when record/replay found that this socket's peer is a guest of
1051    /// this container, so only guest progress can end a wait on it, as for a
1052    /// socketpair endpoint. See `Detcore::classify_socket_peer`.
1053    #[serde(default)]
1054    guest_peer: bool,
1055    /// True when `accept(2)` or `accept4(2)` returned this socket. Record and
1056    /// replay keep such a connection only in the log, and replay stands in an
1057    /// eventfd for it. See `is_accepted_connection`.
1058    #[serde(default)]
1059    accepted_connection: bool,
1060    /// True when a receive with `MSG_ERRQUEUE` on this socket never waits on
1061    /// Linux, whatever the socket's blocking mode. Set by `socket(2)`
1062    /// (`error_queue_read_never_waits`) and inherited by the connections
1063    /// `accept(2)` returns.
1064    #[serde(default)]
1065    error_queue_never_waits: bool,
1066    /// True when only guests hold this object's other end, so only a guest
1067    /// can change whether it is readable or writable: a pipe or socketpair a
1068    /// guest created, an eventfd, an `AF_UNIX` socket whose peer is the
1069    /// holder's own process (a connection it made or accepted), or a pipe or
1070    /// socket the process received from itself whose object it already held
1071    /// through a guest-held description. Cleared, permanently, when a guest passes the
1072    /// object, or any object while it holds one, over `SCM_RIGHTS` on a
1073    /// socket that is not guest-held itself. A received descriptor that joins
1074    /// its sender's open file (`crate::syscalls::scm_rights`) shares this
1075    /// flag, as a dup does; one that joins nothing starts without it. See
1076    /// `is_guest_held`.
1077    #[serde(default)]
1078    guest_held: bool,
1079    /// True when a guest made this socket with `socket(2)` or
1080    /// `socketpair(2)` in this run and has not passed it out of the guest
1081    /// since, so only guests hold it. Never set for a received, inherited or
1082    /// accepted description. A connection is marked guest-held only when both
1083    /// its connecting socket and its listener have this flag
1084    /// (`Detcore::classify_guest_held_peer`): a socket's peer credentials name
1085    /// its peer, not every holder of its open file. Cleared, permanently,
1086    /// when a guest passes it over `SCM_RIGHTS` on a socket that is not
1087    /// guest-held. See `is_guest_created`.
1088    #[serde(default)]
1089    guest_created: bool,
1090    /// True when record/replay runs a blocking `accept(2)` on this socket in
1091    /// the caller's turn, one nonblocking attempt at a time. `socket(2)` sets
1092    /// it for a stream socket the container created. See
1093    /// `is_accept_in_turn_listener`.
1094    // TODO-HUMAN-REVIEW(PR-3908)
1095    #[serde(default)]
1096    accept_in_turn_eligible: bool,
1097    /// True once a guest's `shutdown(2)` with `SHUT_RD` or `SHUT_RDWR` shut
1098    /// this socket's read side. A polled blocking receive on it then asks the
1099    /// kernel whether the call would still wait. Permanent. See
1100    /// `read_shutdown_requested`.
1101    #[serde(default)]
1102    read_shutdown_requested: bool,
1103    /// True once this socket was named in an `SCM_RIGHTS` message, so a
1104    /// process outside Detcore's view may hold it. Permanent. See
1105    /// `is_exported`.
1106    #[serde(default)]
1107    exported: bool,
1108    /// The receive timeout the last successful `setsockopt(SO_RCVTIMEO)` on
1109    /// this socket set, which a blocking accept in turn waits by. See
1110    /// `receive_timeout`.
1111    #[serde(default)]
1112    receive_timeout: ReceiveTimeout,
1113    // AUTONOMOUS-BOT-IMPLEMENTED
1114    // TODO-HUMAN-REVIEW(#2373)
1115    /// The `flock(2)` mode this open file description currently holds, as the
1116    /// bare `LOCK_SH`/`LOCK_EX` constant, or `None` when it holds no lock.
1117    ///
1118    /// `flock` locks are a property of the open file description, not of the
1119    /// descriptor slot and not of the inode, which is exactly the granularity
1120    /// this struct models: `dup`/`fork` aliases share one lock, two separate
1121    /// `open`s of the same file contend with each other.
1122    ///
1123    /// Detcore tracks this only so `handle_flock` can tell a first acquisition
1124    /// (where a failed `LOCK_NB` probe changes nothing) from a *conversion* of
1125    /// an already-held lock (where Linux drops the old lock before it can fail).
1126    /// It is a cache of what Detcore itself granted, never an authority: it is
1127    /// written only after the kernel reports success.
1128    #[serde(default)]
1129    flock_mode: Option<i32>,
1130    /// Whether `flock_mode` describes the kernel state. Descriptors created by
1131    /// an intercepted syscall start known-unlocked; descriptors discovered
1132    /// after entering the guest can already carry a lock.
1133    #[serde(default)]
1134    flock_mode_known: bool,
1135    /// Whether Detcore has EVER known this description's lock state.
1136    ///
1137    /// This separates two different unknowns that `flock_mode_known == false`
1138    /// otherwise collapses. A descriptor Detcore never observed -- stdin,
1139    /// stdout, stderr, a live-discovered fd -- has no cached claim at all, so
1140    /// there is nothing about it that can be STALE. A descriptor that was known
1141    /// and then invalidated by a process copy does have a claim that may now be
1142    /// wrong. Only the second is a reason to refuse `vfork`.
1143    #[serde(default)]
1144    flock_mode_ever_known: bool,
1145    /// Whether Detcore may ever have granted a `flock(2)` `LOCK_SH` or
1146    /// `LOCK_EX` through this Detcore model of the description.
1147    ///
1148    /// Set by every successful grant Detcore serves through the description,
1149    /// INCLUDING while `flock_mode_known` is false: a forgotten description no
1150    /// longer records which mode it holds, but it still records that it may
1151    /// hold one. Descriptions Detcore never observed (`mark_flock_mode_unobserved`)
1152    /// set it as well, since a lock may have been taken before Detcore saw
1153    /// them. Never cleared, and copied with the description at fork.
1154    ///
1155    /// It records only grants Detcore served through THIS model of the
1156    /// description. A lock taken natively (by a copied `vfork` child), by a
1157    /// sibling process whose copy of the model was split from this one at a
1158    /// fork, or through another model of the same kernel description (an
1159    /// `SCM_RIGHTS` receiver's) does not set it. False therefore does NOT
1160    /// mean unlocked; it means no grant through this model, so no Detcore
1161    /// claim about this description can exist here.
1162    ///
1163    /// `has_unsafe_vfork_flock_state` uses it to tell an invalidated
1164    /// description through which this process's Detcore granted a lock from
1165    /// one that was invalidated by a fork without such a grant (a pipe a
1166    /// `make` created before forking a job). In the second case no process
1167    /// holds a known mode for the description, so a copied `vfork` child that
1168    /// changes its lock natively makes no Detcore claim stale. A serialized
1169    /// description without the field reads as true, the conservative value;
1170    /// that default applies only to self-describing (map-based) formats.
1171    /// Not printed, so descriptions log as before.
1172    #[serde(default = "flock_ever_granted_when_absent")]
1173    flock_ever_granted: bool,
1174    /// A timerfd's expirations as a function of virtual time, once the guest
1175    /// has armed or disarmed it through Detcore (`VirtualTimerfd`). `None` for
1176    /// every other description, and for a timerfd Detcore leaves to the host
1177    /// (record/replay, or no virtual time).
1178    #[serde(default)]
1179    timerfd: Option<VirtualTimerfd>,
1180    /// The pipe or FIFO this description is an end of, for handing the turn
1181    /// to a thread polling it once another thread readies it
1182    /// (`--pipe-wake-latency`, https://github.com/rrnewton/hermit/issues/4380).
1183    /// Set only while that option is on; `None` otherwise, and for a
1184    /// descriptor whose channel Detcore does not know.
1185    #[serde(default)]
1186    channel: Option<crate::pipe_wake::ChannelEnd>,
1187    /// True once Detcore read this description's OFD and flock locks from its
1188    /// fdinfo after a call through it (`Detcore::refresh_description_locks`):
1189    /// from then on, each table's last close or replacement of it is reported
1190    /// to `procfs::locks::LockLedger`. Shared by every alias, as the
1191    /// description is. Not printed, so descriptions log as before.
1192    #[serde(default)]
1193    lock_tracked: bool,
1194    /// True once this description was registered on reception over
1195    /// `SCM_RIGHTS` (`Detcore::refresh_description_locks`): the sender's
1196    /// entry in `procfs::locks::LockLedger` may name the same kernel
1197    /// description under another identity. Not printed.
1198    #[serde(default)]
1199    received_for_locks: bool,
1200    /// True once this description may hold an OFD lock: an `F_OFD_SETLK` or
1201    /// `F_OFD_SETLKW` went through it, its fdinfo showed one, or its fdinfo
1202    /// could not be read. DBT refuses a copied `vfork` child, which would run
1203    /// its lock calls natively, while one does. Not printed.
1204    #[serde(default)]
1205    may_hold_ofd_lock: bool,
1206    /// True when this is an open file of a per-process file that follows
1207    /// its record's sequence (`ProcfsFile::follows_record_sequence`), which
1208    /// Linux keeps in the one `struct file` every holder shares, and this
1209    /// process's Detcore cannot know that sequence: a rendered file rebuilt
1210    /// from a placeholder that reached this process over `SCM_RIGHTS` when
1211    /// neither its table nor its registry knows the open file
1212    /// (`Detcore::adopt_received_placeholder`,
1213    /// `FileMetadata::adopt_placeholder`), or such a file the kernel opened,
1214    /// received or found so (`tool_local::kernel_record_file_link`). Every
1215    /// read, positioned read, vectored read and seek through it is refused by
1216    /// name (`Detcore::refuse_unknown_record_sequence`), so it never moves a
1217    /// sequence another holder's Detcore keeps, nor reads a record Linux would
1218    /// not show (on SaBRe, where a refusal that ended the run would strand it,
1219    /// `refusal_would_strand_the_run`, the call gets EOPNOTSUPP). Shared by
1220    /// every alias, copied at fork,
1221    /// and carried across exec (`ExecProcfsRecord`). Not printed.
1222    #[serde(default)]
1223    record_sequence_unknown: bool,
1224    /// For an `AF_UNIX` socket: its type, name and peer, and the
1225    /// descriptors queued to it in `SCM_RIGHTS` messages
1226    /// (`crate::syscalls::scm_rights`). It names descriptions by handles
1227    /// that reach them only in the tool process that serves every guest
1228    /// (ptrace, KVM), so it is never serialized. Not printed.
1229    #[serde(skip)]
1230    rights: SocketRights,
1231    /// How many copies of this description the `SCM_RIGHTS` messages
1232    /// Detcore queued carry in flight, counted by guest calls alone: the send
1233    /// that queues a copy adds it, and the receive that installs it or the
1234    /// call that makes Linux release it takes it away
1235    /// ([`InFlightOpenFile::release`]). A copy whose queue went any other way
1236    /// (with a table the backend released, at no particular point of the
1237    /// guest's schedule, or past [`MAX_QUEUED_RIGHTS_MESSAGES`]) stays
1238    /// counted for the rest of the run. While any is counted, the
1239    /// description is never released, nor its socket's port, nor its queue
1240    /// ([`DetFd::is_last_reference`]). Not printed.
1241    #[serde(skip)]
1242    in_flight: usize,
1243    /// What Linux's fdinfo of this description's file prints that no call
1244    /// reads back, recorded where the guest set it through Detcore
1245    /// ([`FdinfoFacts`]). `None` for a description of no such kind, and for
1246    /// one Detcore did not create (inherited, received without joining the
1247    /// description its sender's table modeled, or rediscovered after an
1248    /// exec), whose fdinfo it then cannot render. Shared by every alias,
1249    /// as the file is. Not printed, so descriptions log as before.
1250    #[serde(default)]
1251    fdinfo_facts: Option<FdinfoFacts>,
1252    /// For one of the container's standard streams whose open file
1253    /// description the launcher passed on another of them too, as the
1254    /// launcher's `kcmp(KCMP_FILE)` of the descriptors it handed the guest
1255    /// answered (`Config::standard_stream_aliases`, which
1256    /// `FileMetadata::setup_stdio` applies): the group's key, the identity of its
1257    /// lowest stream, by which `procfs::locks::LockLedger` takes the group as
1258    /// the one description it is. Shared by every alias. Not printed.
1259    #[serde(default)]
1260    stdio_alias_group: Option<OpenFileId>,
1261}
1262
1263/// State of an anonymous-inode file that its `/proc/<pid>/fdinfo/<fd>`
1264/// prints and no call reads back, as the guest set it through Detcore's
1265/// handlers (package K slice K8-h, `Detcore::render_own_fdinfo`).
1266#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1267pub(crate) enum FdinfoFacts {
1268    /// An eventfd: whether `eventfd2` made it with `EFD_SEMAPHORE`
1269    /// (`eventfd-semaphore`, fs/eventfd.c).
1270    Eventfd {
1271        /// `EFD_SEMAPHORE`.
1272        semaphore: bool,
1273    },
1274    /// A signalfd: the signals it reads, as `signalfd4` last set them, bit
1275    /// `n - 1` for signal `n`, without `SIGKILL` and `SIGSTOP`, which Linux
1276    /// takes out (`sigmask:`, fs/signalfd.c).
1277    Signalfd {
1278        /// The mask.
1279        mask: u64,
1280    },
1281    /// A timerfd: its clock, the flags of its last `timerfd_settime`, and
1282    /// Linux's own timer (`clockid:`, `settime flags:`, `ticks:` and
1283    /// `it_value:`, fs/timerfd.c).
1284    Timerfd {
1285        /// The clock `timerfd_create` named.
1286        clockid: i32,
1287        /// `TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET` of the last setting;
1288        /// 0 before any.
1289        settime_flags: u32,
1290        /// Linux's own timer, as the guest's settings, reads and
1291        /// `timerfd_gettime` calls left it.
1292        linux: LinuxTimerfd,
1293    },
1294}
1295/// Written out, not derived, so that `channel` is printed only when it is set:
1296/// a run without `--pipe-wake-latency` logs every description exactly as
1297/// before the field existed.
1298impl std::fmt::Debug for OpenFileDescription {
1299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1300        let mut debug = f.debug_struct("OpenFileDescription");
1301        debug.field("id", &self.id);
1302        debug.field("ty", &self.ty);
1303        debug.field("inherited_external_read", &self.inherited_external_read);
1304        debug.field("pidfd_target", &self.pidfd_target);
1305        debug.field("procfs_reached", &self.procfs_reached);
1306        if let Some(placeholder) = &self.placeholder {
1307            debug.field("placeholder", placeholder);
1308        }
1309        debug.field("status_flags", &self.status_flags);
1310        debug.field("path", &self.path);
1311        debug.field("inode", &self.inode);
1312        debug.field("dirty", &self.dirty);
1313        debug.field("physically_nonblocking", &self.physically_nonblocking);
1314        debug.field("fifo_awaiting_guest_peer", &self.fifo_awaiting_guest_peer);
1315        debug.field("named_fifo", &self.named_fifo);
1316        if self.blocking_mode_unknown {
1317            debug.field("blocking_mode_unknown", &true);
1318        }
1319        debug.field("stat", &self.stat);
1320        debug.field("random_device_offset", &self.random_device_offset);
1321        debug.field("random_device_stream", &self.random_device_stream);
1322        debug.field("resource", &self.resource);
1323        debug.field("procfs", &self.procfs);
1324        debug.field("sigalrm_phase1", &self.sigalrm_phase1);
1325        debug.field("directory", &self.directory);
1326        debug.field("directory_host_order", &self.directory_host_order);
1327        debug.field("directory_lock", &self.directory_lock);
1328        debug.field("socket_receive_timestamp", &self.socket_receive_timestamp);
1329        debug.field("sock_diag", &self.sock_diag);
1330        debug.field("netlink_route", &self.netlink_route);
1331        debug.field("loopback_peer", &self.loopback_peer);
1332        debug.field("network_channel", &self.network_channel);
1333        debug.field("network_lowat", &self.network_lowat);
1334        debug.field("network_socket", &self.network_socket);
1335        debug.field("socketpair_endpoint", &self.socketpair_endpoint);
1336        debug.field("guest_peer", &self.guest_peer);
1337        debug.field("accepted_connection", &self.accepted_connection);
1338        debug.field("guest_held", &self.guest_held);
1339        debug.field("guest_created", &self.guest_created);
1340        debug.field("accept_in_turn_eligible", &self.accept_in_turn_eligible);
1341        debug.field("read_shutdown_requested", &self.read_shutdown_requested);
1342        debug.field("exported", &self.exported);
1343        debug.field("receive_timeout", &self.receive_timeout);
1344        debug.field("flock_mode", &self.flock_mode);
1345        debug.field("flock_mode_known", &self.flock_mode_known);
1346        debug.field("flock_mode_ever_known", &self.flock_mode_ever_known);
1347        debug.field("timerfd", &self.timerfd);
1348        if let Some(channel) = &self.channel {
1349            debug.field("channel", channel);
1350        }
1351        debug.finish()
1352    }
1353}
1354
1355/// What Detcore keeps of an open file that is a `/proc` directory it serves
1356/// on a placeholder descriptor (`procfs::fd_directory::PlaceholderDirectory`).
1357#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1358pub(crate) struct PlaceholderState {
1359    /// Which directory: `/proc`, or a descriptor directory and the task it
1360    /// is bound to.
1361    pub(crate) directory: crate::procfs::fd_directory::PlaceholderDirectory,
1362    /// The kernel position that tags the descriptor
1363    /// (`procfs::fd_directory::tag`), if it has one.
1364    pub(crate) tag: Option<u64>,
1365    /// The guest's position in a descriptor directory, as Linux numbers it.
1366    /// `/proc`'s position is its directory stream's (`DirectoryStream`).
1367    pub(crate) position: u64,
1368    /// Opened with `O_PATH`: it lists nothing, and reads and seeks fail with
1369    /// `EBADF`.
1370    pub(crate) path_only: bool,
1371}
1372
1373/// A timerfd's expirations counted on Detcore's virtual clock instead of the
1374/// host's (timerfd_create(2)).
1375///
1376/// The host timerfd behind the descriptor is never armed with the guest's
1377/// setting, so the host cannot make it readable at a host-timed moment.
1378/// Instead, when a thread of the process observes the descriptor after the
1379/// virtual deadline (`crate::syscalls::timerfd`), Detcore counts the
1380/// expirations here and makes the host descriptor readable before the guest's
1381/// probe runs; a `read` then returns the count kept here.
1382#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1383pub(crate) struct VirtualTimerfd {
1384    /// The next expiration not yet counted, or `None` when disarmed.
1385    pub(crate) deadline: Option<LogicalTime>,
1386    /// The reload interval in nanoseconds, 0 for a one-shot timer.
1387    pub(crate) interval_ns: u64,
1388    /// Expirations counted but not yet read.
1389    pub(crate) unread: u64,
1390}
1391
1392impl VirtualTimerfd {
1393    /// The state `timerfd_settime` leaves: armed at `deadline` (or disarmed
1394    /// with `None`), with no expiration pending, as Linux discards the count.
1395    /// A disarmed timer keeps `interval_ns`, which `timerfd_gettime` reports.
1396    pub(crate) fn set(deadline: Option<LogicalTime>, interval_ns: u64) -> Self {
1397        VirtualTimerfd {
1398            deadline,
1399            interval_ns,
1400            unread: 0,
1401        }
1402    }
1403
1404    /// Counts every expiration at or before `now`, as Linux would have by
1405    /// then, and returns how many were added. A periodic timer moves to its
1406    /// first expiration after `now`; a one-shot timer disarms.
1407    pub(crate) fn expire_through(&mut self, now: LogicalTime) -> u64 {
1408        let Some(deadline) = self.deadline else {
1409            return 0;
1410        };
1411        if now < deadline {
1412            return 0;
1413        }
1414        let elapsed = now.as_nanos() - deadline.as_nanos();
1415        let added = match elapsed.checked_div(self.interval_ns) {
1416            // A one-shot timer expires once and disarms.
1417            None => {
1418                self.deadline = None;
1419                1
1420            }
1421            Some(periods) => {
1422                let added = 1 + periods;
1423                self.deadline = Some(LogicalTime::from_nanos(
1424                    deadline
1425                        .as_nanos()
1426                        .saturating_add(added.saturating_mul(self.interval_ns)),
1427                ));
1428                added
1429            }
1430        };
1431        self.unread = self.unread.saturating_add(added);
1432        added
1433    }
1434
1435    /// `timerfd_gettime`'s answer at `now`, after `expire_through(now)`: the
1436    /// time left to the next expiration (0 when disarmed) and the interval,
1437    /// both in nanoseconds.
1438    pub(crate) fn remaining(&self, now: LogicalTime) -> (u64, u64) {
1439        let left = self.deadline.map_or(0, |deadline| {
1440            deadline.as_nanos().saturating_sub(now.as_nanos())
1441        });
1442        (left, self.interval_ns)
1443    }
1444}
1445
1446/// What Linux's own timer behind a timerfd holds (`ctx->t` and `ctx->ticks`
1447/// in fs/timerfd.c), which only the timer's fdinfo shows: Linux counts one
1448/// expiration when its timer fires and leaves it stopped, and counts the
1449/// periods after that one only when a read or `timerfd_gettime` forwards it,
1450/// where Detcore's model counts every expiration at each probe
1451/// ([`VirtualTimerfd::expire_through`]).
1452#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1453pub(crate) struct LinuxTimerfd {
1454    /// Where the timer would fire next, or `None` when it will not.
1455    pub(crate) deadline: Option<LogicalTime>,
1456    /// The expirations counted as of the last forward that no read has
1457    /// taken, without one since then.
1458    pub(crate) ticks: u64,
1459}
1460
1461impl LinuxTimerfd {
1462    /// The state `timerfd_settime` leaves: armed at `deadline`, nothing
1463    /// counted.
1464    pub(crate) fn set(deadline: Option<LogicalTime>) -> Self {
1465        LinuxTimerfd { deadline, ticks: 0 }
1466    }
1467
1468    /// What Linux's `timerfd_gettime` does at `now` to a timer of interval
1469    /// `interval_ns` (`do_timerfd_gettime`): one that has fired is forwarded
1470    /// past `now`, counting every period that ended since it fired. A
1471    /// one-shot timer that has fired is not restarted; its one expiration
1472    /// stays counted. Nothing for a timer that has not fired.
1473    pub(crate) fn forward(&mut self, now: LogicalTime, interval_ns: u64) {
1474        let Some(deadline) = self.deadline else {
1475            return;
1476        };
1477        if now < deadline {
1478            return;
1479        }
1480        let elapsed = now.as_nanos() - deadline.as_nanos();
1481        match elapsed.checked_div(interval_ns) {
1482            None => {
1483                self.ticks = self.ticks.saturating_add(1);
1484                self.deadline = None;
1485            }
1486            Some(periods) => {
1487                let counted = 1 + periods;
1488                self.ticks = self.ticks.saturating_add(counted);
1489                self.deadline = Some(LogicalTime::from_nanos(
1490                    deadline
1491                        .as_nanos()
1492                        .saturating_add(counted.saturating_mul(interval_ns)),
1493                ));
1494            }
1495        }
1496    }
1497
1498    /// What a successful read leaves (`timerfd_read_iter`): the timer
1499    /// forwarded past the read, as Detcore's model is after the probe that
1500    /// made the read ready (`next`, its [`VirtualTimerfd::deadline`]), and
1501    /// nothing counted.
1502    pub(crate) fn after_read(&mut self, next: Option<LogicalTime>) {
1503        self.deadline = next;
1504        self.ticks = 0;
1505    }
1506
1507    /// What Linux's `timerfd_show` prints at `now`: its count, with the one
1508    /// expiration of a timer that has fired since its last forward, and the
1509    /// time left to that timer (`timerfd_get_remaining`, 0 once it has fired
1510    /// or when it will not), in nanoseconds.
1511    pub(crate) fn view(&self, now: LogicalTime) -> (u64, u64) {
1512        match self.deadline {
1513            Some(deadline) if now >= deadline => (self.ticks.saturating_add(1), 0),
1514            Some(deadline) => (
1515                self.ticks,
1516                deadline.as_nanos().saturating_sub(now.as_nanos()),
1517            ),
1518            None => (self.ticks, 0),
1519        }
1520    }
1521}
1522
1523/// `OpenFileDescription::flock_ever_granted` for a serialized description
1524/// that lacks the field: may have been granted, the conservative answer.
1525fn flock_ever_granted_when_absent() -> bool {
1526    true
1527}
1528
1529impl PartialEq for DetFd {
1530    fn eq(&self, other: &Self) -> bool {
1531        self.fd == other.fd
1532    }
1533}
1534
1535impl Eq for DetFd {}
1536
1537impl Hash for DetFd {
1538    // fd is owned by process and is unique per process/thread
1539    fn hash<H: Hasher>(&self, state: &mut H) {
1540        self.fd.hash(state);
1541    }
1542}
1543
1544/// If the flags specify O_NONBLOCK.
1545fn oflags_nonblocking(flags: i32) -> bool {
1546    let o_nonblock = OFlag::O_NONBLOCK.bits();
1547    flags & o_nonblock == o_nonblock
1548}
1549
1550impl DetFd {
1551    /// Publish a local description before another process or image can use
1552    /// it. Every escaped alias switches together under the existing mutex.
1553    /// Failure leaves the local model intact and must stop the lifecycle.
1554    pub(crate) fn promote(&self) -> Result<(), SharedOpenFileError> {
1555        let mut state = self.open_file.lock().expect("open file mutex poisoned");
1556        let OpenFileState::Local(description) = &*state else {
1557            return Ok(());
1558        };
1559        let shared = SharedOpenFile {
1560            id: description.id,
1561            directory_lock: Arc::clone(&description.directory_lock),
1562        };
1563        let OpenFileState::Local(description) =
1564            std::mem::replace(&mut *state, OpenFileState::Shared(shared))
1565        else {
1566            unreachable!()
1567        };
1568        let model = OpenFileModel(description);
1569        let result = SHARED_OPEN_FILE_CHANNEL
1570            .get()
1571            .ok_or_else(|| SharedOpenFileError("no shared open file channel installed".into()))
1572            .and_then(|channel| {
1573                // Only the backends that run Detcore in each guest process
1574                // install a channel. They discover received descriptors, so
1575                // Detcore neither pairs their sockets nor queues on them, and
1576                // the bookkeeping the model does not serialize is empty.
1577                debug_assert!(
1578                    model.0.rights.peer.is_none() && model.0.rights.queued.is_empty(),
1579                    "a promoted description carries SCM_RIGHTS bookkeeping"
1580                );
1581                channel.promote(&model)
1582            });
1583        if result.is_err() {
1584            *state = OpenFileState::Local(model.0);
1585        }
1586        result
1587    }
1588
1589    pub(crate) fn is_shared(&self) -> bool {
1590        matches!(
1591            *self.open_file.lock().expect("open file mutex poisoned"),
1592            OpenFileState::Shared(_)
1593        )
1594    }
1595
1596    /// A new process needs its own unlocked runtime lock, but the same
1597    /// canonical identity. The caller interns aliases after copying the table.
1598    pub(crate) fn fork_shared_handle(&self) -> Self {
1599        let state = self.open_file.lock().expect("open file mutex poisoned");
1600        let mut copied = self.clone();
1601        if let OpenFileState::Shared(shared) = &*state {
1602            copied.open_file = Arc::new(Mutex::new(OpenFileState::Shared(SharedOpenFile {
1603                id: shared.id,
1604                directory_lock: Arc::default(),
1605            })));
1606        }
1607        copied
1608    }
1609
1610    /// Restore an authenticated survivor from the existing exec handover.
1611    pub(crate) fn shared_survivor(fd: RawFd, id: OpenFileId) -> Self {
1612        Self {
1613            fd,
1614            fd_flags: 0,
1615            open_file: Arc::new(Mutex::new(OpenFileState::Shared(SharedOpenFile {
1616                id,
1617                directory_lock: Arc::default(),
1618            }))),
1619        }
1620    }
1621
1622    /// Only a copied local flock cache can become stale at fork. Shared
1623    /// models retain kernel-observed lock changes without a pre-grant RPC.
1624    pub(crate) fn forget_local_flock_mode_at_fork(&self) {
1625        let mut state = self.open_file.lock().expect("open file mutex poisoned");
1626        if let OpenFileState::Local(description) = &mut *state {
1627            description.flock_mode = None;
1628            description.flock_mode_known = false;
1629        }
1630    }
1631
1632    /// Inspect only local ownership and the skipped directory lease. This
1633    /// never takes a canonical shared-OFD lease or contacts its channel.
1634    /// The caller separately owns quiescence across this temporary probe.
1635    pub(crate) fn validate_native_exec_runtime_state(
1636        &self,
1637    ) -> Result<(OpenFileId, usize), crate::native_exec::NativeExecCutError> {
1638        use crate::native_exec::NativeExecCutError;
1639
1640        let state = crate::native_exec::try_native_exec_lock(&self.open_file, "open_file")?;
1641        let (id, directory_lock) = match &*state {
1642            OpenFileState::Local(description) => (description.id, &description.directory_lock),
1643            OpenFileState::Shared(shared) => (shared.id, &shared.directory_lock),
1644        };
1645        let _lease = directory_lock
1646            .try_lock()
1647            .ok_or(NativeExecCutError::LiveCapability("directory lease"))?;
1648        Ok((id, Arc::as_ptr(&self.open_file) as usize))
1649    }
1650
1651    /// create a new detfd from rawfd
1652    pub fn new(fd: RawFd, flags: OFlag, ty: FdType, id: OpenFileId) -> Self {
1653        let bits = flags.bits();
1654        DetFd {
1655            fd,
1656            fd_flags: bits & OFlag::O_CLOEXEC.bits(),
1657            open_file: Arc::new(Mutex::new(OpenFileState::Local(OpenFileDescription {
1658                id,
1659                ty,
1660                pidfd_target: None,
1661                procfs_reached: None,
1662                procfs_link_itself: false,
1663                placeholder: None,
1664                inherited_external_read: None,
1665                status_flags: bits & !OFlag::O_CLOEXEC.bits(),
1666                path: None,
1667                inode: None,
1668                dirty: false,
1669                fifo_awaiting_guest_peer: false,
1670                named_fifo: false,
1671                stat: None,
1672                random_device_offset: 0,
1673                random_device_stream: RandomDeviceStream::default(),
1674                resource: None,
1675                procfs: None,
1676                sigalrm_phase1: None,
1677                directory: None,
1678                directory_host_order: false,
1679                directory_lock: Default::default(),
1680                socket_receive_timestamp: None,
1681                sock_diag: false,
1682                netlink_route: false,
1683                loopback_peer: false,
1684                network_channel: false,
1685                network_lowat: None,
1686                network_socket: NetworkSocketKind::NotInet,
1687                socketpair_endpoint: false,
1688                guest_peer: false,
1689                accepted_connection: false,
1690                error_queue_never_waits: false,
1691                guest_held: false,
1692                guest_created: false,
1693                accept_in_turn_eligible: false,
1694                read_shutdown_requested: false,
1695                exported: false,
1696                receive_timeout: ReceiveTimeout::None,
1697                flock_mode: None,
1698                flock_mode_known: true,
1699                flock_mode_ever_known: true,
1700                flock_ever_granted: false,
1701                timerfd: None,
1702                channel: None,
1703                lock_tracked: false,
1704                received_for_locks: false,
1705                may_hold_ofd_lock: false,
1706                record_sequence_unknown: false,
1707                rights: SocketRights::default(),
1708                in_flight: 0,
1709                fdinfo_facts: None,
1710                stdio_alias_group: None,
1711                // By default, we assume it matches the flags we were given:
1712                physically_nonblocking: oflags_nonblocking(bits),
1713                blocking_mode_unknown: false,
1714            }))),
1715        }
1716    }
1717
1718    /// Runs `f` on this open file description's model while holding the lock
1719    /// every alias shares. One call is one atomic step on the model, whether
1720    /// the model is local or shared (see [`OpenFileState`]). `f` must not
1721    /// access this description through another `DetFd` method or alias, and
1722    /// must not await.
1723    fn with_description<R>(&self, f: impl FnOnce(&mut OpenFileDescription) -> R) -> R {
1724        let mut state = self.open_file.lock().expect("open file mutex poisoned");
1725        match &mut *state {
1726            OpenFileState::Local(description) => f(description),
1727            OpenFileState::Shared(shared) => with_shared_description(shared.id, f),
1728        }
1729    }
1730
1731    /// A handle on this descriptor's open file description that does not
1732    /// keep it alive ([`OpenFileHandle`]).
1733    pub(crate) fn open_file_handle(&self) -> OpenFileHandle {
1734        OpenFileHandle(Arc::downgrade(&self.open_file))
1735    }
1736
1737    /// This descriptor's open file description, for an `SCM_RIGHTS` message
1738    /// that carries it ([`InFlightOpenFile`]): `held` while the message is
1739    /// queued, or with the legacy ownership that keeps nothing alive.
1740    pub(crate) fn in_flight_open_file(&self, held: bool) -> InFlightOpenFile {
1741        InFlightOpenFile(if held {
1742            InFlight::Held(Arc::clone(&self.open_file))
1743        } else {
1744            InFlight::Legacy(Arc::downgrade(&self.open_file))
1745        })
1746    }
1747
1748    /// update fd
1749    pub fn with_fd(mut self, fd: RawFd) -> Self {
1750        self.fd = fd;
1751        self
1752    }
1753    /// change fd type
1754    pub fn with_type(self, ty: FdType) -> Self {
1755        self.set_type(ty);
1756        self
1757    }
1758    /// Change the type of this descriptor's open file description, which every
1759    /// alias shares.
1760    pub(crate) fn set_type(&self, ty: FdType) {
1761        self.with_description(|d| d.ty = ty);
1762    }
1763    /// Make this descriptor's open file description a random device that
1764    /// reads `stream`.
1765    pub(crate) fn set_random_device(&self, stream: RandomDeviceStream) {
1766        self.with_description(|d| {
1767            d.ty = FdType::Rng;
1768            d.random_device_stream = stream;
1769        });
1770    }
1771    /// The stream this random-device open file reads, which every alias shares.
1772    pub(crate) fn random_device_stream(&self) -> RandomDeviceStream {
1773        self.with_description(|d| d.random_device_stream)
1774    }
1775    /// Set per-slot descriptor flags on a newly duplicated fd.
1776    pub fn with_fd_flags(mut self, flags: OFlag) -> Self {
1777        self.fd_flags = flags.bits() & OFlag::O_CLOEXEC.bits();
1778        self
1779    }
1780    /// set path associated with `fd`
1781    pub fn with_path<P: AsRef<Path>>(self, path: P) -> Self {
1782        let path = Some(PathBuf::from(path.as_ref()));
1783        self.with_description(|d| d.path = path);
1784        self
1785    }
1786    /// set virtual inode
1787    pub fn with_inode(self, inode: DetInode) -> Self {
1788        self.with_description(|d| d.inode = Some(inode));
1789        self
1790    }
1791    /// set dirty flag
1792    pub fn with_dirty(self, dirty: bool) -> Self {
1793        self.with_description(|d| d.dirty = dirty);
1794        self
1795    }
1796    /// update statbuf
1797    pub fn with_stat<S: Into<Option<DetStat>>>(self, stat: S) -> Self {
1798        let stat = stat.into();
1799        self.with_description(|d| d.stat = stat);
1800        self
1801    }
1802    /// set resource id
1803    pub fn with_resource<S: Into<Option<ResourceID>>>(self, resource: S) -> Self {
1804        let resource = resource.into();
1805        self.with_description(|d| d.resource = resource);
1806        self
1807    }
1808
1809    /// Update the scheduler resource shared by aliases of this open file.
1810    pub(crate) fn set_resource<S: Into<Option<ResourceID>>>(&self, resource: S) {
1811        let resource = resource.into();
1812        self.with_description(|d| d.resource = resource);
1813    }
1814
1815    pub(crate) fn inherited_external_read(&self) -> Option<InheritedExternalRead> {
1816        self.with_description(|d| d.inherited_external_read.clone())
1817    }
1818
1819    pub(crate) fn set_inherited_external_read(&self, terminal: InheritedExternalRead) {
1820        self.with_description(|d| d.inherited_external_read = Some(terminal));
1821    }
1822
1823    /// If fd is non blocking
1824    pub fn is_nonblocking(&self) -> bool {
1825        self.with_description(|d| oflags_nonblocking(d.status_flags))
1826    }
1827
1828    /// Whether close-on-exec is set for this descriptor slot.
1829    pub fn is_cloexec(&self) -> bool {
1830        self.fd_flags & OFlag::O_CLOEXEC.bits() != 0
1831    }
1832
1833    /// Update close-on-exec for this descriptor slot only.
1834    pub fn set_cloexec(&mut self, enabled: bool) {
1835        self.fd_flags = if enabled { OFlag::O_CLOEXEC.bits() } else { 0 };
1836    }
1837
1838    /// Update both the logical (guest-visible) and physical (scheduler)
1839    /// nonblocking status for every alias of this open file description. Use this
1840    /// only when the physical fd genuinely tracks the guest's request; when
1841    /// Detcore forces the fd physically nonblocking for the scheduler, update the
1842    /// logical view alone via [`Self::set_logical_nonblocking`].
1843    pub fn set_nonblocking(&self, enabled: bool) {
1844        self.with_description(|description| {
1845            if enabled {
1846                description.status_flags |= OFlag::O_NONBLOCK.bits();
1847            } else {
1848                description.status_flags &= !OFlag::O_NONBLOCK.bits();
1849            }
1850            description.physically_nonblocking = enabled;
1851            // The guest has now set the mode itself.
1852            description.blocking_mode_unknown = false;
1853        })
1854    }
1855
1856    /// Update only the logical (guest-visible) O_NONBLOCK status flag, leaving
1857    /// the physical (scheduler) nonblocking state untouched. This lets a guest
1858    /// clear O_NONBLOCK while Detcore keeps the fd physically nonblocking, which
1859    /// the scheduler relies on for nonblockize-and-retry.
1860    pub fn set_logical_nonblocking(&self, enabled: bool) {
1861        self.with_description(|description| {
1862            if enabled {
1863                description.status_flags |= OFlag::O_NONBLOCK.bits();
1864            } else {
1865                description.status_flags &= !OFlag::O_NONBLOCK.bits();
1866            }
1867            // The guest has now set the mode itself.
1868            description.blocking_mode_unknown = false;
1869        })
1870    }
1871
1872    /// Makes this descriptor refer to `other`'s open file description object.
1873    pub(crate) fn share_open_file_with(&mut self, other: &DetFd) {
1874        self.open_file = Arc::clone(&other.open_file);
1875    }
1876
1877    /// Stable identity shared by dup and fork aliases. A shared handle keeps
1878    /// its identity locally, so this never takes the canonical model.
1879    pub fn open_file_id(&self) -> OpenFileId {
1880        match &*self.open_file.lock().expect("open file mutex poisoned") {
1881            OpenFileState::Local(description) => description.id,
1882            OpenFileState::Shared(shared) => shared.id,
1883        }
1884    }
1885
1886    /// Give this open file description the identity `id` of the one it is a
1887    /// copy of in Linux: a placeholder descriptor received over `SCM_RIGHTS`,
1888    /// whose sender's description Linux shares, so that the locks Detcore
1889    /// follows by description (`procfs::locks::LockLedger`) are that one's.
1890    /// Its other state stays its own, as for every received descriptor
1891    /// (<https://github.com/rrnewton/hermit/issues/4670>). A shared
1892    /// description (one promoted to Detcore's global state) keeps its
1893    /// identity; placeholders are never promoted.
1894    pub(crate) fn adopt_open_file_id(&self, id: OpenFileId) {
1895        if let OpenFileState::Local(description) =
1896            &mut *self.open_file.lock().expect("open file mutex poisoned")
1897        {
1898            description.id = id;
1899        }
1900    }
1901
1902    /// Number of references that retain this open file description: the
1903    /// modeled descriptor slots of every table that names it, and the queued
1904    /// `SCM_RIGHTS` messages that still hold it ([`InFlightOpenFile`]). A
1905    /// table of an exited process, and a queue that goes with one, count
1906    /// until the backend releases that table, at no particular point of the
1907    /// guest's schedule; see [`Self::is_last_reference`] for the decision
1908    /// that does not depend on it for queued copies.
1909    pub(crate) fn open_file_alias_count(&self) -> usize {
1910        Arc::strong_count(&self.open_file)
1911    }
1912
1913    /// Whether this descriptor is the description's last reference, so that
1914    /// its going releases the description, as Linux's last `fput` releases
1915    /// the `struct file` (a socket's port, a receiving socket's queue): no
1916    /// other modeled descriptor names it, and no copy of it is in flight
1917    /// ([`OpenFileDescription::in_flight`]). The copies in flight are those
1918    /// guest calls queued and have not received or released, whatever holds
1919    /// them now, so the backend's release of an exited process's table never
1920    /// decides it for a copy in flight: only the other tables' descriptors
1921    /// do, as a forked child's table, which counts until the backend releases
1922    /// it, always did.
1923    pub(crate) fn is_last_reference(&self) -> bool {
1924        Arc::strong_count(&self.open_file) == 1 && self.with_description(|d| d.in_flight == 0)
1925    }
1926
1927    /// How many copies of this description are in flight
1928    /// ([`OpenFileDescription::in_flight`]).
1929    pub(crate) fn copies_in_flight(&self) -> usize {
1930        self.with_description(|d| d.in_flight)
1931    }
1932
1933    /// File type attached to the open file description.
1934    pub fn ty(&self) -> FdType {
1935        self.with_description(|d| d.ty)
1936    }
1937
1938    /// This timerfd's virtual-time model, if Detcore keeps one.
1939    pub(crate) fn virtual_timerfd(&self) -> Option<VirtualTimerfd> {
1940        self.with_description(|d| d.timerfd)
1941    }
1942
1943    /// Replace this timerfd's virtual-time model, under every alias.
1944    pub(crate) fn set_virtual_timerfd(&self, timerfd: Option<VirtualTimerfd>) {
1945        self.with_description(|d| {
1946            debug_assert_eq!(d.ty, FdType::Timerfd);
1947            d.timerfd = timerfd;
1948        })
1949    }
1950
1951    /// What this description's fdinfo prints that no call reads back, where
1952    /// the guest set it through Detcore ([`FdinfoFacts`]).
1953    pub(crate) fn fdinfo_facts(&self) -> Option<FdinfoFacts> {
1954        self.with_description(|d| d.fdinfo_facts)
1955    }
1956
1957    /// Record what this description's fdinfo prints that no call reads back,
1958    /// under every alias.
1959    pub(crate) fn set_fdinfo_facts(&self, facts: FdinfoFacts) {
1960        self.with_description(|d| d.fdinfo_facts = Some(facts))
1961    }
1962
1963    /// Record the process identity carried by a newly created pidfd.
1964    pub(crate) fn set_pidfd_target(&self, target: DetPid) {
1965        self.with_description(|description| {
1966            debug_assert_eq!(description.ty, FdType::Pidfd);
1967            description.pidfd_target = Some(target);
1968        })
1969    }
1970
1971    /// Return the process identity carried by this pidfd, when known.
1972    pub(crate) fn pidfd_target(&self) -> Option<DetPid> {
1973        self.with_description(|d| d.pidfd_target)
1974    }
1975
1976    /// Record where the `/proc` selector spelling this open file was opened
1977    /// by led (`OpenFileDescription::procfs_reached`).
1978    /// `incarnation` is the identity of the task the selector reached, where
1979    /// Detcore asked its process table (`procfs::TaskIncarnation`): a name
1980    /// below it is that task's while no other task holds its numbers.
1981    pub(crate) fn set_procfs_reached(
1982        &self,
1983        reached: PathBuf,
1984        incarnation: Option<crate::procfs::TaskIncarnation>,
1985    ) {
1986        self.with_description(|d| d.procfs_reached = Some((reached, incarnation)))
1987    }
1988
1989    /// Record that this open file is a `/proc` selector link itself
1990    /// (`OpenFileDescription::procfs_link_itself`).
1991    pub(crate) fn set_procfs_link_itself(&self) {
1992        self.with_description(|d| d.procfs_link_itself = true)
1993    }
1994
1995    /// Whether this open file is a `/proc` selector link itself.
1996    pub(crate) fn procfs_link_itself(&self) -> bool {
1997        self.with_description(|d| d.procfs_link_itself)
1998    }
1999
2000    /// The placeholder directory this open file is, if it is one
2001    /// (`OpenFileDescription::placeholder`).
2002    pub(crate) fn placeholder(&self) -> Option<PlaceholderState> {
2003        self.with_description(|d| d.placeholder)
2004    }
2005
2006    /// Make this open file a placeholder directory, or change its state.
2007    pub(crate) fn set_placeholder(&self, state: PlaceholderState) {
2008        self.with_description(|d| d.placeholder = Some(state))
2009    }
2010
2011    /// The task directory this open file is, if Detcore knows it
2012    /// (`OpenFileDescription::procfs_reached`).
2013    pub(crate) fn procfs_reached(
2014        &self,
2015    ) -> Option<(PathBuf, Option<crate::procfs::TaskIncarnation>)> {
2016        self.with_description(|d| d.procfs_reached.clone())
2017    }
2018
2019    /// The pipe or FIFO this descriptor is an end of, when
2020    /// `--pipe-wake-latency` is on and Detcore knows it
2021    /// (`crate::pipe_wake`).
2022    pub(crate) fn channel(&self) -> Option<crate::pipe_wake::ChannelEnd> {
2023        self.with_description(|d| d.channel)
2024    }
2025
2026    /// Name the pipe or FIFO this descriptor's open file description is an
2027    /// end of. Shared by every alias of the description.
2028    pub(crate) fn set_channel(&self, channel: crate::pipe_wake::ChannelEnd) {
2029        self.with_description(|d| d.channel = Some(channel))
2030    }
2031
2032    /// Resource attached to the open file description.
2033    pub fn resource(&self) -> Option<ResourceID> {
2034        self.with_description(|d| d.resource.clone())
2035    }
2036
2037    // AUTONOMOUS-BOT-IMPLEMENTED
2038    // TODO-HUMAN-REVIEW(PR-1096): Review canonical random-device cursor sharing.
2039    /// Return the cursor shared by aliases of this random-device open file.
2040    #[cfg(test)]
2041    pub(crate) fn random_device_offset(&self) -> u64 {
2042        self.with_description(|d| d.random_device_offset)
2043    }
2044
2045    // AUTONOMOUS-BOT-IMPLEMENTED
2046    // TODO-HUMAN-REVIEW(PR-1096): Review canonical random-device cursor sharing.
2047    /// Advance the cursor shared by aliases of this random-device open file.
2048    #[cfg(test)]
2049    pub(crate) fn advance_random_device_offset(&self, count: usize) {
2050        self.with_description(|description| {
2051            description.random_device_offset = description
2052                .random_device_offset
2053                .saturating_add(count as u64);
2054        })
2055    }
2056
2057    /// Copy from the shared random-device cursor and commit the returned byte count.
2058    ///
2059    /// The description stays locked throughout the synchronous copy. The closure
2060    /// must not access this description through another `DetFd` method or alias.
2061    /// Clone the descriptor and drop its metadata guard before entering this
2062    /// transaction; the closure must not acquire Detcore metadata/description
2063    /// locks or await. This preserves the metadata-before-description lock order.
2064    /// A large copy can block relaxed-mode aliases for its bounded duration.
2065    /// An error leaves the cursor unchanged; it does not undo guest memory writes.
2066    pub(crate) fn with_random_device_stream<R>(
2067        &self,
2068        copy: impl FnOnce(u64) -> Result<usize, R>,
2069    ) -> Result<usize, R> {
2070        self.with_description(|description| {
2071            let copied = copy(description.random_device_offset)?;
2072            description.random_device_offset = description
2073                .random_device_offset
2074                .saturating_add(copied as u64);
2075            Ok(copied)
2076        })
2077    }
2078
2079    /// Path used to open this file description, when it was observable.
2080    pub(crate) fn path(&self) -> Option<PathBuf> {
2081        self.with_description(|d| d.path.clone())
2082    }
2083
2084    /// Record the resolved path used to open this file description.
2085    pub(crate) fn set_path<P: AsRef<Path>>(&self, path: P) {
2086        let path = Some(path.as_ref().to_path_buf());
2087        self.with_description(|d| d.path = path);
2088    }
2089
2090    /// Attach deterministic procfs snapshot state to this open file description.
2091    pub(crate) fn set_procfs(&self, procfs: ProcfsFile) {
2092        self.with_description(|d| d.procfs = Some(procfs));
2093    }
2094
2095    /// The procfs path of a file Detcore opened with no kernel object behind
2096    /// it (`ProcfsFile::is_rendered`): this descriptor is a placeholder.
2097    pub(crate) fn procfs_rendered_path(&self) -> Option<&'static Path> {
2098        self.with_description(|d| d.procfs.as_ref().and_then(ProcfsFile::opened_path))
2099    }
2100
2101    /// The procfs path of a file whose every byte Detcore renders
2102    /// (`ProcfsFile::rendered_path`), whether Detcore opened it as a
2103    /// placeholder or the kernel opened it: through a symbolic link or `..`,
2104    /// after `chroot`, or under record and replay. `files` are the files the
2105    /// session renders (`Detcore::rendered_files`).
2106    pub(crate) fn procfs_renderable_path(
2107        &self,
2108        files: crate::procfs::RenderedFiles,
2109    ) -> Option<&'static Path> {
2110        self.with_description(|d| {
2111            d.procfs
2112                .as_ref()
2113                .and_then(|procfs| procfs.served_rendered_path(files))
2114        })
2115    }
2116
2117    /// Whether Linux reads this procfs file through `read_iter`
2118    /// (`ProcfsFile::has_read_iter`).
2119    pub(crate) fn procfs_has_read_iter(&self) -> bool {
2120        self.with_description(|d| d.procfs.as_ref().is_none_or(ProcfsFile::has_read_iter))
2121    }
2122
2123    /// Whether Detcore keeps this procfs file's kernel position at its own
2124    /// cursor: a placeholder, a fixed file the kernel opened (whose snapshot
2125    /// Detcore renders without reading the kernel's file), or a process's
2126    /// `maps` whose snapshot Detcore rendered from the backend's memory map
2127    /// (`ProcfsFile::snapshot_from_memory_map`), which did not read the
2128    /// kernel's file either. Linux's `fdinfo` shows that position, and a
2129    /// descriptor of the open file that Detcore's model does not join (one
2130    /// received over `SCM_RIGHTS` that Detcore records as an open file of its
2131    /// own) reads the kernel's file from it. A backend that rediscovers
2132    /// descriptors after `exec` reads the position back.
2133    pub(crate) fn procfs_keeps_kernel_position(&self) -> bool {
2134        self.with_description(|d| {
2135            d.procfs.as_ref().is_some_and(|procfs| {
2136                procfs.is_rendered()
2137                    || procfs.snapshot_is_fixed()
2138                    || procfs.snapshot_from_memory_map()
2139            })
2140        })
2141    }
2142
2143    /// Note that this process's `maps` snapshot was rendered from the
2144    /// backend's memory map (`ProcfsFile::note_rendered_from_memory_map`).
2145    pub(crate) fn note_procfs_rendered_from_memory_map(&self) {
2146        self.with_description(|d| {
2147            if let Some(procfs) = d.procfs.as_mut() {
2148                procfs.note_rendered_from_memory_map();
2149            }
2150        })
2151    }
2152
2153    /// Whether this procfs file's snapshot seeks only from the start or the
2154    /// position (`ProcfsFile::seeks_only_from_start_or_position`).
2155    pub(crate) fn procfs_seeks_only_from_start_or_position(&self) -> bool {
2156        self.with_description(|d| {
2157            d.procfs
2158                .as_ref()
2159                .is_some_and(ProcfsFile::seeks_only_from_start_or_position)
2160        })
2161    }
2162
2163    /// Whether a short copy of a read of this procfs file is `EFAULT`
2164    /// (`ProcfsFile::faults_on_a_short_copy`).
2165    pub(crate) fn procfs_faults_on_a_short_copy(&self) -> bool {
2166        self.with_description(|d| {
2167            d.procfs
2168                .as_ref()
2169                .is_some_and(ProcfsFile::faults_on_a_short_copy)
2170        })
2171    }
2172
2173    /// The bytes a procfs file's snapshot starts from in place of a capture
2174    /// of the kernel's file, if it takes none: a rendered file's template
2175    /// (`ProcfsFile::rendering_template`), or nothing for a fixed file the
2176    /// kernel opened (through a symbolic link, after `chroot`, under record
2177    /// and replay), whose bytes are the description's whatever the kernel's
2178    /// file holds (`ProcfsFile::snapshot_is_fixed`).
2179    pub(crate) fn procfs_rendering_template(&self) -> Option<&'static [u8]> {
2180        self.with_description(|d| {
2181            d.procfs.as_ref().and_then(|procfs| {
2182                if procfs.is_rendered() {
2183                    Some(procfs.rendering_template())
2184                } else if procfs.snapshot_is_fixed() {
2185                    Some(&b""[..])
2186                } else {
2187                    None
2188                }
2189            })
2190        })
2191    }
2192
2193    /// Whether Detcore opened this procfs file without a kernel object behind
2194    /// it and its first read takes its snapshot at an uptime the placeholder
2195    /// carries (`ProcfsFile::snapshot_carries_uptime`,
2196    /// `Detcore::first_read_uptime`).
2197    pub(crate) fn procfs_rendered_at_first_read(&self) -> bool {
2198        self.with_description(|d| {
2199            d.procfs
2200                .as_ref()
2201                .is_some_and(|procfs| procfs.is_rendered() && procfs.snapshot_carries_uptime())
2202        })
2203    }
2204
2205    /// Whether this procfs open file description still needs its initial snapshot.
2206    pub(crate) fn procfs_needs_snapshot(&self) -> bool {
2207        self.with_description(|d| d.procfs.as_ref().is_some_and(ProcfsFile::needs_snapshot))
2208    }
2209
2210    /// Whether every read of this open file description reads the process's
2211    /// memory again (`ProcfsFile::reads_memory_at_every_read`): a process's
2212    /// `cmdline` Detcore opened.
2213    pub(crate) fn procfs_reads_memory_at_every_read(&self) -> bool {
2214        self.with_description(|d| {
2215            d.procfs
2216                .as_ref()
2217                .is_some_and(ProcfsFile::reads_memory_at_every_read)
2218        })
2219    }
2220
2221    /// Whether reads of this open file description are served from a procfs
2222    /// snapshot, already taken or still to be taken.
2223    pub(crate) fn procfs_serves_snapshot(&self) -> bool {
2224        self.with_description(|d| {
2225            d.procfs
2226                .as_ref()
2227                .is_some_and(|procfs| procfs.needs_snapshot() || procfs.position().1.is_some())
2228        })
2229    }
2230
2231    /// Signal phase 1's kernel account of this file, if one was recorded.
2232    pub(crate) fn sigalrm_phase1_provenance(
2233        &self,
2234    ) -> Option<crate::sigalrm_phase1::Phase1Provenance> {
2235        self.with_description(|d| d.sigalrm_phase1)
2236    }
2237
2238    /// Record signal phase 1's kernel account of this file.
2239    pub(crate) fn set_sigalrm_phase1_provenance(
2240        &self,
2241        provenance: crate::sigalrm_phase1::Phase1Provenance,
2242    ) {
2243        self.with_description(|d| d.sigalrm_phase1 = Some(provenance));
2244    }
2245
2246    /// Whether this procfs file's snapshot cannot wait to be taken
2247    /// (`ProcfsFile::capture_cannot_wait`).
2248    pub(crate) fn procfs_capture_cannot_wait(&self) -> bool {
2249        self.with_description(|d| {
2250            d.procfs
2251                .as_ref()
2252                .is_some_and(ProcfsFile::capture_cannot_wait)
2253        })
2254    }
2255
2256    /// Whether this procfs snapshot consumes deterministic random bytes.
2257    // AUTONOMOUS-BOT-IMPLEMENTED
2258    // TODO-HUMAN-REVIEW(PR-955): Review deterministic kernel UUID generation.
2259    /// Whether this snapshot has maps-format headers needing determinized
2260    /// device and inode columns.
2261    pub(crate) fn procfs_needs_mapping_identities(&self) -> bool {
2262        self.with_description(|d| {
2263            d.procfs
2264                .as_ref()
2265                .is_some_and(ProcfsFile::needs_mapping_identities)
2266        })
2267    }
2268
2269    pub(crate) fn procfs_private_mapping_format(
2270        &self,
2271    ) -> Option<crate::procfs::private_maps::Format> {
2272        self.with_description(|d| {
2273            d.procfs
2274                .as_ref()
2275                .and_then(ProcfsFile::private_mapping_format)
2276        })
2277    }
2278
2279    pub(crate) fn procfs_maps_target(&self) -> Option<crate::procfs::MapsTarget> {
2280        self.with_description(|d| d.procfs.as_ref().and_then(ProcfsFile::maps_target))
2281    }
2282
2283    /// What a process's `maps` was bound to at its open
2284    /// (`ProcfsFile::memory_map`).
2285    pub(crate) fn procfs_memory_map(&self) -> Option<crate::procfs::memory_map::Binding> {
2286        self.with_description(|d| d.procfs.as_ref().and_then(ProcfsFile::memory_map))
2287    }
2288
2289    /// Carry the opened Maps/Smaps description, including its old target and cursor,
2290    /// through the authenticated exec handoff instead of rediscovering a target.
2291    pub(crate) fn maps_for_exec(&self) -> Option<ProcfsFile> {
2292        self.with_description(|d| {
2293            d.procfs
2294                .as_ref()
2295                .filter(|p| p.private_mapping_format().is_some())
2296                .cloned()
2297        })
2298    }
2299
2300    /// Carry a per-process file that follows its record's sequence
2301    /// (`ProcfsFile::follows_record_sequence`), its sequence included,
2302    /// through the authenticated exec handoff instead of rediscovering it
2303    /// with none (`ThreadState::procfs_records_for_exec`).
2304    pub(crate) fn procfs_record_for_exec(&self) -> Option<ProcfsFile> {
2305        self.with_description(|d| {
2306            d.procfs
2307                .as_ref()
2308                .filter(|p| p.follows_record_sequence() && p.private_mapping_format().is_none())
2309                .cloned()
2310        })
2311    }
2312
2313    /// Whether this procfs file lists the guest's own address space, so
2314    /// its capture must not create a mapping that the listing would show.
2315    pub(crate) fn procfs_lists_address_space(&self) -> bool {
2316        self.with_description(|d| {
2317            d.procfs
2318                .as_ref()
2319                .is_some_and(ProcfsFile::lists_address_space)
2320        })
2321    }
2322
2323    /// The view of the mounts this kernel-opened procfs file reads, as its
2324    /// open pinned it (`ProcfsFile::pinned_mount_view`).
2325    pub(crate) fn procfs_pinned_mount_view(&self) -> Option<bool> {
2326        self.with_description(|d| d.procfs.as_ref().and_then(ProcfsFile::pinned_mount_view))
2327    }
2328
2329    /// Which rendering of the container mount table this procfs file is, if
2330    /// it is one (`ProcfsFile::mount_table_rendering`).
2331    pub(crate) fn procfs_mount_table_rendering(
2332        &self,
2333    ) -> Option<crate::procfs::MountTableRendering> {
2334        self.with_description(|d| {
2335            d.procfs
2336                .as_ref()
2337                .and_then(ProcfsFile::mount_table_rendering)
2338        })
2339    }
2340
2341    pub(crate) fn procfs_needs_mountinfo_identities(&self) -> bool {
2342        self.with_description(|d| {
2343            d.procfs
2344                .as_ref()
2345                .is_some_and(ProcfsFile::needs_mountinfo_identities)
2346        })
2347    }
2348
2349    /// Whether this descriptor's procfs file is `/proc/locks`, whose rows
2350    /// Detcore filters to the guest's own (`procfs::locks`).
2351    pub(crate) fn procfs_lists_locks(&self) -> bool {
2352        self.with_description(|d| d.procfs.as_ref().is_some_and(ProcfsFile::lists_locks))
2353    }
2354
2355    pub(crate) fn procfs_needs_random_uuid(&self) -> bool {
2356        self.with_description(|d| d.procfs.as_ref().is_some_and(ProcfsFile::needs_random_uuid))
2357    }
2358
2359    pub(crate) fn procfs_is_cpuinfo(&self) -> bool {
2360        self.with_description(|d| d.procfs.as_ref().is_some_and(ProcfsFile::is_cpuinfo))
2361    }
2362
2363    /// Whether this is a rendered `/proc/diskstats`, whose snapshot starts
2364    /// from the host's table as the run first read it
2365    /// (`ProcfsFile::renders_host_diskstats`).
2366    pub(crate) fn procfs_renders_host_diskstats(&self) -> bool {
2367        self.with_description(|d| {
2368            d.procfs
2369                .as_ref()
2370                .is_some_and(ProcfsFile::renders_host_diskstats)
2371        })
2372    }
2373
2374    /// Whether this is a task's `arch_status` (`ProcfsFile::is_arch_status`).
2375    pub(crate) fn procfs_is_arch_status(&self) -> bool {
2376        self.with_description(|d| d.procfs.as_ref().is_some_and(ProcfsFile::is_arch_status))
2377    }
2378
2379    pub(crate) fn procfs_needs_boot_time(&self) -> bool {
2380        self.with_description(|d| d.procfs.as_ref().is_some_and(ProcfsFile::needs_boot_time))
2381    }
2382
2383    /// The kernel process numbers a process's `stat` or `status` named by
2384    /// number holds in `contents` (`ProcfsFile::kernel_numbers_held`).
2385    pub(crate) fn procfs_kernel_numbers_held(&self, contents: &[u8]) -> Vec<i32> {
2386        self.with_description(|d| {
2387            d.procfs
2388                .as_ref()
2389                .map_or_else(Vec::new, |file| file.kernel_numbers_held(contents))
2390        })
2391    }
2392
2393    /// The guest thread whose signal mask the status snapshot about to be
2394    /// taken reports (`ProcfsFile::status_mask_thread`).
2395    pub(crate) fn procfs_status_mask_thread(&self, contents: &[u8]) -> Option<i32> {
2396        self.with_description(|d| {
2397            d.procfs
2398                .as_ref()
2399                .and_then(|file| file.status_mask_thread(contents))
2400        })
2401    }
2402
2403    /// The task a `schedstat` describes (`ProcfsFile::schedstat_thread`).
2404    pub(crate) fn procfs_schedstat_thread(&self) -> Option<DetTid> {
2405        self.with_description(|d| d.procfs.as_ref().and_then(ProcfsFile::schedstat_thread))
2406    }
2407
2408    /// Before a positioned read at `position`
2409    /// (`ProcfsFile::drop_schedstat_snapshot_for_read_at`).
2410    pub(crate) fn procfs_drop_schedstat_snapshot_for_read_at(&self, position: u64) {
2411        self.with_description(|d| {
2412            if let Some(procfs) = d.procfs.as_mut() {
2413                procfs.drop_schedstat_snapshot_for_read_at(position);
2414            }
2415        });
2416    }
2417
2418    /// The guest's numbers for the kernel's numbers in the snapshot about to
2419    /// be taken (`ProcfsFile::set_guest_numbers`).
2420    pub(crate) fn set_procfs_guest_numbers(&self, numbers: std::collections::BTreeMap<i32, i32>) {
2421        self.with_description(|d| {
2422            if let Some(file) = d.procfs.as_mut() {
2423                file.set_guest_numbers(numbers);
2424            }
2425        })
2426    }
2427
2428    /// Initialize the deterministic snapshot shared by all aliases.
2429    // TODO-HUMAN-REVIEW(PR-723): Review procfs snapshot identity parameters.
2430    // TODO-HUMAN-REVIEW(PR-955): Review deterministic UUID snapshot input.
2431    pub(crate) fn initialize_procfs(&self, contents: Vec<u8>, context: ProcfsSnapshotContext) {
2432        self.with_description(|d| {
2433            d.procfs
2434                .as_mut()
2435                .expect("procfs fd disappeared while taking its snapshot")
2436                .initialize(contents, context)
2437        });
2438    }
2439
2440    /// Preview the snapshot bytes at its shared offset, with that offset,
2441    /// without consuming them.
2442    pub(crate) fn preview_procfs(&self, maximum: usize) -> Option<(usize, Vec<u8>)> {
2443        self.with_description(|description| {
2444            let procfs = description.procfs.as_ref()?;
2445            let (offset, _) = procfs.position();
2446            procfs.take_at(offset, maximum).map(|bytes| (offset, bytes))
2447        })
2448    }
2449
2450    /// Advance the shared offset by the bytes a read copied to the guest.
2451    pub(crate) fn commit_procfs_read(&self, offset: usize, copied: usize) {
2452        self.with_description(|d| {
2453            d.procfs
2454                .as_mut()
2455                .expect("procfs fd disappeared while committing a read")
2456                .commit_read(offset, copied)
2457        });
2458    }
2459
2460    /// Whether this is a process's `stat` or a task's `comm` Detcore opened
2461    /// as rendered, rendered from the task's facts
2462    /// (`ProcfsFile::renders_task_record`).
2463    pub(crate) fn procfs_renders_task_record(&self) -> bool {
2464        self.with_description(|d| {
2465            d.procfs
2466                .as_ref()
2467                .is_some_and(ProcfsFile::renders_task_record)
2468        })
2469    }
2470
2471    /// Whether this is a rendered per-process file that follows its one
2472    /// record's sequence (`ProcfsFile::follows_record_sequence`).
2473    pub(crate) fn procfs_follows_record_sequence(&self) -> bool {
2474        self.with_description(|d| {
2475            d.procfs
2476                .as_ref()
2477                .is_some_and(ProcfsFile::follows_record_sequence)
2478        })
2479    }
2480
2481    /// Whether Linux runs the record's `show` for a read at `position`, and
2482    /// if so drop the buffered record (`ProcfsFile::drop_stale_task_snapshot`).
2483    pub(crate) fn procfs_drop_stale_task_snapshot(&self, position: usize) -> bool {
2484        self.with_description(|d| {
2485            d.procfs
2486                .as_mut()
2487                .is_some_and(|procfs| procfs.drop_stale_task_snapshot(position))
2488        })
2489    }
2490
2491    /// Record where a positioned read left a record's sequence
2492    /// (`ProcfsFile::note_read_end`).
2493    pub(crate) fn procfs_note_read_end(&self, position: usize) {
2494        self.with_description(|d| {
2495            if let Some(procfs) = d.procfs.as_mut() {
2496                procfs.note_read_end(position);
2497            }
2498        });
2499    }
2500
2501    /// Record the outcome of rendering a record for a read at `position`
2502    /// (`ProcfsFile::note_task_record_rendered`).
2503    pub(crate) fn procfs_note_task_record_rendered(&self, position: usize, rendered: bool) {
2504        self.with_description(|d| {
2505            if let Some(procfs) = d.procfs.as_mut() {
2506                procfs.note_task_record_rendered(position, rendered);
2507            }
2508        });
2509    }
2510
2511    /// Move a record's cursor as `seq_lseek` does
2512    /// (`ProcfsFile::seek_task_record`): whether the caller must run its
2513    /// `show` now.
2514    pub(crate) fn seek_procfs_task_record(&self, offset: usize) -> bool {
2515        self.with_description(|d| {
2516            d.procfs
2517                .as_mut()
2518                .is_some_and(|procfs| procfs.seek_task_record(offset))
2519        })
2520    }
2521
2522    /// Leave a record whose `show` failed at a seek as `seq_lseek` does
2523    /// (`ProcfsFile::reset_task_record`).
2524    pub(crate) fn reset_procfs_task_record(&self) {
2525        self.with_description(|d| {
2526            if let Some(procfs) = d.procfs.as_mut() {
2527                procfs.reset_task_record();
2528            }
2529        });
2530    }
2531
2532    /// Before a fork copies this process's tool state where the tool runs in
2533    /// each guest process: a per-process file that follows its record's
2534    /// sequence keeps that sequence in Detcore's global state from then on
2535    /// (`ProcfsFile::share_sequence`). Only a description this process holds
2536    /// itself; one already in the global state is shared whole.
2537    pub(crate) fn share_procfs_sequence(&self) -> bool {
2538        let mut state = self.open_file.lock().expect("open file mutex poisoned");
2539        match &mut *state {
2540            OpenFileState::Local(description) => description
2541                .procfs
2542                .as_mut()
2543                .is_some_and(ProcfsFile::share_sequence),
2544            OpenFileState::Shared(_) => false,
2545        }
2546    }
2547
2548    /// The identity of this descriptor's open file description, where its
2549    /// per-process file's record sequence lives in Detcore's global state
2550    /// between operations (`ProcfsFile::sequence_is_shared`).
2551    pub(crate) fn procfs_shared_sequence(&self) -> Option<OpenFileId> {
2552        let state = self.open_file.lock().expect("open file mutex poisoned");
2553        match &*state {
2554            OpenFileState::Local(description) => description
2555                .procfs
2556                .as_ref()
2557                .is_some_and(ProcfsFile::sequence_is_shared)
2558                .then_some(description.id),
2559            OpenFileState::Shared(_) => None,
2560        }
2561    }
2562
2563    /// This procfs file's record sequence (`ProcfsFile::sequence`).
2564    pub(crate) fn procfs_sequence(&self) -> Option<crate::procfs::ProcfsSequence> {
2565        self.with_description(|d| d.procfs.as_ref().map(ProcfsFile::sequence))
2566    }
2567
2568    /// Take a procfs file's record sequence as the last operation on its
2569    /// description left it (`ProcfsFile::adopt_sequence`).
2570    pub(crate) fn adopt_procfs_sequence(&self, sequence: crate::procfs::ProcfsSequence) {
2571        self.with_description(|d| {
2572            if let Some(procfs) = d.procfs.as_mut() {
2573                procfs.adopt_sequence(sequence);
2574            }
2575        });
2576    }
2577
2578    /// Read a deterministic procfs snapshot without changing its shared cursor.
2579    pub(crate) fn take_procfs_at(&self, offset: usize, maximum: usize) -> Option<Vec<u8>> {
2580        self.with_description(|d| {
2581            d.procfs
2582                .as_ref()
2583                .and_then(|procfs| procfs.take_at(offset, maximum))
2584        })
2585    }
2586
2587    /// Namespace-visible task id and stable proc inode bound at open time.
2588    pub(crate) fn procfs_timer_slack_binding(&self) -> Option<(i32, u64, u64)> {
2589        self.with_description(|d| d.procfs.as_ref().and_then(ProcfsFile::timer_slack_binding))
2590    }
2591
2592    /// Preview bytes without consuming the snapshot shared by dup/fork aliases.
2593    pub(crate) fn preview_procfs_timer_slack(
2594        &self,
2595        value: u64,
2596        maximum: usize,
2597    ) -> Option<TimerSlackReadPreview> {
2598        self.with_description(|d| {
2599            d.procfs
2600                .as_ref()
2601                .and_then(|procfs| procfs.preview_timer_slack(value, maximum))
2602        })
2603    }
2604
2605    /// Commit bytes only after their guest-memory copy succeeds.
2606    pub(crate) fn commit_procfs_timer_slack_read(
2607        &self,
2608        preview: &TimerSlackReadPreview,
2609        copied: usize,
2610    ) {
2611        self.with_description(|d| {
2612            d.procfs
2613                .as_mut()
2614                .expect("timer-slack procfs state disappeared")
2615                .commit_timer_slack_read(preview, copied)
2616        });
2617    }
2618
2619    /// Read a fresh timer-slack value at an explicit offset.
2620    pub(crate) fn take_procfs_timer_slack_at(
2621        &self,
2622        value: u64,
2623        offset: usize,
2624        maximum: usize,
2625    ) -> Option<Vec<u8>> {
2626        self.with_description(|d| {
2627            d.procfs
2628                .as_ref()
2629                .and_then(|procfs| procfs.take_timer_slack_at(value, offset, maximum))
2630        })
2631    }
2632
2633    /// The lock serializing `getdents` and `lseek` on this open file
2634    /// description across every alias of it. For a shared description it
2635    /// covers only this process's aliases: the lock protects unsequentialized
2636    /// threads, and a description is shared only where threads are
2637    /// sequentialized.
2638    pub(crate) fn directory_lock(&self) -> Arc<futures::lock::Mutex<()>> {
2639        match &*self.open_file.lock().expect("open file mutex poisoned") {
2640            OpenFileState::Local(description) => Arc::clone(&description.directory_lock),
2641            OpenFileState::Shared(shared) => Arc::clone(&shared.directory_lock),
2642        }
2643    }
2644
2645    /// Whether a `getdents` call has created a directory stream here. Only a
2646    /// successful read of the host directory creates one, so this also proves
2647    /// the open file is a directory.
2648    pub(crate) fn has_directory_stream(&self) -> bool {
2649        self.with_description(|d| d.directory.is_some())
2650    }
2651
2652    /// Whether `getdents` on this open file reads the host directory one
2653    /// kernel buffer at a time instead of from a sorted stream.
2654    pub(crate) fn directory_in_host_order(&self) -> bool {
2655        self.with_description(|d| d.directory_host_order)
2656    }
2657
2658    /// Read this open file's directory one kernel buffer at a time until it
2659    /// is seeked back to 0: its position was moved before the first
2660    /// `getdents`, or a read of the host directory after the first failed, so
2661    /// no snapshot of the whole directory could be taken. A guest buffer too
2662    /// small for the next entry does not land here: the stream answers it
2663    /// with `EINVAL`, as Linux does. Any stream is dropped, so `lseek`
2664    /// reaches the kernel again.
2665    pub(crate) fn use_host_directory_order(&self) {
2666        self.with_description(|description| {
2667            description.directory_host_order = true;
2668            description.directory = None;
2669        })
2670    }
2671
2672    /// Serve this open file's directory as a sorted stream again, from its
2673    /// next `getdents`: its kernel position is back at 0.
2674    pub(crate) fn use_directory_stream(&self) {
2675        self.with_description(|d| d.directory_host_order = false);
2676    }
2677
2678    /// Whether the next `getdents` must read the host directory.
2679    pub(crate) fn directory_needs_snapshot(&self) -> bool {
2680        self.with_description(|d| {
2681            d.directory
2682                .as_ref()
2683                .is_none_or(DirectoryStream::needs_snapshot)
2684        })
2685    }
2686
2687    /// Install a snapshot freshly read in host order, creating the stream at
2688    /// position 0 if this is the open file's first `getdents`.
2689    ///
2690    /// `mark` is the inode pool's state read just before the host directory
2691    /// was (see `SnapshotMark`), and `inodes` how to key the inode numbers of
2692    /// its entries, decided then too.
2693    pub(crate) fn install_directory_snapshot(
2694        &self,
2695        entries: Vec<DirEntry>,
2696        mark: SnapshotMark,
2697        inodes: DirectoryInodes,
2698        order: DirectoryOrder,
2699    ) {
2700        self.with_description(|d| {
2701            let stream = d.directory.get_or_insert_with(DirectoryStream::default);
2702            stream.install(entries, order);
2703            stream.note_snapshot_mark(mark);
2704            stream.note_snapshot_inodes(inodes);
2705        });
2706    }
2707
2708    /// Run `f` on the directory stream shared by every alias of this open file.
2709    /// `EBADF` if there is none: under `--no-sequentialize-threads`, another
2710    /// thread can close or replace the descriptor after its caller found one.
2711    pub(crate) fn with_directory_stream<R>(
2712        &self,
2713        f: impl FnOnce(&mut DirectoryStream) -> R,
2714    ) -> Result<R, Errno> {
2715        self.with_description(|d| d.directory.as_mut().map(f).ok_or(Errno::EBADF))
2716    }
2717
2718    /// The position of this open file's directory stream, 0 before it has
2719    /// one.
2720    pub(crate) fn directory_stream_position(&self) -> u64 {
2721        self.with_description(|d| d.directory.as_ref().map_or(0, DirectoryStream::position))
2722    }
2723
2724    /// Move this open file's directory stream to `position`, creating the
2725    /// stream, without a snapshot, if it has none (`DirectoryStream::seek`):
2726    /// for a directory Detcore lists from its model alone, whose stream a
2727    /// seek may come before.
2728    pub(crate) fn seek_directory_stream(&self, position: u64) {
2729        self.with_description(|d| {
2730            d.directory
2731                .get_or_insert_with(DirectoryStream::default)
2732                .seek(position)
2733        })
2734    }
2735
2736    /// Return the shared procfs cursor and initialized snapshot length.
2737    pub(crate) fn procfs_position(&self) -> Option<(usize, Option<usize>)> {
2738        self.with_description(|d| d.procfs.as_ref().map(ProcfsFile::position))
2739    }
2740
2741    /// Whether this is a descriptor's fdinfo Detcore opened as rendered
2742    /// (`ProcfsFile::rendered_fdinfo_target`).
2743    pub(crate) fn procfs_is_rendered_fdinfo(&self) -> bool {
2744        self.with_description(|d| {
2745            d.procfs
2746                .as_ref()
2747                .is_some_and(|procfs| procfs.is_rendered() && procfs.is_fdinfo())
2748        })
2749    }
2750
2751    pub(crate) fn procfs_target_fd(&self) -> Option<i32> {
2752        self.with_description(|d| d.procfs.as_ref().and_then(ProcfsFile::target_fd))
2753    }
2754
2755    /// Move the cursor shared by every alias of a procfs open file as the
2756    /// guest's `lseek` does (`ProcfsFile::seek_to`).
2757    pub(crate) fn seek_procfs(&self, offset: usize) {
2758        self.with_description(|d| {
2759            d.procfs
2760                .as_mut()
2761                .expect("procfs fd disappeared while seeking it")
2762                .seek_to(offset)
2763        });
2764    }
2765
2766    /// Update the cursor shared by every alias of a procfs open file.
2767    pub(crate) fn set_procfs_offset(&self, offset: usize) {
2768        self.with_description(|d| {
2769            d.procfs
2770                .as_mut()
2771                .expect("procfs fd disappeared while updating its offset")
2772                .set_offset(offset)
2773        });
2774    }
2775
2776    /// Cached stat data attached to the backing object.
2777    pub fn stat(&self) -> Option<DetStat> {
2778        self.with_description(|d| d.stat)
2779    }
2780
2781    /// Whether Detcore has made the open file description physically nonblocking.
2782    pub fn physically_nonblocking(&self) -> bool {
2783        self.with_description(|d| d.physically_nonblocking)
2784    }
2785
2786    pub(crate) fn status_flags(&self) -> i32 {
2787        self.with_description(|d| d.status_flags)
2788    }
2789
2790    /// Mark every alias of this open file description physically nonblocking.
2791    pub fn set_physically_nonblocking(&self) {
2792        self.with_description(|d| d.physically_nonblocking = true);
2793    }
2794
2795    /// Whether the guest's blocking mode of this open file description is
2796    /// unknown (`OpenFileDescription::blocking_mode_unknown`).
2797    pub(crate) fn blocking_mode_unknown(&self) -> bool {
2798        self.with_description(|d| d.blocking_mode_unknown)
2799    }
2800
2801    /// Mark the guest's blocking mode of every alias of this open file
2802    /// description unknown (`OpenFileDescription::blocking_mode_unknown`).
2803    pub(crate) fn mark_blocking_mode_unknown(&self) {
2804        self.with_description(|d| d.blocking_mode_unknown = true);
2805    }
2806
2807    /// Whether this is a FIFO that may still become an internally polled pipe
2808    /// (see `OpenFileDescription::fifo_awaiting_guest_peer`).
2809    pub(crate) fn fifo_awaiting_guest_peer(&self) -> bool {
2810        self.with_description(|d| d.fifo_awaiting_guest_peer)
2811    }
2812
2813    /// Whether this is a FIFO opened by path that joined internal polling
2814    /// (see `OpenFileDescription::named_fifo`).
2815    pub(crate) fn is_named_fifo(&self) -> bool {
2816        self.with_description(|d| d.named_fifo)
2817    }
2818
2819    /// Mark a `Regular` FIFO as one that may become an internally polled pipe.
2820    pub(crate) fn mark_fifo_awaiting_guest_peer(&self) {
2821        self.with_description(|d| {
2822            debug_assert_eq!(d.ty, FdType::Regular);
2823            d.fifo_awaiting_guest_peer = true;
2824        });
2825    }
2826
2827    /// The FIFO is now physically nonblocking (Detcore set `O_NONBLOCK`, here
2828    /// or through another process sharing the open file description): treat
2829    /// it from now on as a scheduler-managed pipe, as `pipe2` creates.
2830    pub(crate) fn fifo_joins_internal_polling(&self) {
2831        self.with_description(|d| {
2832            debug_assert!(d.fifo_awaiting_guest_peer);
2833            d.fifo_awaiting_guest_peer = false;
2834            d.named_fifo = true;
2835            d.ty = FdType::Pipe;
2836            d.physically_nonblocking = true;
2837        });
2838    }
2839
2840    /// Update file status flags for every alias of this open file description.
2841    pub fn set_status_flags(&self, flags: i32) {
2842        self.with_description(|description| {
2843            // F_SETFL cannot change how this description was opened. Access
2844            // checks must retain these bits even when an alias passes only
2845            // O_NONBLOCK (whose access-mode bits happen to spell O_RDONLY).
2846            let mutable = (OFlag::O_APPEND
2847                | OFlag::O_ASYNC
2848                | OFlag::O_DIRECT
2849                | OFlag::O_NOATIME
2850                | OFlag::O_NONBLOCK)
2851                .bits();
2852            description.status_flags = (description.status_flags & !mutable) | (flags & mutable);
2853            description.physically_nonblocking = oflags_nonblocking(description.status_flags);
2854            // The guest has now set the mode itself.
2855            description.blocking_mode_unknown = false;
2856        })
2857    }
2858
2859    // TODO-HUMAN-REVIEW(PR-912): Review open-file sharing of socket receive timestamps.
2860    /// Record the logical time at which a socket delivered its most recent packet.
2861    pub(crate) fn set_socket_receive_timestamp(&self, timestamp: LogicalTime) {
2862        self.with_description(|d| d.socket_receive_timestamp = Some(timestamp));
2863    }
2864
2865    /// Return the last receive timestamp shared by every alias of this socket.
2866    pub(crate) fn socket_receive_timestamp(&self) -> Option<LogicalTime> {
2867        self.with_description(|d| d.socket_receive_timestamp)
2868    }
2869
2870    // AUTONOMOUS-BOT-IMPLEMENTED
2871    // TODO-HUMAN-REVIEW(PR-1064)
2872    /// Mark this open file as a `NETLINK_SOCK_DIAG` socket. Shared across every
2873    /// dup/fork alias of the same open file description.
2874    pub(crate) fn set_sock_diag(&self) {
2875        self.with_description(|d| d.sock_diag = true);
2876    }
2877
2878    // TODO-HUMAN-REVIEW(PR-1064)
2879    /// Whether this open file is a `NETLINK_SOCK_DIAG` socket whose dump replies
2880    /// must have their socket inode numbers determinized.
2881    pub(crate) fn is_sock_diag(&self) -> bool {
2882        self.with_description(|d| d.sock_diag)
2883    }
2884
2885    /// Mark this open file as a `NETLINK_ROUTE` socket. Like `set_sock_diag`
2886    /// this applies to the open file description, so a dup or fork alias of the
2887    /// same socket is covered too.
2888    pub(crate) fn set_netlink_route(&self) {
2889        self.with_description(|d| d.netlink_route = true);
2890    }
2891
2892    /// Whether this open file is a `NETLINK_ROUTE` socket whose link-dump
2893    /// replies carry live interface counters.
2894    pub(crate) fn is_netlink_route(&self) -> bool {
2895        self.with_description(|d| d.netlink_route)
2896    }
2897
2898    /// Update whether this open file is connected to a loopback peer.
2899    pub(crate) fn set_loopback_peer(&self, loopback_peer: bool) {
2900        self.with_description(|d| d.loopback_peer = loopback_peer);
2901    }
2902
2903    /// Whether this open file is a socket connected to a loopback peer.
2904    pub(crate) fn is_loopback_peer(&self) -> bool {
2905        self.with_description(|d| d.loopback_peer)
2906    }
2907
2908    /// Mark this open file as an external network trace channel.
2909    pub(crate) fn set_network_channel(&self) {
2910        self.with_description(|d| d.network_channel = true);
2911    }
2912
2913    /// Whether this open file is an external network trace channel.
2914    pub(crate) fn is_network_channel(&self) -> bool {
2915        self.with_description(|d| d.network_channel)
2916    }
2917
2918    /// Record the socket's `SO_RCVLOWAT`.
2919    pub(crate) fn set_network_lowat(&self, lowat: usize) {
2920        self.with_description(|d| d.network_lowat = Some(lowat));
2921    }
2922
2923    /// The socket's `SO_RCVLOWAT`.
2924    pub(crate) fn network_lowat(&self) -> usize {
2925        self.with_description(|d| d.network_lowat.unwrap_or(1))
2926    }
2927
2928    /// Record the socket's family and type for the network trace.
2929    pub(crate) fn set_network_socket(&self, kind: NetworkSocketKind) {
2930        self.with_description(|d| d.network_socket = kind);
2931    }
2932
2933    /// The socket's family and type as the network trace classified it.
2934    pub(crate) fn network_socket(&self) -> NetworkSocketKind {
2935        self.with_description(|d| d.network_socket)
2936    }
2937
2938    /// Mark this open file as one end of a `socketpair(2)`. Like
2939    /// `set_netlink_route` this applies to the open file description, so a dup
2940    /// or fork alias of the same endpoint is covered too -- which is the case
2941    /// that matters, since the usual way to use a socketpair is to fork and let
2942    /// each side inherit one end.
2943    pub(crate) fn set_socketpair_endpoint(&self) {
2944        self.with_description(|d| d.socketpair_endpoint = true);
2945    }
2946
2947    /// Make this socket and `peer`, the two sockets of one `socketpair`, each
2948    /// other's peer for `SCM_RIGHTS` bookkeeping (`queue_rights_to_peer`);
2949    /// `stream` says whether they are `SOCK_STREAM`. Kept by every alias,
2950    /// as the description is.
2951    pub(crate) fn pair_sockets(&self, peer: &DetFd, stream: bool) {
2952        self.pair_with(&peer.open_file_handle(), stream);
2953    }
2954
2955    /// As [`Self::pair_sockets`], with the other socket named by a handle on
2956    /// its description: an accepted connection and the socket whose
2957    /// `connect` made it. Nothing if that socket is gone.
2958    pub(crate) fn pair_with(&self, peer: &OpenFileHandle, stream: bool) {
2959        let this = self.open_file_handle();
2960        if peer
2961            .with_description(|d| {
2962                d.rights.peer = Some(this);
2963                d.rights.stream = stream;
2964            })
2965            .is_some()
2966        {
2967            self.with_description(|d| {
2968                d.rights.peer = Some(peer.clone());
2969                d.rights.stream = stream;
2970            });
2971        }
2972    }
2973
2974    /// Record that this is an `AF_UNIX` socket of type `unix_type`
2975    /// (`SOCK_STREAM`, `SOCK_SEQPACKET` or `SOCK_DGRAM`).
2976    pub(crate) fn set_unix_type(&self, unix_type: i32) {
2977        self.with_description(|d| d.rights.unix_type = Some(unix_type));
2978    }
2979
2980    /// This `AF_UNIX` socket's type, if Detcore saw it made.
2981    pub(crate) fn unix_type(&self) -> Option<i32> {
2982        self.with_description(|d| d.rights.unix_type)
2983    }
2984
2985    /// Record the name a `bind` gave this `AF_UNIX` socket, and which bind
2986    /// of that name it was (`generation`).
2987    pub(crate) fn set_unix_name(&self, name: Box<[u8]>, generation: u64) {
2988        self.with_description(|d| {
2989            d.rights.unix_name = Some((name, generation));
2990            // Any socket can now send to it by name, unseen by the queue.
2991            give_up_completeness(&mut d.rights);
2992        });
2993    }
2994
2995    /// This socket and `peer`, the two sockets of a datagram or seqpacket
2996    /// `socketpair`, which nothing but each other can reach, start with
2997    /// queues that hold every message (`SocketRights::complete`).
2998    pub(crate) fn mark_complete_pair(&self, peer: &DetFd) {
2999        self.with_description(|d| d.rights.complete = true);
3000        peer.with_description(|d| d.rights.complete = true);
3001    }
3002
3003    /// Messages Detcore does not see sent may now reach this socket: a
3004    /// `bind` gave it a name, or it set `SO_PASSCRED`, which gives it one at
3005    /// its next send (`unix_autobind`). Its queue no longer holds every
3006    /// message (`SocketRights::complete`).
3007    pub(crate) fn give_up_completeness(&self) {
3008        self.with_description(|d| give_up_completeness(&mut d.rights));
3009    }
3010
3011    /// Another descriptor table may now hold this socket without Detcore
3012    /// modeling it as this description: it was passed over `SCM_RIGHTS` or
3013    /// binder, where the receipt may record an open file of its own. A send
3014    /// through that copy reaches this socket's peer unseen, and a receive
3015    /// through it takes from this socket's queue unseen, so neither queue
3016    /// holds every message any more (`SocketRights::complete`).
3017    pub(crate) fn give_up_pair_completeness(&self) {
3018        let peer = self.with_description(|d| {
3019            give_up_completeness(&mut d.rights);
3020            d.rights.peer.clone()
3021        });
3022        if let Some(peer) = peer {
3023            peer.with_description(|d| give_up_completeness(&mut d.rights));
3024        }
3025    }
3026
3027    /// Whether this socket's peer has a queue that holds every message
3028    /// (`SocketRights::complete`), which a send of data alone through this
3029    /// socket must join (`Detcore::note_send_without_control`).
3030    pub(crate) fn peer_queue_is_complete(&self) -> bool {
3031        self.with_description(|d| d.rights.peer.clone())
3032            .and_then(|peer| peer.with_description(|d| d.rights.complete))
3033            .unwrap_or(false)
3034    }
3035
3036    /// The name a `bind` gave this `AF_UNIX` socket, which bind of that name
3037    /// it was, and the socket's type, if Detcore knows all three.
3038    pub(crate) fn unix_name_and_type(&self) -> Option<(Box<[u8]>, u64, i32)> {
3039        self.with_description(|d| {
3040            let (name, generation) = d.rights.unix_name.clone()?;
3041            Some((name, generation, d.rights.unix_type?))
3042        })
3043    }
3044
3045    /// This datagram socket's messages no longer reach the peer Detcore
3046    /// paired it with: a `connect` gave it another peer, or none
3047    /// (`unix_dgram_connect`). Neither socket is the other's peer any more,
3048    /// so no send through either queues on the other; this socket's queue is
3049    /// forgotten, as Linux purges its receive queue when its peer changes
3050    /// (`unix_dgram_disconnected`); and neither queue follows its socket's
3051    /// messages one for one any more (`SocketRights::lockstep_lost`), since
3052    /// the old peer can still name this socket, and this socket the old peer.
3053    /// A connect to the socket that already is its peer changes nothing in
3054    /// Linux; Detcore forgets the queue then too, which costs only joins.
3055    /// The same holds when a send through it finds its peer gone
3056    /// (`unix_dgram_sendmsg` purges the queue, `ECONNREFUSED`). The messages
3057    /// forgotten, for their descriptions to be released
3058    /// (`Detcore::settle_released_rights`).
3059    pub(crate) fn unpair_rights_peer(&self) -> Vec<SentDescriptor> {
3060        let (peer, purged) = self.with_description(|d| {
3061            d.rights.lockstep_lost = true;
3062            d.rights.suspects = 0;
3063            d.rights.complete = false;
3064            (
3065                d.rights.peer.take(),
3066                d.rights
3067                    .queued
3068                    .drain(..)
3069                    .flat_map(|message| message.descriptors)
3070                    .collect::<Vec<_>>(),
3071            )
3072        });
3073        if let Some(peer) = peer {
3074            peer.with_description(|d| {
3075                d.rights.peer = None;
3076                d.rights.lockstep_lost = true;
3077                give_up_completeness(&mut d.rights);
3078            });
3079        }
3080        purged
3081    }
3082
3083    /// Every message queued to this socket, taken as its description goes
3084    /// with its last reference (`FileMetadata::remove_fd`), for their
3085    /// descriptions to be released (`Detcore::settle_released_rights`), as
3086    /// Linux releases them when the receiving socket closes
3087    /// (`unix_release_sock`).
3088    pub(crate) fn take_all_queued_rights(&self) -> Vec<SentDescriptor> {
3089        self.with_description(|d| {
3090            d.rights.suspects = 0;
3091            d.rights
3092                .queued
3093                .drain(..)
3094                .flat_map(|message| message.descriptors)
3095                .collect()
3096        })
3097    }
3098
3099    /// Whether this socket has a peer Detcore knows (`pair_sockets`,
3100    /// `pair_with`), and if so whether the pair is `SOCK_STREAM`.
3101    pub(crate) fn rights_peer_is_stream(&self) -> Option<bool> {
3102        self.with_description(|d| d.rights.peer.is_some().then_some(d.rights.stream))
3103    }
3104
3105    /// Queue on this socket's peer one message of `bytes` bytes sent through
3106    /// this socket, with the descriptors it carried, in order (`sent`): `Ok`
3107    /// once queued, each copy now counted in flight
3108    /// ([`OpenFileDescription::in_flight`]), or, for a message of data alone,
3109    /// also once passed over because the peer's queue does not hold every
3110    /// message (`SocketRights::complete`), where data alone has no place to
3111    /// keep; `Err` with the descriptors back when this socket has no peer
3112    /// Detcore knows, or the peer is gone.
3113    ///
3114    /// A stream's message of data alone after another joins it, as a stream's
3115    /// reads count bytes only. At [`MAX_QUEUED_RIGHTS_MESSAGES`] the queue
3116    /// gives up its messages of data alone, if it held any
3117    /// ([`give_up_completeness`]), and no longer holds every message; if it is
3118    /// still full, it drops the oldest message, unreleased, as Linux still
3119    /// holds it, and no longer follows its messages one for one.
3120    pub(crate) fn queue_rights_to_peer(
3121        &self,
3122        sent: Vec<SentDescriptor>,
3123        bytes: usize,
3124    ) -> Result<(), Vec<SentDescriptor>> {
3125        let Some(peer) = self.with_description(|d| d.rights.peer.clone()) else {
3126            return Err(sent);
3127        };
3128        // Counted outside the peer's lock: a copy may be of the peer itself.
3129        let copies: Vec<InFlightOpenFile> = sent
3130            .iter()
3131            .filter_map(|sent| sent.open_file.clone())
3132            .collect();
3133        let mut message = Some(QueuedMessage {
3134            descriptors: sent,
3135            bytes,
3136        });
3137        let outcome = peer.with_description(|d| {
3138            let rights = &mut d.rights;
3139            let message = message.take().expect("a message is queued once");
3140            if rights.queued.len() >= MAX_QUEUED_RIGHTS_MESSAGES {
3141                // No room to keep messages of data alone: none carried a
3142                // descriptor, so the queue stays in lockstep without them.
3143                give_up_completeness(rights);
3144            }
3145            if message.descriptors.is_empty() {
3146                if !rights.complete {
3147                    return (false, None);
3148                }
3149                // A stream's reads count bytes, not messages: data alone that
3150                // follows data alone joins it.
3151                if rights.stream
3152                    && let Some(newest) = rights.queued.back_mut()
3153                    && newest.descriptors.is_empty()
3154                {
3155                    newest.bytes += message.bytes;
3156                    return (true, None);
3157                }
3158            }
3159            let dropped = (rights.queued.len() >= MAX_QUEUED_RIGHTS_MESSAGES)
3160                .then(|| {
3161                    rights.lockstep_lost = true;
3162                    rights.suspects = rights.suspects.saturating_sub(1);
3163                    rights.queued.pop_front()
3164                })
3165                .flatten();
3166            rights.queued.push_back(message);
3167            (true, dropped)
3168        });
3169        match outcome {
3170            Some((queued, dropped)) => {
3171                if queued {
3172                    for copy in &copies {
3173                        copy.enter_flight();
3174                    }
3175                }
3176                // Linux still holds the message dropped to make room: what
3177                // it carried stays counted in flight.
3178                drop(dropped);
3179                Ok(())
3180            }
3181            None => Err(message
3182                .take()
3183                .map(|message| message.descriptors)
3184                .unwrap_or_default()),
3185        }
3186    }
3187
3188    /// A message this socket sent may have carried descriptors that were not
3189    /// queued on its peer, or reached the peer out of the queue's order
3190    /// (`SocketRights::lockstep_lost`).
3191    pub(crate) fn lose_peer_lockstep(&self) {
3192        if let Some(peer) = self.with_description(|d| d.rights.peer.clone()) {
3193            peer.with_description(|d| {
3194                d.rights.lockstep_lost = true;
3195                give_up_completeness(&mut d.rights);
3196            });
3197        }
3198    }
3199
3200    /// A call took up to `taken` messages from this socket that Detcore
3201    /// could not match to its queue. The queue is kept: Detcore cannot tell
3202    /// whether the messages the call took carried descriptors, and Linux
3203    /// still delivers the others, in order. The call may have taken as many
3204    /// of the oldest queued messages, which are suspect from now on
3205    /// (`SocketRights::suspects`), and forgotten when a receive takes a later
3206    /// one (`Self::take_queued_rights`). Where the queue holds every message
3207    /// (`SocketRights::complete`) but holds none, the call took one it never
3208    /// held, which may have carried descriptors: the queue is out of
3209    /// lockstep. Under record and replay (`legacy`), the rule existing
3210    /// recordings were made with: any such call while something is queued
3211    /// puts the queue out of lockstep. A stream socket's queue no longer holds
3212    /// every message after such a call (`give_up_completeness`), as one stream
3213    /// read takes messages of data alone along with at most one that carried
3214    /// descriptors.
3215    pub(crate) fn lose_lockstep_if_queued(&self, taken: usize, legacy: bool) {
3216        self.with_description(|d| {
3217            let rights = &mut d.rights;
3218            if rights.stream && !rights.queued.is_empty() {
3219                // A stream read takes any number of messages of data alone
3220                // before the one whose descriptors it detaches: the suspects
3221                // count only messages that carried descriptors.
3222                give_up_completeness(rights);
3223            }
3224            if !rights.queued.is_empty() {
3225                if legacy {
3226                    rights.lockstep_lost = true;
3227                }
3228                rights.suspects = rights
3229                    .suspects
3230                    .saturating_add(taken)
3231                    .min(rights.queued.len());
3232            } else if rights.complete {
3233                rights.lockstep_lost = true;
3234                give_up_completeness(rights);
3235            }
3236        });
3237    }
3238
3239    /// Whether a message is queued to this socket.
3240    pub(crate) fn has_queued_rights(&self) -> bool {
3241        self.with_description(|d| !d.rights.queued.is_empty())
3242    }
3243
3244    /// A call took the oldest message Linux held for this datagram or
3245    /// seqpacket socket without naming its descriptors: a read with no
3246    /// control buffer, or a receive whose message Linux delivered with none.
3247    /// Where the queue holds every message (`SocketRights::complete`) and has
3248    /// nothing suspect, that is the oldest queued message, which `fits` (what
3249    /// the call reports of it) must agree with: it is taken, and its
3250    /// descriptors returned for their descriptions to be released, as Linux
3251    /// releases them. `None` otherwise, for the caller to count the call as
3252    /// one that may have taken a message unseen
3253    /// ([`Self::lose_lockstep_if_queued`]); a queue whose oldest message does
3254    /// not fit, or that holds none, missed a message, so it is out of
3255    /// lockstep and no longer holds every message.
3256    pub(crate) fn take_head_message(
3257        &self,
3258        fits: impl FnOnce(&QueuedMessage) -> bool,
3259    ) -> Option<Vec<SentDescriptor>> {
3260        self.with_description(|d| {
3261            let rights = &mut d.rights;
3262            if !rights.complete || rights.suspects > 0 {
3263                return None;
3264            }
3265            match rights.queued.front() {
3266                Some(head) if fits(head) => rights.queued.pop_front().map(|head| head.descriptors),
3267                _ => {
3268                    rights.lockstep_lost = true;
3269                    give_up_completeness(rights);
3270                    None
3271                }
3272            }
3273        })
3274    }
3275
3276    /// A receive took one message of data alone from this datagram or
3277    /// seqpacket socket, delivered without descriptors and without
3278    /// `MSG_CTRUNC`. A queue of descriptors alone loses nothing to it. One
3279    /// that holds every message (`SocketRights::complete`) takes its oldest,
3280    /// which must be of data alone ([`Self::take_head_message`]), or, while
3281    /// some are suspect, counts one more message that may be gone.
3282    pub(crate) fn take_data_only_message(&self) {
3283        let suspect = self.with_description(|d| d.rights.complete && d.rights.suspects > 0);
3284        if suspect {
3285            self.lose_lockstep_if_queued(1, false);
3286        } else {
3287            let _ = self.take_head_message(|head| head.descriptors.is_empty());
3288        }
3289    }
3290
3291    /// A stream read took `read` bytes from this socket without naming the
3292    /// descriptors it detached: a read with no control buffer, or a receive
3293    /// that installed none. Where the queue holds every message
3294    /// (`SocketRights::complete`) and has nothing suspect, the bytes come
3295    /// from the oldest messages on ([`stream_walk`]), and `fits` must agree
3296    /// with the descriptors they detach, if any: then they are taken, and the
3297    /// descriptors returned for their descriptions to be released, as Linux
3298    /// releases them. `None` otherwise, for the caller to count the call as
3299    /// one that may have taken a message unseen
3300    /// ([`Self::lose_lockstep_if_queued`]); a queue the read does not fit
3301    /// missed a message, so it is out of lockstep and no longer holds every
3302    /// message.
3303    pub(crate) fn take_stream_bytes(
3304        &self,
3305        read: usize,
3306        fits: impl FnOnce(Option<&[SentDescriptor]>) -> bool,
3307    ) -> Option<Vec<SentDescriptor>> {
3308        self.with_description(|d| {
3309            let rights = &mut d.rights;
3310            if !rights.complete || !rights.stream || rights.suspects > 0 {
3311                return None;
3312            }
3313            match stream_walk(rights, read) {
3314                Some(walk)
3315                    if fits(
3316                        walk.detached
3317                            .map(|index| rights.queued[index].descriptors.as_slice()),
3318                    ) =>
3319                {
3320                    Some(apply_stream_walk(rights, walk))
3321                }
3322                _ => {
3323                    rights.lockstep_lost = true;
3324                    give_up_completeness(rights);
3325                    None
3326                }
3327            }
3328        })
3329    }
3330
3331    /// How many bytes a stream read of this socket that asked for `asked`
3332    /// took, where Linux does not report it: one that took its bytes and then
3333    /// failed copying an output out (`EFAULT`). Where the queue holds every
3334    /// message (`SocketRights::complete`) and has nothing suspect, Linux took
3335    /// what [`stream_read_count`] says, provided it copied every byte; `None`
3336    /// otherwise.
3337    pub(crate) fn stream_read_count(&self, asked: usize) -> Option<usize> {
3338        self.with_description(|d| {
3339            let rights = &d.rights;
3340            (rights.complete && rights.stream && rights.suspects == 0)
3341                .then(|| stream_read_count(rights, asked))
3342        })
3343    }
3344
3345    /// The descriptors of the message queued to this socket that a receive,
3346    /// which installed descriptors whose identities are `received` from one
3347    /// message (`truncated` when Linux set `MSG_CTRUNC` for it), took: the
3348    /// oldest queued message they name ([`names_sent_rights`]), or `None`;
3349    /// whether the queue still follows the socket's messages one for one
3350    /// (`SocketRights::lockstep_lost`); and whether the receipt may join the
3351    /// descriptions that message holds ([`TakenRights::joinable`]).
3352    ///
3353    /// Linux delivers a socket's messages in order, so the messages queued
3354    /// before it were taken by calls Detcore could not match: they are
3355    /// forgotten with it ([`TakenRights::forgotten`]). A `MSG_PEEK` receive
3356    /// (`peek`) leaves every message queued, as Linux does.
3357    ///
3358    /// The message the receive took is the oldest Linux still held, so it is
3359    /// at most the first past the suspects (`SocketRights::suspects`). A
3360    /// match there or among them is trusted only where no later suspect, nor
3361    /// the first message past them, names the same files: otherwise the
3362    /// receive may have taken that later one after the matched one was
3363    /// released unseen, and the matched message is a stale entry, which is
3364    /// not joined; the messages that may be that receive's own stay suspect.
3365    /// A match beyond the first message past the suspects names a message
3366    /// Linux cannot have delivered yet, so this receive took one that was
3367    /// never queued here, naming the same files, or one a call Detcore did
3368    /// not count took: `None`, and every message up to the matched one stays
3369    /// queued, as Linux still holds them, suspect from now on, with the queue
3370    /// out of lockstep. A peek may name a later message (`SO_PEEK_OFF`): it
3371    /// is trusted wherever it is, where the queue is in lockstep and no other
3372    /// queued message names the same files, so that no other message can be
3373    /// the one peeked. A receipt that names no queued message took one never
3374    /// queued here: the queue is out of lockstep.
3375    ///
3376    /// A stream receive of `bytes` bytes from a queue that holds every
3377    /// message (`SocketRights::complete`) took those bytes from the oldest
3378    /// messages on ([`stream_walk`]): the message whose descriptors they
3379    /// detached is the one taken, where the received descriptors name it; a
3380    /// queue the receive does not fit missed a message, and is out of lockstep
3381    /// and no longer holds every message.
3382    ///
3383    /// Under record and replay (`legacy`) the rule existing recordings were
3384    /// made with holds: a match anywhere takes the matched message and
3385    /// forgets those before it, and a match past the first puts the queue out
3386    /// of lockstep.
3387    pub(crate) fn take_queued_rights(
3388        &self,
3389        received: &[FileIdentity],
3390        truncated: bool,
3391        peek: bool,
3392        legacy: bool,
3393        bytes: Option<usize>,
3394    ) -> Option<TakenRights> {
3395        self.with_description(|d| {
3396            let rights = &mut d.rights;
3397            let names =
3398                |sent: &QueuedMessage| names_sent_rights(&sent.descriptors, received, truncated);
3399            // A stream receive whose queue holds every message took its bytes
3400            // from the oldest messages on, and the descriptors it installed are
3401            // those of the message whose descriptors those bytes detached.
3402            if rights.stream
3403                && rights.complete
3404                && rights.suspects == 0
3405                && !peek
3406                && !legacy
3407                && let Some(read) = bytes
3408            {
3409                match stream_walk(rights, read) {
3410                    Some(walk)
3411                        if walk
3412                            .detached
3413                            .is_some_and(|index| names(&rights.queued[index])) =>
3414                    {
3415                        return Some(TakenRights {
3416                            sent: apply_stream_walk(rights, walk),
3417                            lockstep: !rights.lockstep_lost,
3418                            joinable: true,
3419                            forgotten: Vec::new(),
3420                        });
3421                    }
3422                    _ => {
3423                        rights.lockstep_lost = true;
3424                        give_up_completeness(rights);
3425                    }
3426                }
3427            }
3428            let Some(index) = rights.queued.iter().position(names) else {
3429                if !legacy && !peek {
3430                    rights.lockstep_lost = true;
3431                    give_up_completeness(rights);
3432                }
3433                return None;
3434            };
3435            // Where each call takes one message, a message's place is its
3436            // index; a stream read takes messages of data alone along with one
3437            // that carried descriptors, so there a message's place counts only
3438            // those that carried some (the suspects count them alike).
3439            let stream = rights.stream;
3440            let mut carried = 0;
3441            let places: Vec<usize> = rights
3442                .queued
3443                .iter()
3444                .enumerate()
3445                .map(|(index, message)| {
3446                    let place = if stream { carried } else { index };
3447                    carried += usize::from(!message.descriptors.is_empty());
3448                    place
3449                })
3450                .collect();
3451            let place = places[index];
3452            let suspects = rights.suspects;
3453            let ambiguous = rights
3454                .queued
3455                .iter()
3456                .zip(&places)
3457                .skip(index + 1)
3458                .take_while(|(_, later)| **later <= suspects)
3459                .any(|(sent, _)| names(sent));
3460            let unique = !rights.lockstep_lost && !rights.queued.iter().skip(index + 1).any(names);
3461            let joinable = (place <= suspects && !ambiguous) || (peek && unique);
3462            if legacy {
3463                if index > 0 {
3464                    rights.lockstep_lost = true;
3465                }
3466            } else if place > suspects && !peek {
3467                // Up to the match, every message stays, suspect. Where messages
3468                // of data alone go first, a stream's places are its indices.
3469                if stream {
3470                    give_up_completeness(rights);
3471                    rights.suspects = (place + 1).min(rights.queued.len());
3472                } else {
3473                    rights.suspects = (index + 1).min(rights.queued.len());
3474                    give_up_completeness(rights);
3475                }
3476                rights.lockstep_lost = true;
3477                return None;
3478            }
3479            let (sent, forgotten) = if peek {
3480                (
3481                    rights
3482                        .queued
3483                        .get(index)
3484                        .map(|message| message.descriptors.clone()),
3485                    Vec::new(),
3486                )
3487            } else {
3488                rights.suspects = if joinable || place > suspects {
3489                    0
3490                } else {
3491                    suspects - place
3492                };
3493                let forgotten = rights
3494                    .queued
3495                    .drain(..index)
3496                    .flat_map(|message| message.descriptors)
3497                    .collect();
3498                let sent = rights.queued.pop_front().map(|message| message.descriptors);
3499                rights.suspects = rights.suspects.min(rights.queued.len());
3500                (sent, forgotten)
3501            };
3502            sent.map(|sent| TakenRights {
3503                sent,
3504                lockstep: !rights.lockstep_lost,
3505                joinable,
3506                forgotten,
3507            })
3508        })
3509    }
3510
3511    /// Whether this open file originated in `socketpair(2)`, for live internal
3512    /// routing. Guest-only readiness instead requires `is_guest_held`.
3513    pub(crate) fn is_socketpair_endpoint(&self) -> bool {
3514        self.with_description(|d| d.socketpair_endpoint)
3515    }
3516
3517    /// Mark this socket as one whose peer is a guest of this container. Like
3518    /// `set_socketpair_endpoint` it applies to the open file description, so
3519    /// dup, fork and `SCM_RIGHTS` aliases are covered too.
3520    pub(crate) fn set_guest_peer(&self) {
3521        self.with_description(|d| d.guest_peer = true);
3522    }
3523
3524    /// Whether this socket uses internal execution routing: a `socketpair(2)`
3525    /// endpoint, or a socket whose peer record/replay found to be a guest.
3526    /// Detcore keeps such a socket physically nonblocking
3527    /// and polls a blocking call on it in the caller's turn, as for a pipe
3528    /// (`syscall_targets_internal_fd`).
3529    pub(crate) fn is_internal_socket(&self) -> bool {
3530        self.with_description(|d| d.socketpair_endpoint || d.guest_peer)
3531    }
3532
3533    /// Mark this open file as one whose other end only guests hold (see
3534    /// `is_guest_held`). It applies to the open file description, so dup and
3535    /// fork aliases are covered too.
3536    pub(crate) fn set_guest_held(&self) {
3537        self.with_description(|d| d.guest_held = true);
3538    }
3539
3540    /// Withdraw `set_guest_held`, permanently: a process outside Detcore's
3541    /// view may now hold this object or its other end.
3542    pub(crate) fn clear_guest_held(&self) {
3543        self.with_description(|d| d.guest_held = false);
3544    }
3545
3546    /// Whether only guests hold this object's other end, so that only a guest
3547    /// action can change its readiness: what `Resources::polls_guest_state_only`
3548    /// asks of every descriptor a poll waits on.
3549    pub(crate) fn is_guest_held(&self) -> bool {
3550        self.with_description(|d| d.guest_held)
3551    }
3552
3553    /// Mark this socket as one a guest made in this run (see
3554    /// `is_guest_created`), for its open file description.
3555    pub(crate) fn set_guest_created(&self) {
3556        self.with_description(|d| d.guest_created = true);
3557    }
3558
3559    /// Withdraw `set_guest_created`, permanently: the socket was passed out
3560    /// of the guest.
3561    pub(crate) fn clear_guest_created(&self) {
3562        self.with_description(|d| d.guest_created = false);
3563    }
3564
3565    /// Whether a guest made this socket with `socket(2)` or `socketpair(2)`
3566    /// in this run and never passed it out of the guest, so only guests hold
3567    /// it. Production code reads it through an `OpenFileHandle`.
3568    #[cfg(test)]
3569    pub(crate) fn is_guest_created(&self) -> bool {
3570        self.with_description(|d| d.guest_created)
3571    }
3572
3573    /// Mark this open file as a connection returned by `accept(2)` or
3574    /// `accept4(2)`. It applies to the open file description, so dup and fork
3575    /// aliases are covered too.
3576    pub(crate) fn set_accepted_connection(&self) {
3577        self.with_description(|d| d.accepted_connection = true);
3578    }
3579
3580    /// Record whether a receive with `MSG_ERRQUEUE` on this socket never
3581    /// waits. It applies to the open file description, so dup and fork aliases
3582    /// are covered too.
3583    pub(crate) fn set_error_queue_never_waits(&self, never_waits: bool) {
3584        self.with_description(|d| d.error_queue_never_waits = never_waits);
3585    }
3586
3587    /// Whether a receive with `MSG_ERRQUEUE` on this socket never waits.
3588    pub(crate) fn error_queue_never_waits(&self) -> bool {
3589        self.with_description(|d| d.error_queue_never_waits)
3590    }
3591
3592    /// Whether `accept(2)` or `accept4(2)` returned this open file.
3593    pub(crate) fn is_accepted_connection(&self) -> bool {
3594        self.with_description(|d| d.accepted_connection)
3595    }
3596
3597    /// Mark this open file as a listener whose blocking `accept(2)` runs in
3598    /// the caller's turn under record/replay. It applies to the open file
3599    /// description, so dup and fork aliases are covered too.
3600    pub(crate) fn set_accept_in_turn_eligible(&self) {
3601        self.with_description(|d| d.accept_in_turn_eligible = true);
3602    }
3603
3604    /// Whether a blocking `accept(2)` on this open file runs in the caller's
3605    /// turn: it was marked eligible and has not been exported.
3606    pub(crate) fn is_accept_in_turn_listener(&self) -> bool {
3607        self.with_description(|d| d.accept_in_turn_eligible && !d.exported)
3608    }
3609
3610    /// Whether this open file was marked eligible, exported or not.
3611    pub(crate) fn is_accept_in_turn_eligible(&self) -> bool {
3612        self.with_description(|d| d.accept_in_turn_eligible)
3613    }
3614
3615    /// Mark this socket as one whose read side a guest shut down. It applies
3616    /// to the open file description, so dup and fork aliases are covered too.
3617    pub(crate) fn set_read_shutdown_requested(&self) {
3618        self.with_description(|d| d.read_shutdown_requested = true);
3619    }
3620
3621    /// Whether a guest shut this socket's read side down. Only such a datagram
3622    /// socket can have a blocking receive that Linux ends at once while its
3623    /// nonblocking attempt fails with `EAGAIN`, because Linux sets a datagram
3624    /// socket's read shutdown only in its own `shutdown(2)`
3625    /// (`syscalls::helpers::finish_after_read_shutdown`).
3626    pub(crate) fn read_shutdown_requested(&self) -> bool {
3627        self.with_description(|d| d.read_shutdown_requested)
3628    }
3629
3630    /// Mark this open file description's locks as followed by
3631    /// `procfs::locks::LockLedger`. Permanent.
3632    pub(crate) fn set_lock_tracked(&self) {
3633        self.with_description(|d| d.lock_tracked = true);
3634    }
3635
3636    /// Whether `procfs::locks::LockLedger` follows this description's locks.
3637    pub(crate) fn is_lock_tracked(&self) -> bool {
3638        self.with_description(|d| d.lock_tracked)
3639    }
3640
3641    /// The alias group of this description, where it is one of the
3642    /// container's standard streams that is one open file description with
3643    /// another of them (`FileMetadata::setup_stdio`).
3644    pub(crate) fn stdio_alias_group(&self) -> Option<OpenFileId> {
3645        self.with_description(|d| d.stdio_alias_group)
3646    }
3647
3648    /// Put this description, one of the container's standard streams, in
3649    /// alias group `group`.
3650    pub(crate) fn set_stdio_alias_group(&self, group: OpenFileId) {
3651        self.with_description(|d| d.stdio_alias_group = Some(group));
3652    }
3653
3654    /// Mark this description as registered on reception over `SCM_RIGHTS`.
3655    /// Permanent.
3656    pub(crate) fn set_received_for_locks(&self) {
3657        self.with_description(|d| d.received_for_locks = true);
3658    }
3659
3660    /// Whether this description was registered on reception over
3661    /// `SCM_RIGHTS`.
3662    pub(crate) fn is_received_for_locks(&self) -> bool {
3663        self.with_description(|d| d.received_for_locks)
3664    }
3665
3666    /// Mark this description as one that may hold an OFD lock. Permanent.
3667    pub(crate) fn set_may_hold_ofd_lock(&self) {
3668        self.with_description(|d| d.may_hold_ofd_lock = true);
3669    }
3670
3671    /// Whether this description may hold an OFD lock.
3672    pub(crate) fn may_hold_ofd_lock(&self) -> bool {
3673        self.with_description(|d| d.may_hold_ofd_lock)
3674    }
3675
3676    /// Mark this open file's record sequence as one Detcore cannot know
3677    /// here (`OpenFileDescription::record_sequence_unknown`). Permanent.
3678    /// Only a local description, which every procfs file's is.
3679    pub(crate) fn mark_record_sequence_unknown(&self) {
3680        let mut state = self.open_file.lock().expect("open file mutex poisoned");
3681        if let OpenFileState::Local(description) = &mut *state {
3682            description.record_sequence_unknown = true;
3683        }
3684    }
3685
3686    /// Whether this open file's record sequence is one Detcore cannot know
3687    /// here (`OpenFileDescription::record_sequence_unknown`). Read without a
3688    /// lease, so a shared model (a random device, a timerfd), which is never
3689    /// so marked, answers false at no cost.
3690    pub(crate) fn record_sequence_unknown(&self) -> bool {
3691        let state = self.open_file.lock().expect("open file mutex poisoned");
3692        match &*state {
3693            OpenFileState::Local(description) => description.record_sequence_unknown,
3694            OpenFileState::Shared(_) => false,
3695        }
3696    }
3697
3698    /// Give this descriptor's open file, which it alone refers to and which
3699    /// Detcore just made for a rendered file's placeholder, the identity `id`
3700    /// of the open file the placeholder belongs to, as this process's
3701    /// registry names it (`FileMetadata::adopt_placeholder`,
3702    /// `Detcore::adopt_received_placeholder`), so it takes the record
3703    /// sequence that open file keeps in Detcore's global state.
3704    pub(crate) fn rebind_open_file_id(&self, id: OpenFileId) {
3705        let mut state = self.open_file.lock().expect("open file mutex poisoned");
3706        if let OpenFileState::Local(description) = &mut *state {
3707            description.id = id;
3708        }
3709    }
3710
3711    /// Mark this open file as passed over `SCM_RIGHTS`. Permanent.
3712    pub(crate) fn set_exported(&self) {
3713        self.with_description(|d| d.exported = true);
3714    }
3715
3716    /// Whether this open file was passed over `SCM_RIGHTS`.
3717    pub(crate) fn is_exported(&self) -> bool {
3718        self.with_description(|d| d.exported)
3719    }
3720
3721    /// Record the receive timeout a successful `setsockopt(SO_RCVTIMEO)` set.
3722    pub(crate) fn set_receive_timeout(&self, timeout: ReceiveTimeout) {
3723        self.with_description(|d| d.receive_timeout = timeout);
3724    }
3725
3726    /// The receive timeout `setsockopt(SO_RCVTIMEO)` calls set through any
3727    /// descriptor Detcore tracks for this open file description, dup, fork and
3728    /// exec aliases included. A copy that reached a guest some other way, which
3729    /// Detcore tracks as a description of its own, does not update it; record
3730    /// checks the model against the kernel before a blocking accept relies on
3731    /// it.
3732    pub(crate) fn receive_timeout(&self) -> ReceiveTimeout {
3733        self.with_description(|d| d.receive_timeout)
3734    }
3735
3736    // AUTONOMOUS-BOT-IMPLEMENTED
3737    // TODO-HUMAN-REVIEW(#2373)
3738    /// Return the known `flock(2)` state for this open file description.
3739    ///
3740    /// The outer `None` means Detcore did not observe the descriptor's history.
3741    /// `Some(None)` means known-unlocked; `Some(Some(mode))` means the kernel
3742    /// granted `LOCK_SH` or `LOCK_EX` through this handler.
3743    pub(crate) fn known_flock_mode(&self) -> Option<Option<i32>> {
3744        self.with_description(|description| {
3745            description
3746                .flock_mode_known
3747                .then_some(description.flock_mode)
3748        })
3749    }
3750
3751    // AUTONOMOUS-BOT-IMPLEMENTED
3752    // TODO-HUMAN-REVIEW(#2373)
3753    /// Record the `flock(2)` mode this open file description now holds. Every
3754    /// `dup`/`fork` alias observes the change, matching the kernel, where one
3755    /// `flock` lock belongs to the whole open file description.
3756    ///
3757    /// A grant (`Some`) also marks the description as one that may hold a lock
3758    /// ([`Self::flock_may_have_been_granted`]), even while its mode is unknown
3759    /// and the mode itself is therefore not recorded.
3760    pub(crate) fn set_flock_mode(&self, mode: Option<i32>) {
3761        self.with_description(|description| {
3762            if mode.is_some() {
3763                description.flock_ever_granted = true;
3764            }
3765            if description.flock_mode_known {
3766                description.flock_mode = mode;
3767            }
3768        })
3769    }
3770
3771    /// Mark the kernel lock state unknown without changing the kernel lock.
3772    /// This is required for live-discovered and externally received file
3773    /// descriptions, whose history Detcore did not observe.
3774    pub(crate) fn forget_flock_mode(&self) {
3775        self.with_description(|description| {
3776            description.flock_mode = None;
3777            description.flock_mode_known = false;
3778        })
3779    }
3780
3781    /// Record that Detcore never observed this description's lock history, as
3782    /// opposed to having known it and then invalidated it. Used for descriptors
3783    /// that existed before Detcore began observing the guest.
3784    pub(crate) fn mark_flock_mode_unobserved(&self) {
3785        self.forget_flock_mode();
3786        self.with_description(|description| {
3787            description.flock_mode_ever_known = false;
3788            // A lock may have been taken before Detcore saw the description.
3789            description.flock_ever_granted = true;
3790        })
3791    }
3792
3793    /// True when a `flock` lock may ever have been granted through this open
3794    /// file description, so that it may still hold one. False only for a
3795    /// description Detcore created and through which it never served a
3796    /// successful `LOCK_SH` or `LOCK_EX`.
3797    pub(crate) fn flock_may_have_been_granted(&self) -> bool {
3798        self.with_description(|description| description.flock_ever_granted)
3799    }
3800
3801    /// True when a cached lock claim existed and may now be wrong.
3802    pub(crate) fn flock_mode_may_be_stale(&self) -> bool {
3803        self.with_description(|description| {
3804            description.flock_mode_ever_known && !description.flock_mode_known
3805        })
3806    }
3807}
3808
3809impl fmt::Display for DetFd {
3810    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3811        write!(f, "DetFd({})", self.fd)
3812    }
3813}
3814
3815/// Restores descriptor aliasing after deserialization: every descriptor with
3816/// the same `OpenFileId` refers to one open file description object again.
3817///
3818/// Serialization writes an `Arc`'s contents, not its identity, so two
3819/// descriptors that shared one description (after `dup`) come back as two
3820/// independent copies, and a read through one would no longer move the
3821/// other's cursor. Within one table an `OpenFileId` names exactly one open
3822/// file description, so the copies with one id are identical, and the first
3823/// (lowest descriptor number) is kept. Descriptor flags (`FD_CLOEXEC`) stay
3824/// per descriptor.
3825pub(crate) fn intern_open_files(handles: &mut std::collections::HashMap<RawFd, DetFd>) {
3826    let mut fds: Vec<RawFd> = handles.keys().copied().collect();
3827    fds.sort_unstable();
3828    let mut first: std::collections::HashMap<OpenFileId, DetFd> = std::collections::HashMap::new();
3829    for fd in fds {
3830        let detfd = handles.get_mut(&fd).expect("listed descriptor");
3831        match first.get(&detfd.open_file_id()) {
3832            Some(kept) => detfd.share_open_file_with(kept),
3833            None => {
3834                first.insert(detfd.open_file_id(), detfd.clone());
3835            }
3836        }
3837    }
3838}
3839
3840impl DetFd {
3841    /// A copy of this local description's model, for tests of the global
3842    /// store.
3843    #[cfg(test)]
3844    pub(crate) fn model_for_test(&self) -> OpenFileModel {
3845        self.with_description(|d| {
3846            OpenFileModel(serde_json::from_value(serde_json::to_value(&*d).unwrap()).unwrap())
3847        })
3848    }
3849
3850    /// Moves this open file description's model out to a test's canonical
3851    /// store and makes every alias a shared handle, keeping this process's
3852    /// directory lock. Production promotion uses the acknowledged lease protocol.
3853    #[cfg(test)]
3854    fn share_for_test(&self) -> OpenFileModel {
3855        let mut state = self.open_file.lock().expect("open file mutex poisoned");
3856        let OpenFileState::Local(description) = &*state else {
3857            panic!("open file description is already shared");
3858        };
3859        let shared = SharedOpenFile {
3860            id: description.id,
3861            directory_lock: Arc::clone(&description.directory_lock),
3862        };
3863        let OpenFileState::Local(description) =
3864            std::mem::replace(&mut *state, OpenFileState::Shared(shared))
3865        else {
3866            unreachable!("checked above");
3867        };
3868        OpenFileModel(description)
3869    }
3870}
3871
3872#[cfg(test)]
3873pub(crate) fn install_test_shared_open_file_channel() {
3874    let _ = tests::MemoryChannel::installed();
3875}
3876
3877#[cfg(test)]
3878mod tests {
3879    use super::*;
3880
3881    #[test]
3882    fn random_devices_are_character_devices_one_eight_and_one_nine() {
3883        let chr = libc::S_IFCHR | 0o666;
3884        assert!(is_random_device(chr, libc::makedev(1, 8)));
3885        assert!(is_random_device(chr, libc::makedev(1, 9)));
3886        // /dev/null, /dev/zero and /dev/full share major 1.
3887        for minor in [3, 5, 7] {
3888            assert!(!is_random_device(chr, libc::makedev(1, minor)));
3889        }
3890        // The same numbers on another kind of file name no device.
3891        for kind in [libc::S_IFBLK, libc::S_IFREG, libc::S_IFIFO] {
3892            assert!(!is_random_device(kind | 0o666, libc::makedev(1, 9)));
3893        }
3894        assert!(!is_random_device(chr, libc::makedev(9, 1)));
3895    }
3896
3897    /// The protocols whose receive handler reads the error queue never wait
3898    /// for it, with the type's SOCK_NONBLOCK and SOCK_CLOEXEC bits or not;
3899    /// AF_UNIX, AF_NETLINK and IPv4 L2TP wait, and a protocol or family that is
3900    /// not modelled keeps Detcore's polling.
3901    #[test]
3902    fn error_queue_reads_never_wait_on_the_protocols_that_read_the_queue() {
3903        let flags = libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC;
3904        for family in [libc::AF_INET, libc::AF_INET6] {
3905            let ping = if family == libc::AF_INET {
3906                libc::IPPROTO_ICMP
3907            } else {
3908                libc::IPPROTO_ICMPV6
3909            };
3910            for (socket_type, protocol) in [
3911                (libc::SOCK_STREAM, 0),
3912                (libc::SOCK_STREAM, libc::IPPROTO_TCP),
3913                (libc::SOCK_STREAM, libc::IPPROTO_MPTCP),
3914                (libc::SOCK_STREAM, libc::IPPROTO_SCTP),
3915                (libc::SOCK_SEQPACKET, 0),
3916                (libc::SOCK_SEQPACKET, libc::IPPROTO_SCTP),
3917                (libc::SOCK_DGRAM, 0),
3918                (libc::SOCK_DGRAM, libc::IPPROTO_UDP),
3919                (libc::SOCK_DGRAM, libc::IPPROTO_UDPLITE),
3920                (libc::SOCK_DGRAM, ping),
3921                (libc::SOCK_RAW, libc::IPPROTO_RAW),
3922                (libc::SOCK_RAW, libc::IPPROTO_UDP),
3923            ] {
3924                assert!(error_queue_read_never_waits(family, socket_type, protocol));
3925                assert!(error_queue_read_never_waits(
3926                    family,
3927                    socket_type | flags,
3928                    protocol
3929                ));
3930            }
3931            for (socket_type, protocol) in [
3932                (libc::SOCK_DGRAM, libc::IPPROTO_TCP),
3933                (libc::SOCK_STREAM, libc::IPPROTO_UDP),
3934                // IPPROTO_SMC: not modelled.
3935                (libc::SOCK_STREAM, 256),
3936                (libc::SOCK_RDM, 0),
3937            ] {
3938                assert!(!error_queue_read_never_waits(family, socket_type, protocol));
3939            }
3940        }
3941        // ICMP ping is IPv4's protocol only, ICMPv6 IPv6's.
3942        assert!(!error_queue_read_never_waits(
3943            libc::AF_INET6,
3944            libc::SOCK_DGRAM,
3945            libc::IPPROTO_ICMP
3946        ));
3947        assert!(!error_queue_read_never_waits(
3948            libc::AF_INET,
3949            libc::SOCK_DGRAM,
3950            libc::IPPROTO_ICMPV6
3951        ));
3952        // l2tp_ip6_recvmsg reads the error queue, l2tp_ip_recvmsg waits.
3953        assert!(error_queue_read_never_waits(
3954            libc::AF_INET6,
3955            libc::SOCK_DGRAM,
3956            IPPROTO_L2TP
3957        ));
3958        assert!(!error_queue_read_never_waits(
3959            libc::AF_INET,
3960            libc::SOCK_DGRAM,
3961            IPPROTO_L2TP
3962        ));
3963        for socket_type in [libc::SOCK_RAW, libc::SOCK_DGRAM] {
3964            assert!(error_queue_read_never_waits(
3965                libc::AF_PACKET,
3966                socket_type,
3967                0
3968            ));
3969        }
3970        for socket_type in [libc::SOCK_STREAM, libc::SOCK_DGRAM, libc::SOCK_SEQPACKET] {
3971            assert!(!error_queue_read_never_waits(libc::AF_UNIX, socket_type, 0));
3972        }
3973        assert!(!error_queue_read_never_waits(
3974            libc::AF_NETLINK,
3975            libc::SOCK_RAW,
3976            libc::NETLINK_ROUTE
3977        ));
3978        assert!(!error_queue_read_never_waits(
3979            libc::AF_VSOCK,
3980            libc::SOCK_STREAM,
3981            0
3982        ));
3983    }
3984
3985    #[test]
3986    fn native_exec_directory_lease_probe_preserves_idle_stream_and_aliases() {
3987        use crate::native_exec::NativeExecCutError;
3988
3989        let id = OpenFileId::new(DetTid::from_raw(42), 7);
3990        let fd = DetFd::new(30, OFlag::O_DIRECTORY, FdType::Regular, id);
3991        let alias = fd.clone().with_fd(31);
3992        fd.install_directory_snapshot(
3993            vec![
3994                DirEntry {
3995                    name: b"b".to_vec(),
3996                    ino: 19,
3997                    off: 41,
3998                    ty: libc::DT_REG,
3999                },
4000                DirEntry {
4001                    name: b"a".to_vec(),
4002                    ino: 17,
4003                    off: 43,
4004                    ty: libc::DT_REG,
4005                },
4006            ],
4007            SnapshotMark {
4008                retirements: 3,
4009                threads_counted: 7,
4010            },
4011            DirectoryInodes::new(11, None),
4012            DirectoryOrder::NameBytes,
4013        );
4014        alias
4015            .with_directory_stream(|stream| stream.advance(1))
4016            .unwrap();
4017        let before = serde_json::to_value(&fd).unwrap();
4018        assert_eq!(
4019            fd.validate_native_exec_runtime_state().unwrap(),
4020            alias.validate_native_exec_runtime_state().unwrap()
4021        );
4022        assert_eq!(serde_json::to_value(&fd).unwrap(), before);
4023        let lock = alias.directory_lock();
4024        let lease = lock.try_lock().unwrap();
4025        assert_eq!(
4026            fd.validate_native_exec_runtime_state(),
4027            Err(NativeExecCutError::LiveCapability("directory lease"))
4028        );
4029        assert!(
4030            lock.try_lock().is_none(),
4031            "validation must not release the owned lease"
4032        );
4033        drop(lease);
4034        assert_eq!(serde_json::to_value(&fd).unwrap(), before);
4035        assert_eq!(
4036            fd.with_directory_stream(|stream| stream.position())
4037                .unwrap(),
4038            1
4039        );
4040        let guard = fd.open_file.lock().unwrap();
4041        assert_eq!(
4042            alias.validate_native_exec_runtime_state(),
4043            Err(NativeExecCutError::LockUnavailable("open_file"))
4044        );
4045        drop(guard);
4046        fd.validate_native_exec_runtime_state().unwrap();
4047    }
4048
4049    #[test]
4050    fn native_exec_shared_descriptor_probe_never_takes_canonical_model() {
4051        let id = OpenFileId::new(DetTid::from_raw(42), 9);
4052        let fd = DetFd::new(30, OFlag::empty(), FdType::Rng, id);
4053        fd.advance_random_device_offset(37);
4054        let model = fd.share_for_test();
4055        let alias = fd.clone().with_fd(31);
4056        // Do not install a channel. with_description on this handle would
4057        // need one; a local runtime probe must not touch canonical state.
4058        assert_eq!(
4059            fd.validate_native_exec_runtime_state().unwrap(),
4060            alias.validate_native_exec_runtime_state().unwrap()
4061        );
4062        assert_eq!(model.0.random_device_offset, 37);
4063        let lock = alias.directory_lock();
4064        let lease = lock.try_lock().unwrap();
4065        assert_eq!(
4066            fd.validate_native_exec_runtime_state(),
4067            Err(crate::native_exec::NativeExecCutError::LiveCapability(
4068                "directory lease"
4069            ))
4070        );
4071        drop(lease);
4072        assert_eq!(model.0.random_device_offset, 37);
4073    }
4074
4075    #[test]
4076    fn random_access_modes_survive_status_updates_through_aliases() {
4077        for mode in [
4078            OFlag::O_RDONLY,
4079            OFlag::O_WRONLY,
4080            OFlag::O_RDWR,
4081            OFlag::O_ACCMODE,
4082            OFlag::O_PATH,
4083        ] {
4084            let original = DetFd::new(
4085                3,
4086                mode,
4087                FdType::Rng,
4088                OpenFileId::new(DetTid::from_raw(1), 0),
4089            );
4090            let alias = original.clone().with_fd(4);
4091            for update in [
4092                OFlag::O_NONBLOCK,
4093                OFlag::O_RDWR,
4094                OFlag::O_WRONLY,
4095                OFlag::O_PATH | OFlag::O_NONBLOCK,
4096            ] {
4097                alias.set_status_flags(update.bits());
4098                assert_eq!(
4099                    original.status_flags() & (OFlag::O_ACCMODE | OFlag::O_PATH).bits(),
4100                    mode.bits()
4101                );
4102                assert_eq!(
4103                    original.is_nonblocking(),
4104                    update.contains(OFlag::O_NONBLOCK)
4105                );
4106            }
4107        }
4108    }
4109
4110    #[test]
4111    fn random_status_updates_preserve_immutable_flags_and_replace_mutable_flags() {
4112        let immutable = OFlag::O_RDWR | OFlag::O_SYNC;
4113        let original = DetFd::new(
4114            3,
4115            immutable | OFlag::O_APPEND | OFlag::O_NONBLOCK,
4116            FdType::Rng,
4117            OpenFileId::new(DetTid::from_raw(1), 0),
4118        );
4119        let alias = original.clone().with_fd(4);
4120        alias.set_status_flags((OFlag::O_WRONLY | OFlag::O_ASYNC | OFlag::O_NOATIME).bits());
4121        assert_eq!(
4122            original.status_flags(),
4123            (immutable | OFlag::O_ASYNC | OFlag::O_NOATIME).bits()
4124        );
4125        assert!(!original.physically_nonblocking());
4126        alias.set_status_flags(OFlag::O_NONBLOCK.bits());
4127        assert_eq!(
4128            original.status_flags(),
4129            (immutable | OFlag::O_NONBLOCK).bits()
4130        );
4131        assert!(original.physically_nonblocking());
4132    }
4133
4134    #[test]
4135    fn dup_shares_open_file_state_but_not_slot_flags() {
4136        let owner = DetTid::from_raw(10);
4137        let original = DetFd::new(
4138            3,
4139            OFlag::O_NONBLOCK,
4140            FdType::Socket,
4141            OpenFileId::new(owner, 0),
4142        );
4143        let duplicate = original.clone().with_fd(4).with_fd_flags(OFlag::O_CLOEXEC);
4144
4145        assert_eq!(original.open_file_id(), duplicate.open_file_id());
4146        assert!(
4147            !original.is_cloexec(),
4148            "dup must not alter the source fd flags"
4149        );
4150        assert!(
4151            duplicate.is_cloexec(),
4152            "dup3(O_CLOEXEC) applies to the new slot"
4153        );
4154        assert!(
4155            duplicate.is_nonblocking(),
4156            "dup must preserve shared status flags"
4157        );
4158
4159        duplicate.set_status_flags(OFlag::empty().bits());
4160        assert!(
4161            !original.is_nonblocking(),
4162            "status flag changes through one alias must be visible through every alias"
4163        );
4164
4165        let timestamp = LogicalTime::from_nanos(2_345_678_901);
4166        original.set_socket_receive_timestamp(timestamp);
4167        assert_eq!(duplicate.socket_receive_timestamp(), Some(timestamp));
4168
4169        assert!(!original.is_sock_diag());
4170        original.set_sock_diag();
4171        assert!(
4172            duplicate.is_sock_diag(),
4173            "sock_diag marking through one alias must be visible through every alias"
4174        );
4175
4176        assert!(!original.is_guest_held());
4177        duplicate.set_guest_held();
4178        assert!(
4179            original.is_guest_held(),
4180            "guest-held marking through one alias must be visible through every alias"
4181        );
4182        original.clear_guest_held();
4183        assert!(
4184            !duplicate.is_guest_held(),
4185            "withdrawing guest-held state through one alias must withdraw it for every alias"
4186        );
4187
4188        assert!(!original.is_loopback_peer());
4189        duplicate.set_loopback_peer(true);
4190        assert!(
4191            original.is_loopback_peer(),
4192            "loopback-peer marking through one alias must be visible through every alias"
4193        );
4194        original.set_loopback_peer(false);
4195        assert!(
4196            !duplicate.is_loopback_peer(),
4197            "reconnects through one alias must clear loopback state for every alias"
4198        );
4199
4200        assert!(!original.is_internal_socket());
4201        duplicate.set_guest_peer();
4202        assert!(
4203            original.is_internal_socket(),
4204            "a guest peer belongs to the open file, so every alias must see it"
4205        );
4206        assert!(!original.is_socketpair_endpoint());
4207    }
4208
4209    /// `flock(2)` locks belong to the open file description, so every `dup`
4210    /// alias must see one lock, while a separate `open` of the same file is a
4211    /// distinct contender with its own state.
4212    #[test]
4213    fn flock_mode_is_shared_by_dup_aliases_and_private_to_separate_opens() {
4214        let owner = DetTid::from_raw(10);
4215        let original = DetFd::new(
4216            3,
4217            OFlag::empty(),
4218            FdType::Regular,
4219            OpenFileId::new(owner, 0),
4220        );
4221        let duplicate = original.clone().with_fd(4);
4222        let separate_open = DetFd::new(
4223            5,
4224            OFlag::empty(),
4225            FdType::Regular,
4226            OpenFileId::new(owner, 1),
4227        );
4228
4229        assert_eq!(original.known_flock_mode(), Some(None));
4230        original.set_flock_mode(Some(libc::LOCK_SH));
4231        assert_eq!(
4232            duplicate.known_flock_mode(),
4233            Some(Some(libc::LOCK_SH)),
4234            "a dup alias shares the open file description's flock lock"
4235        );
4236        assert_eq!(
4237            separate_open.known_flock_mode(),
4238            Some(None),
4239            "a separate open of the same file is an independent flock contender"
4240        );
4241
4242        duplicate.set_flock_mode(Some(libc::LOCK_EX));
4243        assert_eq!(original.known_flock_mode(), Some(Some(libc::LOCK_EX)));
4244        duplicate.set_flock_mode(None);
4245        assert_eq!(
4246            original.known_flock_mode(),
4247            Some(None),
4248            "LOCK_UN through one alias releases the whole open file description"
4249        );
4250
4251        duplicate.forget_flock_mode();
4252        assert_eq!(
4253            original.known_flock_mode(),
4254            None,
4255            "unknown state must be shared by every alias of the open file description"
4256        );
4257        original.set_flock_mode(Some(libc::LOCK_SH));
4258        assert_eq!(
4259            duplicate.known_flock_mode(),
4260            None,
4261            "a later call cannot make externally mutable lock state reliable again"
4262        );
4263    }
4264
4265    #[test]
4266    fn pidfd_target_is_shared_by_descriptor_aliases() {
4267        let target = DetPid::from_raw(41);
4268        let original = DetFd::new(
4269            3,
4270            OFlag::O_CLOEXEC,
4271            FdType::Pidfd,
4272            OpenFileId::new(DetTid::from_raw(7), 0),
4273        );
4274        let duplicate = original.clone().with_fd(4);
4275
4276        assert_eq!(original.pidfd_target(), None);
4277        original.set_pidfd_target(target);
4278        assert_eq!(original.pidfd_target(), Some(target));
4279        assert_eq!(duplicate.pidfd_target(), Some(target));
4280    }
4281
4282    #[test]
4283    fn toggling_logical_nonblocking_preserves_physical() {
4284        // Models FIONBIO on an fd that Detcore forced physically nonblocking for
4285        // the scheduler: the guest-visible flag changes, but the physical state
4286        // must survive.
4287        let owner = DetTid::from_raw(10);
4288        let fd = DetFd::new(3, OFlag::empty(), FdType::Socket, OpenFileId::new(owner, 0));
4289        fd.set_physically_nonblocking();
4290        fd.set_logical_nonblocking(true);
4291        assert!(fd.is_nonblocking());
4292        assert!(fd.physically_nonblocking());
4293
4294        fd.set_logical_nonblocking(false);
4295        assert!(
4296            !fd.is_nonblocking(),
4297            "the guest must observe O_NONBLOCK cleared"
4298        );
4299        assert!(
4300            fd.physically_nonblocking(),
4301            "the scheduler's physical nonblocking state must be preserved"
4302        );
4303
4304        fd.set_logical_nonblocking(true);
4305        assert!(fd.is_nonblocking());
4306        assert!(
4307            fd.physically_nonblocking(),
4308            "setting the logical flag must not discard physical tracking"
4309        );
4310
4311        // The both-flags setter still tracks physical alongside logical.
4312        fd.set_nonblocking(false);
4313        assert!(!fd.is_nonblocking());
4314        assert!(!fd.physically_nonblocking());
4315    }
4316
4317    /// An inotify instance received without its sender's open file
4318    /// description has an unknown blocking mode, shared by every alias, until
4319    /// the guest sets the mode: F_SETFL (`set_status_flags`), a FIONBIO that
4320    /// Detcore satisfies logically (`set_logical_nonblocking`) or forwards
4321    /// (`set_nonblocking`).
4322    #[test]
4323    fn an_unknown_blocking_mode_is_known_once_the_guest_sets_it() {
4324        let owner = DetTid::from_raw(10);
4325        let setters: [fn(&DetFd); 3] = [
4326            |fd| fd.set_status_flags(0),
4327            |fd| fd.set_logical_nonblocking(true),
4328            |fd| fd.set_nonblocking(false),
4329        ];
4330        for (index, set) in setters.into_iter().enumerate() {
4331            let fd = DetFd::new(
4332                3,
4333                OFlag::O_NONBLOCK,
4334                FdType::Inotify,
4335                OpenFileId::new(owner, index as u64),
4336            );
4337            let alias = fd.clone().with_fd(4);
4338            assert!(!fd.blocking_mode_unknown());
4339            fd.mark_blocking_mode_unknown();
4340            assert!(alias.blocking_mode_unknown(), "setter {index}: alias");
4341            set(&alias);
4342            assert!(!fd.blocking_mode_unknown(), "setter {index}");
4343        }
4344    }
4345
4346    #[test]
4347    fn procfs_offsets_are_shared_by_dup_aliases() {
4348        let owner = DetTid::from_raw(10);
4349        let original = DetFd::new(
4350            3,
4351            OFlag::empty(),
4352            FdType::Regular,
4353            OpenFileId::new(owner, 0),
4354        );
4355        // A `seq_file` file, which serves reads at any position.
4356        original.set_procfs(ProcfsFile::from_path(Path::new("/proc/version")).unwrap());
4357        original.initialize_procfs(
4358            Vec::new(),
4359            ProcfsSnapshotContext {
4360                virtual_pid: 1,
4361                ..ProcfsSnapshotContext::default()
4362            },
4363        );
4364        let duplicate = original.clone().with_fd(4);
4365
4366        assert_eq!(original.preview_procfs(2).unwrap(), (0, b"Li".to_vec()));
4367        assert_eq!(duplicate.procfs_position().unwrap().0, 0);
4368        original.commit_procfs_read(0, 2);
4369        assert_eq!(duplicate.procfs_position().unwrap().0, 2);
4370        assert_eq!(duplicate.take_procfs_at(4, 1).unwrap(), b"x");
4371        assert_eq!(original.procfs_position().unwrap().0, 2);
4372
4373        // A read that copied nothing leaves the shared cursor in place.
4374        let (offset, _) = duplicate.preview_procfs(4).unwrap();
4375        duplicate.commit_procfs_read(offset, 0);
4376        assert_eq!(original.procfs_position().unwrap().0, 2);
4377
4378        duplicate.set_procfs_offset(0);
4379        assert_eq!(
4380            original.preview_procfs(128).unwrap(),
4381            (
4382                0,
4383                crate::procfs::FixedProcfsFile::Version
4384                    .contents(&crate::machine::MachineDescription::default())
4385            )
4386        );
4387    }
4388
4389    #[test]
4390    fn random_device_offsets_are_shared_by_dup_aliases() {
4391        let owner = DetTid::from_raw(10);
4392        let original = DetFd::new(3, OFlag::empty(), FdType::Rng, OpenFileId::new(owner, 0));
4393        let duplicate = original.clone().with_fd(4);
4394
4395        assert_eq!(original.random_device_offset(), 0);
4396        duplicate.advance_random_device_offset(50);
4397        assert_eq!(original.random_device_offset(), 50);
4398    }
4399
4400    #[test]
4401    fn random_device_stream_commits_returned_prefix_for_dup_aliases() {
4402        let owner = DetTid::from_raw(10);
4403        let id = OpenFileId::new(owner, 0);
4404        let original = DetFd::new(3, OFlag::empty(), FdType::Rng, id);
4405        let duplicate = original.clone().with_fd(4);
4406        original.advance_random_device_offset(7);
4407
4408        assert_eq!(
4409            duplicate.with_random_device_stream(|offset| {
4410                assert_eq!(offset, 7);
4411                // A copy requesting eight bytes completed only this prefix.
4412                Ok::<_, ()>(3)
4413            }),
4414            Ok(3)
4415        );
4416        assert_eq!(original.random_device_offset(), 10);
4417        assert_eq!(duplicate.random_device_offset(), 10);
4418        assert_eq!(
4419            original.with_random_device_stream(|offset| {
4420                assert_eq!(offset, 10);
4421                Ok::<_, ()>(2)
4422            }),
4423            Ok(2)
4424        );
4425        assert_eq!(original.random_device_offset(), 12);
4426        assert_eq!(duplicate.random_device_offset(), 12);
4427        assert_eq!(original.open_file_id(), id);
4428        assert_eq!(duplicate.open_file_id(), id);
4429        assert!(Arc::ptr_eq(&original.open_file, &duplicate.open_file));
4430    }
4431
4432    #[test]
4433    fn random_device_stream_preserves_error_identity_and_cursor() {
4434        let owner = DetTid::from_raw(10);
4435        let original = DetFd::new(3, OFlag::empty(), FdType::Rng, OpenFileId::new(owner, 0));
4436        let duplicate = original.clone().with_fd(4);
4437        original.advance_random_device_offset(11);
4438        let failure = Arc::new(String::from("copy failure"));
4439
4440        let error = duplicate
4441            .with_random_device_stream(|offset| {
4442                assert_eq!(offset, 11);
4443                Err(Arc::clone(&failure))
4444            })
4445            .unwrap_err();
4446        assert!(Arc::ptr_eq(&error, &failure));
4447        assert_eq!(original.random_device_offset(), 11);
4448        assert_eq!(duplicate.random_device_offset(), 11);
4449        assert_eq!(original.open_file_id(), duplicate.open_file_id());
4450    }
4451
4452    #[test]
4453    fn random_device_stream_saturates_independently_of_partitioning() {
4454        let owner = DetTid::from_raw(10);
4455        for initial in [u64::MAX - 2, u64::MAX] {
4456            let whole = DetFd::new(3, OFlag::empty(), FdType::Rng, OpenFileId::new(owner, 0));
4457            let split = DetFd::new(4, OFlag::empty(), FdType::Rng, OpenFileId::new(owner, 1));
4458            whole.with_description(|d| d.random_device_offset = initial);
4459            split.with_description(|d| d.random_device_offset = initial);
4460
4461            assert_eq!(
4462                whole.with_random_device_stream(|offset| {
4463                    assert_eq!(offset, initial);
4464                    Ok::<_, ()>(4)
4465                }),
4466                Ok(4)
4467            );
4468            assert_eq!(whole.random_device_offset(), u64::MAX);
4469            assert_eq!(
4470                split.with_random_device_stream(|offset| {
4471                    assert_eq!(offset, initial);
4472                    Ok::<_, ()>(1)
4473                }),
4474                Ok(1)
4475            );
4476            assert_eq!(split.random_device_offset(), initial.saturating_add(1));
4477            assert_eq!(
4478                split.with_random_device_stream(|offset| {
4479                    assert_eq!(offset, initial.saturating_add(1));
4480                    Ok::<_, ()>(3)
4481                }),
4482                Ok(3)
4483            );
4484            assert_eq!(split.random_device_offset(), u64::MAX);
4485        }
4486    }
4487
4488    #[test]
4489    fn random_device_stream_holds_alias_lock_while_copying() {
4490        let owner = DetTid::from_raw(10);
4491        let original = DetFd::new(3, OFlag::empty(), FdType::Rng, OpenFileId::new(owner, 0));
4492        let duplicate = original.clone().with_fd(4);
4493
4494        assert_eq!(
4495            original.with_random_device_stream(|offset| {
4496                assert_eq!(offset, 0);
4497                // A nonblocking attempt cannot deadlock the copy, and observes
4498                // whether another thread could access the shared cursor here.
4499                assert!(
4500                    std::thread::spawn(move || {
4501                        matches!(
4502                            duplicate.open_file.try_lock(),
4503                            Err(std::sync::TryLockError::WouldBlock)
4504                        )
4505                    })
4506                    .join()
4507                    .unwrap()
4508                );
4509                Ok::<_, ()>(5)
4510            }),
4511            Ok(5)
4512        );
4513        assert_eq!(original.random_device_offset(), 5);
4514    }
4515
4516    /// A path argument whose conversion checks that no alias's description
4517    /// lock is held while it converts.
4518    struct PathProbe {
4519        alias: DetFd,
4520    }
4521
4522    impl AsRef<Path> for PathProbe {
4523        fn as_ref(&self) -> &Path {
4524            assert!(
4525                self.alias.open_file.try_lock().is_ok(),
4526                "an argument conversion ran under the description lock"
4527            );
4528            Path::new("/probe")
4529        }
4530    }
4531
4532    #[test]
4533    fn argument_conversions_run_before_the_description_lock() {
4534        let original = DetFd::new(
4535            3,
4536            OFlag::O_RDONLY,
4537            FdType::Regular,
4538            OpenFileId::new(DetTid::from_raw(11), 0),
4539        );
4540        let alias = original.clone().with_fd(4);
4541        original.set_path(PathProbe {
4542            alias: alias.clone(),
4543        });
4544        let original = original.with_path(PathProbe {
4545            alias: alias.clone(),
4546        });
4547        assert_eq!(alias.path(), Some(PathBuf::from("/probe")));
4548        drop(original);
4549    }
4550
4551    #[test]
4552    fn separate_opens_have_distinct_identity() {
4553        let owner = DetTid::from_raw(10);
4554        let first = DetFd::new(
4555            3,
4556            OFlag::empty(),
4557            FdType::Regular,
4558            OpenFileId::new(owner, 0),
4559        );
4560        let second = DetFd::new(
4561            4,
4562            OFlag::empty(),
4563            FdType::Regular,
4564            OpenFileId::new(owner, 1),
4565        );
4566
4567        assert_ne!(first.open_file_id(), second.open_file_id());
4568    }
4569
4570    /// The canonical store of the tests' shared open file descriptions, and
4571    /// the channel installed for this test process. Each test uses its own
4572    /// creator task, so tests running in parallel never share a description.
4573    #[derive(Default)]
4574    pub(super) struct MemoryChannel {
4575        store: Mutex<std::collections::HashMap<OpenFileId, Canonical>>,
4576    }
4577
4578    struct Canonical {
4579        /// `None` while a lease holds the model.
4580        model: Option<OpenFileModel>,
4581        sequence: u64,
4582        takes: usize,
4583    }
4584
4585    struct InstalledMemoryChannel(&'static MemoryChannel);
4586
4587    impl SharedOpenFileChannel for InstalledMemoryChannel {
4588        fn promote(&self, model: &OpenFileModel) -> Result<(), SharedOpenFileError> {
4589            let mut store = self.0.store.lock().unwrap();
4590            if store.contains_key(&model.id()) {
4591                return Err(SharedOpenFileError("duplicate test promotion".into()));
4592            }
4593            let copied = serde_json::from_value(serde_json::to_value(model).unwrap()).unwrap();
4594            store.insert(
4595                model.id(),
4596                Canonical {
4597                    model: Some(copied),
4598                    sequence: 0,
4599                    takes: 0,
4600                },
4601            );
4602            Ok(())
4603        }
4604
4605        fn take(&self, id: OpenFileId) -> Result<OpenFileLease, SharedOpenFileError> {
4606            let mut store = self.0.store.lock().unwrap();
4607            let canonical = store
4608                .get_mut(&id)
4609                .ok_or_else(|| SharedOpenFileError(format!("{id:?} is not shared")))?;
4610            let model = canonical
4611                .model
4612                .take()
4613                .ok_or_else(|| SharedOpenFileError(format!("{id:?} is already leased")))?;
4614            canonical.sequence += 1;
4615            canonical.takes += 1;
4616            Ok(OpenFileLease {
4617                id,
4618                sequence: canonical.sequence,
4619                model,
4620            })
4621        }
4622
4623        fn publish(&self, lease: OpenFileLease) -> Result<(), SharedOpenFileError> {
4624            let mut store = self.0.store.lock().unwrap();
4625            let canonical = store
4626                .get_mut(&lease.id)
4627                .ok_or_else(|| SharedOpenFileError(format!("{:?} is not shared", lease.id)))?;
4628            if canonical.model.is_some() || canonical.sequence != lease.sequence {
4629                return Err(SharedOpenFileError(format!(
4630                    "{:?}: publish names no current lease",
4631                    lease.id
4632                )));
4633            }
4634            canonical.model = Some(lease.model);
4635            Ok(())
4636        }
4637    }
4638
4639    impl MemoryChannel {
4640        pub(super) fn installed() -> &'static MemoryChannel {
4641            static CHANNEL: OnceLock<MemoryChannel> = OnceLock::new();
4642            let channel = CHANNEL.get_or_init(MemoryChannel::default);
4643            // Only the first installation is kept, and it is this channel.
4644            let _ = install_shared_open_file_channel(Box::new(InstalledMemoryChannel(channel)));
4645            channel
4646        }
4647
4648        fn share(&self, detfd: &DetFd) {
4649            let model = detfd.share_for_test();
4650            let previous = self.store.lock().unwrap().insert(
4651                model.0.id,
4652                Canonical {
4653                    model: Some(model),
4654                    sequence: 0,
4655                    takes: 0,
4656                },
4657            );
4658            assert!(previous.is_none());
4659        }
4660
4661        fn takes(&self, id: OpenFileId) -> usize {
4662            self.store.lock().unwrap()[&id].takes
4663        }
4664
4665        /// Runs `f` on the canonical model, which must not be leased.
4666        fn canonical<R>(&self, id: OpenFileId, f: impl FnOnce(&OpenFileDescription) -> R) -> R {
4667            let store = self.store.lock().unwrap();
4668            f(&store[&id]
4669                .model
4670                .as_ref()
4671                .expect("a lease outlived its method call")
4672                .0)
4673        }
4674    }
4675
4676    #[test]
4677    fn a_shared_handle_reaches_the_canonical_model_through_every_alias() {
4678        let channel = MemoryChannel::installed();
4679        let id = OpenFileId::new(DetTid::from_raw(9001), 0);
4680        let original = DetFd::new(3, OFlag::O_RDWR, FdType::Regular, id);
4681        let alias = original.clone().with_fd(4);
4682        // A clone that escaped the descriptor table before the promotion.
4683        let escaped = original.clone();
4684        channel.share(&original);
4685
4686        alias.set_status_flags(OFlag::O_NONBLOCK.bits());
4687        assert_eq!(channel.takes(id), 1);
4688        assert!(channel.canonical(id, |model| oflags_nonblocking(model.status_flags)));
4689        assert!(original.is_nonblocking());
4690        assert!(escaped.is_nonblocking());
4691        assert_eq!(
4692            escaped.status_flags() & OFlag::O_ACCMODE.bits(),
4693            OFlag::O_RDWR.bits()
4694        );
4695        assert_eq!(channel.takes(id), 4);
4696        assert_eq!(original.open_file_alias_count(), 3);
4697    }
4698
4699    /// A promoted description has one canonical model, so a grant through a
4700    /// handle another process forked from it is a grant every handle sees.
4701    /// `has_unsafe_vfork_flock_state` therefore keeps refusing on a lock a
4702    /// sibling took after the fork, without any per-process copy of the fact.
4703    #[test]
4704    fn promoted_description_shares_its_grant_fact_with_forked_handles() {
4705        let _channel = MemoryChannel::installed();
4706        let id = OpenFileId::new(DetTid::from_raw(9041), 0);
4707        let parent = DetFd::new(3, OFlag::O_RDONLY, FdType::Rng, id);
4708        parent.promote().unwrap();
4709        let sibling = parent.fork_shared_handle();
4710        parent.forget_local_flock_mode_at_fork();
4711        sibling.forget_local_flock_mode_at_fork();
4712        assert_eq!(parent.known_flock_mode(), Some(None));
4713        assert!(!parent.flock_mode_may_be_stale());
4714        assert!(!parent.flock_may_have_been_granted());
4715
4716        sibling.set_flock_mode(Some(libc::LOCK_SH));
4717        assert_eq!(parent.known_flock_mode(), Some(Some(libc::LOCK_SH)));
4718        assert!(parent.flock_may_have_been_granted());
4719        sibling.set_flock_mode(None);
4720        assert_eq!(parent.known_flock_mode(), Some(None));
4721        assert!(parent.flock_may_have_been_granted());
4722    }
4723
4724    #[test]
4725    fn promoted_fork_handles_keep_one_cursor_without_pregrant_access() {
4726        let channel = MemoryChannel::installed();
4727        let id = OpenFileId::new(DetTid::from_raw(9010), 0);
4728        let parent = DetFd::new(3, OFlag::O_RDONLY, FdType::Rng, id);
4729        let alias = parent.clone().with_fd(4).with_fd_flags(OFlag::O_CLOEXEC);
4730        parent
4731            .with_random_device_stream::<Errno>(|offset| {
4732                assert_eq!(offset, 0);
4733                Ok(7)
4734            })
4735            .unwrap();
4736        parent.set_flock_mode(Some(libc::LOCK_SH));
4737        parent.promote().unwrap();
4738        alias.promote().unwrap(); // The escaped alias does not publish twice.
4739        let takes = channel.takes(id);
4740        let lock = parent.directory_lock();
4741        let held = lock.try_lock().unwrap();
4742        let child = alias.fork_shared_handle();
4743        child.forget_local_flock_mode_at_fork();
4744        assert_eq!(
4745            channel.takes(id),
4746            takes,
4747            "child metadata construction must be RPC-free"
4748        );
4749        assert!(child.directory_lock().try_lock().is_some());
4750        assert!(child.is_cloexec());
4751        assert_eq!(child.known_flock_mode(), Some(Some(libc::LOCK_SH)));
4752        child
4753            .with_random_device_stream::<Errno>(|offset| {
4754                assert_eq!(offset, 7);
4755                Ok(5)
4756            })
4757            .unwrap();
4758        parent
4759            .with_random_device_stream::<Errno>(|offset| {
4760                assert_eq!(offset, 12);
4761                Ok(3)
4762            })
4763            .unwrap();
4764        assert_eq!(
4765            child.with_random_device_stream::<Errno>(|offset| {
4766                assert_eq!(offset, 15);
4767                Err(Errno::EFAULT)
4768            }),
4769            Err(Errno::EFAULT)
4770        );
4771        child.set_flock_mode(None);
4772        assert_eq!(parent.known_flock_mode(), Some(None));
4773        drop(child);
4774        let successor = DetFd::shared_survivor(3, id);
4775        successor
4776            .with_random_device_stream::<Errno>(|offset| {
4777                assert_eq!(offset, 15);
4778                Ok(0)
4779            })
4780            .unwrap();
4781        let reopened = DetFd::new(
4782            3,
4783            OFlag::O_RDONLY,
4784            FdType::Rng,
4785            OpenFileId::new(DetTid::from_raw(9010), 1),
4786        );
4787        reopened.promote().unwrap();
4788        reopened
4789            .with_random_device_stream::<Errno>(|offset| {
4790                assert_eq!(offset, 0);
4791                Ok(1)
4792            })
4793            .unwrap();
4794        successor
4795            .with_random_device_stream::<Errno>(|offset| {
4796                assert_eq!(offset, 15);
4797                Ok(0)
4798            })
4799            .unwrap();
4800        drop(held);
4801    }
4802
4803    #[test]
4804    fn identity_and_directory_lock_never_take_the_shared_model() {
4805        let channel = MemoryChannel::installed();
4806        let id = OpenFileId::new(DetTid::from_raw(9002), 0);
4807        let original = DetFd::new(3, OFlag::O_RDONLY, FdType::Regular, id);
4808        let local_lock = original.directory_lock();
4809        channel.share(&original);
4810
4811        assert_eq!(original.open_file_id(), id);
4812        assert!(Arc::ptr_eq(&original.directory_lock(), &local_lock));
4813        assert!(Arc::ptr_eq(
4814            &original.clone().with_fd(4).directory_lock(),
4815            &local_lock
4816        ));
4817        assert_eq!(channel.takes(id), 0);
4818    }
4819
4820    #[test]
4821    fn a_shared_handle_serializes_only_its_identity() {
4822        let channel = MemoryChannel::installed();
4823        let id = OpenFileId::new(DetTid::from_raw(9003), 0);
4824        let original = DetFd::new(3, OFlag::O_RDONLY, FdType::Regular, id).with_path("/a");
4825        channel.share(&original);
4826
4827        let encoded = serde_json::to_string(&original).unwrap();
4828        assert!(encoded.contains("Shared"), "{encoded}");
4829        assert!(!encoded.contains("status_flags"), "{encoded}");
4830        assert!(!encoded.contains("/a"), "{encoded}");
4831        assert_eq!(channel.takes(id), 0);
4832
4833        // A restored table interns its shared handles by identity, as it
4834        // does local ones, without taking the model.
4835        let mut handles = std::collections::HashMap::new();
4836        for fd in [3, 4] {
4837            let restored: DetFd = serde_json::from_str(&encoded).unwrap();
4838            handles.insert(fd, restored.with_fd(fd));
4839        }
4840        intern_open_files(&mut handles);
4841        assert_eq!(handles[&4].open_file_alias_count(), 2);
4842        assert_eq!(channel.takes(id), 0);
4843        assert_eq!(handles[&4].path(), Some(PathBuf::from("/a")));
4844        assert_eq!(channel.takes(id), 1);
4845    }
4846
4847    #[test]
4848    fn a_failed_random_device_copy_still_ends_the_lease() {
4849        let channel = MemoryChannel::installed();
4850        let id = OpenFileId::new(DetTid::from_raw(9004), 0);
4851        let original = DetFd::new(3, OFlag::empty(), FdType::Rng, id);
4852        channel.share(&original);
4853
4854        assert_eq!(
4855            original.with_random_device_stream(|_| Err::<usize, _>(())),
4856            Err(())
4857        );
4858        assert_eq!(channel.canonical(id, |model| model.random_device_offset), 0);
4859        assert_eq!(
4860            original.with_random_device_stream(|offset| {
4861                assert_eq!(offset, 0);
4862                Ok::<_, ()>(5)
4863            }),
4864            Ok(5)
4865        );
4866        assert_eq!(channel.canonical(id, |model| model.random_device_offset), 5);
4867        assert_eq!(channel.takes(id), 2);
4868    }
4869
4870    fn at(ns: u64) -> LogicalTime {
4871        LogicalTime::from_nanos(ns)
4872    }
4873
4874    /// A one-shot timerfd counts one expiration once virtual time reaches its
4875    /// deadline, then disarms; before the deadline it counts none.
4876    #[test]
4877    fn a_one_shot_timerfd_expires_once_at_its_virtual_deadline() {
4878        let mut timer = VirtualTimerfd::set(Some(at(1_000)), 0);
4879        assert_eq!(timer.expire_through(at(999)), 0);
4880        assert_eq!(timer.remaining(at(999)), (1, 0));
4881        assert_eq!(timer.expire_through(at(1_000)), 1);
4882        assert_eq!(
4883            timer,
4884            VirtualTimerfd {
4885                deadline: None,
4886                interval_ns: 0,
4887                unread: 1
4888            }
4889        );
4890        assert_eq!(timer.expire_through(at(5_000)), 0);
4891        assert_eq!(timer.remaining(at(5_000)), (0, 0));
4892    }
4893
4894    /// A periodic timerfd observed late counts every expiration it missed,
4895    /// exactly, and its next deadline stays on the original grid: the count
4896    /// is a function of virtual time, not of how often it was observed.
4897    #[test]
4898    fn a_periodic_timerfd_counts_every_missed_expiration_on_its_grid() {
4899        let mut once = VirtualTimerfd::set(Some(at(1_000)), 300);
4900        assert_eq!(once.expire_through(at(1_950)), 4);
4901        assert_eq!(once.deadline, Some(at(2_200)));
4902        assert_eq!(once.remaining(at(1_950)), (250, 300));
4903
4904        let mut often = VirtualTimerfd::set(Some(at(1_000)), 300);
4905        let mut added = 0;
4906        for now in [1_000, 1_100, 1_300, 1_599, 1_950] {
4907            added += often.expire_through(at(now));
4908        }
4909        assert_eq!(added, 4);
4910        assert_eq!(often, once);
4911    }
4912
4913    /// What a timerfd's fdinfo shows follows Linux's own timer
4914    /// (`LinuxTimerfd`), which counts one expiration when it fires and the
4915    /// periods after it only when `timerfd_gettime` or a read forwards it,
4916    /// whatever Detcore's probes counted meanwhile; a one-shot timer that
4917    /// fired is not restarted.
4918    #[test]
4919    fn a_timerfds_fdinfo_view_follows_linuxs_forwarding() {
4920        let mut model = VirtualTimerfd::set(Some(at(1_000)), 300);
4921        let mut linux = LinuxTimerfd::set(Some(at(1_000)));
4922        assert_eq!(linux.view(at(400)), (0, 600));
4923        model.expire_through(at(1_950));
4924        assert_eq!(linux.view(at(1_950)), (1, 0));
4925        linux.forward(at(1_950), 300);
4926        assert_eq!(linux.view(at(1_950)), (4, 250));
4927        assert_eq!(linux.view(at(2_200)), (5, 0));
4928        assert_eq!(model.expire_through(at(2_300)), 1);
4929        linux.after_read(model.deadline);
4930        assert_eq!(linux.view(at(2_300)), (0, 200));
4931
4932        let mut once_model = VirtualTimerfd::set(Some(at(10)), 0);
4933        let mut once = LinuxTimerfd::set(Some(at(10)));
4934        assert_eq!(once.view(at(5)), (0, 5));
4935        assert_eq!(once.view(at(10)), (1, 0));
4936        once.forward(at(20), 0);
4937        assert_eq!(once.view(at(20)), (1, 0));
4938        once_model.expire_through(at(20));
4939        once.after_read(once_model.deadline);
4940        assert_eq!(once.view(at(30)), (0, 0));
4941
4942        assert_eq!(LinuxTimerfd::set(None).view(at(5)), (0, 0));
4943    }
4944
4945    /// A timer disarmed with a zero value keeps its interval for
4946    /// `timerfd_gettime`, as Linux does, and never expires.
4947    #[test]
4948    fn a_disarmed_timerfd_keeps_its_interval_and_never_expires() {
4949        let mut timer = VirtualTimerfd::set(None, 300);
4950        assert_eq!(timer.expire_through(at(u64::MAX / 2)), 0);
4951        assert_eq!(timer.remaining(at(5)), (0, 300));
4952        assert_eq!(timer.unread, 0);
4953    }
4954
4955    /// Setting a timerfd discards its unread expirations, as Linux does.
4956    #[test]
4957    fn setting_a_timerfd_discards_unread_expirations() {
4958        let mut timer = VirtualTimerfd::set(Some(at(10)), 0);
4959        timer.expire_through(at(20));
4960        assert_eq!(timer.unread, 1);
4961        assert_eq!(VirtualTimerfd::set(None, 0).unread, 0);
4962    }
4963
4964    /// The guest-held mark (`DetFd::is_guest_held`) lives on the open file, so
4965    /// a received descriptor carries it only by joining the open file a
4966    /// guest's send queued on the receiving socket (`take_queued_rights`,
4967    /// `OpenFileHandle::descriptor`), and then shares the mark and its
4968    /// withdrawal with the sender's copy. A receipt nothing queued names, such
4969    /// as one from a process outside the guest, is a description of its own
4970    /// and starts unmarked.
4971    #[test]
4972    fn a_received_descriptor_is_guest_held_only_through_its_senders_open_file() {
4973        let owner = DetTid::from_raw(10);
4974        let open =
4975            |fd, ty, sequence| DetFd::new(fd, OFlag::O_RDWR, ty, OpenFileId::new(owner, sequence));
4976        let (left, right) = (open(3, FdType::Socket, 0), open(4, FdType::Socket, 1));
4977        left.pair_sockets(&right, false);
4978        let pipe_end = open(5, FdType::Pipe, 2);
4979        pipe_end.set_guest_held();
4980        assert!(
4981            left.queue_rights_to_peer(
4982                vec![SentDescriptor {
4983                    file: (1, 50),
4984                    open_file: Some(pipe_end.in_flight_open_file(true)),
4985                    lock_copy: None,
4986                }],
4987                0,
4988            )
4989            .is_ok()
4990        );
4991        let sent = right
4992            .take_queued_rights(&[(1, 50)], false, false, false, None)
4993            .expect("the send queued it")
4994            .sent;
4995        let joined = sent[0]
4996            .open_file
4997            .as_ref()
4998            .and_then(|handle| handle.descriptor(6, false))
4999            .expect("the sender's open file is still modeled");
5000        assert!(joined.is_guest_held(), "a join shares the sender's mark");
5001        joined.clear_guest_held();
5002        assert!(!pipe_end.is_guest_held(), "and its withdrawal");
5003
5004        assert!(
5005            right
5006                .take_queued_rights(&[(1, 60)], false, false, false, None)
5007                .is_none(),
5008            "nothing queued names a descriptor a guest did not send"
5009        );
5010        assert!(
5011            !open(7, FdType::Pipe, 3).is_guest_held(),
5012            "a description of its own starts unmarked"
5013        );
5014    }
5015
5016    /// The provenance a connection needs to be guest-held
5017    /// (`DetFd::is_guest_created`) lives on the open file too: a duplicate or
5018    /// a join shares it and its withdrawal, a handle reads it while the
5019    /// description is modeled, and a description of its own, such as a
5020    /// received socket that joined nothing, starts without it.
5021    #[test]
5022    fn a_socket_is_guest_created_only_through_its_own_open_file() {
5023        let owner = DetTid::from_raw(10);
5024        let made = DetFd::new(3, OFlag::O_RDWR, FdType::Socket, OpenFileId::new(owner, 0));
5025        assert!(!made.is_guest_created(), "nothing starts guest-created");
5026        made.set_guest_created();
5027        let handle = made.open_file_handle();
5028        let joined = handle.descriptor(4, false).expect("still modeled");
5029        assert!(joined.is_guest_created(), "a join shares it");
5030        assert!(handle.is_guest_created(), "and a handle reads it");
5031        joined.clear_guest_created();
5032        assert!(!made.is_guest_created(), "and its withdrawal");
5033        assert!(!handle.is_guest_created());
5034        let received = DetFd::new(5, OFlag::O_RDWR, FdType::Socket, OpenFileId::new(owner, 1));
5035        assert!(
5036            !received.is_guest_created(),
5037            "a description of its own starts without it"
5038        );
5039        drop((made, joined));
5040        assert!(
5041            !OpenFileHandle::dangling().is_guest_created(),
5042            "a description no longer modeled is not guest-created"
5043        );
5044    }
5045
5046    /// The descriptors a socket's messages carried are taken in order
5047    /// (`DetFd::take_queued_rights`): a peek leaves its message queued, a
5048    /// receive of fewer descriptors than a message carried names it only where
5049    /// Linux set `MSG_CTRUNC`, a receive that names a message Linux cannot
5050    /// have delivered yet takes nothing, leaving every message queued, and one
5051    /// that names no queued message puts the queue out of lockstep.
5052    #[test]
5053    fn queued_rights_are_taken_in_order() {
5054        let owner = DetTid::from_raw(10);
5055        let socket = |fd, sequence| {
5056            DetFd::new(
5057                fd,
5058                OFlag::O_RDWR,
5059                FdType::Socket,
5060                OpenFileId::new(owner, sequence),
5061            )
5062        };
5063        let (left, right) = (socket(3, 0), socket(4, 1));
5064        left.pair_sockets(&right, false);
5065        let file = |inode: u64| (1, inode);
5066        let sent = |inodes: &[u64]| -> Vec<SentDescriptor> {
5067            inodes
5068                .iter()
5069                .map(|&inode| SentDescriptor {
5070                    file: file(inode),
5071                    open_file: None,
5072                    lock_copy: None,
5073                })
5074                .collect()
5075        };
5076        let inodes = |taken: Option<TakenRights>| {
5077            taken.map(|taken| {
5078                taken
5079                    .sent
5080                    .iter()
5081                    .map(|sent| sent.file.1)
5082                    .collect::<Vec<_>>()
5083            })
5084        };
5085
5086        for message in [&[10][..], &[20, 21], &[30], &[40]] {
5087            assert!(matches!(
5088                left.queue_rights_to_peer(sent(message), 1),
5089                Ok(())
5090            ));
5091        }
5092        assert_eq!(
5093            inodes(right.take_queued_rights(&[file(10)], false, true, false, None)),
5094            Some(vec![10]),
5095            "a peek names the first message"
5096        );
5097        assert_eq!(
5098            inodes(right.take_queued_rights(&[file(10)], false, false, false, None)),
5099            Some(vec![10]),
5100            "and leaves it queued"
5101        );
5102        assert_eq!(
5103            right
5104                .take_queued_rights(&[file(20)], true, false, false, None)
5105                .map(|taken| (taken.sent.len(), taken.lockstep)),
5106            Some((2, true)),
5107            "a truncated receive takes the whole message, in lockstep"
5108        );
5109        assert!(
5110            right
5111                .take_queued_rights(&[file(10)], false, true, false, None)
5112                .is_none(),
5113            "a message taken is no longer queued"
5114        );
5115        assert!(
5116            right
5117                .take_queued_rights(&[file(40)], false, false, false, None)
5118                .is_none(),
5119            "a receipt that names a message Linux cannot have delivered yet (no call \
5120             took the one before it) took one never queued here: it takes nothing"
5121        );
5122        let next = right.take_queued_rights(&[file(30)], false, false, false, None);
5123        assert_eq!(
5124            next.as_ref()
5125                .map(|taken| (taken.lockstep, taken.joinable, taken.forgotten.len())),
5126            Some((false, true, 0)),
5127            "Linux still delivers the messages before it, in order, and they are joined"
5128        );
5129        assert_eq!(inodes(next), Some(vec![30]));
5130        assert_eq!(
5131            inodes(right.take_queued_rights(&[file(40)], false, false, false, None)),
5132            Some(vec![40])
5133        );
5134
5135        // One descriptor of a message that carried two, without MSG_CTRUNC,
5136        // names no queued message: the message it came with was never queued
5137        // here, and the queue is out of lockstep. A peek changes nothing.
5138        let (other_left, other_right) = (socket(5, 3), socket(6, 4));
5139        other_left.pair_sockets(&other_right, false);
5140        assert!(matches!(
5141            other_left.queue_rights_to_peer(sent(&[90, 91]), 1),
5142            Ok(())
5143        ));
5144        assert!(
5145            other_right
5146                .take_queued_rights(&[file(90)], false, true, false, None)
5147                .is_none()
5148        );
5149        assert_eq!(
5150            other_right
5151                .take_queued_rights(&[file(90), file(91)], false, true, false, None)
5152                .map(|taken| taken.lockstep),
5153            Some(true)
5154        );
5155        assert!(
5156            other_right
5157                .take_queued_rights(&[file(90)], false, false, false, None)
5158                .is_none()
5159        );
5160        assert_eq!(
5161            other_right
5162                .take_queued_rights(&[file(90), file(91)], false, false, false, None)
5163                .map(|taken| (taken.lockstep, taken.joinable)),
5164            Some((false, true))
5165        );
5166
5167        // Under record and replay, such a receipt takes its match and forgets
5168        // the messages before it, as recordings were made with.
5169        for message in [&[70][..], &[80]] {
5170            assert!(matches!(
5171                left.queue_rights_to_peer(sent(message), 1),
5172                Ok(())
5173            ));
5174        }
5175        let legacy = right.take_queued_rights(&[file(80)], false, false, true, None);
5176        assert_eq!(
5177            legacy
5178                .as_ref()
5179                .map(|taken| (taken.joinable, taken.forgotten.len())),
5180            Some((false, 1))
5181        );
5182
5183        // A message sent through `right` reaches `left`; a socket with no
5184        // known peer queues nothing.
5185        assert!(matches!(right.queue_rights_to_peer(sent(&[50]), 1), Ok(())));
5186        assert_eq!(
5187            inodes(right.take_queued_rights(&[file(50)], false, false, false, None)),
5188            None
5189        );
5190        assert_eq!(
5191            inodes(left.take_queued_rights(&[file(50)], false, false, false, None)),
5192            Some(vec![50])
5193        );
5194        assert!(matches!(
5195            socket(5, 2).queue_rights_to_peer(sent(&[60]), 1),
5196            Err(back) if back.len() == 1
5197        ));
5198    }
5199
5200    /// After calls Detcore could not match took messages, a receipt joins
5201    /// none that Linux may have released already: the first message past
5202    /// them, or one among them that no later one among them shadows
5203    /// (`SocketRights::suspects`).
5204    #[test]
5205    fn a_message_linux_may_have_released_is_never_joined() {
5206        let owner = DetTid::from_raw(10);
5207        let socket = |fd, sequence| {
5208            DetFd::new(
5209                fd,
5210                OFlag::O_RDWR,
5211                FdType::Socket,
5212                OpenFileId::new(owner, sequence),
5213            )
5214        };
5215        let sent = |inode: u64| {
5216            vec![SentDescriptor {
5217                file: (1, inode),
5218                open_file: None,
5219                lock_copy: None,
5220            }]
5221        };
5222        let take = |socket: &DetFd, inode: u64, peek: bool| {
5223            socket
5224                .take_queued_rights(&[(1, inode)], false, peek, false, None)
5225                .map(|taken| (taken.joinable, taken.forgotten.len()))
5226        };
5227
5228        // Two messages naming one file, the first taken unseen (an EFAULT
5229        // after the dequeue): the receipt of the second matches the first,
5230        // which may be the stale one, so neither is joined, and the second,
5231        // left in the queue, stays suspect.
5232        let (left, right) = (socket(3, 0), socket(4, 1));
5233        left.pair_sockets(&right, false);
5234        for inode in [10, 10] {
5235            assert!(matches!(left.queue_rights_to_peer(sent(inode), 1), Ok(())));
5236        }
5237        right.lose_lockstep_if_queued(1, false);
5238        assert_eq!(take(&right, 10, true), Some((false, 0)), "a peek alike");
5239        assert_eq!(take(&right, 10, false), Some((false, 0)));
5240        assert!(matches!(left.queue_rights_to_peer(sent(10), 1), Ok(())));
5241        assert_eq!(
5242            take(&right, 10, false),
5243            Some((false, 0)),
5244            "the received message's own entry is never joined"
5245        );
5246
5247        // A data-only message taken unseen: the next message names other
5248        // files, so its receipt is trusted, and the queue past it is not
5249        // suspect any more.
5250        let (left, right) = (socket(3, 2), socket(4, 3));
5251        left.pair_sockets(&right, false);
5252        for inode in [20, 21] {
5253            assert!(matches!(left.queue_rights_to_peer(sent(inode), 1), Ok(())));
5254        }
5255        right.lose_lockstep_if_queued(1, false);
5256        assert_eq!(take(&right, 20, false), Some((true, 0)));
5257        assert_eq!(take(&right, 21, false), Some((true, 0)));
5258
5259        // Two calls took messages unseen; the third message is the first
5260        // Linux can still hold, and its receipt forgets the two before it.
5261        let (left, right) = (socket(3, 4), socket(4, 5));
5262        left.pair_sockets(&right, false);
5263        for inode in [30, 31, 32, 33] {
5264            assert!(matches!(left.queue_rights_to_peer(sent(inode), 1), Ok(())));
5265        }
5266        right.lose_lockstep_if_queued(1, false);
5267        right.lose_lockstep_if_queued(1, false);
5268        assert_eq!(take(&right, 32, false), Some((true, 2)));
5269        assert_eq!(
5270            take(&right, 33, false),
5271            Some((true, 0)),
5272            "after a trusted receipt nothing is suspect"
5273        );
5274
5275        // With one call unmatched, a receipt that names the third message
5276        // names one Linux cannot have delivered yet: it takes nothing, and
5277        // the messages up to it stay queued, suspect, as Linux still holds
5278        // them; the next receipt of the first is trusted.
5279        let (left, right) = (socket(3, 6), socket(4, 7));
5280        left.pair_sockets(&right, false);
5281        for inode in [40, 41, 42] {
5282            assert!(matches!(left.queue_rights_to_peer(sent(inode), 1), Ok(())));
5283        }
5284        right.lose_lockstep_if_queued(1, false);
5285        assert_eq!(take(&right, 42, false), None);
5286        assert_eq!(take(&right, 40, false), Some((true, 0)));
5287        assert_eq!(take(&right, 41, false), Some((true, 0)));
5288        assert_eq!(take(&right, 42, false), Some((true, 0)));
5289    }
5290
5291    /// A socketpair of `owner`'s table, and descriptor 5 of a description
5292    /// of type `ty` to send through it.
5293    fn pair_and_sent(owner: DetTid, ty: FdType) -> (DetFd, DetFd, DetFd) {
5294        let detfd =
5295            |fd, ty, sequence| DetFd::new(fd, OFlag::O_RDWR, ty, OpenFileId::new(owner, sequence));
5296        let (left, right) = (detfd(3, FdType::Socket, 0), detfd(4, FdType::Socket, 1));
5297        left.pair_sockets(&right, false);
5298        (left, right, detfd(5, ty, 2))
5299    }
5300
5301    /// Queue `sent` through `left` (`held` or with the legacy ownership),
5302    /// drop the sender's descriptor, and return a handle that tells whether
5303    /// the description is still alive.
5304    fn queue_and_close(left: &DetFd, sent: DetFd, held: bool) -> OpenFileHandle {
5305        let handle = sent.open_file_handle();
5306        assert!(matches!(
5307            left.queue_rights_to_peer(
5308                vec![SentDescriptor {
5309                    file: (1, 50),
5310                    open_file: Some(sent.in_flight_open_file(held)),
5311                    lock_copy: None,
5312                }],
5313                1
5314            ),
5315            Ok(())
5316        ));
5317        drop(sent);
5318        handle
5319    }
5320
5321    /// A queued message keeps the descriptions it carries alive, as Linux's
5322    /// in-flight reference keeps each `struct file`: the receipt names the
5323    /// sender's description after the sender's last descriptor of it went,
5324    /// by a close or with the table of a sender that exited, whenever the
5325    /// backend releases that table
5326    /// (https://github.com/rrnewton/hermit/issues/4930). The description
5327    /// counts the copy in flight until the receipt lands it in the
5328    /// receiving table (`OpenFileDescription::in_flight`).
5329    #[test]
5330    fn a_queued_message_keeps_its_descriptions_alive() {
5331        let (left, right, sent) = pair_and_sent(DetTid::from_raw(10), FdType::Eventfd);
5332        let id = sent.open_file_id();
5333        let alias = sent.clone();
5334        let handle = queue_and_close(&left, sent, true);
5335        assert_eq!(
5336            alias.open_file_alias_count(),
5337            2,
5338            "the queue's reference holds it"
5339        );
5340        assert_eq!(alias.copies_in_flight(), 1, "and counts in flight");
5341        assert!(!alias.is_last_reference(), "so no close releases it");
5342        drop(alias);
5343        assert!(
5344            handle.descriptor(5, false).is_some(),
5345            "the queued message holds the description"
5346        );
5347        let taken = right
5348            .take_queued_rights(&[(1, 50)], false, false, false, None)
5349            .unwrap();
5350        assert!(taken.lockstep && taken.forgotten.is_empty());
5351        let copy = taken.sent[0].open_file.as_ref().unwrap();
5352        let received = copy.descriptor(7, true).unwrap();
5353        copy.land();
5354        assert_eq!(received.copies_in_flight(), 0, "landed");
5355        assert_eq!(
5356            received.open_file_id(),
5357            id,
5358            "the receipt is the sender's description"
5359        );
5360        drop(taken);
5361        assert!(
5362            handle.descriptor(5, false).is_some(),
5363            "the received descriptor holds it now"
5364        );
5365        drop(received);
5366        assert!(
5367            handle.descriptor(5, false).is_none(),
5368            "and nothing else does"
5369        );
5370    }
5371
5372    /// Under record and replay a queued message keeps the ownership existing
5373    /// recordings were made with: it holds nothing, so a description only it
5374    /// names is gone for every decision, a receipt's join and a pending
5375    /// connection's pairing alike.
5376    #[test]
5377    fn a_legacy_queued_message_keeps_nothing_alive() {
5378        let (left, right, sent) = pair_and_sent(DetTid::from_raw(10), FdType::Socket);
5379        let handle = queue_and_close(&left, sent, false);
5380        assert!(
5381            handle.descriptor(5, false).is_none(),
5382            "the description is gone"
5383        );
5384        let taken = right
5385            .take_queued_rights(&[(1, 50)], false, false, false, None)
5386            .unwrap();
5387        assert!(
5388            taken.sent[0]
5389                .open_file
5390                .as_ref()
5391                .unwrap()
5392                .descriptor(7, false)
5393                .is_none(),
5394            "and the receipt cannot name it"
5395        );
5396    }
5397
5398    /// A receive Detcore could not match, a peek among them, keeps every
5399    /// queued message, and with it the descriptions it holds: Linux still
5400    /// delivers them. The queue lets go where Linux releases a message: a
5401    /// later message taken after calls Detcore could not match forgets the
5402    /// ones before it, a datagram `connect` (or a send that finds the peer
5403    /// gone) purges the queue, and the receiving socket's last reference
5404    /// takes the queue with it. Each release by a guest call uncounts the
5405    /// copy in flight ([`InFlightOpenFile::release`]).
5406    #[test]
5407    fn a_queue_lets_go_only_where_linux_releases_the_messages() {
5408        let owner = DetTid::from_raw(10);
5409        let release = |sent: Vec<SentDescriptor>| {
5410            sent.into_iter()
5411                .filter_map(|sent| sent.open_file.and_then(InFlightOpenFile::release))
5412                .count()
5413        };
5414
5415        // A receive Detcore could not match keeps the queue, the message it
5416        // may have taken suspect, in lockstep: no message reached the socket
5417        // unqueued. Under record and replay it puts the queue out of
5418        // lockstep, as recordings were made with.
5419        for legacy in [false, true] {
5420            let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5421            let handle = queue_and_close(&left, sent, true);
5422            right.lose_lockstep_if_queued(1, legacy);
5423            assert!(handle.descriptor(5, false).is_some(), "kept with the queue");
5424            assert!(matches!(
5425                right.take_queued_rights(&[(1, 50)], false, false, legacy, None),
5426                Some(taken) if taken.sent.len() == 1
5427                    && taken.lockstep == !legacy
5428                    && taken.joinable
5429            ));
5430        }
5431
5432        // A later message taken after a call Detcore could not match forgets
5433        // the earlier one, which that call took.
5434        let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5435        let handle = queue_and_close(&left, sent, true);
5436        let later = |inode| SentDescriptor {
5437            file: (1, inode),
5438            open_file: None,
5439            lock_copy: None,
5440        };
5441        assert!(matches!(
5442            left.queue_rights_to_peer(vec![later(60)], 1),
5443            Ok(())
5444        ));
5445        right.lose_lockstep_if_queued(1, false);
5446        let taken = right
5447            .take_queued_rights(&[(1, 60)], false, false, false, None)
5448            .unwrap();
5449        assert_eq!(release(taken.forgotten), 1, "the last reference goes");
5450        assert!(handle.descriptor(5, false).is_none(), "released with it");
5451
5452        // A datagram `connect` of the receiving socket purges its queue.
5453        let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5454        let handle = queue_and_close(&left, sent, true);
5455        let purged = right.unpair_rights_peer();
5456        assert_eq!(purged.len(), 1);
5457        assert!(handle.descriptor(5, false).is_some(), "until released");
5458        assert_eq!(release(purged), 1);
5459        assert!(
5460            handle.descriptor(5, false).is_none(),
5461            "purged with the queue"
5462        );
5463
5464        // The receiving socket's last reference takes its queue.
5465        let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5466        let handle = queue_and_close(&left, sent, true);
5467        let taken = right.take_all_queued_rights();
5468        assert_eq!(release(taken), 1);
5469        assert!(
5470            handle.descriptor(5, false).is_none(),
5471            "gone with the socket"
5472        );
5473
5474        // A release keeps a description another descriptor still names.
5475        let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5476        let alias = sent.clone();
5477        queue_and_close(&left, sent, true);
5478        assert_eq!(release(right.take_all_queued_rights()), 0);
5479        assert!(alias.is_last_reference(), "now the last reference");
5480        drop(left);
5481    }
5482
5483    /// Whether a descriptor is its description's last reference, which
5484    /// decides a socket's port and queue at its close, depends only on what
5485    /// guest calls did, not on when the backend releases a table that holds
5486    /// a queue (https://github.com/rrnewton/hermit/issues/4930): a copy in
5487    /// flight in the queue of a socket whose only holder exited counts
5488    /// whether or not the backend has dropped that holder's table yet, and
5489    /// for the rest of the run, as does one dropped past the queue's bound.
5490    #[test]
5491    fn a_copy_in_flight_decides_the_last_reference_whatever_drops_its_queue() {
5492        let owner = DetTid::from_raw(10);
5493        for teardown_first in [false, true] {
5494            let (left, right, sent) = pair_and_sent(owner, FdType::Socket);
5495            let alias = sent.clone();
5496            queue_and_close(&left, sent, true);
5497            if teardown_first {
5498                // The receiving socket went with a table the backend released.
5499                drop(right);
5500                assert_eq!(alias.open_file_alias_count(), 1, "nothing holds the copy");
5501            }
5502            assert!(
5503                !alias.is_last_reference(),
5504                "teardown_first={teardown_first}: the copy in flight counts either way"
5505            );
5506            assert_eq!(alias.copies_in_flight(), 1);
5507        }
5508
5509        // Past the bound, the oldest message is dropped from the queue, and
5510        // what it carried stays counted.
5511        let (left, _right, sent) = pair_and_sent(owner, FdType::Socket);
5512        let alias = sent.clone();
5513        queue_and_close(&left, sent, true);
5514        let filler = || SentDescriptor {
5515            file: (1, 60),
5516            open_file: None,
5517            lock_copy: None,
5518        };
5519        for _ in 0..MAX_QUEUED_RIGHTS_MESSAGES {
5520            assert!(matches!(
5521                left.queue_rights_to_peer(vec![filler()], 1),
5522                Ok(())
5523            ));
5524        }
5525        assert_eq!(alias.open_file_alias_count(), 1, "dropped from the queue");
5526        assert!(!alias.is_last_reference(), "but never released");
5527    }
5528
5529    /// Where a socket's queue holds every message (the two sockets of a
5530    /// datagram `socketpair`), a call that takes one without naming its
5531    /// descriptors (a `read`) takes the oldest queued one, data alone or not,
5532    /// and releases what it carried, as Linux does
5533    /// ([`DetFd::take_head_message`]); a queue that may miss a message (a
5534    /// bound socket, or one out of lockstep) takes nothing.
5535    #[test]
5536    fn a_complete_queue_gives_up_the_message_a_read_takes() {
5537        let owner = DetTid::from_raw(10);
5538        let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5539        left.mark_complete_pair(&right);
5540        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 3), Ok(())));
5541        let alias = sent.clone();
5542        queue_and_close(&left, sent, true);
5543        let data_only = right.take_head_message(|head| head.bytes == 3);
5544        assert!(matches!(data_only, Some(ref sent) if sent.is_empty()));
5545        let carried = right.take_head_message(|head| head.bytes == 1).unwrap();
5546        assert_eq!(carried.len(), 1);
5547        assert!(!alias.is_last_reference());
5548        assert_eq!(
5549            carried
5550                .into_iter()
5551                .filter_map(|sent| sent.open_file.and_then(InFlightOpenFile::release))
5552                .count(),
5553            0
5554        );
5555        assert!(alias.is_last_reference(), "released at the read");
5556
5557        // A message that does not fit what the call reports leaves the queue
5558        // out of lockstep, taking nothing.
5559        let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5560        left.mark_complete_pair(&right);
5561        queue_and_close(&left, sent, true);
5562        assert!(right.take_head_message(|head| head.bytes == 2).is_none());
5563        assert!(right.take_head_message(|_| true).is_none(), "for good");
5564
5565        // A bound socket can be sent to by name: its queue may miss one.
5566        let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5567        left.mark_complete_pair(&right);
5568        right.set_unix_name(Box::from(&b"\0bound"[..]), 1);
5569        queue_and_close(&left, sent, true);
5570        assert!(right.take_head_message(|_| true).is_none());
5571        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 1), Ok(())));
5572        assert!(
5573            right
5574                .take_queued_rights(&[(1, 50)], false, false, false, None)
5575                .is_some()
5576                && !right.has_queued_rights(),
5577            "and data alone is not queued"
5578        );
5579    }
5580
5581    /// A queue that no longer holds every message keeps only the messages
5582    /// that carried descriptors, and the suspects among them
5583    /// (`give_up_completeness`): here two calls took messages unseen, the
5584    /// message of data alone and the first descriptor's, so a receipt of the
5585    /// second descriptor's message forgets the first. At the queue's bound, a
5586    /// queue that held every message gives up the messages of data alone
5587    /// first, before it drops anything Linux still holds, and stays in
5588    /// lockstep. A stream's runs of data alone are one message.
5589    #[test]
5590    fn a_queue_that_gives_up_completeness_keeps_its_descriptors() {
5591        let owner = DetTid::from_raw(10);
5592        let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5593        left.mark_complete_pair(&right);
5594        let carried = |inode| {
5595            vec![SentDescriptor {
5596                file: (1, inode),
5597                open_file: None,
5598                lock_copy: None,
5599            }]
5600        };
5601        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 1), Ok(())));
5602        queue_and_close(&left, sent, true);
5603        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 1), Ok(())));
5604        assert!(matches!(left.queue_rights_to_peer(carried(60), 1), Ok(())));
5605        right.lose_lockstep_if_queued(1, false);
5606        right.lose_lockstep_if_queued(1, false);
5607        left.give_up_pair_completeness();
5608        let taken = right
5609            .take_queued_rights(&[(1, 60)], false, false, false, None)
5610            .unwrap();
5611        assert!(taken.joinable && taken.lockstep);
5612        assert_eq!(
5613            taken
5614                .forgotten
5615                .iter()
5616                .map(|sent| sent.file)
5617                .collect::<Vec<_>>(),
5618            [(1, 50)],
5619            "the suspect message that carried a descriptor is forgotten"
5620        );
5621
5622        let (left, right, _) = pair_and_sent(owner, FdType::Pipe);
5623        left.mark_complete_pair(&right);
5624        for _ in 1..MAX_QUEUED_RIGHTS_MESSAGES {
5625            assert!(matches!(left.queue_rights_to_peer(Vec::new(), 1), Ok(())));
5626        }
5627        assert!(matches!(left.queue_rights_to_peer(carried(70), 1), Ok(())));
5628        assert!(matches!(left.queue_rights_to_peer(carried(80), 1), Ok(())));
5629        assert_eq!(
5630            right
5631                .take_queued_rights(&[(1, 70)], false, false, false, None)
5632                .map(|taken| (taken.lockstep, taken.joinable)),
5633            Some((true, true)),
5634            "nothing that carried a descriptor was dropped, and the queue stays in lockstep"
5635        );
5636        assert!(
5637            right
5638                .take_queued_rights(&[(1, 80)], false, false, false, None)
5639                .is_some()
5640        );
5641
5642        let (left, right, _) = pair_and_sent(owner, FdType::Pipe);
5643        left.pair_sockets(&right, true);
5644        left.mark_complete_pair(&right);
5645        for _ in 0..2 * MAX_QUEUED_RIGHTS_MESSAGES {
5646            assert!(matches!(left.queue_rights_to_peer(Vec::new(), 1), Ok(())));
5647        }
5648        assert_eq!(
5649            right
5650                .take_stream_bytes(2 * MAX_QUEUED_RIGHTS_MESSAGES, |detached| detached
5651                    .is_none())
5652                .map(|released| released.len()),
5653            Some(0),
5654            "a stream's data alone is one run of bytes, held whole"
5655        );
5656    }
5657
5658    /// A receive that took one message of data alone from a datagram or
5659    /// seqpacket socket (`DetFd::take_data_only_message`) loses nothing from a
5660    /// queue of descriptors alone; from a queue that holds every message it
5661    /// takes the oldest, which must be of data alone, or, while some are
5662    /// suspect, counts one more message that may be gone.
5663    #[test]
5664    fn a_receipt_of_data_alone_takes_only_from_a_complete_queue() {
5665        let owner = DetTid::from_raw(10);
5666        let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5667        queue_and_close(&left, sent, true);
5668        right.take_data_only_message();
5669        assert!(matches!(
5670            right.take_queued_rights(&[(1, 50)], false, false, false, None),
5671            Some(taken) if taken.joinable && taken.lockstep
5672        ));
5673
5674        let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5675        left.mark_complete_pair(&right);
5676        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 1), Ok(())));
5677        queue_and_close(&left, sent, true);
5678        right.take_data_only_message();
5679        assert!(
5680            right
5681                .take_head_message(|head| !head.descriptors.is_empty())
5682                .is_some(),
5683            "the message of data alone went first"
5684        );
5685
5686        let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5687        left.mark_complete_pair(&right);
5688        queue_and_close(&left, sent, true);
5689        right.take_data_only_message();
5690        assert!(
5691            right.take_head_message(|_| true).is_none(),
5692            "a queue whose oldest message carried a descriptor missed one"
5693        );
5694
5695        let (left, right, sent) = pair_and_sent(owner, FdType::Pipe);
5696        left.mark_complete_pair(&right);
5697        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 1), Ok(())));
5698        queue_and_close(&left, sent, true);
5699        right.lose_lockstep_if_queued(1, false);
5700        right.take_data_only_message();
5701        assert!(matches!(
5702            right.take_queued_rights(&[(1, 50)], false, false, false, None),
5703            Some(taken) if taken.joinable && taken.forgotten.is_empty()
5704        ));
5705    }
5706
5707    /// A stream socket's queue that holds every message gives up bytes from
5708    /// its oldest messages on (`DetFd::take_stream_bytes`): a message's
5709    /// descriptors go with the first of its bytes a read takes, a read of no
5710    /// bytes that reached the socket detaches the oldest message's descriptors
5711    /// alone, and a read that takes more bytes than the queue holds, or goes
5712    /// past a message whose descriptors it detached, misses a message, leaving
5713    /// the queue out of lockstep without the messages of data alone. A
5714    /// receipt of descriptors takes the message its bytes reach
5715    /// (`DetFd::take_queued_rights` with the receive's bytes), and a peek the
5716    /// first message that carried descriptors.
5717    #[test]
5718    fn a_stream_queue_gives_up_bytes_from_its_oldest_messages() {
5719        let owner = DetTid::from_raw(10);
5720        let stream_pair = |sequence| {
5721            let socket = |fd, sequence| {
5722                DetFd::new(
5723                    fd,
5724                    OFlag::O_RDWR,
5725                    FdType::Socket,
5726                    OpenFileId::new(owner, sequence),
5727                )
5728            };
5729            let (left, right) = (socket(3, sequence), socket(4, sequence + 1));
5730            left.pair_sockets(&right, true);
5731            left.mark_complete_pair(&right);
5732            (left, right)
5733        };
5734        let carried = |inode| {
5735            vec![SentDescriptor {
5736                file: (1, inode),
5737                open_file: None,
5738                lock_copy: None,
5739            }]
5740        };
5741        let inodes = |taken: Option<Vec<SentDescriptor>>| {
5742            taken.map(|taken| taken.iter().map(|sent| sent.file.1).collect::<Vec<_>>())
5743        };
5744
5745        let (left, right) = stream_pair(0);
5746        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 3), Ok(())));
5747        assert!(matches!(left.queue_rights_to_peer(carried(50), 2), Ok(())));
5748        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 4), Ok(())));
5749        assert_eq!(inodes(right.take_stream_bytes(2, |_| true)), Some(vec![]));
5750        assert_eq!(
5751            inodes(right.take_stream_bytes(2, |_| true)),
5752            Some(vec![50]),
5753            "the descriptors go with the first of their message's bytes"
5754        );
5755        assert_eq!(inodes(right.take_stream_bytes(5, |_| true)), Some(vec![]));
5756        assert!(!right.has_queued_rights(), "every byte read");
5757        assert!(
5758            right.take_stream_bytes(1, |_| true).is_none(),
5759            "more than it held"
5760        );
5761        assert!(
5762            matches!(left.queue_rights_to_peer(Vec::new(), 1), Ok(()))
5763                && !right.has_queued_rights(),
5764            "no longer holds every message"
5765        );
5766
5767        let (left, right) = stream_pair(2);
5768        assert!(matches!(left.queue_rights_to_peer(carried(60), 1), Ok(())));
5769        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 5), Ok(())));
5770        assert!(
5771            right.take_stream_bytes(3, |_| true).is_none(),
5772            "no read goes past a message whose descriptors it detached"
5773        );
5774        assert!(
5775            right
5776                .take_queued_rights(&[(1, 60)], false, false, false, None)
5777                .is_some()
5778        );
5779
5780        let (left, right) = stream_pair(4);
5781        assert!(matches!(left.queue_rights_to_peer(carried(70), 2), Ok(())));
5782        assert_eq!(
5783            inodes(right.take_stream_bytes(0, |detached| detached.is_some())),
5784            Some(vec![70]),
5785            "a read of no bytes detaches the oldest message's descriptors"
5786        );
5787        assert_eq!(inodes(right.take_stream_bytes(2, |_| true)), Some(vec![]));
5788
5789        let (left, right) = stream_pair(6);
5790        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 3), Ok(())));
5791        assert!(matches!(left.queue_rights_to_peer(carried(80), 1), Ok(())));
5792        let taken = right
5793            .take_queued_rights(&[(1, 80)], false, false, false, Some(4))
5794            .unwrap();
5795        assert!(taken.joinable && taken.lockstep && taken.forgotten.is_empty());
5796        assert_eq!(
5797            taken
5798                .sent
5799                .iter()
5800                .map(|sent| sent.file.1)
5801                .collect::<Vec<_>>(),
5802            [80]
5803        );
5804        assert!(!right.has_queued_rights());
5805
5806        // A peek reads past data alone to the first message that carried
5807        // descriptors, which is first among those that did, even where a later
5808        // one names the same files.
5809        let (left, right) = stream_pair(8);
5810        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 3), Ok(())));
5811        assert!(matches!(left.queue_rights_to_peer(carried(90), 1), Ok(())));
5812        assert!(matches!(left.queue_rights_to_peer(carried(90), 1), Ok(())));
5813        assert_eq!(
5814            right
5815                .take_queued_rights(&[(1, 90)], false, true, false, None)
5816                .map(|taken| taken.joinable),
5817            Some(true)
5818        );
5819    }
5820
5821    /// A stream read that reports no count (it failed copying an output out
5822    /// after it took its bytes) took what Linux takes for the bytes it asked
5823    /// for (`DetFd::stream_read_count`): bytes from the oldest messages on,
5824    /// stopping where it has them all, after the message whose descriptors
5825    /// it detached, or where the queue runs out. A queue that does not hold
5826    /// every message, or holds a suspect one, cannot say.
5827    #[test]
5828    fn a_stream_read_takes_what_linux_takes_for_what_it_asked() {
5829        let owner = DetTid::from_raw(10);
5830        let stream_pair = |sequence| {
5831            let socket = |fd, sequence| {
5832                DetFd::new(
5833                    fd,
5834                    OFlag::O_RDWR,
5835                    FdType::Socket,
5836                    OpenFileId::new(owner, sequence),
5837                )
5838            };
5839            let (left, right) = (socket(3, sequence), socket(4, sequence + 1));
5840            left.pair_sockets(&right, true);
5841            left.mark_complete_pair(&right);
5842            (left, right)
5843        };
5844        let carried = |inode| {
5845            vec![SentDescriptor {
5846                file: (1, inode),
5847                open_file: None,
5848                lock_copy: None,
5849            }]
5850        };
5851
5852        let (left, right) = stream_pair(0);
5853        assert_eq!(right.stream_read_count(4), Some(0), "an empty queue");
5854        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 3), Ok(())));
5855        assert!(matches!(left.queue_rights_to_peer(carried(50), 2), Ok(())));
5856        assert!(matches!(left.queue_rights_to_peer(Vec::new(), 4), Ok(())));
5857        assert_eq!(right.stream_read_count(0), Some(0));
5858        assert_eq!(right.stream_read_count(2), Some(2), "all it asked for");
5859        assert_eq!(
5860            right.stream_read_count(4),
5861            Some(4),
5862            "into the message that carried descriptors"
5863        );
5864        assert_eq!(
5865            right.stream_read_count(100),
5866            Some(5),
5867            "no further than the message whose descriptors it detached"
5868        );
5869        assert_eq!(
5870            right.take_stream_bytes(5, |_| true).map(|sent| sent.len()),
5871            Some(1)
5872        );
5873        assert_eq!(right.stream_read_count(100), Some(4), "the queue runs out");
5874
5875        let (left, right) = stream_pair(2);
5876        assert!(matches!(left.queue_rights_to_peer(carried(60), 2), Ok(())));
5877        assert!(matches!(left.queue_rights_to_peer(carried(61), 2), Ok(())));
5878        assert_eq!(right.stream_read_count(1), Some(1), "part of a message");
5879        right.lose_lockstep_if_queued(1, false);
5880        assert_eq!(
5881            right.stream_read_count(1),
5882            None,
5883            "after a read that may have taken a message unseen"
5884        );
5885
5886        let (left, right) = stream_pair(4);
5887        assert!(matches!(left.queue_rights_to_peer(carried(70), 2), Ok(())));
5888        right.give_up_completeness();
5889        assert_eq!(
5890            right.stream_read_count(1),
5891            None,
5892            "a queue that no longer holds every message"
5893        );
5894    }
5895
5896    /// A peek may reach a later message (`SO_PEEK_OFF`). It is trusted
5897    /// wherever it is while the queue is in lockstep and no other queued
5898    /// message names the same files; otherwise only within the suspects.
5899    #[test]
5900    fn a_peek_may_name_a_later_message() {
5901        let owner = DetTid::from_raw(10);
5902        let socket = |fd, sequence| {
5903            DetFd::new(
5904                fd,
5905                OFlag::O_RDWR,
5906                FdType::Socket,
5907                OpenFileId::new(owner, sequence),
5908            )
5909        };
5910        let carried = |inode| {
5911            vec![SentDescriptor {
5912                file: (1, inode),
5913                open_file: None,
5914                lock_copy: None,
5915            }]
5916        };
5917        let peek = |socket: &DetFd, inode| {
5918            socket
5919                .take_queued_rights(&[(1, inode)], false, true, false, None)
5920                .map(|taken| taken.joinable)
5921        };
5922        let (left, right) = (socket(3, 0), socket(4, 1));
5923        left.pair_sockets(&right, false);
5924        for inode in [10, 20] {
5925            assert!(matches!(
5926                left.queue_rights_to_peer(carried(inode), 1),
5927                Ok(())
5928            ));
5929        }
5930        assert_eq!(peek(&right, 20), Some(true), "unique, in lockstep");
5931        assert!(matches!(left.queue_rights_to_peer(carried(20), 1), Ok(())));
5932        assert_eq!(peek(&right, 20), Some(false), "another message names it");
5933
5934        let (left, right) = (socket(5, 2), socket(6, 3));
5935        left.pair_sockets(&right, false);
5936        for inode in [10, 20] {
5937            assert!(matches!(
5938                left.queue_rights_to_peer(carried(inode), 1),
5939                Ok(())
5940            ));
5941        }
5942        left.lose_peer_lockstep();
5943        assert_eq!(peek(&right, 20), Some(false), "out of lockstep");
5944    }
5945
5946    /// The last held reference to a queued socket lets go of it with its own
5947    /// queue ([`InFlightOpenFile::into_queue_if_last`]), and a long chain of
5948    /// sockets each queued on the one before is dropped without recursing,
5949    /// so no guest can overflow the tool thread's stack by building one.
5950    #[test]
5951    fn a_chain_of_queued_sockets_is_released_without_recursion() {
5952        let owner = DetTid::from_raw(10);
5953        let socket = |sequence| {
5954            DetFd::new(
5955                3,
5956                OFlag::O_RDWR,
5957                FdType::Socket,
5958                OpenFileId::new(owner, sequence),
5959            )
5960        };
5961        let first = socket(0);
5962        let (mut left, mut right) = (socket(1), socket(2));
5963        left.pair_sockets(&right, false);
5964        // `first` is queued on `right`; each later pair's receiving socket is
5965        // queued on the pair after it.
5966        let sentinel = first.open_file_handle();
5967        assert!(matches!(
5968            left.queue_rights_to_peer(
5969                vec![SentDescriptor {
5970                    file: (1, 0),
5971                    open_file: Some(first.in_flight_open_file(true)),
5972                    lock_copy: None,
5973                }],
5974                1
5975            ),
5976            Ok(())
5977        ));
5978        drop(first);
5979        for sequence in 0..50_000 {
5980            let (next_left, next_right) = (socket(3 + 2 * sequence), socket(4 + 2 * sequence));
5981            next_left.pair_sockets(&next_right, false);
5982            assert!(matches!(
5983                next_left.queue_rights_to_peer(
5984                    vec![SentDescriptor {
5985                        file: (1, 1),
5986                        open_file: Some(right.in_flight_open_file(true)),
5987                        lock_copy: None,
5988                    }],
5989                    1
5990                ),
5991                Ok(())
5992            ));
5993            drop(left);
5994            drop(right);
5995            left = next_left;
5996            right = next_right;
5997        }
5998        assert!(
5999            sentinel.descriptor(3, false).is_some(),
6000            "the chain holds the first socket"
6001        );
6002        drop(left);
6003        drop(right);
6004        assert!(sentinel.descriptor(3, false).is_none(), "and lets it go");
6005    }
6006}