hermit/metadata.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
9use std::collections::BTreeMap;
10use std::env;
11use std::fs;
12use std::path::Path;
13use std::path::PathBuf;
14
15use detcore::BlockingMode;
16use detcore_model::config::MountInfoRootRewrite;
17use reverie::process::Command;
18use serde::Deserialize;
19use serde::Serialize;
20
21use crate::consts::METADATA_NAME;
22use crate::error::Context;
23use crate::error::Error;
24
25/// Hermit record version. Recorded as part of hermit-record, hermit-replay
26/// will check this version and will fail if hermit-record version is newer.
27#[derive(
28 Debug,
29 Clone,
30 Copy,
31 Eq,
32 PartialEq,
33 Ord,
34 PartialOrd,
35 Deserialize,
36 Serialize
37)]
38#[repr(transparent)]
39pub struct RecordVersion(u32);
40impl RecordVersion {
41 /// Check if the recorder/replayer version is compatible with a given
42 /// recording (trace).
43 pub fn compatible_with(&self, other: &RecordVersion) -> bool {
44 self == other
45 }
46}
47
48/// hermit record/replay version.
49// NB: Increase the version number when there are breaking changes, i.e.:
50// when new syscalls or event schemas are added.
51//
52// AUTONOMOUS-BOT-IMPLEMENTED
53// TODO-HUMAN-REVIEW(#2373)
54// 0x10b -> 0x10c: flock(2) stopped being a Detcore no-op and now reaches
55// `record_or_replay`, so a post-fix run emits a `Return` event per flock call.
56// Replay re-issues calls for materialized descriptors and fails closed when it
57// cannot reproduce the lock side effect. A 0x10b recording contains NO flock
58// event at all -- the old handler returned Ok(0) before ever reaching the
59// recorder, even though flock was already classified Determinized -- so
60// replaying one under this build would read the next thread event for every
61// flock and desynchronize the stream. The version gate must refuse it.
62//
63// TODO-HUMAN-REVIEW(#2370)
64// 0x10d -> 0x10e: record/replay now carries successful exec image/path state and
65// an explicit ppoll timeout-pointer shape. Older readers cannot interpret those
66// events without desynchronizing the stream, so the format advances once.
67//
68// TODO-HUMAN-REVIEW(#2272)
69// 0x10c -> 0x10d: `Ppoll` becomes its OWN event rather than sharing `Poll`'s,
70// because ppoll additionally copies out a timeout and must preserve it on
71// EINTR and on a partial EFAULT copy-out; and `Poll` itself now records the
72// partial `revents` copy-out an error return leaves behind. The recorded
73// stream therefore carries a different event shape for both syscalls.
74//
75// ONE increment covers BOTH halves deliberately. This branch bumped twice --
76// once per format change -- but a reader either understands the new stream or
77// it does not, so what matters is that the version differs from every stream
78// a different shape was written under. Landing two increments would imply a
79// 0x10d recording exists that this build can read and it cannot: no build
80// ever wrote one.
81//
82// ⚠️ THIS BUMP IS WHY THE REBASE COULD NOT SIMPLY TAKE EITHER SIDE. This
83// change was authored against 0x10a and bumped to 0x10b; main has since gone
84// to 0x10c for the unrelated flock work. Keeping the branch's 0x10b would
85// move the version BACKWARDS and let a 0x10c flock recording be replayed by a
86// build whose ppoll events have a different shape. Keeping main's 0x10c
87// unchanged would be worse: the format would change with no bump at all, so a
88// recording made here would claim 0x10c while containing a `Ppoll` event the
89// 0x10c reader does not know -- exactly the desynchronization the paragraph
90// above exists to prevent. The version must go FORWARD once more.
91//
92// AUTONOMOUS-BOT-IMPLEMENTED
93// TODO-HUMAN-REVIEW(#2407)
94// 0x10e -> 0x10f: pidfd_getfd(2) is now explicitly subscribed and recorded.
95// Earlier builds let the syscall bypass the recorder, so a successful call
96// changed the guest descriptor table without a corresponding stream event.
97// The replayer now consumes one exact pidfd_getfd event and reissues the
98// syscall to reproduce the kernel side effect; accepting a 0x10e stream would
99// therefore consume the next event at every pidfd_getfd call.
100//
101// 0x10f -> 0x110: metadata now carries the recording namespace's proven
102// Hermit-owned mount IDs. Detcore applies that map after both the recorder's
103// raw read and the replayer's ReadV2 copyout. An older recording has no map and
104// would replay different guest-visible mountinfo bytes under this sanitizer,
105// so accepting it would violate replay fidelity even though the event enum did
106// not change.
107//
108// 0x110 -> 0x111: metadata now also carries the recording namespace's full
109// raw mount-ID order. That makes fdinfo's mnt_id use the same canonical identity
110// as mountinfo during replay instead of consulting the unrelated replay
111// namespace or collapsing all descriptors to one mount.
112// 0x111 -> 0x113: fdinfo mount identities are now keyed by each observed raw
113// mount ID rather than broad descriptor classes. Metadata carries only the
114// completed namespace's mountinfo row/parent order; pseudo-filesystem IDs that
115// are absent from mountinfo are allocated afterward in deterministic guest
116// observation order. Replay must rebuild that mapping from recording-time raw
117// bytes, or pidfs/nsfs/anon_inodefs can be collapsed or rejected. The draft
118// 0x112 format was never shipped.
119//
120// 0x113 -> 0x114: recordings persist the completed guest's mountinfo order and
121// unlisted fdinfo mount-ID order, and event streams use stable recording-local
122// thread IDs rather than host PID allocation. An older reader cannot rebuild
123// either mapping reliably for a plain public record API recording.
124//
125// 0x114 -> 0x115: event stream filenames now encode deterministic process-tree
126// pedigrees instead of process-local allocation order. Older readers look for
127// integer filenames and cannot replay a recording containing child streams.
128//
129// 0x115 -> 0x116: event stream filenames are fixed-size SHA-256 names and each
130// data/debug stream begins with its complete process-tree identity. Older
131// readers cannot skip or validate these headers.
132// 0x116 -> 0x119: nonpositioned random-device readv now uses Detcore's
133// shared canonical cursor and no longer emits or consumes recorder ReadvV2
134// events. Old event streams cannot be replayed under this routing.
135// 0x117 and 0x118 identify incompatible retained RNG, clock, and network
136// recordings from parallel development; do not reinterpret those identities.
137// 0x119 -> 0x11a: random-device preadv/preadv2 now use Detcore's canonical
138// generator, including the shared cursor for preadv2(-1). They no longer emit
139// or consume ReadvV2 events; accepting an older recording would desynchronize
140// the event stream and replace its recorded random bytes.
141// Early rejection of invalid negative offsets also stops emitting ReadvV2
142// error events for non-random descriptors.
143// 0x11a -> 0x11b: sysinfo uptime rounds exact elapsed logical time up, removing
144// dependence on the calendar epoch's fraction. Detcore recomputes sysinfo during
145// recording/replay, even with virtualize_time=false; replaying an older stream
146// under the new projection could change guest control flow and event consumption.
147// Sysinfo remains determinized rather than gaining a recorded event.
148// 0x11b -> 0x11c: /proc/uptime renders the floor of exact elapsed logical time
149// instead of the difference of the separately truncated clock and epoch, so a
150// fractional epoch no longer moves it (or /proc/stat's idle ticks derived from
151// it) by one second. /proc/stat btime is now derived from the fixed boot
152// instant rather than from the clock minus uptime. Detcore recomputes these
153// fields over the recorded procfs bytes during recording and replay, so an older
154// stream replayed under the new projection could show the guest different bytes.
155// 0x11c -> 0x11d: with time not virtualized, which is always the case for
156// record/replay, detcore passes clock reads to record/replay instead of refusing
157// them. Streams now carry Timespec and Timeofday events for clock_gettime,
158// gettimeofday and time, and clock_getres gains a recorder event (Timespec, or
159// Return for a NULL `res`). An older reader would desynchronize on these events.
160// 0x11d -> 0x11e: guest-semantic madvise advice, which detcore used to refuse
161// with ENOSYS, now reaches the recorder and replayer and emits a Madvise event.
162// 0x11e -> 0x11f: faccessat, faccessat2, chdir, getcwd and the legacy path
163// mutations (rename, link, symlink, chmod, chown, lchown, mknod, rmdir) are
164// recorded instead of running live. A successful chdir or fchdir carries a
165// Bytes event naming the directory it entered. An older reader would
166// desynchronize on these events.
167// 0x11f -> 0x120: descriptor syncs and metadata changes (fsync, fdatasync,
168// syncfs, fchmod, fchown, fsetxattr, fremovexattr) record a Return event, the
169// xattr queries (getxattr, lgetxattr, fgetxattr and the listxattr family)
170// record a Bytes event (Return for a zero-size query), and zero-count read and
171// pread64 calls are recorded instead of running live. An older reader would
172// desynchronize on these events.
173// 0x120 -> 0x121: splice and tee refuse with EINVAL instead of ENOSYS; vmsplice
174// keeps ENOSYS. Record and replay always fail closed, and Detcore computes this
175// refusal itself rather than recording it, so replay recomputes the errno. The
176// guest can branch on it: GNU grep falls back to read/write on EINVAL but
177// reports a read error and exits 2 on ENOSYS. Replaying an older stream under
178// the new errno could change guest control flow and event consumption, although
179// no event shape changed.
180// 0x121 -> 0x122: Detcore numbers raw mount IDs in the order the guest first
181// observes them through mountinfo or fdinfo, instead of by row position in the
182// run's starting mountinfo. Metadata carries that order as
183// `mount_id_assignment_order`, replacing `fdinfo_unlisted_mount_ids`, and
184// replay numbers every raw ID from it. An older recording's metadata cannot
185// rebuild the mapping its guest saw, so replaying it could change the mount IDs
186// the guest reads and its control flow.
187// 0x122 -> 0x123: Detcore's inode pool starts at 9000 instead of 1 and every
188// numbering request consumes a number, so the deterministic inode numbers
189// differ (https://github.com/rrnewton/hermit/issues/2897). Even with
190// virtualize_metadata off, as record/replay runs, the procfs sanitizer renders
191// pool numbers into /proc/*/fdinfo `ino:` lines and /proc/*/maps and smaps
192// inode columns over the recorded raw bytes, during recording and again during
193// replay. An older recording replayed under the new numbering could show the
194// guest different bytes, although no event shape changed.
195// 0x123 -> 0x124: accept and accept4 record an `Accept` event, and bind, listen
196// and shutdown record a `Return` event, instead of running live during replay
197// (https://github.com/rrnewton/hermit/issues/3864). An older recording has no
198// events for these calls, so this replayer would consume another call's event.
199// 0x124 -> 0x125: a blocking accept on a listener the container created runs in
200// the caller's turn (https://github.com/rrnewton/hermit/issues/3880). It
201// records one `Accept` event per nonblocking attempt, where an older recording
202// has a single `Accept` event for the backgrounded call. (Its timeout comes
203// from Detcore's model of `setsockopt`, not from a recorded event.) Connections accepted under record
204// are no longer made nonblocking. An older recording would hand this replayer
205// the wrong number of events. A recording also carries a `rejoins` file, the
206// turns at which backgrounded calls rejoined the run queue, which replay
207// requires.
208// 0x125 -> 0x126: in the precise futex mode a FUTEX_WAKE that wakes fewer
209// waiters than are waiting takes the longest waiters first, as Linux does,
210// instead of the most recent ones, and a FUTEX_WAKE_BITSET leaves the waiters
211// it skips in arrival order (https://github.com/rrnewton/hermit/issues/3917).
212// Futex waits and wakes are not recorded events; replay recomputes which thread
213// each wake takes, so an older recording replayed under the new order could run
214// a different schedule, although no event shape changed. Schedule and
215// preemption files written by `--record-preemptions-to` carry no version and
216// are likewise tied to the old order.
217// 0x126 -> 0x127: a vfork barrier ends at the child's release edge (its exec
218// or `Exit` grant) whether or not the parent's death has been seen. With a
219// live parent, the scheduler then commits no turn until the parent's
220// continuation is posted, where the child used to keep running alone for a
221// host-timed number of turns (https://github.com/rrnewton/hermit/issues/3984).
222// Turn selection is not recorded; replay recomputes it, so an older recording
223// containing a vfork replayed under the new rule could run a different
224// schedule, although no event shape changed.
225// 0x127 -> 0x128: on ptrace, `prctl(PR_SET_CHILD_SUBREAPER)` and
226// `PR_GET_CHILD_SUBREAPER` run live in record and replay alike instead of
227// failing with ENOSYS, and the scheduler re-parents orphans to a child
228// subreaper (https://github.com/rrnewton/hermit/issues/3997). An older
229// recording saw ENOSYS for both calls; this replayer would run them and could
230// take another branch and a different schedule, although no event shape
231// changed.
232// 0x128 -> 0x129: record and replay set
233// `supports_blocked_wait_signal_interruption`, so the scheduler models
234// signal targets as `hermit run` does on ptrace
235// (https://github.com/rrnewton/hermit/issues/3963). A thread that a committed
236// signal wakes out of a backgrounded call rejoins at the release barrier, a
237// precise FUTEX_WAIT or an rt_sigtimedwait ends only for a signal that would end
238// it on Linux, and an emulated pause or nanosleep ends for another thread's
239// signal. (poll, epoll_wait, select and pselect6 are logged background calls
240// here, which the kernel itself ends.) Replay recomputes all of these, so an
241// older recording replayed here could run a different schedule, although no
242// event shape changed.
243// 0x129 -> 0x12a: a SIGCHLD the scheduler sends at a commit point (a child's
244// child-exit timer, or a POSIX timer) ends a precise FUTEX_WAIT that catches
245// it, as on Linux, where it was held until the call returned
246// (https://github.com/rrnewton/hermit/issues/4005). Replay recomputes precise
247// futex results, so an older recording replayed here could take the other
248// branch, although no event shape changed (Codex review of
249// https://github.com/rrnewton/hermit/pull/4039).
250// 0x12a -> 0x12b: on ptrace, a clone without CLONE_THREAD and with a
251// non-SIGCHLD exit signal is its own process to the Tool, with its own pid and
252// its kernel parent (https://github.com/rrnewton/hermit/issues/4012). Detcore
253// recomputes calls that name that process (getpid-based prlimit, its exit and
254// its parent's notification) from it, so an older recording replayed here
255// could take another branch, although no event shape changed.
256// 0x12b -> 0x12c: a precise FUTEX_WAIT woken for a signal that is no longer
257// pending for it re-reads its word and answers EAGAIN if the word changed
258// (EFAULT if unreadable), where it answered 0 as a wakeup; Linux's woken waiter
259// reaches the same EAGAIN through -ERESTARTSYS and the restarted wait. A signal
260// still pending for it is answered before the word is compared
261// (https://github.com/rrnewton/hermit/issues/4033). Replay recomputes
262// precise futex results, so an older recording replayed here could see another
263// result and take another branch, although no event shape changed.
264// 0x12c -> 0x12d: a precise FUTEX_WAIT or FUTEX_WAIT_BITSET checks its timeout
265// and bitset before the word, and reads the word with user-mode permissions, as
266// Linux's `futex_wait_setup` does: EFAULT for an unmapped or PROT_NONE word,
267// where the tracer's read gave EIO or compared a PROT_NONE word's value
268// (https://github.com/rrnewton/hermit/issues/4030). Replay recomputes precise
269// futex results, so an older recording replayed here could see another errno
270// and take another branch, although no event shape changed (Codex review of
271// https://github.com/rrnewton/hermit/pull/4061).
272// 0x12d -> 0x12e: a pidfd_send_signal to a process with one live thread tells
273// the scheduler the signal is pending for that thread, as kill does, so a
274// receiver parked in a blocking call is woken to take it
275// (https://github.com/rrnewton/hermit/issues/4099). Replay recomputes
276// the schedule, so an older recording replayed here could be scheduled
277// differently, although no event shape changed.
278// 0x12e -> 0x12f: a kill to a zombie (exited, its parent can still wait for
279// it) answers 0, as Linux's kill_pid_info does, where it answered ESRCH; a
280// kill(pid, 0) to an exited process answers from the same model; and an
281// rt_sigaction(SIGCHLD) tells the scheduler whether the process now reaps its
282// children automatically (https://github.com/rrnewton/hermit/issues/4091).
283// Replay recomputes these answers and the schedule, so an older recording
284// replayed here could see another result and take another branch, although
285// no event shape changed.
286// 0x12f -> 0x130: `epoll_pwait` is recorded with the event `epoll_wait`
287// records, and replay serves it from the recording. It used to run live in
288// record and replay alike, so an older recording holds no event for it and this
289// replayer would desynchronize at the first one.
290// 0x130 -> 0x131: an rt_sigqueueinfo to a zombie is forwarded to the kernel,
291// which checks the siginfo and then finds the zombie (0), where it answered
292// ESRCH (https://github.com/rrnewton/hermit/issues/4091). Replay recomputes
293// the answer, so an older recording replayed here could see another result
294// and take another branch, although no event shape changed.
295// 0x131 -> 0x132: a process-directed signal naming a live non-leader thread's
296// id goes to that thread's process, as Linux's kill_pid_info does, where it
297// answered ESRCH (https://github.com/rrnewton/hermit/issues/4094). Replay
298// recomputes the answer, so an older recording replayed here could see another
299// result and take another branch, although no event shape changed.
300// 0x132 -> 0x133: record readmits a finished background call while the only
301// runnable work queued is threads that yielded and pollers, where it used to
302// wait for every yielder to stop (https://github.com/rrnewton/hermit/issues/4092).
303// Replay follows the logged readmission turn only once its gate opens, and an
304// older replayer's gate stays shut in that state, so it could not consume the
305// readmission: it would refuse, or hang behind a yielder that never stops
306// (Codex review of https://github.com/rrnewton/hermit/pull/4120).
307// 0x133 -> 0x134:
308// a masked or zero-timeout ppoll on a stack with no scratch below its red zone
309// stages its private timeout and mask in the red zone, where it returned EFAULT
310// and recorded nothing, and a masked record/replay pselect6 that replay could
311// not deliver an interruption of is refused
312// (https://github.com/rrnewton/hermit/issues/4113,
313// https://github.com/rrnewton/hermit/issues/4112). An older recording of such a
314// ppoll holds no event for the call, so this replayer would desynchronize at it
315// (Claude review of https://github.com/rrnewton/hermit/pull/4127).
316// 0x134 -> 0x135: an rt_sigqueueinfo to a gone process makes the kernel check
317// its siginfo (EFAULT, E2BIG, EPERM) before answering ESRCH, where Detcore
318// answered ESRCH first; an rt_sigqueueinfo existence check (signal 0) answers
319// from the same exit model (https://github.com/rrnewton/hermit/issues/4124).
320// Replay recomputes the answer, so an older recording replayed here could see
321// another result and take another branch, although no event shape changed.
322// 0x135 -> 0x136: on ptrace, a guest-sent SIGKILL retires its victims at the
323// sending turn instead of at their host-timed deregistration
324// (https://github.com/rrnewton/hermit/issues/3994). Turn and wait selection
325// are not recorded; replay recomputes them, so an older recording replayed
326// under the new rule could run a different schedule. The metadata also gains
327// `replay_refused`, which an older replayer would not check.
328// 0x136 -> 0x137: `epoll_pwait2` is recorded with the event `epoll_wait`
329// records, and replay serves it from the recording; it ran live before
330// (https://github.com/rrnewton/hermit/issues/4088).
331// 0x137 -> 0x138: a guest send of a default-stop signal records SIGHUP and
332// SIGCONT as host-timed for every process, which an orphaned process group
333// can receive at an exit's physical moment
334// (https://github.com/rrnewton/hermit/issues/4111), so no gated wait ends for
335// them afterwards. Replay recomputes the schedule, so an older recording
336// replayed here could see such a wait end, although no event shape changed.
337// Record and replay refuse the newly admitted signal to the sender's own
338// process (https://github.com/rrnewton/hermit/issues/4034), so that change
339// alters no recording.
340// 0x138 -> 0x139: pidfd_send_signal with a PIDFD_SIGNAL_THREAD or
341// PIDFD_SIGNAL_THREAD_GROUP flag, or through a /proc/<pid> directory, is
342// forwarded, where Detcore answered EINVAL or EBADF without an event
343// (https://github.com/rrnewton/hermit/issues/4108). An older recording holds no
344// event for such a call, so this replayer would desynchronize at it.
345// 0x13b -> 0x13c: pidfd_send_signal through the PIDFD_SELF_THREAD or
346// PIDFD_SELF_THREAD_GROUP sentinel is forwarded, where Detcore answered EBADF
347// without an event (https://github.com/rrnewton/hermit/issues/4147). An older
348// recording holds no event for such a call, so this replayer would
349// desynchronize at it.
350// 0x13c -> 0x13d: timer_create with SIGEV_THREAD_ID answers EINVAL for a
351// thread outside the caller's process (and for an invalid notify value, where
352// it answered ENOSYS), and the timer's expirations go to that thread alone,
353// dropped once it has exited, where they went to the whole process
354// (https://github.com/rrnewton/hermit/issues/4164). Replay recomputes both, so
355// an older recording replayed here could see different answers and deliveries.
356// 0x13d -> 0x13e: a vfork child that Detcore backgrounded, and that the vfork
357// gate waits for while every queued thread is excluded, is readmitted as soon
358// as its call finishes, without a rejoin entry: before it, no turn can commit
359// (https://github.com/rrnewton/hermit/issues/4163). An older recorder spun
360// forever in that state, and replay recomputes the schedule, so an older
361// recording replayed here could see the readmission where its recorder did
362// not.
363// 0x13e -> 0x13f: a guest in a nested PID namespace has its pids translated
364// (https://github.com/rrnewton/hermit/issues/4155,
365// https://github.com/rrnewton/hermit/issues/4090): kill and its relatives
366// deliver where they answered ESRCH, fork returns the nested pid, waits reap
367// where they answered ECHILD, the init's own default signals are dropped, and
368// orphans go to the nested init. Replay recomputes these, so an older
369// recording of such a guest could diverge.
370// 0x13f -> 0x140: a record/replay pselect6 that may wait reads its temporary
371// mask at the call's entry, where Linux copies it, and recording waits on a
372// private copy; replay read it after the grant
373// (https://github.com/rrnewton/hermit/issues/4095). An older recording whose
374// sibling rewrote the mask between the two can replay differently here.
375// 0x140 -> 0x141: sched_getaffinity accepts any length that is a nonzero
376// multiple of sizeof(long), as Linux does for one CPU, and copies and returns
377// min(len, 16), where it answered EINVAL below 16 bytes
378// (https://github.com/rrnewton/hermit/issues/4160). Replay answers the call
379// itself, so an older recording that saw EINVAL would see success here.
380// 0x141 -> 0x142: a signal that another task sends ends a restartable call
381// that Detcore polls internally (a blocking pipe read or write, readv, recv,
382// send, accept4, connect, and the legacy wait4 polling) when the kernel would
383// deliver it, where the poll ignored it and retried
384// (https://github.com/rrnewton/hermit/issues/4187). The recorder logs each
385// polled EAGAIN but not the scheduler's signaled answer, so recordings made
386// before and after this change do not replay on each other's replayer.
387// 0x142 -> 0x143: epoll_ctl is recorded, and replay answers it from the
388// recording (https://github.com/rrnewton/hermit/issues/31). An older recording
389// has no event for it.
390// 0x143 -> 0x144 (https://github.com/rrnewton/hermit/pull/4131):
391// a child-exit SIGCHLD the scheduler sends ends an emulated pause or nanosleep
392// only when the thread's mask leaves it unblocked and it is caught or
393// default-fatal, and a still-queued sleep is woken for it
394// (https://github.com/rrnewton/hermit/issues/4126). Replay recomputes such a
395// sleep's result, so an older recording of a blocked or default SIGCHLD during
396// a nanosleep holds the EINTR it used to return, and this replayer would answer
397// 0: a successful replay with different output (Codex review of
398// https://github.com/rrnewton/hermit/pull/4131).
399// 0x144 -> 0x145: an fcntl that writes its result through its argument
400// (F_GETOWN_EX, which glibc uses for F_GETOWN; F_GETLK, F_GETLK64,
401// F_OFD_GETLK; the write-hint queries) records the bytes it wrote, and replay
402// writes them back (https://github.com/rrnewton/hermit/issues/2289). An older
403// recording has no such event.
404// 0x145 -> 0x146: a successful exec takes back the POSIX timer signals
405// Detcore sent that are still queued, as Linux discards them
406// (flush_itimer_signals; https://github.com/rrnewton/hermit/issues/4184),
407// where they survived the exec and reached the new image. Replay recomputes
408// this, so an older recording replayed here could see a delivery its recorder
409// saw and this replayer drops.
410// 0x146 -> 0x147: while a vfork barrier stands, the scheduler selects other
411// runnable threads when no barrier child is queued to run a turn and none is in
412// the background (each is blocked on another thread or polling a call that is
413// not ready) (https://github.com/rrnewton/hermit/issues/4194). A polling child
414// used to run alone; replay recomputes the schedule, so an older recording of a
415// vfork child that polled for a host event, with other threads runnable, could
416// replay differently. A backgrounded child is still waited for, as before.
417// 0x147 -> 0x148: get_mempolicy answers as a one-node Linux machine
418// (https://github.com/rrnewton/hermit/issues/4214): MPOL_F_MEMS_ALLOWED fills
419// the node mask with node 0 and the default policy's mask is written empty,
420// where Detcore left the guest's buffer unwritten, and flag combinations Linux
421// refuses are EINVAL, where Detcore answered 0. Replay answers the call
422// itself, so an older recording of such a call would replay with different
423// answers.
424// 0x148 -> 0x149: when the guest's CPUID comes from Detcore's table,
425// /proc/cpuinfo describes that virtual CPU, generated from the table, where it
426// showed the host's model, flags and caches
427// (https://github.com/rrnewton/hermit/issues/4161). Record virtualizes CPUID,
428// and Detcore renders procfs over the recorded bytes during recording and
429// again during replay, so an older recording would replay with different bytes.
430// 0x149 -> 0x14a: the procfs files that describe the kernel and its fixed
431// limits (/proc/version, /proc/cmdline and 12 kernel and vm sysctls)
432// are procfs files whose contents Detcore fixes from its machine description
433// (https://github.com/rrnewton/hermit/issues/4266), where they were ordinary
434// files whose host bytes were recorded as read. Replay serves a classified
435// procfs file itself, so an older recording of a read of one would replay
436// with different events.
437// 0x14a -> 0x14b: a guest's thread-directed SIGCHLD (tgkill, pthread_kill,
438// pthread_sigqueue) that the sender's turn recorded ends a precise futex wait, before or after the wait
439// parks, when the waiter's mask leaves it unblocked and it is caught
440// (https://github.com/rrnewton/hermit/issues/4132). The same copy has ended an
441// emulated pause or nanosleep since
442// https://github.com/rrnewton/hermit/pull/4177, which changed those results
443// with no bump of its own; this one covers both. Replay recomputes both
444// results, so an older recording of such a send would replay with the wait or
445// the sleep ended early: a successful replay with different output.
446// 0x14b -> 0x14c: under record, a vfork child whose call runs in the
447// background no longer stops every other thread; it is readmitted as soon as
448// the call finishes and the turn is written to the rejoin log, which replay
449// follows (https://github.com/rrnewton/hermit/issues/4211). An older recording
450// has no rejoin entry for such a readmission, so this replay could not follow
451// it.
452// 0x14c -> 0x14d: the kernel's tables (/proc/interrupts, softirqs, zoneinfo,
453// meminfo, modules and 16 others) are fixed files that Detcore renders from
454// its machine description with no kernel read
455// (https://github.com/rrnewton/hermit/issues/4266), where Detcore sanitized
456// the host's file and kept its shape: its interrupt names, zones and one
457// column per host CPU. And every open whose spelling Detcore does not
458// classify (nearly every open outside /proc and /sys) now records a readlink
459// of the descriptor, which replay answers, so both runs classify it alike. An
460// older recording would replay with different bytes and events.
461// 0x14d -> 0x14e: a child-exit SIGCHLD the scheduler sends a parked precise
462// futex waiter is placed at that commit point, so the waiter's woken turn is
463// granted ahead of a runnable sibling rather than deferred behind it, and the
464// waiter, not the sibling, takes the signal and returns EINTR
465// (https://github.com/rrnewton/hermit/issues/4304). Replay recomputes the
466// schedule and futex results, so an older recording of such an exit would
467// replay with the handler in another thread and a different futex result.
468// 0x14e -> 0x14f: a timer's process-directed signal (alarm, an itimer, a
469// POSIX timer) is sent to a thread that does not block it, not to the thread
470// that armed it or the one chaos drew when that thread blocks it, and that
471// thread is the one woken (https://github.com/rrnewton/hermit/issues/4320).
472// Replay recomputes the send, so an older recording of a timer whose arming
473// thread blocked its signal would replay with another thread woken. And a
474// background rt_sigsuspend that the host reports as ERESTARTSYS returns
475// ERESTARTNOHAND, the only result Linux's rt_sigsuspend has, so an older
476// recording of one would replay with a different result and restart.
477// 0x14f -> 0x150: a robust futex word that one exit's walk finds owned by its
478// dying owner reads as marked FUTEX_OWNER_DIED for the rest of that exit, as
479// Linux's own store makes it (https://github.com/rrnewton/hermit/issues/4238).
480// A word reached twice, through an alias or an entry that is its own
481// successor, was woken again, waking a second waiter Linux leaves to time out.
482// Replay recomputes the wakes, so an older recording of such an exit would
483// replay with one wake fewer: a successful replay with different output.
484// 0x150 -> 0x151 (taken at land time, as the reservation file's land-time
485// numbering rule gives a ready change HIGHEST_SHIPPED + 1): a pause or
486// nanosleep that a child-exit SIGCHLD from the scheduler wakes is resumed
487// first, as a woken futex waiter is, so the sleeper, not a running sibling,
488// takes the signal and returns EINTR
489// (https://github.com/rrnewton/hermit/issues/4333). Replay recomputes the
490// schedule, so an older recording of such an exit would replay with the
491// handler in another thread and the sleep still asleep.
492// 0x151 -> 0x152 (taken at land time, as the reservation file's land-time
493// numbering rule gives a ready change HIGHEST_SHIPPED + 1): a call that looks
494// up a /proc name outside the --proc level (kallsyms, sysvipc,
495// sys/kernel/core_pattern, ...) is issued with that name replaced by one
496// procfs cannot hold, so its recorded answer is Linux's for a missing name
497// (https://github.com/rrnewton/hermit/issues/4266). An older recording holds
498// the kernel's answer for the name itself.
499// 0x152 -> 0x153 (taken at land time, as the reservation file's land-time
500// numbering rule gives a ready change HIGHEST_SHIPPED + 1): `minimal` is the
501// default /proc level, and a recording stores its level (`record start
502// --proc`), which its replay runs at
503// (https://github.com/rrnewton/hermit/issues/4266). An older recording ran at
504// `standard` without saying so, and would replay at `minimal`, where the
505// standard level's names are missing.
506// 0x153 -> 0x154 (taken at land time, as the reservation file's land-time
507// numbering rule gives a ready change HIGHEST_SHIPPED + 1): a call outside the
508// schedule that begins while a signal the scheduler sent, and every thread
509// blocked, waits in the shared queue, and that the call's mask admits and the
510// process catches, is armed for the release barrier at its grant and rejoins
511// at a fixed point (https://github.com/rrnewton/hermit/issues/4234). Under
512// record it was readmitted through the rejoin log instead, so an older
513// recording would replay with that call rejoining at another turn.
514// 0x154 -> 0x155 (taken at land time, as the reservation file's land-time
515// numbering rule gives a ready change HIGHEST_SHIPPED + 1): a recording can carry the x86-64-v3 CPU profile and the
516// host xstate facts its guest's CPUID leaf 0xD and XCOMP answers came from
517// (https://github.com/rrnewton/hermit/issues/4243). An older replayer would
518// ignore them and replay a v3 recording against the v2 table, so its guest
519// would see a different CPU.
520// 0x155 -> 0x156: a call that looks up a name in a process's or a thread's
521// directory outside the --proc level (limits, wchan, mem, attr, ...) is
522// issued with that name replaced by one procfs cannot hold, as for the
523// system-wide names at 0x152 (https://github.com/rrnewton/hermit/issues/4266).
524// An older recording holds the kernel's answer for the name itself.
525// 0x156 -> 0x157 (taken at land time, as the reservation file's land-time
526// numbering rule gives a ready change HIGHEST_SHIPPED + 1): a standard
527// signal Linux discards when it is sent (ignored, default-ignored with no
528// handler, or uncaught to a PID namespace's init) no longer ends a futex wait,
529// a sleep, a waitid, or an rt_sigtimedwait or signalfd take once a handler is
530// installed, though ptrace still queues its copy; the scheduler records each
531// send's fate in its turn (https://github.com/rrnewton/hermit/issues/4293).
532// Replay recomputes these results, so an older recording of such a send would
533// replay with the wait ended early: a successful replay with different output.
534// 0x157 -> 0x158 (taken at land time, as the reservation file's land-time
535// numbering rule gives a ready change HIGHEST_SHIPPED + 1): Detcore answers
536// readlink of the /proc links it knows the targets of (self, thread-self,
537// mounts, net, the caller's namespace links) without a system call, and reads
538// the caller's descriptor link spelled through its thread as
539// /proc/thread-self/fd/N (https://github.com/rrnewton/hermit/issues/4266). An
540// older recording holds a kernel readlink of the guest's own spelling for each
541// of those.
542// 0x158 -> 0x159 (taken at land time, as the reservation file's land-time
543// numbering rule gives a ready change HIGHEST_SHIPPED + 1): /proc/<pid>/cgroup
544// is a procfs file whose every hierarchy reads as for a process in the root
545// cgroup (https://github.com/rrnewton/hermit/issues/4378), where it was an
546// unclassified open that recorded a readlink of its descriptor and the host's
547// bytes. Replay serves a classified procfs file itself, so an older recording
548// of a read of one would replay with different events.
549// 0x159 -> 0x15a (taken at land time, as the reservation file's land-time
550// numbering rule gives a ready change HIGHEST_SHIPPED + 1; reserved as
551// 0x15a): /proc/loadavg is a fixed procfs file, whose bytes come from
552// Detcore's table with no kernel read, also under record/replay
553// (https://github.com/rrnewton/hermit/issues/4213). An older recording holds
554// the captured kernel read, which a new replay never takes, so it would go
555// out of step.
556// 0x15a -> 0x15b (taken at land time, as the reservation file's land-time
557// numbering rule gives a ready change HIGHEST_SHIPPED + 1): a pause or
558// nanosleep that a child-exit SIGCHLD wakes, in a process whose send window is
559// unusable, claims the signal in record and replay as it does in a plain run,
560// unless another thread outside the schedule could take it
561// (https://github.com/rrnewton/hermit/issues/4343). An older recording of such
562// an exit holds the sleep returning 0 or never waking, and would replay
563// against a schedule in which it returns EINTR.
564// 0x15b -> 0x15c (taken at land time, as the reservation file's land-time
565// numbering rule gives a ready change HIGHEST_SHIPPED + 1): under record and
566// replay a background read whose descriptor number a later close,
567// close_range, dup2 or dup3 replaces before the read enters the kernel, or
568// which was not open at its grant, keeps a woken sleep from claiming its
569// SIGCHLD (https://github.com/rrnewton/hermit/issues/4410). An older
570// recording of such a sleep holds the claim, which a new replay would not make.
571// 0x15c -> 0x15d (taken at land time, as the reservation file's land-time
572// numbering rule gives a ready change HIGHEST_SHIPPED + 1; reserved as
573// 0x15d): /proc/uptime is a procfs file Detcore renders
574// (https://github.com/rrnewton/hermit/issues/4213), and a /proc/uptime the
575// kernel opened (under record and replay, after chroot) now takes Detcore's
576// rendered-file path for readv, preadv, preadv2 and lseek. A vectored read of
577// it records a capture where an older binary refused it with ENOSYS, so an
578// older recording containing one would replay out of step.
579// 0x15d -> 0x15e (taken at land time, as the reservation file's land-time
580// numbering rule gives a ready change HIGHEST_SHIPPED + 1; reserved as
581// 0x15e): clone3 with set_tid asking for an id a task of the run had, other
582// than the pid of the caller's own zombie child, is refused
583// (https://github.com/rrnewton/hermit/issues/4370). An older recording in
584// which such a clone3 succeeded would replay to the refusal.
585// 0x15e -> 0x15f (taken at land time; https://github.com/rrnewton/hermit/issues/4273,
586// R2b-1): pipe read/write/writev woken by a kept signal decided in the sender's
587// turn. REVERTED by 0x160.
588// 0x15f -> 0x160 (taken at land time, HIGHEST_SHIPPED + 1): the 0x15f change is
589// reverted, because its writev turn rule ended an atomic writev to a full pipe
590// with a short count with no signal involved (writev_determinism's atomic
591// full-pipe case: 4096 of 4096+ written, Linux blocks and completes it). Pipe
592// calls woken by signals decide as they did at 0x15e; a 0x15f recording of
593// such a wake would replay to another result, so the version moves forward.
594// 0x160 -> 0x161 (taken at land time, HIGHEST_SHIPPED + 1; reserved as
595// 0x160): a pidfd from clone(CLONE_PIDFD) or clone3's pidfd field is tracked
596// as pidfd_open's is (https://github.com/rrnewton/hermit/issues/4430), so
597// pidfd_send_signal through it is forwarded and recorded where Detcore
598// answered EBADF without recording anything. An older recording holding such
599// a send would replay out of step.
600// 0x161 -> 0x162 (taken at land time, HIGHEST_SHIPPED + 1; reserved as
601// 0x163): Detcore sends a parent-death signal (PR_SET_PDEATHSIG with a
602// standard signal other than SIGKILL, SIGSTOP and SIGALRM) itself when the
603// parent thread leaves, and arms the kernel with 0
604// (https://github.com/rrnewton/hermit/issues/4440). An older recording of
605// such a prctl or of the kernel's signal would replay out of step.
606// 0x162 -> 0x163 (taken at land time, as the reservation file's land-time
607// numbering rule gives a ready change HIGHEST_SHIPPED + 1; reserved as
608// 0x15f): /proc/stat is a procfs file Detcore renders
609// (https://github.com/rrnewton/hermit/issues/4213), and a /proc/stat the
610// kernel opened (under record and replay, after chroot) now takes Detcore's
611// rendered-file path for readv, preadv, preadv2 and lseek, as /proc/uptime
612// did at 0x15d. A vectored read of it records a capture where an older binary
613// refused it with ENOSYS, so an older recording containing one would replay
614// out of step; and every read of it shows Detcore's layout, not the host's
615// lines, so an older recording's guest saw other bytes.
616// 0x163 -> 0x164 (taken at land time, HIGHEST_SHIPPED + 1): R2b-1 relanded
617// (https://github.com/rrnewton/hermit/issues/4273): a pipe read, a pipe write
618// or a pipe writev woken by a kept signal ends as Linux's does, and a pipe
619// writev makes its first attempt in the slice of its own entry, so the kernel
620// imports the iovec array the call began with. (The 0x15f writev requested a
621// turn first and then read the array again, so another thread's rewrite in
622// that turn changed the bytes an atomic writev to a full pipe wrote:
623// writev_determinism's atomic full-pipe case.) Replay recomputes these
624// results, so an older recording of such a wake would replay to another
625// result.
626// 0x164 -> 0x165 (taken at land time, as the reservation file's land-time
627// numbering rule gives a ready change HIGHEST_SHIPPED + 1): sendfile records
628// the bytes it copied and where its input stopped, and replay applies them
629// instead of copying again (https://github.com/rrnewton/hermit/issues/4416).
630// An older recording holds sendfile's count alone, which a new replay cannot
631// read as its event.
632// 0x165 -> 0x166 (taken at land time, HIGHEST_SHIPPED + 1): a joiner woken by
633// a thread's CLONE_CHILD_CLEARTID is admitted at the next run-queue drain, in
634// DetTid order with the other admissions, where it was pushed at once
635// (https://github.com/rrnewton/hermit/issues/4362, part 2 step 0b). That moves
636// it behind a lower-DetTid admission already pending, which turn selection,
637// unrecorded, recomputes: an older recording could replay another order
638// (Codex review of https://github.com/rrnewton/hermit/pull/4453, P2).
639// 0x166 -> 0x167 (HIGHEST_SHIPPED + 1): a descriptor opened through one of
640// the guest's own descriptor links (/proc/self/fd/N, /dev/fd/N and their
641// thread and numeric spellings) records the path of descriptor N, not the
642// link's spelling, so a name relative to it is looked up below that file as
643// Linux looks it up (https://github.com/rrnewton/hermit/issues/4213,
644// https://github.com/rrnewton/hermit/issues/4485). Under record and replay
645// such a name is now classified, and checked against the --proc level, as
646// the procfs file it is (a name outside the level is answered as absent
647// before any open is recorded, where the alias spelling let the open reach
648// the kernel), so an older recording with such an open would replay out of
649// step.
650// 0x167 -> 0x168 (taken at land time, HIGHEST_SHIPPED + 1): under record and
651// replay, a POSIX timer's expiry is sent as an SI_TIMER record carrying the
652// timer's id and si_value (rt_sigqueueinfo, rt_tgsigqueueinfo), not a bare
653// kill or tgkill, so exec's own flush_itimer_signals removes it, and the
654// scheduler no longer takes timer copies back at exec
655// (https://github.com/rrnewton/hermit/issues/4262). An older recording's
656// guest saw si_code 0 and no si_value for such a signal, and a replay would
657// show it SI_TIMER.
658// 0x168 -> 0x169 (reserved as 0x169): /proc/cpuinfo is a procfs file Detcore
659// renders where it answers the guest's CPUID instruction from its own table
660// (CPUID virtualized and intercepted;
661// https://github.com/rrnewton/hermit/issues/4213), and there a /proc/cpuinfo
662// the kernel opened (under record and replay, after chroot) takes Detcore's
663// rendered-file path for readv, preadv, preadv2 and lseek, as /proc/stat did
664// at 0x163. A vectored read of it records a capture where an older binary
665// refused it with ENOSYS, so an older recording containing one would replay
666// out of step.
667// 0x169 -> 0x16a (reserved as 0x16a): a relative name below a directory
668// descriptor Detcore has no record of (one received over SCM_RIGHTS on
669// ptrace or KVM) is the kernel's lookup where Detcore answered EBADF before
670// the recorder (https://github.com/rrnewton/hermit/issues/4213, K1-0), so an
671// openat, stat, access or readlinkat through such a descriptor is now
672// recorded, and an older recording containing one would replay out of step.
673// 0x16a -> 0x16b was K3a (the recorded container mount table), shipped in
674// f8a824cf and reverted in ec169b91. 0x16b -> 0x16c (no format change from
675// 0x16a): this tree has no mount table, so it takes a new label rather than
676// reusing 0x16b. A K3a recording is then refused by the exact-match gate
677// instead of being replayed without its table, and the shipped 0x16b label is
678// not moved backward.
679// 0x16c -> 0x16d: K3a again (https://github.com/rrnewton/hermit/issues/4213):
680// a recording carries its container's mount table, and Detcore serves
681// /proc/<pid>/mountinfo and /proc/<pid>/mounts (and /proc/mounts) from it in
682// place of the kernel's bytes. A mounts file is now a procfs file Detcore
683// serves from a snapshot, where it was read through to the kernel, and
684// neither file captures the kernel's bytes, so an older recording that reads
685// one would replay out of step.
686// 0x16d -> 0x16e (number to be set at land): a restartable call on an
687// internal pipe or socket (read, readv, recv*, accept4, the scalar pipe
688// write) blocks every blockable signal before its attempt when a signal is
689// pending (R2b-2 stage 2, https://github.com/rrnewton/hermit/issues/4273), so
690// a pending signal no longer stops the recorded attempt: the attempt's result
691// is recorded where an older recording has the stopped attempt's ERESTARTSYS,
692// and an older recording containing one would replay out of step.
693// 0x16e -> 0x16f: Detcore keeps only the guest's own rows of /proc/locks,
694// matching OFD and flock rows against the locks the guest's own open file
695// descriptions hold, which their fdinfo names. After every successful
696// F_OFD_SETLK or F_OFD_SETLKW, every flock call, and every successful
697// pidfd_getfd, Detcore reads the descriptor's /proc/self/fdinfo through the
698// recorder (an openat, its reads and a close), and replay matches the
699// recorded /proc/locks rows against those recorded bytes. An older recording
700// has no such events there and would replay out of step.
701// 0x16f -> 0x170 was R2b-2 stage 3 (a recorded private SIGCHLD ends gated
702// waits and pipe calls), shipped in 4381d927 and reverted in a6fb8ad0.
703// 0x170 -> 0x171 (no format change from 0x16f): this tree does not end those
704// calls on a recorded private SIGCHLD, so it takes a new label rather than
705// reusing 0x170. A stage 3 recording is then refused by the exact-match gate
706// instead of being replayed to other results, and the shipped 0x170 label is
707// not moved backward.
708// 0x171 -> 0x172: replay recomputes which /proc/locks rows are the guest's
709// from the recorded raw rows and fdinfo reads, and that decision changed
710// after 0x16f's format shipped: the descriptor-table ledger (d8117115) stops
711// a description counting when no guest table references it and hides what it
712// cannot follow, an fdinfo that cannot be opened or read no longer stops the
713// run at the lock call but at the next /proc/locks read while that
714// description is still referenced, and the fdinfo is read through
715// /proc/thread-self. A 0x16f or 0x171 recording would replay different guest
716// bytes, or refuse where it recorded a run that went on, so it is refused
717// here instead.
718// 0x172 -> 0x173 (R2b-2 stage 3 re-landed; 0x170's format under a new
719// label): a `SIGCHLD` a guest sent a thread, which the schedule recorded in
720// its private queue, ends that thread's gated wait (poll, ppoll, epoll_wait,
721// epoll_pwait, rt_sigtimedwait) and its restartable pipe read, readv or
722// scalar write, as on Linux, where it was held until the call returned
723// (https://github.com/rrnewton/hermit/issues/4273). Such a turn also blocks
724// signals in place before its timerfd publication. The recorded decisions
725// change, so an older recording containing one would replay out of step.
726// 0x173 -> 0x176 (reserved as 0x176; 0x174 and 0x175 are reserved by other
727// lanes, and whichever lands later takes HIGHEST_SHIPPED + 1): Detcore reads
728// a description's fdinfo after a lock call only through an anchor proved to
729// be procfs. The recorded sequence is now an openat of the guest's /proc
730// (O_PATH), its fstatfs and fstat, an openat2 of thread-self/fdinfo/<fd>
731// beneath it (RESOLVE_BENEATH | RESOLVE_NO_XDEV), that entry's fstat, the
732// reads and two closes, where an older recording holds one openat of
733// /proc/thread-self/fdinfo/<fd>, so it would replay out of step.
734// 0x176 -> 0x177 (reserved as 0x175; taken at land time as HIGHEST_SHIPPED + 1
735// after 0x176 shipped first): record/replay retains ptrace's actual
736// timer-delivery capability, so its stored Config and preemption.timer_enabled
737// summary report the supported mechanism. The shared timer gate preserves
738// capable ptrace arming and logical expiry, while timerless configurations no
739// longer issue physical arms/rearms or manufacture skid state. This
740// observable-behavior correction takes a new label even though the event
741// layout is unchanged.
742// 0x177 -> 0x178 (reserved as 0x178; 0x177 is the dbt lane's, landed as
743// 9231425b): the procfs anchor's fix-forward also runs fstatfs on the opened
744// fdinfo entry (it must be procfs too, so a sibling's dup2 over the anchor's
745// descriptor number cannot pass) and closes the entry on every path, so the
746// recorded sequence after each lock call is now the anchor's openat, fstatfs
747// and fstat, the openat2, the entry's fstatfs and fstat, the reads and two
748// closes. A 0x176 or 0x177 recording has no entry fstatfs and would replay
749// out of step.
750// 0x178 -> 0x179 (reserved as queue hint 0x17b in minibeads dh-gxmvo; takes
751// HIGHEST_SHIPPED + 1 at land): a vfork parent resumes at its child's
752// successful exec or exit, as Linux resumes it (`vfork(2)`), instead of once
753// nothing else can run, and record logs that readmission in the rejoin log at
754// step 2a. Every recording of a vfork now holds a different schedule, so an
755// older one would replay out of step at its first vfork.
756// 0x179 -> 0x17a (taken at land time as HIGHEST_SHIPPED + 1; reserved as
757// 0x17a): fresh-image DBT Tool start and postexec now run before the first
758// application instruction with its actual entry PC and live SP. Earlier images
759// admitted those hooks at their first syscall, after guest instructions had
760// executed. The lifecycle timing/context is observable even with an unchanged
761// wire layout, so recordings require a new behavior version.
762// 0x17a -> 0x17b: the vfork rejoin (0x179, 7ff8743b) is reverted, because it
763// broke record/replay of make -jN (minibeads dh-ojrf): a vfork parent is again
764// held until nothing else can run, and the rejoin log entries are gone. A
765// recording made with the rejoin is refused by the exact-match gate.
766// 0x17b -> 0x17c (takes HIGHEST_SHIPPED + 1 at land): a thread readmitted from
767// a blocking pool whose call a signal ended keeps that call's temporary mask
768// until its continuation turn (`continuation_masks`), instead of the kernel's
769// SigBlk, which replay does not share with record (minibeads dh-ojrf). Replay
770// now chooses the SIGCHLD taker, a timer signal's fate and a discard as record
771// did, so an older recording could replay a different order there.
772// 0x17c -> 0x17d (takes HIGHEST_SHIPPED + 1 at land): the vfork rejoin
773// re-lands on the continuation-mask fix: a vfork parent again resumes at its
774// child's successful exec or exit (`vfork(2)`), with the readmission logged at
775// step 2a, so every recording of a vfork holds a different schedule.
776// 0x17d -> 0x17e (taken at land time, HIGHEST_SHIPPED + 1): a signal that stops
777// a call's first injection (its mask or action probe) before the probe runs now
778// makes the call restart after delivery (-ERESTARTNOINTR), as if the signal came
779// first (https://github.com/rrnewton/hermit/issues/4526), where the call failed
780// with the probe's -ERESTARTSYS. A recording made before this holds that EINTR
781// and would replay out of step.
782// 0x17e -> 0x17f (taken at land time, HIGHEST_SHIPPED + 1): the remaining
783// first-probe sites restart too (masked epoll_pwait2, zero-timeout masked ppoll,
784// record/replay pselect6's entry read), and masked ppoll, epoll_pwait2 and
785// rt_sigtimedwait read the virtual clock after their first probe instead of
786// before it (https://github.com/rrnewton/hermit/issues/4526). A recording made
787// before this can hold the not-run probe's EINTR at those entries.
788// 0x17f -> 0x180 (HIGHEST_SHIPPED + 1 when it lands): a signal delivery that
789// continues the thread's previous handler (no resumption by the scheduler in
790// between) runs before the other threads under record and replay too
791// (https://github.com/rrnewton/hermit/issues/4505). A recording whose guest
792// is signaled again before a handler returns now has a different schedule,
793// and an older recording would replay out of step.
794// 0x180 -> 0x181 (reserved in minibeads dh-gxmvo as 0x17e; 0x17e, 0x17f and
795// 0x180 were taken at land by c4de94bb, 4de8f523 and 1e5ebe34, so this takes
796// HIGHEST_SHIPPED + 1): after the read whose length shows it reached the end
797// of the kernel's lock list, the /proc/locks capture drops only the records
798// whose granted lock is one already captured (the rows the kernel prints
799// again, renumbered, when the host adds a lock between two reads, whatever
800// their waiters) and keeps every other record; main 50727c68 dropped all the
801// bytes after that read, at 0x17d. The recorded calls and their layout are
802// unchanged, but replay recomputes the guest's bytes from the recorded
803// reads, as for the /proc/locks filter at 0x171 -> 0x172 above: a 0x17d,
804// 0x17e, 0x17f or 0x180 recording (0x17d from before or after 50727c68)
805// whose capture caught the race would replay different guest bytes, so it
806// is refused here.
807// 0x181 -> 0x182 (coordinator-assigned HIGHEST_SHIPPED + 1): private DBT
808// client/runtime images now stay inside DynamoRIO's reserved arena instead
809// of consuming application address space. Guest mappings can therefore use
810// different addresses from an older runtime even though the record schema
811// is unchanged; refuse old recordings rather than replay different values.
812// 0x182 -> 0x183 (provisional dh-eowbq reservation in dh-gxmvo; coordinator
813// rederives HIGHEST_SHIPPED + 1 at landing): authenticated DynamoRIO bootstrap
814// PT_LOAD pages are runtime-private in the guest's maps view. The ownership
815// snapshot/task-state layout grows, which can also shift later mappings.
816// Replaying older recordings could produce different bytes or addresses.
817// 0x183 -> 0x184 (provisional dh-ldymk reservation in dh-gxmvo; coordinator
818// rederives at landing): project existing runtime-private ownership from whole
819// smaps sections and refuse unsupported mixed-VMA accounting.
820// 0x184 -> 0x185 (provisional: 0x185 is HIGHEST_SHIPPED + 1 at this base; the
821// lander re-checks HIGHEST_SHIPPED + 1 at land): Detcore classifies a random
822// device by its device number, character 1:8 or 1:9, whatever spelling reached
823// it, and under record and replay takes that number from an `fstat` recorded
824// after every open that is not O_PATH. An older recording read an alternate
825// spelling (/dev//urandom, a symbolic link, /proc/self/fd/N) as recorded kernel
826// reads and has no such `fstat`, while this replayer answers those reads from
827// the seeded stream and expects the `fstat`; replaying it would print other
828// bytes and fall out of step, so it is refused here
829// (https://github.com/rrnewton/hermit/issues/4544).
830// 0x185 -> 0x186 (reserved dh-8abwk / dh-gxmvo-c4f85c08835; coordinator
831// confirms landing order): coherent runtime-private NUMA projection changes
832// guest procfs bytes and refuses unsupported observation domains. ABI15 and
833// private capture storage can also shift runtime layout. Older recordings must
834// not replay with different values.
835// 0x186 -> 0x187: a recording stores the schedule it ran under (`record start
836// --chaos --seed=N`), and replay follows it. An older reader would replay a
837// chaos recording under the default schedule and fall out of step, so it is
838// refused here.
839// 0x187 -> 0x188 (allocated at land time as HIGHEST_SHIPPED + 1, from the
840// branch's provisional 0x18b): a send or receive with MSG_DONTWAIT on a socketpair end the guest
841// left blocking runs once, as on Linux, where Detcore polled it until it
842// stopped failing with EAGAIN. A recording of such a call that polled until
843// another thread wrote holds those retries and the data, while this build
844// returns the first EAGAIN to the guest and falls out of step. One that
845// succeeded at once also took a polling turn that this build no longer
846// takes, so its schedule differs. Both are refused here.
847// 0x188 -> 0x189 (renumbered at land time from the branch's 0x18e, the next
848// HIGHEST_SHIPPED + 1 in this train): the recorder stores a
849// poll's raw ERESTARTSYS, and this change maps a recorded ERESTARTSYS
850// to ERESTARTNOHAND instead of rerunning the poll, so an old recording
851// can replay a different result.
852// 0x189 -> 0x18a (ledger-assigned for this change): each open file description
853// of a random device reads a stream of its own, named by its opener and its
854// table's count of random-device opens, where every one read the run seed's
855// single stream from its start. An older recording's guest read the old
856// bytes, while this replayer serves the new ones and falls out of step, so it
857// is refused here.
858// 0x18a -> 0x18b (ledger-assigned for this change): an epoll_wait, or an
859// epoll_pwait or epoll_pwait2 without a mask, that Detcore runs in the kernel
860// now waits with the signals the guest ignores blocked, in record and in replay
861// (`IgnoredSignalShield`), so such a signal no longer ends it with EINTR. The
862// scheduler reads that mask when it commits the wait, so an older recording,
863// made under the guest's own mask, could choose another SIGCHLD taker or
864// readmission point in replay; it is refused here.
865// 0x18b -> 0x18c (0x18c reserved for the 4681 change by coord3): a `poll`,
866// `select`, or `ppoll` or `pselect6` naming no mask, that Detcore runs in
867// the kernel, now waits with the signals the guest ignores blocked, in
868// record and in replay, so such a signal no longer ends it with a restart
869// code. An older recording holds the restarts and was made under the
870// guest's own mask; it is refused here.
871// 0x18c -> 0x18f (ledger-assigned for this change; 0x18d is 4676's and 0x18e
872// is reassigned by the ledger): a descriptor received over SCM_RIGHTS joins
873// the open file its send modeled, named without asking the kernel, instead of
874// getting a description of its own. An older recording's table holds other
875// descriptions for those descriptors, so it is refused here.
876// 0x18f -> 0x190 (ledger-assigned for this change in landing order; its
877// reserved 0x18d could not be taken after 0x18f shipped): at a fatal
878// signal's delivery Detcore now retires the process's other threads in that
879// turn, where it left them to the host. An older recording's schedule has
880// those threads running on, so it is refused here.
881// 0x191 -> 0x193 (ledger-assigned 0x193 for this change; 0x192 is reserved
882// for PMU-H): every /proc status file Detcore renders now shows TracerPid 0,
883// by any path to the task, where it showed 1 on a task's own paths
884// (/proc/self, /proc/thread-self). Detcore renders the status file over the
885// recorded procfs bytes during recording and replay, so an older stream
886// replayed under the new rendering would show the guest 0 where it read 1,
887// and a guest that branches on the value, as a death test that looks for a
888// debugger does, would diverge. It is refused here.
889// 0x193 -> 0x195 (ledger-assigned 0x195 for this change; 0x194 is reserved
890// for PMU-H): the kernel's restart of a poll a signal ended in the kernel is
891// the guest's own restart_syscall, run in place and recorded with the poll's
892// outputs, where it was a poll Detcore injected in its stead. A recording
893// holds restart_syscall at that point instead of poll, so an older replayer
894// would refuse it as a desync, and this one would refuse an older recording
895// the same way; the exact-match gate names the cause instead.
896// 0x195 -> 0x196 (ledger-assigned 0x196 for this change): a run without a
897// PID namespace of its own (--no-namespace) can number its guest's process
898// and thread IDs from a chosen base, and the preemption record carries that
899// numbering so a replay under another one is refused. A recording made
900// before the numbering existed names no guest IDs, so an older replayer
901// would key its threads differently and diverge; it is refused here.
902// 0x196 -> 0x197 (coordinator-assigned for the combined guest-poll change):
903// Unix connections completed outside their turn, or normalized from EISCONN,
904// no longer establish a trustworthy pairing from the address read afterward.
905// Losing those pairings also changes binary record/replay: sends through a
906// known peer record fstat probes for carried SCM_RIGHTS descriptors, while an
907// unpaired send does not, and received aliases can follow different routing.
908// An older stream can therefore contain a different probe/turn sequence even
909// though the new idle-poller time jumps are excluded from binary record/replay.
910// Refuse that stream before starting the guest rather than desynchronizing it.
911// https://github.com/rrnewton/hermit/pull/4864
912// 0x197 -> 0x198 (HIGHEST_SHIPPED+1 at the change's rebase onto main
913// fd59b85c, under land-time numbering; reserved in dev-hermit
914// ai_docs/hermit-record-version-reservations.md and minibeads dh-gxmvo):
915// Detcore renders a task's statm, io and
916// arch_status, and /proc/diskstats, with no kernel file (the kvm lane's slice
917// N3). Six changes reach recordings:
918// - every spelling of a thread's io (/proc/thread-self/io,
919// /proc/<pid>/task/<tid>/io, a name relative to a task directory) is now
920// classified, so Detcore takes a capture read of the kernel's file where
921// the guest's own read went to the kernel before; an older recording has
922// no such read, and its replay desyncs;
923// - a kernel-opened arch_status under record and replay now reads the
924// virtual CPU's bytes (empty for Hermit's CPU profiles) where it read the
925// recorded kernel line, so the guest's read returns a different count;
926// - a kernel-opened /proc/diskstats now takes Detcore's rendered-file path
927// for readv, preadv, preadv2 and lseek, where readv and preadv were
928// ENOSYS and lseek was injected;
929// - every per-process seq file the kernel opened takes a new capture read
930// at each read or seek that runs show, where later reads were served from
931// the first read's snapshot;
932// - a seek of such a file through a descriptor that cannot read goes to the
933// kernel's file as an absolute seek to Detcore's position, recorded;
934// - a received empty regular file with a link has its descriptor's link
935// read, recorded, to tell whether it is such a file.
936// An older recording that reads any of these is refused here instead of
937// failing mid-replay.
938pub(crate) const RECORD_VERSION: RecordVersion = RecordVersion(0x198);
939
940/// The highest RECORD_VERSION this project has ever shipped.
941///
942/// RECORD_VERSION must never move BACKWARD onto it. This is a COMPILE-TIME
943/// assertion rather than a test on purpose: it is decidable from two constants,
944/// so it should break the build rather than a test run, and it cannot be
945/// skipped, filtered or left unrun.
946///
947/// WHY IT EXISTS AT ALL. The rejection set in `record_version_requires_an_exact_match`
948/// used to be a list of specific versions, and `!compatible_with(0x10a)` was
949/// catching a backward move BY ACCIDENT. Replacing that list with a window derived
950/// from RECORD_VERSION fixes its silent narrowing but cannot catch a regression,
951/// because a derived window re-derives from whatever the constant currently says.
952/// Dropping the list without this floor would have traded a stale check for a
953/// weaker one.
954///
955/// The failure it guards is near, not hypothetical: a long-stale branch that bumped
956/// 0x109 -> 0x10a while main advanced to 0x10e regresses the constant the moment its
957/// conflict is resolved by taking the branch side. The build would then stamp the
958/// current schema with a label an older reader already claims, and the exact-match
959/// gate would accept a stream whose shape it does not know -- the desynchronization
960/// the version exists to prevent.
961///
962/// RAISE THIS IN THE SAME COMMIT THAT RAISES RECORD_VERSION.
963const HIGHEST_SHIPPED_RECORD_VERSION: u32 = 0x198;
964
965const _: () = assert!(
966 RECORD_VERSION.0 >= HIGHEST_SHIPPED_RECORD_VERSION,
967 "RECORD_VERSION is BELOW HIGHEST_SHIPPED_RECORD_VERSION. A recording made by this \
968 build would carry a label an older reader already claims. If this fired during a \
969 rebase, the version constant was resolved BACKWARD -- merge it forward."
970);
971
972/// How a recording schedules guest threads. Replay schedules them the same
973/// way, so a schedule-dependent failure that one chaos seed provokes replays.
974#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
975pub enum RecordedSchedule {
976 /// Detcore's default scheduler.
977 #[default]
978 Default,
979 /// Chaos scheduling, as `hermit run --chaos` does, driven by `seed`.
980 Chaos {
981 /// Seed of the scheduler's random number generator.
982 seed: u64,
983 },
984}
985
986impl RecordedSchedule {
987 /// Makes a record/replay configuration schedule threads this way. Only the
988 /// scheduler is seeded: the guest's own randomness is recorded, not seeded.
989 pub(crate) fn apply(self, config: &mut detcore::Config) {
990 if let Self::Chaos { seed } = self {
991 config.chaos = true;
992 config.sched_seed = Some(seed);
993 }
994 }
995}
996
997/// Choices `hermit record start` makes that the recording stores in its
998/// `metadata.json` and that its replays honor.
999#[derive(Debug, Clone, Copy, Default)]
1000pub struct RecordOptions {
1001 /// Run the guest with an isolated loopback interface instead of the host
1002 /// network (`record start --network local`).
1003 pub local_networking: bool,
1004 /// How much of `/proc` the guest sees.
1005 pub proc_level: detcore_model::config::ProcLevel,
1006 /// The CPU the guest sees.
1007 pub cpu_profile: detcore_model::cpu_profile::CpuProfile,
1008 /// How the guest's threads are scheduled.
1009 pub schedule: RecordedSchedule,
1010}
1011
1012/// Metadata associated with the recording. This is serialized as a JSON file.
1013#[derive(Debug, Serialize, Deserialize)]
1014pub struct Metadata {
1015 /// The real path to the program.
1016 pub exe: PathBuf,
1017 /// The name of the program.
1018 pub program: String,
1019 /// The first argument passed to the program.
1020 pub arg0: String,
1021 /// Program arguments (not including arg0).
1022 pub args: Vec<String>,
1023 /// The working directory of the program.
1024 pub current_dir: PathBuf,
1025 /// The hostname in the UTS namespace used by the program.
1026 pub hostname: Option<String>,
1027 /// The domainname in the UTS namespace used by the program.
1028 pub domainname: Option<String>,
1029 /// The environment variables used by the program.
1030 pub envs: BTreeMap<String, String>,
1031 /// Hermit record/replay version.
1032 pub version: RecordVersion,
1033 /// Why this recording cannot be replayed faithfully, when the recording
1034 /// run said so (`detcore::detlog::replay_refusal`): replay refuses before
1035 /// starting the guest (<https://github.com/rrnewton/hermit/issues/3994>).
1036 #[serde(default)]
1037 pub replay_refused: Option<String>,
1038 /// Actual guest proc mount mode after setup. Missing in older recordings or
1039 /// when the guest's proc mount could not be inspected; diagnostic only.
1040 #[serde(default)]
1041 pub proc_readonly: Option<bool>,
1042 /// Recording-namespace mount roots proven to be Hermit-owned.
1043 ///
1044 /// Replay consumes the recorder's raw syscall buffers, so it must apply the
1045 /// recording-time raw mount IDs rather than IDs from the fresh replay
1046 /// namespace. Older recordings default to no private-root rewrites.
1047 #[serde(default)]
1048 pub mountinfo_root_rewrites: Vec<MountInfoRootRewrite>,
1049 /// Recording-namespace raw mount IDs in canonical row order.
1050 #[serde(default)]
1051 pub mountinfo_mount_ids: Vec<u64>,
1052 /// Whether `mountinfo_mount_ids` is a producer-owned snapshot, including a
1053 /// valid empty snapshot.
1054 #[serde(default)]
1055 pub mountinfo_mount_ids_captured: bool,
1056 /// Recording-time raw mount IDs in the order Detcore numbered them, which
1057 /// is the order the guest first observed them through mountinfo or fdinfo.
1058 /// Replay numbers each raw ID by its position in this list.
1059 #[serde(default)]
1060 pub mount_id_assignment_order: Vec<u64>,
1061 /// The recording container's mount table, which Detcore renders as the
1062 /// guest's `/proc/<pid>/mountinfo` and `/proc/<pid>/mounts` in place of the
1063 /// kernel's bytes, at recording and again at replay
1064 /// (<https://github.com/rrnewton/hermit/issues/4213>). `None` in a
1065 /// recording made before 0x16d, whose replay keeps the kernel's bytes.
1066 #[serde(default)]
1067 pub container_mount_table: Option<detcore_model::container_mounts::ContainerMountTable>,
1068 /// Whether the recording ran in an isolated network namespace with only
1069 /// loopback (`record start --network local`). `None` means the recorder
1070 /// did not say, which is every recording made before this field existed;
1071 /// those recordings ran on the host network. A replay, served to gdb or
1072 /// not, uses the same network, so syscalls the replayer re-executes see the
1073 /// same interfaces and ports they were recorded against.
1074 #[serde(default)]
1075 pub local_networking: Option<bool>,
1076 /// The `/proc` level the recording ran at (`record start --proc`, or the
1077 /// default where the caller names none). Record and replay both run at
1078 /// it, so a replay sees the names its recording saw. Every recorder
1079 /// writes it; `None` is only a hand-written metadata.json, which takes
1080 /// the default.
1081 #[serde(default)]
1082 pub proc_level: Option<detcore_model::config::ProcLevel>,
1083 /// The CPU the guest saw (<https://github.com/rrnewton/hermit/issues/4243>).
1084 /// Absent means x86-64-v2.
1085 #[serde(default)]
1086 pub cpu_profile: detcore_model::cpu_profile::CpuProfile,
1087 /// Under x86-64-v3, the host facts the guest's leaf 0xD and XCOMP answers
1088 /// came from. Replay refuses a host where any of them differs.
1089 #[serde(default)]
1090 pub cpu_profile_host: Option<detcore_model::cpu_profile::HostXstate>,
1091 /// Under x86-64-v3, the hex SHA-256 of the full CPUID table and the bound
1092 /// host facts (`detcore::virtual_cpuid_digest`). Replay recomputes it.
1093 #[serde(default)]
1094 pub cpu_table_digest: Option<String>,
1095 /// Under x86-64-v3, what the host qualification leaves uncovered
1096 /// (`detcore_model::cpu_profile::V3_RESIDUAL`), so that a reader of the
1097 /// recording sees it.
1098 #[serde(default)]
1099 pub cpu_profile_residual: Option<String>,
1100 /// How the recording scheduled guest threads (`record start --chaos`).
1101 /// Absent means the default scheduler.
1102 #[serde(default)]
1103 pub schedule: RecordedSchedule,
1104}
1105
1106impl Metadata {
1107 /// Creates a new metadata object, populating it with information about a
1108 /// command.
1109 pub fn new(command: &Command) -> Result<Self, Error> {
1110 let exe = command.find_program()?;
1111
1112 let program = command.get_program().to_string_lossy().into_owned();
1113 let arg0 = command.get_arg0().to_string_lossy().into_owned();
1114
1115 let args = command
1116 .get_args()
1117 .map(|arg| arg.to_string_lossy().into_owned())
1118 .collect();
1119
1120 let envs = command
1121 .get_captured_envs()
1122 .into_iter()
1123 .map(|(k, v)| {
1124 (
1125 k.to_string_lossy().into_owned(),
1126 v.to_string_lossy().into_owned(),
1127 )
1128 })
1129 .collect();
1130
1131 let current_dir = command
1132 .get_current_dir()
1133 .map_or_else(|| env::current_dir().unwrap(), ToOwned::to_owned);
1134
1135 let hostname = command
1136 .get_hostname()
1137 .map(|s| s.to_string_lossy().into_owned());
1138 let domainname = command
1139 .get_domainname()
1140 .map(|s| s.to_string_lossy().into_owned());
1141
1142 Ok(Self {
1143 exe,
1144 program,
1145 arg0,
1146 args,
1147 current_dir,
1148 hostname,
1149 domainname,
1150 envs,
1151 replay_refused: None,
1152 version: RECORD_VERSION,
1153 proc_readonly: None,
1154 mountinfo_root_rewrites: Vec::new(),
1155 mountinfo_mount_ids: Vec::new(),
1156 mountinfo_mount_ids_captured: false,
1157 mount_id_assignment_order: Vec::new(),
1158 container_mount_table: None,
1159 local_networking: None,
1160 proc_level: None,
1161 cpu_profile: Default::default(),
1162 cpu_profile_host: None,
1163 cpu_table_digest: None,
1164 cpu_profile_residual: None,
1165 schedule: RecordedSchedule::Default,
1166 })
1167 }
1168
1169 /// Reads the metadata of the recording stored in `dir`.
1170 pub fn load(dir: &Path) -> Result<Self, Error> {
1171 let metadata_path = dir.join(METADATA_NAME);
1172 // Buffered: serde_json reads an unbuffered file one byte per read(2).
1173 serde_json::from_reader(std::io::BufReader::new(
1174 fs::File::open(&metadata_path)
1175 .with_context(|| format!("Failed to open {:?}", metadata_path))?,
1176 ))
1177 .with_context(|| format!("Failed to parse {:?}", metadata_path))
1178 }
1179
1180 /// Constructs a command from the metadata.
1181 pub fn command(&self) -> Command {
1182 // NOTE: We bypass the normal $PATH search here by passing in the
1183 // absolute path to the program directly.
1184 let mut command = Command::new(&self.exe);
1185 command.arg0(&self.arg0);
1186 command.args(&self.args);
1187 command.env_clear();
1188 command.envs(&self.envs);
1189 command.current_dir(&self.current_dir);
1190
1191 if let Some(hostname) = &self.hostname {
1192 command.hostname(hostname);
1193 }
1194
1195 if let Some(domainname) = &self.domainname {
1196 command.domainname(domainname);
1197 }
1198
1199 command
1200 }
1201}
1202
1203pub fn record_or_replay_config(data: &Path) -> detcore::Config {
1204 record_or_replay_config_with_timer_support(data, record_timer_support())
1205}
1206
1207fn record_or_replay_config_with_timer_support(
1208 data: &Path,
1209 timer_support: Result<(), String>,
1210) -> detcore::Config {
1211 // NOTE: Record and replay should use the exact same Detcore configuration.
1212 // Callers add the completed producer namespace's mountinfo order and
1213 // mount-ID assignment order after this common base is built, so replay
1214 // uses recording-time raw IDs rather than IDs from its fresh container.
1215 //
1216 // WHY THIS IS NOT `hermit run --strict`, WRITTEN HERE ON PURPOSE.
1217 //
1218 // `virtualize_time: false` below is DELIBERATE, and the rationale used to live only
1219 // in the separate dev-hermit workspace -- not in this repository, beside the code it
1220 // governs. The cost of that was measured: three agents in one night read this line,
1221 // searched this repo's source, git log, docs/ and issues, found nothing, and could
1222 // not tell a design decision from a determinism bug; two coordinators then spent
1223 // hours treating it as a candidate defect. A decision recorded in a different
1224 // repository from the code it governs is, in practice, undocumented. Hence this
1225 // comment. See rrnewton/hermit#2295.
1226 //
1227 // WHAT RECORD/REPLAY ACTUALLY GUARANTEES. Replay re-executes a recording against the
1228 // recorded syscall data, so what must be reproducible is THIS recording's replay --
1229 // not agreement between two independent recordings. Time is therefore left real: the
1230 // recording captures what the guest actually observed, and replay returns those
1231 // recorded values. `hermit record start --verify` records once, replays that
1232 // recording, and compares the two; its success message says exactly that ("replay
1233 // matched recording") and does not claim more.
1234 //
1235 // WHAT IT DOES NOT GUARANTEE, which is the part that misleads readers. Because time
1236 // is not virtualized, two INDEPENDENT recordings of the same program observe
1237 // different clock values. Demonstrated: `hermit run -- date` twice returns the
1238 // identical virtual epoch, while `hermit record start -- date` twice returns real
1239 // wall-clock times seconds apart. Replay fidelity says nothing about that, and
1240 // the focused `independent_mountinfo_recordings_are_canonical` test compares
1241 // two independent mountinfo recordings after replacing exactly field 3, the
1242 // major:minor device column. With `virtualize_metadata: false`, Linux may
1243 // legitimately allocate a different anonymous-block-device minor for the
1244 // private procfs mount in each container. That test still compares mount ID,
1245 // parent ID, root, mountpoint, options, optional fields, separator,
1246 // filesystem type, source, and superblock options byte-for-byte. It does not
1247 // establish general cross-recording determinism, and a green `--verify`
1248 // remains only a replay-fidelity result.
1249 //
1250 // This configuration differs from `hermit run --strict` on four of the five
1251 // properties that define it (see run.rs: only `sequentialize_threads` matches).
1252 // Any claim that recording is "strict" in that sense is wrong; the `--strict` flag
1253 // on `hermit record` is accepted for command-line compatibility, and its only effect
1254 // is to refuse a host whose retired-branch counter is inexact
1255 // (https://github.com/rrnewton/hermit/issues/3810).
1256 let default_config: detcore::Config = Default::default();
1257 let mut config = detcore::Config {
1258 // Record and replay are determinism claims, so an unsupported syscall
1259 // must invalidate the operation instead of being recorded from or
1260 // replayed against the live host.
1261 panic_on_unsupported_syscalls: true,
1262 // Return a typed error through Reverie rather than unwinding across the
1263 // backend callback. The tracer owns process-tree cleanup on that error.
1264 exit_on_unsupported_syscall: true,
1265 shutdown_on_unsupported_syscall: false,
1266 unsupported_syscall_report_fd: None,
1267 panic_on_rcb_overshoot: false,
1268 sequentialize_threads: true,
1269 runs_post_fork: default_config.runs_post_fork,
1270 // Record/replay keeps a partial Detcore subscription. Complete coverage
1271 // of the Determinized classification begins in v0x10a; madvise policy
1272 // semantics begin in v0x102.
1273 passthru_opt: true,
1274 deterministic_io: false,
1275 virtualize_time: crate::RECORD_REPLAY_VIRTUALIZES_TIME,
1276 virtualize_metadata: false,
1277 mountinfo_root_rewrites: Vec::new(),
1278 mountinfo_device_rewrites: Vec::new(),
1279 mountinfo_mount_ids: Vec::new(),
1280 mountinfo_mount_ids_captured: false,
1281 mountinfo_mount_namespace: None,
1282 mount_id_assignment_order: Vec::new(),
1283 // The recorder and the replayer set the table from the recording.
1284 container_mount_table: None,
1285 // Detcore serves no placeholder directory under record and replay,
1286 // whose containers mount none, and serves no `/proc` directory there.
1287 directory_placeholder: None,
1288 container_procfs: false,
1289 virtualize_cpuid: true,
1290 // The recorder and the replayer set the profile from the command line
1291 // and the recording; see `record_or_replay_config`'s callers.
1292 cpu_profile: default_config.cpu_profile,
1293 cpu_profile_host: None,
1294 cpu_profile_source: None,
1295 // Record and replay run under the ptrace backend.
1296 backend: <reverie_ptrace::PtraceBackend as reverie::Backend>::capabilities(),
1297 // As `hermit run` sets it for ptrace (`prepare_backend_config` in
1298 // lib.rs). With it off, the scheduler did not model signal targets, so
1299 // a thread asleep in a backgrounded rt_sigsuspend that a child-exit
1300 // SIGCHLD woke was not held at the release barrier: the scheduler could
1301 // pop the next timer, or report a terminal deadlock, before the thread's
1302 // continuation posted (https://github.com/rrnewton/hermit/issues/3963).
1303 // A constant, unlike `hermit run`: replay rebuilds this configuration
1304 // and must decide which signals end a wait exactly as the recording did,
1305 // whatever terminal replay itself runs under. `false` keeps Linux's rule
1306 // for the terminal signals a guest sends itself, so a caught SIGINT
1307 // still ends a FUTEX_WAIT or rt_sigtimedwait in every recording; a
1308 // terminal's own signal during an interactive recording comes from
1309 // outside the container, and replay cannot reproduce where it lands.
1310 guest_may_inherit_a_terminal: false,
1311 in_guest_detlog_forward_policy: None,
1312 in_guest_site_patching_off: false,
1313 in_guest_reserved_signals: 0,
1314 in_guest_refuses_host_timed_producers: false,
1315 in_guest_signal_delivery: false,
1316 // Record and replay never replay a priority history.
1317 annotated_priority_replay: false,
1318 // The first image's link stays the kernel's in record and replay.
1319 launch_image: None,
1320 // Not measured for record and replay.
1321 inherited_seccomp: detcore::InheritedSeccomp::Unknown,
1322 has_uts_namespace: true,
1323 // Record and replay print no notice for an abstract AF_UNIX bind,
1324 // `hermit record start --network=host` included; only `hermit run`
1325 // sets it.
1326 notice_host_network_abstract_binds: false,
1327 // A recording runs in a PID namespace of its own.
1328 guest_ids: detcore::GuestIds::Namespace,
1329 standard_stream_aliases: None,
1330 // The path to the directory where syscalls will be recorded.
1331 replay_data: Some(data.to_path_buf()),
1332 clock_multiplier: None,
1333 epoch: default_config.epoch,
1334 gdbserver: false,
1335 gdbserver_port: default_config.gdbserver_port,
1336 kill_daemons: default_config.kill_daemons,
1337 max_timeslice: default_config.max_timeslice,
1338 target_timeslice: default_config.target_timeslice,
1339 target_timeslice_syscalls_only: default_config.target_timeslice_syscalls_only,
1340 futex_wake_yields: default_config.futex_wake_yields,
1341 // Never in record mode, whatever the default: recordings made before
1342 // the option existed, and record mode's own scheduling, have no
1343 // hand-off (https://github.com/rrnewton/hermit/issues/4380).
1344 pipe_wake_latency: None,
1345 // Record and replay charge every expired timed wait, whatever the value.
1346 expired_wait_charge: None,
1347 // Record and replay register queued sleeps when chosen, whatever the value.
1348 // A `hermit run` option; record and replay do not gather it.
1349 work_span_report: None,
1350 scheduler_turn_cost: default_config.scheduler_turn_cost,
1351 // A `hermit run` option, like `pipe_wake_latency`: record mode's own
1352 // scheduling, and recordings made before the option existed, charge
1353 // every turn in full.
1354 cap_preemption_charge: false,
1355 // A `hermit run` option; record and replay take none.
1356 virtual_timeout: None,
1357 seed: default_config.seed,
1358 rng_seed: default_config.rng_seed,
1359 imprecise_timers: false,
1360 // Record and replay make the same timeslice decisions either way: a
1361 // lone thread's skipped stops are processed at the same branch counts.
1362 eager_lone_preemptions: false,
1363 // Off in record and replay, a constant like the rest: replay rebuilds
1364 // this configuration and must decide every signal question as the
1365 // recording did, and recordings made before the copy was on by
1366 // default (A5 step 2d) read the kernel's `/proc` view throughout.
1367 signal_state_copy: false,
1368 // The copy is off, so no delivery is checked.
1369 signal_delivery_check_fatal: false,
1370 chaos: false,
1371 // `hermit record` draws no chaos slices.
1372 chaos_timeslice_range: None,
1373 chaos_timeslice_draw: Default::default(),
1374 chaos_timeslice: None,
1375 sigint_instakill: false,
1376 warn_non_zero_binds: false,
1377 sched_heuristic: Default::default(),
1378 sched_seed: default_config.sched_seed,
1379 network_trace: Default::default(),
1380 recordreplay_modes: true,
1381 replaying: false,
1382 strict_schedule_boundaries: false,
1383 record_preemptions: false,
1384 record_preemptions_to: None,
1385 replay_preemptions_from: None,
1386 replay_schedule_from: None,
1387 replay_exhausted_panic: false,
1388 replay_continue_fairly: false,
1389 die_on_desync: true,
1390 replay_edited_preemptions: false,
1391 stacktrace_event: Vec::new(),
1392 stacktrace_signal: None,
1393 access_trace: None,
1394 access_trace_from: 0,
1395 access_trace_max_records: None,
1396 access_trace_max_bytes: None,
1397 preemption_stacktrace: false,
1398 preemption_stacktrace_log_file: None,
1399 stop_after_turn: None,
1400 stop_after_iter: None,
1401 signal_at: None,
1402 debug_externalize_sockets: false,
1403 debug_futex_mode: BlockingMode::Precise,
1404 sched_sticky_random_param: 0.0,
1405 no_rcb_time: false,
1406 detlog_heap: false,
1407 detlog_stack: false,
1408 detlog_anon: None,
1409 detlog_regs: false,
1410 detlog_io_buffers: crate::RECORD_REPLAY_HASHES_IO_BUFFERS,
1411 detlog_regs_cadence: 1,
1412 sysinfo_uptime_offset: 120,
1413 memory: default_config.memory,
1414 virtual_cpus: default_config.virtual_cpus,
1415 proc_level: default_config.proc_level,
1416 interrupt_at: vec![],
1417 happens_before: None,
1418 record_host_inputs: false,
1419 host_input_log: None,
1420 host_input_snapshots: Vec::new(),
1421 untraced_code_range: None,
1422 fatal_core_capture: None,
1423 fuzz_futexes: false,
1424 chaos_target_races: false,
1425 chaos_per_thread_slowdown: false,
1426 chaos_slowdown_max_factor: 10.0,
1427 chaos_epoch_length_ns: 0,
1428 fuzz_seed: None,
1429 }
1430 .with_backend(|backend| {
1431 backend.supports_blocked_wait_signal_interruption = true;
1432 backend.runs_guest_code_between_injections = false;
1433 });
1434 if let Err(reason) = timer_support {
1435 disable_timeslice_without_a_timer(&mut config, &reason);
1436 }
1437 config
1438}
1439
1440/// Whether this host can arm Reverie's ptrace timer for a record or replay:
1441/// working perf counters AND a PMU profile for its CPU. Without a profile,
1442/// Reverie gives no task a timer, so a timeslice would be configured and never
1443/// delivered.
1444fn record_timer_support() -> Result<(), String> {
1445 timer_support_from(
1446 reverie_ptrace::is_perf_supported(),
1447 reverie_ptrace::host_pmu_profile(),
1448 )
1449}
1450
1451/// [`record_timer_support`] from what the host answered: whether perf works,
1452/// and whether Reverie has a PMU profile for its CPU.
1453fn timer_support_from(
1454 perf_supported: bool,
1455 profile: Result<(), reverie_ptrace::PmuValidationError>,
1456) -> Result<(), String> {
1457 if !perf_supported {
1458 return Err("hardware perf counters are not supported on this machine".to_owned());
1459 }
1460 profile.map_err(|error| error.to_string())
1461}
1462
1463/// Turn off `config`'s timeslice, which this host cannot deliver (`reason`).
1464fn disable_timeslice_without_a_timer(config: &mut detcore::Config, reason: &str) {
1465 if config.max_timeslice.is_some() {
1466 tracing::warn!("{reason}; Records/Replays run without a timeslice and may randomly fail!");
1467 config.max_timeslice = None;
1468 }
1469}
1470
1471#[cfg(test)]
1472mod tests {
1473 use reverie::Tool;
1474 use reverie::syscalls::Sysno;
1475
1476 use super::*;
1477
1478 // A host whose counters work but whose CPU has no PMU profile (Reverie's
1479 // UnsupportedCpu) records without a timeslice, as a host without counters
1480 // does, rather than keeping one no task can be given.
1481 #[test]
1482 fn a_record_without_a_timer_has_no_timeslice() {
1483 let unknown = || reverie_ptrace::PmuValidationError::UnsupportedCpu {
1484 family: 0x06,
1485 model: 0x01,
1486 };
1487 assert_eq!(timer_support_from(true, Ok(())), Ok(()));
1488 let no_profile = timer_support_from(true, Err(unknown())).unwrap_err();
1489 assert!(no_profile.contains("model 0x1"), "{no_profile}");
1490 let no_perf = timer_support_from(false, Err(unknown())).unwrap_err();
1491 assert!(
1492 no_perf.contains("perf counters are not supported"),
1493 "{no_perf}"
1494 );
1495 let supported = record_or_replay_config_with_timer_support(
1496 Path::new("replay-data"),
1497 timer_support_from(true, Ok(())),
1498 );
1499 assert_eq!(
1500 supported.backend.delivers_preemption_timer_events,
1501 <reverie_ptrace::PtraceBackend as reverie::Backend>::capabilities()
1502 .delivers_preemption_timer_events,
1503 "record and replay use ptrace's timer mechanism"
1504 );
1505 assert_eq!(
1506 supported.max_timeslice,
1507 detcore::Config::default().max_timeslice
1508 );
1509 assert!(supported.use_preemption_timer());
1510 assert!(!supported.chaos);
1511 for reason in [no_perf, no_profile] {
1512 let unavailable =
1513 record_or_replay_config_with_timer_support(Path::new("replay-data"), Err(reason));
1514 assert_eq!(unavailable.max_timeslice, None);
1515 assert!(!unavailable.use_preemption_timer());
1516 assert_eq!(unavailable.backend, supported.backend);
1517 }
1518 let mut config = detcore::Config::default();
1519 assert!(config.max_timeslice.is_some());
1520 disable_timeslice_without_a_timer(
1521 &mut config,
1522 "Unsupported CPU family 0x6, model 0x1: Reverie has no performance-counter profile for it",
1523 );
1524 assert_eq!(config.max_timeslice, None);
1525 // Already off: stays off.
1526 disable_timeslice_without_a_timer(&mut config, "again");
1527 assert_eq!(config.max_timeslice, None);
1528 }
1529
1530 #[test]
1531 fn pre_yield_readmission_recordings_are_refused_by_the_version_gate() {
1532 // A 0x132 recording never readmitted a background call while a
1533 // yielder was queued, and its replayer cannot follow one that did.
1534 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x132)));
1535 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x131)));
1536 }
1537
1538 #[test]
1539 fn pre_held_sleep_signal_recordings_are_refused_by_the_version_gate() {
1540 // A recording from before this epoch of a blocked or default SIGCHLD
1541 // during a nanosleep holds the EINTR the call used to return, where
1542 // this replayer recomputes 0: 0x143 (the previous epoch), 0x142,
1543 // 0x13e, 0x13d, 0x13c, 0x139, 0x138 and 0x135.
1544 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x143)));
1545 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x142)));
1546 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x13e)));
1547 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x13d)));
1548 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x13c)));
1549 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x139)));
1550 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x138)));
1551 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x135)));
1552 }
1553
1554 #[test]
1555 fn pre_tight_stack_ppoll_recordings_are_refused_by_the_version_gate() {
1556 // A recording from before this epoch of a ppoll on a nearly full stack
1557 // holds its EFAULT and no event, which this replayer does not
1558 // reproduce: 0x133 (the previous epoch) and 0x132.
1559 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x133)));
1560 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x132)));
1561 }
1562
1563 #[test]
1564 fn pre_epoll_pwait2_recordings_are_refused_by_the_version_gate() {
1565 // A recording from before this epoch ran epoll_pwait2 live and holds
1566 // no event for it: 0x136 (the previous epoch), 0x135 and 0x134.
1567 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x136)));
1568 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x135)));
1569 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x134)));
1570 }
1571
1572 #[test]
1573 fn pre_epoll_pwait_recordings_are_refused_by_the_version_gate() {
1574 // A recording from before this epoch ran epoll_pwait live and holds
1575 // no event for it, so this replayer would desynchronize at the first
1576 // one: 0x12f (the previous epoch), 0x12e, 0x12d, 0x12c and 0x12b.
1577 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x12f)));
1578 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x12e)));
1579 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x12d)));
1580 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x12c)));
1581 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x12b)));
1582 }
1583
1584 #[test]
1585 fn record_version_requires_an_exact_match() {
1586 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
1587
1588 // DERIVED FROM THE COMPATIBILITY RULE, NOT ENUMERATED.
1589 //
1590 // This assertion used to be a list of specific versions -- 0x10a, 0x10c,
1591 // 0x105, 0x110. A list SILENTLY NARROWS every time RECORD_VERSION
1592 // advances: the named values drift away from the boundary that matters,
1593 // and the test keeps passing while checking less and less. It cannot
1594 // fail for the reason its name gives once the value has moved past the
1595 // range someone happened to write down.
1596 //
1597 // `compatible_with` is exact equality, so the property to assert is
1598 // "every OTHER version is refused". Re-deriving the cases from
1599 // RECORD_VERSION itself means the window travels with the constant and
1600 // cannot go stale.
1601 let current = RECORD_VERSION.0;
1602 for delta in 1..=16u32 {
1603 let older = RecordVersion(current - delta);
1604 assert!(
1605 !RECORD_VERSION.compatible_with(&older),
1606 "recorder at {current:#x} must refuse a recording made at {:#x}",
1607 older.0
1608 );
1609 let newer = RecordVersion(current + delta);
1610 assert!(
1611 !RECORD_VERSION.compatible_with(&newer),
1612 "recorder at {current:#x} must refuse a recording made at {:#x}",
1613 newer.0
1614 );
1615 }
1616 }
1617
1618 #[test]
1619 fn record_and_replay_preserve_partial_subscriptions_and_fail_closed() {
1620 let config = record_or_replay_config(Path::new("replay-data"));
1621 assert!(config.passthru_opt);
1622 assert!(config.panic_on_unsupported_syscalls);
1623 assert!(config.exit_on_unsupported_syscall);
1624 assert!(!config.shutdown_on_unsupported_syscall);
1625 }
1626
1627 /// A recording, its autopilot replay and its gdb-served replay take the
1628 /// same signal continuations only if none of them reads the backend's
1629 /// `rt_sigreturn` reports, which a gdbserver turns off
1630 /// (https://github.com/rrnewton/hermit/pull/4510).
1631 #[test]
1632 fn record_and_replay_never_read_sigreturn_reports() {
1633 let record = record_or_replay_config(Path::new("replay-data"));
1634 let mut replay = record.clone();
1635 replay.replaying = true;
1636 let mut gdb_replay = replay.clone();
1637 gdb_replay.gdbserver = true;
1638 for (name, config) in [("record", record), ("replay", replay), ("gdb", gdb_replay)] {
1639 assert!(!detcore::observes_sigreturns(&config), "{name}");
1640 }
1641 }
1642
1643 /// Record mode never hands the turn to a readied pipe poller, whatever
1644 /// the default (https://github.com/rrnewton/hermit/issues/4380).
1645 #[test]
1646 fn record_and_replay_never_apply_the_pipe_wake_latency() {
1647 assert_eq!(
1648 record_or_replay_config(Path::new("replay-data")).pipe_wake_latency,
1649 None
1650 );
1651 }
1652
1653 #[test]
1654 fn record_and_replay_use_run_default_memory() {
1655 let run_default = detcore::Config::default();
1656 assert_eq!(run_default.memory, 1_000_000_000);
1657 assert_eq!(
1658 record_or_replay_config(Path::new("replay-data")).memory,
1659 run_default.memory
1660 );
1661 }
1662
1663 /// RECORDING DOES NOT VIRTUALIZE TIME, AND THE VERDICT NOW SAYS SO.
1664 ///
1665 /// `virtualize_time: false` here is deliberate (see the rationale block on
1666 /// `record_or_replay_config`), and it is what makes a green
1667 /// `record start --verify` mean something weaker than a green
1668 /// `run --verify`: the replay reproduced *that recording*, not that the guest
1669 /// is deterministic across invocations. Ported from the residual of
1670 /// hermit#2269.
1671 ///
1672 /// Pinned against the shared constant rather than a literal, because the
1673 /// value is now read in two places — the config the run uses, and the
1674 /// `ComparisonOptions` that discloses it in the report. If those drifted, the
1675 /// report would describe a time policy the run did not use, which is the very
1676 /// defect the disclosure exists to close.
1677 #[test]
1678 fn recording_does_not_virtualize_time_as_documented() {
1679 let config = record_or_replay_config(Path::new("replay-data"));
1680 assert!(
1681 !config.virtualize_time,
1682 "record/replay must not virtualize time; a green replay verdict would \
1683 otherwise be mistaken for a determinism result"
1684 );
1685 assert_eq!(
1686 config.virtualize_time,
1687 crate::RECORD_REPLAY_VIRTUALIZES_TIME,
1688 "the config the run uses and the constant the report discloses must be \
1689 the same decision, not two that happen to agree"
1690 );
1691 }
1692
1693 #[test]
1694 fn record_and_replay_subscribe_every_determinized_syscall() {
1695 let config = record_or_replay_config(Path::new("replay-data"));
1696 let record = <detcore::Detcore<hermit_record::Recorder> as Tool>::subscriptions(&config);
1697 let replay = <detcore::Detcore<hermit_replay::Replayer> as Tool>::subscriptions(&config);
1698
1699 for (phase, subscriptions) in [("record", record), ("replay", replay)] {
1700 let delivered = subscriptions.iter_syscalls().collect::<Vec<_>>();
1701 let missing = detcore::all_pinned_syscalls()
1702 .filter(|sysno| detcore::is_determinized_syscall(*sysno))
1703 .filter(|sysno| !delivered.contains(sysno))
1704 .collect::<Vec<_>>();
1705 assert!(
1706 missing.is_empty(),
1707 "{phase} lets Determinized syscalls bypass Detcore: {}",
1708 missing
1709 .iter()
1710 .map(|sysno| sysno.to_string())
1711 .collect::<Vec<_>>()
1712 .join(" ")
1713 );
1714 assert!(
1715 delivered.contains(&Sysno::syslog),
1716 "{phase} must deliver syslog to its deterministic Detcore handler"
1717 );
1718 // chdir is PassThrough for Detcore but its result depends on the
1719 // replay chroot, so record/replay subscribes it (hermit#3598);
1720 // umask, from the same PassThrough group, stays the sentinel that
1721 // record/replay does not subscribe everything.
1722 assert!(
1723 delivered.contains(&Sysno::chdir) && delivered.contains(&Sysno::getcwd),
1724 "{phase} must deliver chdir and getcwd so replay uses recorded results"
1725 );
1726 // A replayed descriptor may be a placeholder, so these must
1727 // return recorded results rather than run
1728 // (https://github.com/rrnewton/hermit/issues/3591).
1729 for sysno in [
1730 Sysno::fsync,
1731 Sysno::fdatasync,
1732 Sysno::syncfs,
1733 Sysno::fchmod,
1734 Sysno::fchown,
1735 Sysno::fsetxattr,
1736 Sysno::fremovexattr,
1737 Sysno::setxattr,
1738 Sysno::lsetxattr,
1739 Sysno::removexattr,
1740 Sysno::lremovexattr,
1741 Sysno::getxattr,
1742 Sysno::lgetxattr,
1743 Sysno::fgetxattr,
1744 Sysno::listxattr,
1745 Sysno::llistxattr,
1746 Sysno::flistxattr,
1747 ] {
1748 assert!(
1749 delivered.contains(&sysno),
1750 "{phase} must deliver {sysno} so replay uses its recorded result"
1751 );
1752 }
1753 assert!(
1754 !delivered.contains(&Sysno::umask),
1755 "{phase} must leave unlisted PassThrough umask unsubscribed"
1756 );
1757 }
1758 }
1759
1760 // AUTONOMOUS-BOT-IMPLEMENTED
1761 // TODO-HUMAN-REVIEW(#2373)
1762 /// A 0x10b recording predates flock forwarding, so it carries no flock event
1763 /// while this replayer expects one per call. Replaying it would consume some
1764 /// other event and desynchronize; the version gate must refuse it instead.
1765 #[test]
1766 fn record_version_rejects_pre_flock_streams() {
1767 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x10b)));
1768 }
1769
1770 /// A 0x122 recording's procfs inode numbers came from the old pool, which
1771 /// started at 1 and counted only new host inodes. Replay re-renders them
1772 /// from the recorded raw bytes with the current pool, so the version gate
1773 /// must refuse the recording rather than show its guest other numbers.
1774 #[test]
1775 fn record_version_rejects_pre_request_ordinal_inode_streams() {
1776 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x122)));
1777 }
1778
1779 /// A 0x123 recording has no events for accept, accept4, bind, listen or
1780 /// shutdown, which this replayer serves from the log, so replaying it would
1781 /// consume other calls' events; the version gate must refuse it.
1782 #[test]
1783 fn record_version_rejects_pre_accept_streams() {
1784 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x123)));
1785 }
1786
1787 /// A 0x124 recording ran a blocking accept in the background, with one
1788 /// `Accept` event, while this replayer serves one event per in-turn
1789 /// attempt; the version gate must refuse it.
1790 #[test]
1791 fn record_version_rejects_backgrounded_accept_streams() {
1792 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x124)));
1793 }
1794
1795 /// A 0x125 recording was made while a futex wake took the most recent
1796 /// waiters; replay recomputes every wake, so under the oldest-first order
1797 /// it could run another schedule. The version gate must refuse it.
1798 #[test]
1799 fn record_version_rejects_newest_first_futex_wake_streams() {
1800 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x125)));
1801 }
1802
1803 /// A 0x126 recording was made while a vfork child kept running alone
1804 /// after its exec until the scheduler happened to see its live parent's
1805 /// continuation; replay recomputes turn selection, so under the release
1806 /// edge it could run another schedule. The version gate must refuse it.
1807 #[test]
1808 fn record_version_rejects_pre_vfork_release_edge_streams() {
1809 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x126)));
1810 }
1811
1812 /// A 0x127 recording saw ENOSYS for the subreaper prctls, which this
1813 /// replayer runs live; replaying it could take another branch. The
1814 /// version gate must refuse it.
1815 #[test]
1816 fn record_version_rejects_pre_child_subreaper_streams() {
1817 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x127)));
1818 }
1819
1820 /// A 0x128 recording was made by a scheduler that did not model signal
1821 /// targets in record and replay; replay recomputes where a signalled background thread rejoins
1822 /// and which signals end a wait, so it could run another schedule. The
1823 /// version gate must refuse it.
1824 #[test]
1825 fn record_version_rejects_streams_without_signal_target_modelling() {
1826 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x128)));
1827 }
1828
1829 /// A 0x129 recording held a committed SIGCHLD in a precise FUTEX_WAIT
1830 /// until the call returned; replay recomputes that wait and now ends it
1831 /// with EINTR, so it would take the other branch with no desync. The
1832 /// version gate must refuse it (Codex review of
1833 /// https://github.com/rrnewton/hermit/pull/4039).
1834 #[test]
1835 fn record_version_rejects_streams_that_held_a_committed_sigchld_in_a_futex_wait() {
1836 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x129)));
1837 }
1838
1839 /// A 0x12a recording gave a raw clone child with a non-SIGCHLD exit
1840 /// signal its creator's process identity; replay recomputes calls that
1841 /// name the process, so it could take another branch. The version gate
1842 /// must refuse it.
1843 #[test]
1844 fn record_version_rejects_streams_from_before_raw_clone_processes() {
1845 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x12a)));
1846 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x129)));
1847 }
1848
1849 #[test]
1850 fn record_version_rejects_streams_that_answered_a_futex_rewait_as_woken() {
1851 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x12b)));
1852 }
1853
1854 #[test]
1855 fn record_version_rejects_streams_that_read_a_futex_wait_word_through_the_tracer() {
1856 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x12c)));
1857 }
1858
1859 #[test]
1860 fn record_version_rejects_streams_whose_pidfd_signals_woke_no_receiver() {
1861 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x12d)));
1862 }
1863
1864 #[test]
1865 fn record_version_rejects_streams_that_answered_a_zombie_kill_with_esrch() {
1866 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x12e)));
1867 }
1868
1869 #[test]
1870 fn record_version_rejects_streams_that_answered_a_zombie_sigqueueinfo_with_esrch() {
1871 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x130)));
1872 }
1873
1874 #[test]
1875 fn record_version_rejects_streams_that_answered_a_thread_id_kill_with_esrch() {
1876 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x131)));
1877 }
1878
1879 #[test]
1880 fn record_version_rejects_streams_that_answered_a_gone_sigqueueinfo_before_its_siginfo() {
1881 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x134)));
1882 }
1883
1884 #[test]
1885 fn record_version_rejects_streams_whose_stop_signals_armed_no_orphaned_group_record() {
1886 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x137)));
1887 }
1888
1889 /// Record and replay run under ptrace with serialized threads, so their
1890 /// scheduler models signal targets (`models_signal_targets` in
1891 /// detcore/src/scheduler.rs), as `hermit run` does on ptrace. Without it,
1892 /// a child-exit SIGCHLD that wakes a thread asleep in a backgrounded
1893 /// rt_sigsuspend does not hold the next pass at the release barrier, and
1894 /// recording `timeout 1 true` reported a false deadlock in 5 to 7 of 12
1895 /// concurrent runs (https://github.com/rrnewton/hermit/issues/3963).
1896 #[test]
1897 fn record_and_replay_model_signal_targets_as_ptrace_run_does() {
1898 let data = tempfile::tempdir().unwrap();
1899 let record = record_or_replay_config(data.path());
1900 let run =
1901 crate::prepare_backend_config(crate::DetConfig::default(), crate::Backend::Ptrace);
1902 assert!(record.sequentialize_threads);
1903 assert!(record.models_signal_targets());
1904 // `run` is a default configuration, whose threads are not serialized
1905 // until the command line asks; its backend facts are ptrace's.
1906 let facts = |config: &crate::DetConfig| {
1907 let backend = &config.backend;
1908 [
1909 (
1910 "supports_blocked_wait_signal_interruption",
1911 backend.supports_blocked_wait_signal_interruption,
1912 ),
1913 (
1914 "kernel_signal_state_is_guests",
1915 backend.kernel_signal_state_is_guests,
1916 ),
1917 (
1918 "kernel_signal_targets_are_predictable",
1919 backend.kernel_signal_targets_are_predictable,
1920 ),
1921 (
1922 "host_task_reads_reach_guest_threads",
1923 backend.host_task_reads_reach_guest_threads,
1924 ),
1925 (
1926 "host_timed_signals_are_physical",
1927 backend.host_timed_signals_are_physical,
1928 ),
1929 ]
1930 };
1931 for ((name, recorded), (_, ran)) in facts(&record).into_iter().zip(facts(&run)) {
1932 assert!(recorded, "{name}");
1933 assert_eq!(recorded, ran, "{name}");
1934 }
1935 assert!(!record.guest_may_inherit_a_terminal);
1936 }
1937
1938 /// A 0x135 recording retired a SIGKILLed victim at its host-timed
1939 /// deregistration; replay recomputes turn selection, so under retirement
1940 /// at the kill turn it could run another schedule. The version gate must
1941 /// refuse it.
1942 #[test]
1943 fn record_version_rejects_pre_sigkill_retirement_streams() {
1944 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x135)));
1945 }
1946
1947 /// A 0x138 recording holds no event for a pidfd_send_signal with a scope
1948 /// flag or a /proc/<pid> descriptor, which this replayer forwards
1949 /// (https://github.com/rrnewton/hermit/issues/4108).
1950 #[test]
1951 fn record_version_rejects_streams_that_answered_pidfd_scope_flags_without_an_event() {
1952 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x138)));
1953 }
1954
1955 /// A 0x13b recording holds no event for a pidfd_send_signal through a
1956 /// PIDFD_SELF sentinel, which this replayer forwards
1957 /// (https://github.com/rrnewton/hermit/issues/4147).
1958 #[test]
1959 fn record_version_rejects_streams_that_answered_pidfd_self_sentinels_without_an_event() {
1960 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x13b)));
1961 }
1962
1963 /// A 0x13c recording was made when a SIGEV_THREAD_ID timer notified its
1964 /// whole process, which this replayer no longer does
1965 /// (https://github.com/rrnewton/hermit/issues/4164).
1966 #[test]
1967 fn record_version_rejects_streams_whose_sigev_thread_id_timers_notified_the_process() {
1968 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x13c)));
1969 }
1970
1971 /// A recording older than 0x13e was made by a recorder that never
1972 /// readmitted a vfork-gated backgrounded child
1973 /// (https://github.com/rrnewton/hermit/issues/4163).
1974 #[test]
1975 fn record_version_rejects_streams_from_before_the_vfork_gate_readmission() {
1976 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x13d)));
1977 }
1978
1979 /// A 0x13e recording was made when Detcore used a nested PID namespace's
1980 /// pids as container pids, which this replayer translates
1981 /// (https://github.com/rrnewton/hermit/issues/4155).
1982 #[test]
1983 fn record_version_rejects_streams_made_before_nested_pid_translation() {
1984 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x13e)));
1985 }
1986
1987 /// A 0x13f or older recording read a waiting pselect6's temporary mask
1988 /// after the scheduler's grant, where this replayer reads it at the call's
1989 /// entry (https://github.com/rrnewton/hermit/issues/4095).
1990 #[test]
1991 fn record_version_rejects_streams_that_read_the_pselect6_mask_after_the_grant() {
1992 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x13f)));
1993 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x13e)));
1994 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x13d)));
1995 }
1996
1997 /// A recording older than 0x141 was made when sched_getaffinity refused a
1998 /// set shorter than 16 bytes, which this replayer accepts
1999 /// (https://github.com/rrnewton/hermit/issues/4160).
2000 #[test]
2001 fn record_version_rejects_streams_whose_short_affinity_sets_were_refused() {
2002 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x13f)));
2003 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x140)));
2004 }
2005
2006 /// A recording older than 0x142 was made by a recorder whose internally
2007 /// polled restartable calls ignored a cross-task signal
2008 /// (https://github.com/rrnewton/hermit/issues/4187).
2009 #[test]
2010 fn record_version_rejects_streams_from_before_signals_ended_polled_calls() {
2011 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x141)));
2012 }
2013
2014 /// A recording older than 0x145 has no bytes for an fcntl that writes its
2015 /// result through its argument, which this replayer writes back
2016 /// (https://github.com/rrnewton/hermit/issues/2289).
2017 #[test]
2018 fn record_version_rejects_streams_without_fcntl_output_events() {
2019 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x144)));
2020 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x143)));
2021 }
2022
2023 /// A recording older than 0x143 has no event for epoll_ctl, which this
2024 /// replayer reads from the recording
2025 /// (https://github.com/rrnewton/hermit/issues/31).
2026 #[test]
2027 fn record_version_rejects_streams_without_epoll_ctl_events() {
2028 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x142)));
2029 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x141)));
2030 }
2031
2032 /// A 0x145 or older recording was made when a POSIX timer's queued signal
2033 /// survived exec, which this replayer takes back
2034 /// (https://github.com/rrnewton/hermit/issues/4184).
2035 #[test]
2036 fn record_version_rejects_streams_whose_timer_signals_survived_exec() {
2037 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x145)));
2038 }
2039
2040 /// A recording older than 0x147 was made by a scheduler that ran a polling
2041 /// vfork child alone while other threads were runnable
2042 /// (https://github.com/rrnewton/hermit/issues/4194).
2043 #[test]
2044 fn record_version_rejects_streams_from_before_the_vfork_gate_fallback() {
2045 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x13f)));
2046 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x146)));
2047 }
2048
2049 /// A recording older than 0x148 was made when get_mempolicy left the node
2050 /// mask unwritten and answered 0 to flags Linux refuses
2051 /// (https://github.com/rrnewton/hermit/issues/4214).
2052 #[test]
2053 fn record_version_rejects_streams_from_before_one_node_get_mempolicy() {
2054 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x147)));
2055 }
2056
2057 /// A recording older than 0x149 was made when /proc/cpuinfo showed the
2058 /// host's CPU while CPUID showed the virtual one
2059 /// (https://github.com/rrnewton/hermit/issues/4161).
2060 #[test]
2061 fn record_version_rejects_streams_from_before_virtual_cpuinfo() {
2062 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x148)));
2063 }
2064
2065 /// A recording older than 0x14a recorded the host's bytes for the procfs
2066 /// files Detcore now fixes from its machine description
2067 /// (https://github.com/rrnewton/hermit/issues/4266).
2068 #[test]
2069 fn record_version_rejects_streams_from_before_fixed_kernel_procfs_files() {
2070 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x148)));
2071 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x149)));
2072 }
2073
2074 /// A recording older than 0x14b was made when a guest's recorded
2075 /// thread-directed SIGCHLD did not end a precise futex wait, or, before
2076 /// https://github.com/rrnewton/hermit/pull/4177 (at 0x148), an emulated
2077 /// pause or nanosleep (https://github.com/rrnewton/hermit/issues/4132).
2078 /// A recording older than 0x151 was made when a `pause` or `nanosleep` a
2079 /// child-exit `SIGCHLD` from the scheduler woke could follow a running
2080 /// sibling, which then took the signal
2081 /// (https://github.com/rrnewton/hermit/issues/4333).
2082 #[test]
2083 fn record_version_rejects_streams_from_before_a_woken_sleep_takes_its_sigchld() {
2084 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x150)));
2085 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x14f)));
2086 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x14e)));
2087 }
2088
2089 /// A recording older than 0x14e was made when a child-exit `SIGCHLD` the
2090 /// scheduler sent a parked futex waiter could be deferred behind a
2091 /// runnable sibling, which then took it
2092 /// (https://github.com/rrnewton/hermit/issues/4304).
2093 /// A recording older than 0x150 was made when one exit's robust-list walk
2094 /// could wake a word it reached twice
2095 /// (https://github.com/rrnewton/hermit/issues/4238).
2096 #[test]
2097 fn record_version_rejects_streams_from_before_one_exit_marks_a_robust_word_once() {
2098 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x14f)));
2099 }
2100
2101 #[test]
2102 fn record_version_rejects_streams_from_before_a_woken_futex_waiter_takes_its_sigchld() {
2103 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x14d)));
2104 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x14b)));
2105 }
2106
2107 /// A recording older than 0x157 was made when a copy Linux discarded at
2108 /// its send ended a wait once a handler was installed
2109 /// (https://github.com/rrnewton/hermit/issues/4293).
2110 #[test]
2111 fn record_version_rejects_streams_from_before_discarded_sends_are_recorded() {
2112 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x154)));
2113 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x155)));
2114 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x156)));
2115 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2116 }
2117
2118 /// A recording older than 0x15a holds the sanitized kernel read of
2119 /// /proc/loadavg, which Detcore now renders as a fixed file
2120 /// (https://github.com/rrnewton/hermit/issues/4213).
2121 #[test]
2122 fn record_version_rejects_streams_from_before_loadavg_is_rendered() {
2123 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x159)));
2124 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2125 }
2126
2127 /// A recording older than 0x15b was made when a sleep a child-exit SIGCHLD
2128 /// woke under record did not claim the signal, so it returned 0 or slept on
2129 /// (https://github.com/rrnewton/hermit/issues/4343).
2130 #[test]
2131 fn record_version_rejects_streams_from_before_a_recorded_sleep_claims_its_sigchld() {
2132 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x15a)));
2133 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2134 }
2135
2136 /// A recording older than 0x15c may hold a claim of a woken sleep's
2137 /// SIGCHLD made while a background read's descriptor had been replaced
2138 /// before the read entered the kernel
2139 /// (https://github.com/rrnewton/hermit/issues/4410).
2140 #[test]
2141 fn record_version_rejects_streams_from_before_replaced_read_descriptors_block_the_claim() {
2142 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x15b)));
2143 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2144 }
2145
2146 /// A recording older than 0x16e has a restartable call's attempt stopped by
2147 /// a pending signal (ERESTARTSYS) where the call now blocks signals first
2148 /// and records the attempt's result (R2b-2 stage 2,
2149 /// https://github.com/rrnewton/hermit/issues/4273).
2150 #[test]
2151 fn record_version_rejects_streams_from_before_restartable_calls_block_first() {
2152 for older in 0x163..=0x16d {
2153 assert!(
2154 !RECORD_VERSION.compatible_with(&RecordVersion(older)),
2155 "{older:#x}"
2156 );
2157 }
2158 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2159 }
2160
2161 /// A recording older than 0x170 held a `SIGCHLD` a guest sent a thread,
2162 /// recorded in its private queue, until the thread's gated wait or
2163 /// restartable pipe call returned, where it now ends the call (R2b-2
2164 /// stage 3, https://github.com/rrnewton/hermit/issues/4273). 0x171 and
2165 /// 0x172 held it too, and 0x170, stage 3's first label, is refused as well.
2166 #[test]
2167 fn record_version_rejects_streams_from_before_a_private_sigchld_ends_a_wait() {
2168 for older in 0x163..=0x172 {
2169 assert!(
2170 !RECORD_VERSION.compatible_with(&RecordVersion(older)),
2171 "{older:#x}"
2172 );
2173 }
2174 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2175 }
2176
2177 /// A recording older than 0x16a answered EBADF, unrecorded, for a relative
2178 /// name below a directory descriptor received over SCM_RIGHTS, where the
2179 /// kernel's lookup is now recorded (K1-0 of
2180 /// https://github.com/rrnewton/hermit/issues/4213).
2181 #[test]
2182 fn record_version_rejects_streams_from_before_received_directory_lookups() {
2183 for older in 0x163..=0x169 {
2184 assert!(
2185 !RECORD_VERSION.compatible_with(&RecordVersion(older)),
2186 "{older:#x}"
2187 );
2188 }
2189 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2190 }
2191
2192 /// A recording older than 0x16d read the kernel's /proc/<pid>/mountinfo
2193 /// and /proc/<pid>/mounts where Detcore now serves the recording
2194 /// container's mount table (K3a of
2195 /// https://github.com/rrnewton/hermit/issues/4213).
2196 #[test]
2197 fn record_version_rejects_streams_from_before_the_container_mount_table() {
2198 // 0x16b is the first K3a's label (shipped, reverted), 0x16c the
2199 // revert's: both are refused by the exact-match gate as well.
2200 for older in 0x163..=0x16c {
2201 assert!(
2202 !RECORD_VERSION.compatible_with(&RecordVersion(older)),
2203 "{older:#x}"
2204 );
2205 }
2206 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2207 }
2208
2209 /// A recording older than 0x169 refused a vectored read of a /proc/cpuinfo
2210 /// the kernel opened (ENOSYS) where Detcore now captures it, as it renders
2211 /// /proc/cpuinfo where CPUID is virtualized and intercepted
2212 /// (https://github.com/rrnewton/hermit/issues/4213).
2213 #[test]
2214 fn record_version_rejects_streams_from_before_cpuinfo_is_rendered() {
2215 for older in 0x163..=0x168 {
2216 assert!(
2217 !RECORD_VERSION.compatible_with(&RecordVersion(older)),
2218 "{older:#x}"
2219 );
2220 }
2221 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2222 }
2223
2224 /// A recording older than 0x168 sent a POSIX timer's expiry as a bare
2225 /// kill or tgkill; it is now an SI_TIMER record with the timer's id and
2226 /// si_value (https://github.com/rrnewton/hermit/issues/4262).
2227 #[test]
2228 fn record_version_rejects_streams_from_before_posix_timer_records_carry_si_timer() {
2229 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x167)));
2230 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2231 }
2232
2233 /// A recording older than 0x167 looked a name relative to a descriptor
2234 /// opened through a descriptor link up below the link's spelling, which
2235 /// names no procfs file, where Detcore now looks it up below the file the
2236 /// link reaches (https://github.com/rrnewton/hermit/issues/4213).
2237 #[test]
2238 fn record_version_rejects_streams_from_before_descriptor_links_record_their_target() {
2239 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x160)));
2240 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x161)));
2241 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x162)));
2242 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x163)));
2243 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x164)));
2244 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x165)));
2245 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x166)));
2246 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2247 }
2248
2249 /// A recording older than 0x163 refused a vectored read of a /proc/stat
2250 /// the kernel opened (ENOSYS) where Detcore now captures it, and showed
2251 /// the guest the host's lines, as Detcore now renders /proc/stat
2252 /// (https://github.com/rrnewton/hermit/issues/4213).
2253 #[test]
2254 fn record_version_rejects_streams_from_before_stat_is_rendered() {
2255 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x15d)));
2256 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x15e)));
2257 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x15f)));
2258 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x160)));
2259 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x161)));
2260 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x162)));
2261 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2262 }
2263
2264 /// A recording older than 0x15d refused a vectored read of a /proc/uptime
2265 /// the kernel opened (ENOSYS) where Detcore now captures it, as it
2266 /// renders /proc/uptime (https://github.com/rrnewton/hermit/issues/4213).
2267 #[test]
2268 fn record_version_rejects_streams_from_before_uptime_is_rendered() {
2269 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x15c)));
2270 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2271 }
2272
2273 /// A recording older than 0x161 may hold a pidfd_send_signal through a
2274 /// clone pidfd that Detcore answered EBADF unrecorded; it is now forwarded
2275 /// and recorded (https://github.com/rrnewton/hermit/issues/4430).
2276 #[test]
2277 fn record_version_rejects_streams_from_before_clone_pidfds_are_known() {
2278 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x160)));
2279 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2280 }
2281
2282 /// A recording older than 0x15e may hold a clone3 that reused, through
2283 /// set_tid, an id an earlier task had; that call is now refused
2284 /// (https://github.com/rrnewton/hermit/issues/4370).
2285 #[test]
2286 fn record_version_rejects_streams_from_before_reused_set_tid_is_refused() {
2287 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x15d)));
2288 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2289 }
2290
2291 /// A recording older than 0x164 was made when a pipe call woken by a kept
2292 /// signal could miss the wake or end on a copy Linux discarded (0x15e and
2293 /// 0x160 to 0x163), or when a pipe writev could write an iovec array
2294 /// another thread rewrote after the call began (0x15f)
2295 /// (https://github.com/rrnewton/hermit/issues/4273).
2296 #[test]
2297 fn record_version_rejects_streams_from_before_kept_wakes_stand() {
2298 for older in 0x15e..=0x163 {
2299 assert!(
2300 !RECORD_VERSION.compatible_with(&RecordVersion(older)),
2301 "{older:#x}"
2302 );
2303 }
2304 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2305 }
2306
2307 /// A recording older than 0x162 may hold a PR_SET_PDEATHSIG the kernel
2308 /// armed and a parent-death signal it sent; Detcore now sends it
2309 /// (https://github.com/rrnewton/hermit/issues/4440).
2310 #[test]
2311 fn record_version_rejects_streams_from_before_parent_death_signals_are_sent_by_detcore() {
2312 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x161)));
2313 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2314 }
2315
2316 /// A recording older than 0x166 was made while a clear-TID joiner was
2317 /// pushed onto the run queue at once, ahead of pending admissions; it is
2318 /// now admitted in DetTid order with them
2319 /// (https://github.com/rrnewton/hermit/issues/4362, part 2 step 0b).
2320 #[test]
2321 fn record_version_rejects_streams_from_before_cleartid_wakes_are_admitted() {
2322 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x165)));
2323 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2324 }
2325
2326 #[test]
2327 fn record_version_rejects_streams_from_before_private_sigchld_ends_a_wait() {
2328 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x14a)));
2329 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x149)));
2330 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x148)));
2331 }
2332
2333 /// A recording older than 0x14c stopped every thread while a vfork child's
2334 /// call ran in the background and logged no rejoin for its readmission
2335 /// (https://github.com/rrnewton/hermit/issues/4211).
2336 #[test]
2337 fn record_version_rejects_streams_from_before_logged_vfork_rejoins() {
2338 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x14b)));
2339 }
2340
2341 /// A recording older than 0x14d recorded the kernel's tables in the
2342 /// host's shape, where Detcore now renders fixed ones
2343 /// (https://github.com/rrnewton/hermit/issues/4266).
2344 #[test]
2345 fn record_version_rejects_streams_from_before_fixed_kernel_tables() {
2346 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x14c)));
2347 }
2348
2349 /// A recording older than 0x14f sent a timer's signal to its arming or
2350 /// drawn thread even when that thread blocked it
2351 /// (https://github.com/rrnewton/hermit/issues/4320).
2352 #[test]
2353 fn record_version_rejects_streams_from_before_mask_aware_timer_targets() {
2354 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x14e)));
2355 }
2356
2357 /// A recording older than 0x152 holds the kernel's answers for /proc
2358 /// names that Detcore now has the kernel answer as missing
2359 /// (https://github.com/rrnewton/hermit/issues/4266).
2360 #[test]
2361 fn record_version_rejects_streams_from_before_procfs_levels() {
2362 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x151)));
2363 }
2364
2365 /// A recording older than 0x153 ran at the `standard` /proc level without
2366 /// storing it, and would replay at the new default, `minimal`
2367 /// (https://github.com/rrnewton/hermit/issues/4266).
2368 #[test]
2369 fn record_version_rejects_streams_from_before_the_minimal_default() {
2370 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x152)));
2371 }
2372
2373 /// A recording older than 0x154 readmitted a call that began with a
2374 /// scheduler signal waiting in the shared queue through the rejoin log
2375 /// (https://github.com/rrnewton/hermit/issues/4234).
2376 #[test]
2377 fn record_version_rejects_streams_from_before_armed_waiting_signal_calls() {
2378 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x153)));
2379 }
2380
2381 /// A recording older than 0x165 holds sendfile's count without its copy,
2382 /// which a new replay needs (https://github.com/rrnewton/hermit/issues/4416).
2383 #[test]
2384 fn record_version_rejects_streams_from_before_sendfile_records_its_copy() {
2385 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x164)));
2386 assert!(RECORD_VERSION.compatible_with(&RECORD_VERSION));
2387 }
2388
2389 /// A replayer older than 0x155 ignores a recording's CPU profile and would
2390 /// replay an x86-64-v3 guest against the v2 table
2391 /// (<https://github.com/rrnewton/hermit/issues/4243>).
2392 #[test]
2393 fn record_version_rejects_streams_from_before_cpu_profiles() {
2394 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x154)));
2395 }
2396
2397 /// A recording older than 0x156 holds the kernel's answers for the names
2398 /// in a process's directory that Detcore now has the kernel answer as
2399 /// missing (https://github.com/rrnewton/hermit/issues/4266).
2400 #[test]
2401 fn record_version_rejects_streams_from_before_per_process_levels() {
2402 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x155)));
2403 }
2404
2405 /// A recording older than 0x158 holds a kernel readlink for each /proc
2406 /// link whose target Detcore now decides itself
2407 /// (https://github.com/rrnewton/hermit/issues/4266).
2408 #[test]
2409 fn record_version_rejects_streams_from_before_decided_procfs_links() {
2410 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x157)));
2411 }
2412
2413 /// A recording older than 0x159 read /proc/<pid>/cgroup as an unclassified
2414 /// file, recording a readlink of its descriptor and the host's bytes
2415 /// (https://github.com/rrnewton/hermit/issues/4378).
2416 #[test]
2417 fn record_version_rejects_streams_from_before_the_process_cgroup_root() {
2418 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x158)));
2419 }
2420
2421 #[test]
2422 fn record_version_rejects_previous_memory_configuration() {
2423 // Metadata does not persist the memory configuration, so a recording made with the
2424 // previous hardcoded value cannot be replayed compatibly with the corrected default.
2425 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x10a)));
2426 }
2427
2428 /// A 0x184 recording read a random device reached through an alternate
2429 /// spelling as recorded kernel reads and recorded no classification
2430 /// `fstat`; this replayer serves those reads from the seeded stream and
2431 /// expects the `fstat`, so the version gate must refuse it.
2432 #[test]
2433 fn record_version_rejects_path_classified_random_device_streams() {
2434 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x184)));
2435 }
2436
2437 /// A 0x186 recording polled a MSG_DONTWAIT send or receive on a
2438 /// A 0x187 recording polled a MSG_DONTWAIT send or receive on a
2439 /// socketpair end until it stopped failing with EAGAIN; this replayer runs
2440 /// it once, so the version gate must refuse it.
2441 #[test]
2442 fn record_version_rejects_polled_dontwait_socketpair_recordings() {
2443 // The version current before this change, on the main this branch
2444 // was rebased onto, not only the version before the original bump.
2445 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x187)));
2446 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x186)));
2447 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x187)));
2448 }
2449
2450 #[test]
2451 fn record_version_rejects_pre_complete_determinized_subscription_streams() {
2452 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x109)));
2453 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x104)));
2454 assert!(!RECORD_VERSION.compatible_with(&RecordVersion(0x102)));
2455 }
2456
2457 /// A recording made before the network choice was stored ran on the host
2458 /// network, and must still load, reporting no choice.
2459 #[test]
2460 fn metadata_without_a_network_choice_loads_as_unknown() {
2461 let metadata: Metadata = serde_json::from_value(serde_json::json!({
2462 "exe": "/bin/true",
2463 "program": "true",
2464 "arg0": "true",
2465 "args": [],
2466 "current_dir": "/",
2467 "hostname": null,
2468 "domainname": null,
2469 "envs": {},
2470 "version": RECORD_VERSION,
2471 }))
2472 .unwrap();
2473 assert_eq!(metadata.local_networking, None);
2474 assert_eq!(metadata.schedule, RecordedSchedule::Default);
2475 }
2476
2477 /// A chaos schedule sets chaos mode and seeds only the scheduler: the
2478 /// guest's random numbers are recorded, not seeded.
2479 #[test]
2480 fn a_chaos_schedule_reaches_the_record_replay_configuration() {
2481 let mut config = record_or_replay_config(Path::new("replay-data"));
2482 RecordedSchedule::Default.apply(&mut config);
2483 assert!(!config.chaos);
2484 assert_eq!(config.sched_seed, None);
2485
2486 let seed = config.seed;
2487 RecordedSchedule::Chaos { seed: 16 }.apply(&mut config);
2488 assert!(config.chaos);
2489 assert_eq!(config.sched_seed(), 16);
2490 assert_eq!(config.seed, seed, "only the scheduler is seeded");
2491 }
2492
2493 #[test]
2494 fn a_recorded_schedule_round_trips_through_metadata_json() {
2495 let mut metadata = Metadata::new(&Command::new("/bin/true")).unwrap();
2496 metadata.schedule = RecordedSchedule::Chaos { seed: 7 };
2497 let loaded: Metadata =
2498 serde_json::from_str(&serde_json::to_string(&metadata).unwrap()).unwrap();
2499 assert_eq!(loaded.schedule, RecordedSchedule::Chaos { seed: 7 });
2500 }
2501}