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}