Skip to main content

detcore_model/
access_trace.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Record types of the access trace (`--record-access-trace-to`).
10//!
11//! The access trace is a sidecar of a run that records its schedule. It says
12//! what each guest operation touched, keyed by the operation's position in that
13//! schedule (the run's own `.events` file), so a reader can line two runs up and
14//! name the conflicting operations whose order differs between them.
15//!
16//! The file is JSON Lines: one [`TraceRecord`] per line, tagged by `"kind"`. The
17//! first line is a [`TraceHeader`]. A run that finishes cleanly ends with a
18//! [`TraceEnd`]; a trace without one was cut short. A record is written when
19//! its last schedule event is recorded, so syscall records follow `post`; a
20//! call that never returned is written when its thread enters its next call,
21//! or at the end of the run. Readers sort by the index they need.
22//!
23//! Every event index here is a position in the recorded schedule: the index the
24//! scheduler gave the event when it recorded it. Syscall records carry the
25//! thread's own syscall count as `nth`, the unit of
26//! [`crate::happens_before::Position::SyscallCount`], so `(tid, nth)` names the
27//! same operation in two runs of one program while its control flow agrees.
28//!
29//! The trace observes; it never feeds back into the run. Nothing in these types
30//! is read by the scheduler.
31//!
32//! Not recorded: some operations touch more than their records say.
33//! - splice and copy_file_range with a NULL offset move the file cursor, but
34//!   record no cursor access;
35//! - poll, select, ppoll and the epoll waits record no access to the
36//!   descriptors or channels they check;
37//! - path operations (unlink, rename, stat by path, the dirfd of the `*at`
38//!   calls) record nothing;
39//! - FUTEX_WAIT_REQUEUE_PI records no access to its second futex word;
40//! - kill(-1) records nothing.
41
42use std::fmt;
43use std::io;
44use std::io::BufRead;
45use std::io::Write;
46
47use reverie_syscalls::Sysno;
48use serde::Deserialize;
49use serde::Serialize;
50
51use crate::fd::DetInode;
52use crate::fd::FdSlot;
53use crate::fd::OpenFileId;
54use crate::futex::FutexID;
55use crate::futex::MmId;
56use crate::pid::DetPid;
57use crate::pid::DetTid;
58
59/// The `format` string every access trace header carries.
60pub const ACCESS_TRACE_FORMAT: &str = "hermit-access-trace";
61
62/// The record format version this crate writes and reads.
63///
64/// Version 2 names the signal disposition observed at the recorded event.
65/// Version 1 claimed a delivery outcome and is rejected rather than reinterpreted.
66pub const ACCESS_TRACE_VERSION: u32 = 2;
67
68/// The bytes of header and records a trace holds at most when
69/// `--access-trace-max-bytes` is not given: 1 GiB. A record takes about 350
70/// bytes, so this is some three million records; a run with more stops its
71/// trace there, without the end line, rather than fill the disk.
72pub const ACCESS_TRACE_DEFAULT_MAX_BYTES: u64 = 1 << 30;
73
74/// At most this many leading bytes of a write to fd 1 or 2 are kept in
75/// [`SyscallRecord::out_bytes`].
76pub const OUT_BYTES_CAP: usize = 256;
77
78/// A position in the run's recorded schedule (its `.events` file).
79pub type EventIndex = u64;
80
81/// One line of an access trace.
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(tag = "kind", rename_all = "snake_case")]
84pub enum TraceRecord {
85    /// First line: format, version and the first recorded index.
86    Header(TraceHeader),
87    /// One completed (or never-returning) syscall.
88    Syscall(SyscallRecord),
89    /// One signal event and its observed disposition.
90    Signal(SignalRecord),
91    /// One synchronization edge Detcore observed.
92    Edge(EdgeRecord),
93    /// One hardware watchpoint hit.
94    WatchHit(WatchHitRecord),
95    /// Last line of a trace whose run finished cleanly.
96    End(TraceEnd),
97}
98
99/// Identifies the file and the slice of the schedule it covers.
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub struct TraceHeader {
102    /// Always [`ACCESS_TRACE_FORMAT`].
103    pub format: String,
104    /// [`ACCESS_TRACE_VERSION`] of the writer.
105    pub version: u32,
106    /// `--access-trace-from`: records whose last event lies before this index
107    /// are omitted. Resource state built before it (who opened an fd) is
108    /// therefore only partly visible.
109    pub from: EventIndex,
110}
111
112impl TraceHeader {
113    /// The header this crate's writer emits for a trace starting at `from`.
114    pub fn new(from: EventIndex) -> Self {
115        Self {
116            format: ACCESS_TRACE_FORMAT.to_string(),
117            version: ACCESS_TRACE_VERSION,
118            from,
119        }
120    }
121}
122
123/// Closes a complete trace.
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct TraceEnd {
126    /// Schedule events the run recorded in total (the `.events` length).
127    pub events: u64,
128    /// Records written between the header and this line.
129    pub records: u64,
130}
131
132/// A thread at one schedule event: one end of an [`EdgeRecord`].
133#[derive(
134    Debug,
135    Clone,
136    Copy,
137    PartialEq,
138    Eq,
139    PartialOrd,
140    Ord,
141    Hash,
142    Serialize,
143    Deserialize
144)]
145pub struct EventRef {
146    /// The thread.
147    pub tid: DetTid,
148    /// The event of that thread.
149    pub event: EventIndex,
150}
151
152/// One syscall, from its prehook event to its posthook event.
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct SyscallRecord {
155    /// Index of the syscall's prehook event.
156    pub pre: EventIndex,
157    /// Index of its posthook event; `None` when the call never returned to
158    /// the guest (`exit`, `exit_group`, a successful `execve`, or a thread
159    /// that died inside it).
160    pub post: Option<EventIndex>,
161    /// Calling thread.
162    pub tid: DetTid,
163    /// Calling thread's process.
164    pub pid: DetPid,
165    /// The calling thread's syscall count including this call (1 for its
166    /// first). A restarted call is counted, and recorded, again.
167    pub nth: u64,
168    /// The syscall, written by name so a trace reads the same on any
169    /// architecture.
170    #[serde(with = "sysno_name")]
171    pub sysno: Sysno,
172    /// Raw argument registers.
173    pub args: [u64; 6],
174    /// The value the guest received: the result, or the negated errno.
175    /// `None` when [`Self::post`] is `None`.
176    pub result: Option<i64>,
177    /// Resources the call used, created or released, in the order of its
178    /// arguments.
179    #[serde(default, skip_serializing_if = "Vec::is_empty")]
180    pub accesses: Vec<Access>,
181    /// The bytes the kernel returned through guest buffers (read, pread,
182    /// readv, recv, recvmsg, ...); `None` when the call returned none. Two
183    /// runs whose calls return the same count but different bytes differ
184    /// here.
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    pub returned: Option<ReturnedBytes>,
187    /// The first [`OUT_BYTES_CAP`] bytes a successful write to fd 1 or 2
188    /// carried, hex-encoded on disk.
189    #[serde(default, skip_serializing_if = "Option::is_none", with = "hex_bytes")]
190    pub out_bytes: Option<Vec<u8>>,
191}
192
193/// The bytes a syscall returned through guest buffers, summarized.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
195pub struct ReturnedBytes {
196    /// Bytes returned, over all of the call's buffers.
197    pub len: u64,
198    /// [`returned_digest`] of the first [`RETURNED_DIGEST_CAP`] of them, in
199    /// buffer order.
200    pub digest: u64,
201    /// `len` exceeds [`RETURNED_DIGEST_CAP`], so bytes past the cap are not in
202    /// the digest: equal digests with this set prove nothing about them.
203    pub truncated: bool,
204}
205
206/// At most this many returned bytes enter [`ReturnedBytes::digest`].
207pub const RETURNED_DIGEST_CAP: u64 = 64 * 1024;
208
209/// The 64-bit FNV-1a hash [`ReturnedBytes::digest`] uses: fold each buffer's
210/// bytes in order into the running value, starting from
211/// [`RETURNED_DIGEST_SEED`].
212pub fn returned_digest(state: u64, bytes: &[u8]) -> u64 {
213    bytes.iter().fold(state, |hash, &b| {
214        (hash ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01b3)
215    })
216}
217
218/// The FNV-1a offset basis, the starting value of [`returned_digest`].
219pub const RETURNED_DIGEST_SEED: u64 = 0xcbf2_9ce4_8422_2325;
220
221/// A half-open byte range `[start, end)`.
222#[derive(
223    Debug,
224    Clone,
225    Copy,
226    PartialEq,
227    Eq,
228    PartialOrd,
229    Ord,
230    Hash,
231    Serialize,
232    Deserialize
233)]
234pub struct ByteRange {
235    /// First byte.
236    pub start: u64,
237    /// One past the last byte.
238    pub end: u64,
239}
240
241impl ByteRange {
242    /// Whether the two ranges share at least one byte.
243    pub fn overlaps(&self, other: &ByteRange) -> bool {
244        self.start < other.end && other.start < self.end
245    }
246}
247
248/// One resource access of a syscall. Section 4 of the M5 design names the
249/// identities; [`Access::conflicts_with`] states which pairs conflict.
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(tag = "resource", rename_all = "snake_case")]
252pub enum Access {
253    /// A descriptor number in one descriptor table.
254    Fd {
255        /// The table and the number.
256        slot: FdSlot,
257        /// For `use` and `close`, the open file the number reached before the
258        /// call; for `open` and `dup`, the open file it holds after. `None`
259        /// when the number held nothing Detcore models.
260        open_file: Option<OpenFileId>,
261        /// What the call did to the slot.
262        op: FdOp,
263    },
264    /// The cursor of an open file description, shared by dup and fork aliases.
265    OpenFile {
266        /// The open file description.
267        open_file: OpenFileId,
268        /// How the call used the cursor.
269        op: CursorOp,
270    },
271    /// File contents.
272    FileContents {
273        /// The file.
274        inode: DetInode,
275        /// Bytes the call read or wrote; `None` when Detcore does not know the
276        /// offset (a cursor-relative read or write).
277        range: Option<ByteRange>,
278        /// Read or write.
279        op: DataOp,
280    },
281    /// Data through a pipe, FIFO, socket or eventfd.
282    Channel {
283        /// The open file the call used.
284        open_file: OpenFileId,
285        /// An identity shared by both ends of the channel when Hermit knows
286        /// both ends (a pipe, a socketpair, a FIFO by its inode); otherwise
287        /// `None`, and the open file is the only identity. Sockets connected
288        /// with connect and accept, inside the container or to a network
289        /// peer outside it, are `None` in this version.
290        id: Option<ChannelId>,
291        /// What kind of channel.
292        channel: ChannelKind,
293        /// Direction of the data, or a change of the end's state.
294        op: ChannelOp,
295    },
296    /// A futex word.
297    Futex {
298        /// The word's key; process-shared futexes match across processes.
299        futex: FutexID,
300        /// The futex operation on this word.
301        op: FutexOp,
302    },
303    /// A range of one address space.
304    Memory {
305        /// The address space.
306        mm: MmId,
307        /// The bytes.
308        range: ByteRange,
309        /// What the call did to them.
310        op: MemoryOp,
311    },
312    /// A timer.
313    Timer {
314        /// The timer.
315        timer: TimerId,
316        /// What the call did to it.
317        op: TimerOp,
318    },
319    /// A thread or process as a whole.
320    Lifetime {
321        /// The target.
322        target: Task,
323        /// What the call did to it.
324        op: LifetimeOp,
325    },
326}
327
328/// What a syscall did to a descriptor slot.
329#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
330#[serde(rename_all = "snake_case")]
331pub enum FdOp {
332    /// The call used the number (read, write, lseek, fstat, ioctl, ...).
333    Use,
334    /// The call installed a new open file at the number (open, openat,
335    /// socket, pipe, accept, eventfd, ...).
336    Open {
337        /// The path the guest named, when there was one.
338        #[serde(default, skip_serializing_if = "Option::is_none")]
339        path: Option<String>,
340    },
341    /// The number now refers to the open file of `source` (dup, dup2, dup3,
342    /// `F_DUPFD`). Replacing an open number also emits a `close` of it.
343    Dup {
344        /// The descriptor that was duplicated.
345        source: i32,
346    },
347    /// The call released the number (close, close_range, dup2 onto it, or
348    /// exec closing a close-on-exec descriptor).
349    Close,
350}
351
352/// How a syscall used an open file's cursor.
353#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
354#[serde(rename_all = "snake_case")]
355pub enum CursorOp {
356    /// Read from the cursor and advanced it.
357    Read,
358    /// Wrote at the cursor and advanced it.
359    Write,
360    /// Moved or read the cursor (lseek).
361    Seek,
362}
363
364/// Read or write of data.
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
366#[serde(rename_all = "snake_case")]
367pub enum DataOp {
368    /// Read.
369    Read,
370    /// Wrote (including truncation).
371    Write,
372}
373
374/// The identity both ends of one channel share.
375#[derive(
376    Debug,
377    Clone,
378    Copy,
379    PartialEq,
380    Eq,
381    PartialOrd,
382    Ord,
383    Hash,
384    Serialize,
385    Deserialize
386)]
387#[serde(rename_all = "snake_case")]
388pub enum ChannelId {
389    /// A pipe or a socketpair, named by the open file of the end its
390    /// creating call returned first (a pipe's read end, `sv[0]` of a
391    /// socketpair). Dup and fork aliases keep it.
392    Pair(OpenFileId),
393    /// A FIFO, named by its inode, which every open of it shares.
394    Inode(DetInode),
395}
396
397/// Kinds of channel.
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
399#[serde(rename_all = "snake_case")]
400pub enum ChannelKind {
401    /// An anonymous pipe.
402    Pipe,
403    /// A named pipe.
404    Fifo,
405    /// `SOCK_STREAM`.
406    StreamSocket,
407    /// `SOCK_DGRAM`.
408    DatagramSocket,
409    /// `SOCK_SEQPACKET`.
410    SeqpacketSocket,
411    /// A socket whose type Detcore does not record.
412    Socket,
413    /// An eventfd counter.
414    Eventfd,
415}
416
417/// What a syscall did to a channel end.
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
419#[serde(rename_all = "snake_case")]
420pub enum ChannelOp {
421    /// Put data in (write, send).
422    Write,
423    /// Took data out, or found none (read, recv).
424    Read,
425    /// Shut down one or both directions.
426    Shutdown,
427    /// Closed this descriptor of the end.
428    Close,
429}
430
431/// A futex operation on one word.
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
433#[serde(rename_all = "snake_case")]
434pub enum FutexOp {
435    /// Waited on the word (`FUTEX_WAIT`, `FUTEX_WAIT_BITSET`,
436    /// `FUTEX_WAIT_REQUEUE_PI`).
437    Wait,
438    /// Woke waiters of the word (`FUTEX_WAKE`, `FUTEX_WAKE_BITSET`, and the
439    /// first word of `FUTEX_WAKE_OP` and of the requeue operations).
440    Wake,
441    /// Received the waiters a requeue moved (`uaddr2` of `FUTEX_REQUEUE`,
442    /// `FUTEX_CMP_REQUEUE`, `FUTEX_CMP_REQUEUE_PI`).
443    RequeueTarget,
444    /// Modified the word, then conditionally woke its waiters (`uaddr2` of
445    /// `FUTEX_WAKE_OP`).
446    ModifyWake,
447    /// Took a priority-inheritance lock (`FUTEX_LOCK_PI`, `FUTEX_LOCK_PI2`,
448    /// `FUTEX_TRYLOCK_PI`).
449    Lock,
450    /// Released one (`FUTEX_UNLOCK_PI`).
451    Unlock,
452}
453
454/// What a syscall did to a range of memory.
455#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
456#[serde(rename_all = "snake_case")]
457pub enum MemoryOp {
458    /// Mapped the range (mmap, the new range of mremap, shmat).
459    Map,
460    /// Unmapped it (munmap, the old range of a moving mremap, shmdt).
461    Unmap,
462    /// Changed its protection (mprotect, pkey_mprotect).
463    Protect,
464    /// Gave advice that can drop its contents (madvise).
465    Advise,
466    /// Moved the program break over it (brk).
467    Brk,
468    /// The kernel read a guest buffer in it for the call (write's buffer).
469    SyscallRead,
470    /// The kernel wrote a guest buffer in it for the call (read's buffer).
471    SyscallWrite,
472}
473
474impl MemoryOp {
475    /// Whether this operation changes the bytes, or which bytes are mapped
476    /// and how: everything but the kernel reading a buffer.
477    pub fn writes_or_remaps(self) -> bool {
478        self != MemoryOp::SyscallRead
479    }
480}
481
482/// Identity of a timer.
483#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
484#[serde(rename_all = "snake_case")]
485pub enum TimerId {
486    /// A POSIX timer (timer_create) of one process.
487    Posix {
488        /// The owning process.
489        process: DetPid,
490        /// The id timer_create returned.
491        id: i32,
492    },
493    /// An interval timer of one process (setitimer; `alarm` is
494    /// `ITIMER_REAL`, which is 0).
495    Interval {
496        /// The owning process.
497        process: DetPid,
498        /// `ITIMER_REAL`, `ITIMER_VIRTUAL` or `ITIMER_PROF`.
499        which: i32,
500    },
501    /// A timerfd, which is an open file.
502    Fd {
503        /// The timerfd's open file.
504        open_file: OpenFileId,
505    },
506}
507
508/// What a syscall did to a timer.
509#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
510#[serde(rename_all = "snake_case")]
511pub enum TimerOp {
512    /// Created it.
513    Create,
514    /// Armed or disarmed it (timer_settime, setitimer, alarm, timerfd_settime).
515    Set,
516    /// Read its remaining time (timer_gettime, getitimer, timerfd_gettime).
517    Get,
518    /// Deleted it.
519    Delete,
520    /// Consumed its expirations (read of a timerfd).
521    Read,
522}
523
524/// A thread, a process, or a process group.
525#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
526#[serde(rename_all = "snake_case")]
527pub enum Task {
528    /// One thread.
529    Thread(DetTid),
530    /// A whole thread group.
531    Process(DetPid),
532    /// Every process of a process group, named by its group id. A
533    /// signal to a group (kill with 0 or a negative pid) also lists one
534    /// `process` target per member Detcore's process table holds when the
535    /// call's posthook is recorded; kill(-1), every process the caller may
536    /// signal, lists none.
537    ProcessGroup(DetPid),
538}
539
540/// What a syscall did to a thread or process.
541#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
542#[serde(rename_all = "snake_case")]
543pub enum LifetimeOp {
544    /// Created it (clone, fork, vfork).
545    Create,
546    /// Ended it (exit ends a thread, exit_group a process).
547    Exit,
548    /// Replaced its image (execve).
549    Exec,
550    /// Sent it a signal (kill, tkill, tgkill, rt_sigqueueinfo, pidfd_send_signal).
551    Signal {
552        /// The signal; 0 probes for existence.
553        signo: i32,
554    },
555    /// Reaped or observed its exit (wait4, waitid).
556    Wait,
557}
558
559impl Access {
560    /// Whether two accesses touch the same resource in a way whose order can
561    /// change a result. The two accesses are assumed to come from different
562    /// threads; the caller checks that.
563    ///
564    /// This is the per-resource rule of the design's section 4, applied to
565    /// syscall data alone. It is deliberately conservative where Detcore does
566    /// not know an extent: an unknown file range overlaps any range.
567    pub fn conflicts_with(&self, other: &Access) -> bool {
568        use Access::*;
569        match (self, other) {
570            (Fd { slot: a, op: x, .. }, Fd { slot: b, op: y, .. }) => {
571                a == b && !(matches!(x, FdOp::Use) && matches!(y, FdOp::Use))
572            }
573            // Every cursor operation reads the position, and read, write and
574            // seek move it, so any two on one description conflict.
575            (OpenFile { open_file: a, .. }, OpenFile { open_file: b, .. }) => a == b,
576            (
577                FileContents {
578                    inode: a,
579                    range: ra,
580                    op: x,
581                },
582                FileContents {
583                    inode: b,
584                    range: rb,
585                    op: y,
586                },
587            ) => {
588                a == b
589                    && (*x == DataOp::Write || *y == DataOp::Write)
590                    && match (ra, rb) {
591                        (Some(ra), Some(rb)) => ra.overlaps(rb),
592                        _ => true,
593                    }
594            }
595            // Two reads race for the same data and two writes for its order,
596            // so any two operations on one channel conflict.
597            (
598                Channel {
599                    open_file: fa,
600                    id: ia,
601                    ..
602                },
603                Channel {
604                    open_file: fb,
605                    id: ib,
606                    ..
607                },
608            ) => fa == fb || (ia.is_some() && ia == ib),
609            (Futex { futex: a, op: x }, Futex { futex: b, op: y }) => {
610                a == b && !(*x == FutexOp::Wait && *y == FutexOp::Wait)
611            }
612            (
613                Memory {
614                    mm: a,
615                    range: ra,
616                    op: x,
617                },
618                Memory {
619                    mm: b,
620                    range: rb,
621                    op: y,
622                },
623            ) => a == b && ra.overlaps(rb) && (x.writes_or_remaps() || y.writes_or_remaps()),
624            (Timer { timer: a, op: x }, Timer { timer: b, op: y }) => {
625                a == b && !(*x == TimerOp::Get && *y == TimerOp::Get)
626            }
627            (Lifetime { target: a, op: x }, Lifetime { target: b, op: y }) => {
628                a == b && !(*x == LifetimeOp::Wait && *y == LifetimeOp::Wait)
629            }
630            _ => false,
631        }
632    }
633}
634
635/// The installed signal disposition observed before submitting its schedule event.
636///
637/// Another thread can change the disposition before the kernel delivers the
638/// signal. This observation does not prove that a handler ran, an action took
639/// effect, or the process died.
640#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
641#[serde(rename_all = "snake_case")]
642pub enum ObservedSignalDisposition {
643    /// A guest handler was installed.
644    Handler,
645    /// `SIG_IGN` was installed.
646    Ignored,
647    /// `SIG_DFL` was installed, regardless of that signal's default action.
648    Default,
649    /// The disposition could not be read.
650    Unknown,
651}
652
653/// The sender of a user-generated signal, from its siginfo.
654#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
655pub struct SignalSender {
656    /// `si_pid`, as a deterministic pid.
657    pub pid: DetPid,
658    /// The sending thread, when Detcore saw the send.
659    pub tid: Option<DetTid>,
660}
661
662/// One signal event and the disposition observed before submitting it.
663#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
664pub struct SignalRecord {
665    /// Index of the recorded `SignalReceived` event.
666    pub event: EventIndex,
667    /// Thread that reported the signal event.
668    pub tid: DetTid,
669    /// Signal number.
670    pub signo: i32,
671    /// Raw Linux `si_code`, whose meaning depends on the particular code.
672    pub code: i32,
673    /// `si_addr` of a kernel-generated SIGSEGV, SIGBUS, SIGILL, SIGFPE or
674    /// SIGTRAP; `None` otherwise.
675    pub addr: Option<u64>,
676    /// Sender of a user-sent signal; `None` for a kernel-generated one.
677    pub sender: Option<SignalSender>,
678    /// Installed disposition observed before submitting the signal event;
679    /// this does not establish the eventual delivery outcome.
680    pub observed_disposition: ObservedSignalDisposition,
681}
682
683/// Kinds of synchronization edge (design section 7). Data edges through
684/// pipes and sockets are derived by the reader from syscall results; they are
685/// not recorded.
686#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
687#[serde(rename_all = "snake_case")]
688pub enum EdgeKind {
689    /// A futex wake (`FUTEX_WAKE`, `FUTEX_WAKE_BITSET`, `FUTEX_WAKE_OP`, the
690    /// woken part of a requeue) to the woken waiter's wait return.
691    FutexWake,
692    /// A requeue that moved a waiter to another word, to that waiter's
693    /// eventual wait return.
694    FutexRequeue,
695    /// A thread or process creation to the child's first event.
696    Create,
697    /// A thread's exit, through its clear-child-tid wake (or the wake of a
698    /// robust futex it held), to the woken waiter's return, from the
699    /// exiting thread's last event.
700    ExitJoin,
701    /// A child process's exit, from its last event, to the parent's wait4
702    /// or waitid return.
703    ChildExit,
704    /// A signal send (kill, tkill, tgkill, rt_sigqueueinfo,
705    /// rt_tgsigqueueinfo) or a POSIX timer's arm (timer_settime), from its
706    /// prehook, to the delivery it caused.
707    Signal,
708}
709
710/// The object an edge synchronizes through.
711#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
712#[serde(rename_all = "snake_case")]
713pub enum SyncObject {
714    /// A futex word.
715    Futex(FutexID),
716    /// A thread (the created child).
717    Thread(DetTid),
718    /// A process (the exited child).
719    Process(DetPid),
720    /// A signal number.
721    Signal(i32),
722}
723
724/// One happens-before edge between two events of two threads. `from` precedes
725/// `to` in the recorded schedule.
726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
727pub struct EdgeRecord {
728    /// What kind of synchronization.
729    #[serde(rename = "edge")]
730    pub kind: EdgeKind,
731    /// The releasing side: the waker's prehook, the creator's clone, the exit.
732    pub from: EventRef,
733    /// The acquiring side: the waiter's posthook, the child's first event.
734    pub to: EventRef,
735    /// The object both sides used.
736    pub object: SyncObject,
737}
738
739/// One frame of a watchpoint hit's stack.
740#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
741pub struct StackFrame {
742    /// Instruction pointer.
743    pub ip: u64,
744    /// Function, when symbolized.
745    #[serde(default, skip_serializing_if = "Option::is_none")]
746    pub function: Option<String>,
747    /// Source file, when known.
748    #[serde(default, skip_serializing_if = "Option::is_none")]
749    pub file: Option<String>,
750    /// Source line, when known.
751    #[serde(default, skip_serializing_if = "Option::is_none")]
752    pub line: Option<u32>,
753}
754
755/// A hardware watchpoint hit (reserved for `--watch`; no writer emits it yet).
756#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
757pub struct WatchHitRecord {
758    /// Schedule events recorded before the hit: it lies between event
759    /// `after - 1` and event `after`.
760    pub after: EventIndex,
761    /// Thread whose access hit.
762    pub tid: DetTid,
763    /// Debug-register slot.
764    pub slot: u8,
765    /// Watched address that was accessed.
766    pub addr: u64,
767    /// Instruction pointer after the access.
768    pub ip: u64,
769    /// Stack, when it was unwound.
770    #[serde(default, skip_serializing_if = "Vec::is_empty")]
771    pub stack: Vec<StackFrame>,
772}
773
774impl TraceRecord {
775    /// Write the record as one JSON line.
776    pub fn write_line<W: Write>(&self, out: &mut W) -> io::Result<()> {
777        serde_json::to_writer(&mut *out, self).map_err(io::Error::from)?;
778        out.write_all(b"\n")
779    }
780
781    /// The record as one JSON line, its newline included: the bytes
782    /// [`TraceRecord::write_line`] writes.
783    pub fn to_line(&self) -> io::Result<Vec<u8>> {
784        let mut line = Vec::new();
785        self.write_line(&mut line)?;
786        Ok(line)
787    }
788}
789
790/// A whole trace, as [`read_access_trace`] returns it.
791#[derive(Debug, Clone, PartialEq, Eq)]
792pub struct AccessTrace {
793    /// The first line.
794    pub header: TraceHeader,
795    /// Everything between the header and the end line, in file order.
796    pub records: Vec<TraceRecord>,
797    /// The end line; `None` when the run did not finish cleanly.
798    pub end: Option<TraceEnd>,
799}
800
801/// Why a trace could not be read.
802#[derive(Debug)]
803pub enum AccessTraceError {
804    /// Reading the file failed.
805    Io(io::Error),
806    /// The file is empty.
807    Empty,
808    /// A line is not a record of this version.
809    BadLine {
810        /// 1-based line number.
811        line: usize,
812        /// What serde reported.
813        message: String,
814    },
815    /// The first line is not a header.
816    MissingHeader,
817    /// The header names another format or version.
818    Unsupported {
819        /// `format` in the file.
820        format: String,
821        /// `version` in the file.
822        version: u32,
823    },
824    /// A header or end line appears where it does not belong.
825    Misplaced {
826        /// 1-based line number.
827        line: usize,
828    },
829}
830
831impl fmt::Display for AccessTraceError {
832    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
833        match self {
834            AccessTraceError::Io(e) => write!(f, "reading the access trace failed: {e}"),
835            AccessTraceError::Empty => write!(
836                f,
837                "the access trace is empty; rerun with --record-access-trace-to and let the run finish"
838            ),
839            AccessTraceError::BadLine { line, message } => {
840                write!(f, "access trace line {line} is not a record: {message}")
841            }
842            AccessTraceError::MissingHeader => write!(
843                f,
844                "the access trace does not start with a header line; it was not written by --record-access-trace-to"
845            ),
846            AccessTraceError::Unsupported { format, version } => write!(
847                f,
848                "the access trace is {format} version {version}; this build reads {ACCESS_TRACE_FORMAT} version {ACCESS_TRACE_VERSION}: record it again with this build"
849            ),
850            AccessTraceError::Misplaced { line } => write!(
851                f,
852                "access trace line {line} is a header or end line in the middle of the file; the file was concatenated or corrupted"
853            ),
854        }
855    }
856}
857
858impl std::error::Error for AccessTraceError {}
859
860impl From<io::Error> for AccessTraceError {
861    fn from(e: io::Error) -> Self {
862        AccessTraceError::Io(e)
863    }
864}
865
866/// Read and validate a whole access trace.
867pub fn read_access_trace<R: BufRead>(input: R) -> Result<AccessTrace, AccessTraceError> {
868    let mut header = None;
869    let mut records = Vec::new();
870    let mut end = None;
871    for (index, line) in input.lines().enumerate() {
872        let number = index + 1;
873        let line = line?;
874        if line.is_empty() {
875            continue;
876        }
877        let record: TraceRecord =
878            serde_json::from_str(&line).map_err(|e| AccessTraceError::BadLine {
879                line: number,
880                message: e.to_string(),
881            })?;
882        if end.is_some() {
883            return Err(AccessTraceError::Misplaced { line: number });
884        }
885        match (record, header.is_some()) {
886            (TraceRecord::Header(h), false) => {
887                if h.format != ACCESS_TRACE_FORMAT || h.version != ACCESS_TRACE_VERSION {
888                    return Err(AccessTraceError::Unsupported {
889                        format: h.format,
890                        version: h.version,
891                    });
892                }
893                header = Some(h);
894            }
895            (_, false) => return Err(AccessTraceError::MissingHeader),
896            (TraceRecord::Header(_), true) => {
897                return Err(AccessTraceError::Misplaced { line: number });
898            }
899            (TraceRecord::End(e), true) => end = Some(e),
900            (record, true) => records.push(record),
901        }
902    }
903    Ok(AccessTrace {
904        header: header.ok_or(AccessTraceError::Empty)?,
905        records,
906        end,
907    })
908}
909
910/// [`Sysno`] by name. Its own serde form is the architecture's number.
911mod sysno_name {
912    use std::str::FromStr;
913
914    use reverie_syscalls::Sysno;
915    use serde::Deserialize;
916    use serde::Deserializer;
917    use serde::Serializer;
918    use serde::de::Error;
919
920    pub fn serialize<S: Serializer>(sysno: &Sysno, s: S) -> Result<S::Ok, S::Error> {
921        s.serialize_str(sysno.name())
922    }
923
924    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Sysno, D::Error> {
925        let name = String::deserialize(d)?;
926        Sysno::from_str(&name).map_err(|_| D::Error::custom(format!("unknown syscall {name:?}")))
927    }
928}
929
930/// Hex encoding for [`SyscallRecord::out_bytes`]: exact for any bytes, and
931/// half the size of serde's array of numbers.
932mod hex_bytes {
933    use std::fmt::Write;
934
935    use serde::Deserialize;
936    use serde::Deserializer;
937    use serde::Serializer;
938    use serde::de::Error;
939
940    pub fn serialize<S: Serializer>(bytes: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
941        match bytes {
942            Some(bytes) => {
943                let mut text = String::with_capacity(bytes.len() * 2);
944                for b in bytes {
945                    write!(text, "{b:02x}").expect("writing to a String cannot fail");
946                }
947                s.serialize_some(&text)
948            }
949            None => s.serialize_none(),
950        }
951    }
952
953    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
954        let Some(text) = Option::<String>::deserialize(d)? else {
955            return Ok(None);
956        };
957        if text.len() % 2 != 0 {
958            return Err(D::Error::custom("odd-length hex string"));
959        }
960        // The two-byte slices below are UTF-8 boundaries only for ASCII.
961        if !text.is_ascii() {
962            return Err(D::Error::custom("invalid hex digit"));
963        }
964        (0..text.len())
965            .step_by(2)
966            .map(|i| {
967                u8::from_str_radix(&text[i..i + 2], 16)
968                    .map_err(|_| D::Error::custom("invalid hex digit"))
969            })
970            .collect::<Result<Vec<u8>, _>>()
971            .map(Some)
972    }
973}
974
975#[cfg(test)]
976mod tests {
977    use pretty_assertions::assert_eq;
978
979    use super::*;
980    use crate::fd::FilesId;
981    use crate::futex::SharedMemoryObjectId;
982
983    fn tid(n: i32) -> DetTid {
984        DetTid::from_raw(n)
985    }
986
987    fn slot(fd: i32) -> FdSlot {
988        FdSlot {
989            files: FilesId::initial(tid(3)),
990            fd,
991        }
992    }
993
994    fn file(creator: i32, sequence: u64) -> OpenFileId {
995        OpenFileId::new(tid(creator), sequence)
996    }
997
998    fn mm() -> MmId {
999        MmId::initial(tid(3))
1000    }
1001
1002    fn close_record() -> SyscallRecord {
1003        SyscallRecord {
1004            pre: 7587,
1005            post: Some(7588),
1006            tid: tid(9),
1007            pid: DetPid::from_raw(3),
1008            nth: 1412,
1009            sysno: Sysno::close,
1010            args: [6, 0, 0, 0, 0, 0],
1011            result: Some(0),
1012            accesses: vec![
1013                Access::Fd {
1014                    slot: slot(6),
1015                    open_file: Some(file(7, 2)),
1016                    op: FdOp::Close,
1017                },
1018                Access::Channel {
1019                    open_file: file(7, 2),
1020                    id: Some(ChannelId::Pair(file(7, 1))),
1021                    channel: ChannelKind::Pipe,
1022                    op: ChannelOp::Close,
1023                },
1024            ],
1025            returned: None,
1026            out_bytes: None,
1027        }
1028    }
1029
1030    /// One record of every kind and every resource variant.
1031    fn every_record() -> Vec<TraceRecord> {
1032        let futex = FutexID::private(mm(), 0x7f00_0000_1000);
1033        let shared = FutexID::shared(
1034            SharedMemoryObjectId::Anonymous {
1035                origin: mm(),
1036                sequence: 1,
1037            },
1038            64,
1039        );
1040        vec![
1041            TraceRecord::Header(TraceHeader::new(100)),
1042            TraceRecord::Syscall(close_record()),
1043            TraceRecord::Syscall(SyscallRecord {
1044                pre: 7589,
1045                post: Some(7590),
1046                tid: tid(7),
1047                pid: DetPid::from_raw(3),
1048                nth: 388,
1049                sysno: Sysno::write,
1050                args: [2, 0x1000, 5, 0, 0, 0],
1051                result: Some(5),
1052                accesses: vec![
1053                    Access::Fd {
1054                        slot: slot(2),
1055                        open_file: Some(file(3, 0)),
1056                        op: FdOp::Use,
1057                    },
1058                    Access::OpenFile {
1059                        open_file: file(3, 0),
1060                        op: CursorOp::Write,
1061                    },
1062                    Access::FileContents {
1063                        inode: DetInode::mint(12),
1064                        range: Some(ByteRange { start: 0, end: 5 }),
1065                        op: DataOp::Write,
1066                    },
1067                    Access::Memory {
1068                        mm: mm(),
1069                        range: ByteRange {
1070                            start: 0x1000,
1071                            end: 0x1005,
1072                        },
1073                        op: MemoryOp::SyscallRead,
1074                    },
1075                    Access::Channel {
1076                        open_file: file(3, 6),
1077                        id: Some(ChannelId::Inode(DetInode::mint(77))),
1078                        channel: ChannelKind::Fifo,
1079                        op: ChannelOp::Shutdown,
1080                    },
1081                ],
1082                returned: None,
1083                out_bytes: Some(b"oops\n".to_vec()),
1084            }),
1085            TraceRecord::Syscall(SyscallRecord {
1086                pre: 7591,
1087                post: Some(7592),
1088                tid: tid(7),
1089                pid: DetPid::from_raw(3),
1090                nth: 389,
1091                sysno: Sysno::openat,
1092                args: [u64::MAX - 99, 0x2000, 0o2, 0, 0, 0],
1093                result: Some(6),
1094                accesses: vec![Access::Fd {
1095                    slot: slot(6),
1096                    open_file: Some(file(7, 3)),
1097                    op: FdOp::Open {
1098                        path: Some("/tmp/misc".to_string()),
1099                    },
1100                }],
1101                returned: Some(ReturnedBytes {
1102                    len: 4,
1103                    digest: returned_digest(RETURNED_DIGEST_SEED, b"data"),
1104                    truncated: false,
1105                }),
1106                out_bytes: None,
1107            }),
1108            TraceRecord::Syscall(SyscallRecord {
1109                pre: 7593,
1110                post: Some(7594),
1111                tid: tid(7),
1112                pid: DetPid::from_raw(3),
1113                nth: 390,
1114                sysno: Sysno::dup3,
1115                args: [6, 5, 0, 0, 0, 0],
1116                result: Some(5),
1117                accesses: vec![
1118                    Access::Fd {
1119                        slot: slot(6),
1120                        open_file: Some(file(7, 3)),
1121                        op: FdOp::Use,
1122                    },
1123                    Access::Fd {
1124                        slot: slot(5),
1125                        open_file: Some(file(3, 1)),
1126                        op: FdOp::Close,
1127                    },
1128                    Access::Fd {
1129                        slot: slot(5),
1130                        open_file: Some(file(7, 3)),
1131                        op: FdOp::Dup { source: 6 },
1132                    },
1133                ],
1134                returned: Some(ReturnedBytes {
1135                    len: 4,
1136                    digest: returned_digest(RETURNED_DIGEST_SEED, b"data"),
1137                    truncated: false,
1138                }),
1139                out_bytes: None,
1140            }),
1141            TraceRecord::Syscall(SyscallRecord {
1142                pre: 8000,
1143                post: Some(8100),
1144                tid: tid(5),
1145                pid: DetPid::from_raw(3),
1146                nth: 12,
1147                sysno: Sysno::futex,
1148                args: [0x7f00_0000_1000, 0x80, 2, 0, 0, 0],
1149                result: Some(-11),
1150                accesses: vec![
1151                    Access::Futex {
1152                        futex,
1153                        op: FutexOp::Wait,
1154                    },
1155                    Access::Futex {
1156                        futex: shared,
1157                        op: FutexOp::RequeueTarget,
1158                    },
1159                ],
1160                returned: Some(ReturnedBytes {
1161                    len: 4,
1162                    digest: returned_digest(RETURNED_DIGEST_SEED, b"data"),
1163                    truncated: false,
1164                }),
1165                out_bytes: None,
1166            }),
1167            TraceRecord::Syscall(SyscallRecord {
1168                pre: 8200,
1169                post: Some(8201),
1170                tid: tid(5),
1171                pid: DetPid::from_raw(3),
1172                nth: 13,
1173                sysno: Sysno::timer_settime,
1174                args: [0, 0, 0x3000, 0, 0, 0],
1175                result: Some(0),
1176                accesses: vec![
1177                    Access::Timer {
1178                        timer: TimerId::Posix {
1179                            process: DetPid::from_raw(3),
1180                            id: 0,
1181                        },
1182                        op: TimerOp::Set,
1183                    },
1184                    Access::Timer {
1185                        timer: TimerId::Interval {
1186                            process: DetPid::from_raw(3),
1187                            which: 0,
1188                        },
1189                        op: TimerOp::Get,
1190                    },
1191                    Access::Timer {
1192                        timer: TimerId::Fd {
1193                            open_file: file(5, 4),
1194                        },
1195                        op: TimerOp::Read,
1196                    },
1197                ],
1198                returned: Some(ReturnedBytes {
1199                    len: 4,
1200                    digest: returned_digest(RETURNED_DIGEST_SEED, b"data"),
1201                    truncated: false,
1202                }),
1203                out_bytes: None,
1204            }),
1205            TraceRecord::Syscall(SyscallRecord {
1206                pre: 9000,
1207                post: None,
1208                tid: tid(3),
1209                pid: DetPid::from_raw(3),
1210                nth: 600,
1211                sysno: Sysno::kill,
1212                args: [4, 9, 0, 0, 0, 0],
1213                result: None,
1214                accesses: vec![
1215                    Access::Lifetime {
1216                        target: Task::Process(DetPid::from_raw(4)),
1217                        op: LifetimeOp::Signal { signo: 9 },
1218                    },
1219                    Access::Lifetime {
1220                        target: Task::Thread(tid(5)),
1221                        op: LifetimeOp::Exit,
1222                    },
1223                    Access::Lifetime {
1224                        target: Task::ProcessGroup(DetPid::from_raw(3)),
1225                        op: LifetimeOp::Wait,
1226                    },
1227                ],
1228                returned: Some(ReturnedBytes {
1229                    len: 4,
1230                    digest: returned_digest(RETURNED_DIGEST_SEED, b"data"),
1231                    truncated: false,
1232                }),
1233                out_bytes: None,
1234            }),
1235            TraceRecord::Signal(SignalRecord {
1236                event: 9001,
1237                tid: tid(7),
1238                signo: 11,
1239                code: 1,
1240                addr: Some(0x10),
1241                sender: None,
1242                observed_disposition: ObservedSignalDisposition::Default,
1243            }),
1244            TraceRecord::Signal(SignalRecord {
1245                event: 9002,
1246                tid: tid(4),
1247                signo: 10,
1248                code: 0,
1249                addr: None,
1250                sender: Some(SignalSender {
1251                    pid: DetPid::from_raw(3),
1252                    tid: Some(tid(3)),
1253                }),
1254                observed_disposition: ObservedSignalDisposition::Handler,
1255            }),
1256            TraceRecord::Edge(EdgeRecord {
1257                kind: EdgeKind::FutexWake,
1258                from: EventRef {
1259                    tid: tid(7),
1260                    event: 8050,
1261                },
1262                to: EventRef {
1263                    tid: tid(5),
1264                    event: 8100,
1265                },
1266                object: SyncObject::Futex(futex),
1267            }),
1268            TraceRecord::Edge(EdgeRecord {
1269                kind: EdgeKind::Create,
1270                from: EventRef {
1271                    tid: tid(3),
1272                    event: 10,
1273                },
1274                to: EventRef {
1275                    tid: tid(5),
1276                    event: 12,
1277                },
1278                object: SyncObject::Thread(tid(5)),
1279            }),
1280            TraceRecord::Edge(EdgeRecord {
1281                kind: EdgeKind::ChildExit,
1282                from: EventRef {
1283                    tid: tid(4),
1284                    event: 20,
1285                },
1286                to: EventRef {
1287                    tid: tid(3),
1288                    event: 22,
1289                },
1290                object: SyncObject::Process(DetPid::from_raw(4)),
1291            }),
1292            TraceRecord::Edge(EdgeRecord {
1293                kind: EdgeKind::Signal,
1294                from: EventRef {
1295                    tid: tid(3),
1296                    event: 30,
1297                },
1298                to: EventRef {
1299                    tid: tid(4),
1300                    event: 31,
1301                },
1302                object: SyncObject::Signal(10),
1303            }),
1304            TraceRecord::WatchHit(WatchHitRecord {
1305                after: 9003,
1306                tid: tid(5),
1307                slot: 0,
1308                addr: 0x7f00_0000_2000,
1309                ip: 0x40_1000,
1310                stack: vec![StackFrame {
1311                    ip: 0x40_1000,
1312                    function: Some("main".to_string()),
1313                    file: Some("racewrite.c".to_string()),
1314                    line: Some(12),
1315                }],
1316            }),
1317            TraceRecord::End(TraceEnd {
1318                events: 9004,
1319                records: 14,
1320            }),
1321        ]
1322    }
1323
1324    #[test]
1325    fn every_record_kind_round_trips_through_json_lines() {
1326        let records = every_record();
1327        let mut buf = Vec::new();
1328        for record in &records {
1329            record.write_line(&mut buf).unwrap();
1330        }
1331        let text = String::from_utf8(buf.clone()).unwrap();
1332        assert_eq!(text.lines().count(), records.len());
1333        for (line, record) in text.lines().zip(&records) {
1334            let back: TraceRecord = serde_json::from_str(line).unwrap();
1335            assert_eq!(&back, record);
1336        }
1337
1338        let trace = read_access_trace(&buf[..]).unwrap();
1339        assert_eq!(trace.header, TraceHeader::new(100));
1340        assert_eq!(trace.records, records[1..records.len() - 1].to_vec());
1341        assert_eq!(
1342            trace.end,
1343            Some(TraceEnd {
1344                events: 9004,
1345                records: 14
1346            })
1347        );
1348    }
1349
1350    #[test]
1351    fn on_disk_spelling_is_stable() {
1352        let mut buf = Vec::new();
1353        TraceRecord::Syscall(close_record())
1354            .write_line(&mut buf)
1355            .unwrap();
1356        assert_eq!(
1357            String::from_utf8(buf).unwrap(),
1358            concat!(
1359                r#"{"kind":"syscall","pre":7587,"post":7588,"tid":9,"pid":3,"nth":1412,"#,
1360                r#""sysno":"close","args":[6,0,0,0,0,0],"result":0,"accesses":["#,
1361                r#"{"resource":"fd","slot":{"files":{"creator":3,"generation":0},"fd":6},"#,
1362                r#""open_file":{"creator":7,"sequence":2},"op":"close"},"#,
1363                r#"{"resource":"channel","open_file":{"creator":7,"sequence":2},"#,
1364                r#""id":{"pair":{"creator":7,"sequence":1}},"#,
1365                r#""channel":"pipe","op":"close"}]}"#,
1366                "\n"
1367            )
1368        );
1369    }
1370
1371    #[test]
1372    fn out_bytes_are_exact_for_non_utf8_output() {
1373        let mut record = close_record();
1374        record.out_bytes = Some(vec![0x00, 0xff, b'\n', 0x80]);
1375        let line = serde_json::to_string(&TraceRecord::Syscall(record.clone())).unwrap();
1376        assert!(line.contains(r#""out_bytes":"00ff0a80""#), "{line}");
1377        let back: TraceRecord = serde_json::from_str(&line).unwrap();
1378        assert_eq!(back, TraceRecord::Syscall(record));
1379        assert!(serde_json::from_str::<TraceRecord>(&line.replace("00ff0a80", "0ff0a80")).is_err());
1380        assert!(
1381            serde_json::from_str::<TraceRecord>(&line.replace("00ff0a80", "zzff0a80")).is_err()
1382        );
1383        assert!(
1384            serde_json::from_str::<TraceRecord>(
1385                &line.replace(r#""sysno":"close""#, r#""sysno":"nosuchcall""#)
1386            )
1387            .is_err()
1388        );
1389    }
1390
1391    /// Malformed output bytes are a parse error at the input line, never a
1392    /// panic from splitting a UTF-8 code point into two-byte hex pairs.
1393    #[test]
1394    fn reader_rejects_non_hex_output_without_panicking() {
1395        let header = serde_json::to_string(&TraceRecord::Header(TraceHeader::new(0))).unwrap();
1396        let input = |encoded: &str| {
1397            let mut call = serde_json::to_value(TraceRecord::Syscall(close_record())).unwrap();
1398            call["out_bytes"] = serde_json::json!(encoded);
1399            format!("{header}\n{call}\n")
1400        };
1401        for (encoded, expected) in [
1402            ("", &[][..]),
1403            ("00ff0a80", &[0x00, 0xff, b'\n', 0x80][..]),
1404            ("aB09", &[0xab, 0x09][..]),
1405        ] {
1406            let trace = read_access_trace(input(encoded).as_bytes()).unwrap();
1407            let [TraceRecord::Syscall(call)] = trace.records.as_slice() else {
1408                panic!("{encoded:?}: {:?}", trace.records);
1409            };
1410            assert_eq!(call.out_bytes.as_deref(), Some(expected), "{encoded:?}");
1411        }
1412        for encoded in ["€a", "a€", "💥", "é", "0", "zz"] {
1413            let error = read_access_trace(input(encoded).as_bytes())
1414                .expect_err("malformed output bytes must be refused");
1415            assert!(
1416                matches!(error, AccessTraceError::BadLine { line: 2, .. }),
1417                "{encoded:?}: {error}"
1418            );
1419            assert!(error.to_string().contains("hex"), "{encoded:?}: {error}");
1420        }
1421    }
1422
1423    #[test]
1424    fn returned_digest_is_fnv1a_over_the_buffers_in_order() {
1425        assert_eq!(
1426            returned_digest(RETURNED_DIGEST_SEED, b""),
1427            RETURNED_DIGEST_SEED
1428        );
1429        // The published FNV-1a 64 test vector for "a".
1430        assert_eq!(
1431            returned_digest(RETURNED_DIGEST_SEED, b"a"),
1432            0xaf63_dc4c_8601_ec8c
1433        );
1434        let split = returned_digest(returned_digest(RETURNED_DIGEST_SEED, b"he"), b"llo");
1435        assert_eq!(split, returned_digest(RETURNED_DIGEST_SEED, b"hello"));
1436        assert_ne!(split, returned_digest(RETURNED_DIGEST_SEED, b"hellp"));
1437    }
1438
1439    #[test]
1440    fn reader_refuses_files_it_cannot_trust() {
1441        let header = serde_json::to_string(&TraceRecord::Header(TraceHeader::new(0))).unwrap();
1442        let syscall = serde_json::to_string(&TraceRecord::Syscall(close_record())).unwrap();
1443        let end = serde_json::to_string(&TraceRecord::End(TraceEnd {
1444            events: 1,
1445            records: 1,
1446        }))
1447        .unwrap();
1448
1449        assert!(matches!(
1450            read_access_trace("".as_bytes()),
1451            Err(AccessTraceError::Empty)
1452        ));
1453        assert!(matches!(
1454            read_access_trace(format!("{syscall}\n").as_bytes()),
1455            Err(AccessTraceError::MissingHeader)
1456        ));
1457        assert!(matches!(
1458            read_access_trace(format!("{header}\n{header}\n").as_bytes()),
1459            Err(AccessTraceError::Misplaced { line: 2 })
1460        ));
1461        assert!(matches!(
1462            read_access_trace(format!("{header}\n{end}\n{syscall}\n").as_bytes()),
1463            Err(AccessTraceError::Misplaced { line: 3 })
1464        ));
1465        assert!(matches!(
1466            read_access_trace(format!("{header}\nnot json\n").as_bytes()),
1467            Err(AccessTraceError::BadLine { line: 2, .. })
1468        ));
1469        let future_version = ACCESS_TRACE_VERSION + 1;
1470        let future = header.replace(
1471            &format!("\"version\":{ACCESS_TRACE_VERSION}"),
1472            &format!("\"version\":{future_version}"),
1473        );
1474        let err = read_access_trace(future.as_bytes()).unwrap_err();
1475        assert!(matches!(
1476            err,
1477            AccessTraceError::Unsupported { version, .. } if version == future_version
1478        ));
1479        assert!(err.to_string().contains("record it again"), "{err}");
1480
1481        let cut = read_access_trace(format!("{header}\n{syscall}\n").as_bytes()).unwrap();
1482        assert_eq!(cut.end, None, "a trace without an end line was cut short");
1483        assert_eq!(cut.records.len(), 1);
1484    }
1485
1486    /// Version 1 called the pre-delivery observation an actual delivery.
1487    /// It must be refused, not silently reinterpreted as the new contract.
1488    #[test]
1489    fn reader_refuses_version_one_delivery_claims() {
1490        let old = concat!(
1491            r#"{"kind":"header","format":"hermit-access-trace","version":1,"from":0}"#,
1492            "\n",
1493            r#"{"kind":"signal","event":0,"tid":3,"signo":10,"code":0,"addr":null,"sender":null,"delivery":"handler"}"#,
1494            "\n",
1495            r#"{"kind":"end","events":1,"records":1}"#,
1496            "\n",
1497        );
1498        let error = read_access_trace(old.as_bytes()).expect_err("version 1 must be refused");
1499        assert!(
1500            matches!(error, AccessTraceError::Unsupported { version: 1, .. }),
1501            "{error}"
1502        );
1503        assert!(error.to_string().contains("record it again"), "{error}");
1504    }
1505
1506    #[test]
1507    fn conflicts_follow_the_per_resource_rules() {
1508        let use6 = Access::Fd {
1509            slot: slot(6),
1510            open_file: Some(file(7, 3)),
1511            op: FdOp::Use,
1512        };
1513        let close6 = Access::Fd {
1514            slot: slot(6),
1515            open_file: Some(file(7, 3)),
1516            op: FdOp::Close,
1517        };
1518        let use6_other_table = Access::Fd {
1519            slot: FdSlot {
1520                files: FilesId::initial(tid(4)),
1521                fd: 6,
1522            },
1523            open_file: None,
1524            op: FdOp::Close,
1525        };
1526        assert!(!use6.conflicts_with(&use6), "two uses commute");
1527        assert!(use6.conflicts_with(&close6), "close against use");
1528        assert!(close6.conflicts_with(&close6), "double close");
1529        assert!(!close6.conflicts_with(&use6_other_table), "other table");
1530
1531        let futex = FutexID::private(mm(), 0x1000);
1532        let wait = Access::Futex {
1533            futex,
1534            op: FutexOp::Wait,
1535        };
1536        let wake = Access::Futex {
1537            futex,
1538            op: FutexOp::Wake,
1539        };
1540        assert!(!wait.conflicts_with(&wait));
1541        assert!(wait.conflicts_with(&wake), "lost wake");
1542
1543        let read = |start, end| Access::FileContents {
1544            inode: DetInode::mint(1),
1545            range: Some(ByteRange { start, end }),
1546            op: DataOp::Read,
1547        };
1548        let write = |range| Access::FileContents {
1549            inode: DetInode::mint(1),
1550            range,
1551            op: DataOp::Write,
1552        };
1553        assert!(!read(0, 10).conflicts_with(&read(0, 10)));
1554        assert!(read(0, 10).conflicts_with(&write(Some(ByteRange { start: 9, end: 12 }))));
1555        assert!(!read(0, 10).conflicts_with(&write(Some(ByteRange { start: 10, end: 12 }))));
1556        assert!(read(0, 10).conflicts_with(&write(None)), "unknown range");
1557
1558        let unmap = Access::Memory {
1559            mm: mm(),
1560            range: ByteRange {
1561                start: 0x1000,
1562                end: 0x3000,
1563            },
1564            op: MemoryOp::Unmap,
1565        };
1566        let buffer = |op| Access::Memory {
1567            mm: mm(),
1568            range: ByteRange {
1569                start: 0x2ff0,
1570                end: 0x3010,
1571            },
1572            op,
1573        };
1574        assert!(unmap.conflicts_with(&buffer(MemoryOp::SyscallRead)));
1575        assert!(buffer(MemoryOp::SyscallRead).conflicts_with(&buffer(MemoryOp::SyscallWrite)));
1576        assert!(!buffer(MemoryOp::SyscallRead).conflicts_with(&buffer(MemoryOp::SyscallRead)));
1577
1578        let kill = Access::Lifetime {
1579            target: Task::Process(DetPid::from_raw(4)),
1580            op: LifetimeOp::Signal { signo: 9 },
1581        };
1582        let exit = Access::Lifetime {
1583            target: Task::Process(DetPid::from_raw(4)),
1584            op: LifetimeOp::Exit,
1585        };
1586        assert!(kill.conflicts_with(&exit));
1587        assert!(
1588            !kill.conflicts_with(&wake),
1589            "different resources never conflict"
1590        );
1591
1592        let end = |open_file, id, op| Access::Channel {
1593            open_file,
1594            id,
1595            channel: ChannelKind::StreamSocket,
1596            op,
1597        };
1598        let pair = Some(ChannelId::Pair(file(3, 1)));
1599        assert!(
1600            end(file(3, 1), pair, ChannelOp::Write).conflicts_with(&end(
1601                file(3, 2),
1602                pair,
1603                ChannelOp::Read
1604            )),
1605            "both ends of one socketpair"
1606        );
1607        assert!(
1608            !end(file(3, 1), None, ChannelOp::Write).conflicts_with(&end(
1609                file(3, 2),
1610                None,
1611                ChannelOp::Read
1612            )),
1613            "unrelated sockets"
1614        );
1615        let fifo = Some(ChannelId::Inode(DetInode::mint(9)));
1616        assert!(
1617            end(file(3, 5), fifo, ChannelOp::Close).conflicts_with(&end(
1618                file(4, 1),
1619                fifo,
1620                ChannelOp::Read
1621            )),
1622            "two opens of one FIFO"
1623        );
1624    }
1625}