1use 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
59pub const ACCESS_TRACE_FORMAT: &str = "hermit-access-trace";
61
62pub const ACCESS_TRACE_VERSION: u32 = 2;
67
68pub const ACCESS_TRACE_DEFAULT_MAX_BYTES: u64 = 1 << 30;
73
74pub const OUT_BYTES_CAP: usize = 256;
77
78pub type EventIndex = u64;
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(tag = "kind", rename_all = "snake_case")]
84pub enum TraceRecord {
85 Header(TraceHeader),
87 Syscall(SyscallRecord),
89 Signal(SignalRecord),
91 Edge(EdgeRecord),
93 WatchHit(WatchHitRecord),
95 End(TraceEnd),
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub struct TraceHeader {
102 pub format: String,
104 pub version: u32,
106 pub from: EventIndex,
110}
111
112impl TraceHeader {
113 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct TraceEnd {
126 pub events: u64,
128 pub records: u64,
130}
131
132#[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 pub tid: DetTid,
148 pub event: EventIndex,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct SyscallRecord {
155 pub pre: EventIndex,
157 pub post: Option<EventIndex>,
161 pub tid: DetTid,
163 pub pid: DetPid,
165 pub nth: u64,
168 #[serde(with = "sysno_name")]
171 pub sysno: Sysno,
172 pub args: [u64; 6],
174 pub result: Option<i64>,
177 #[serde(default, skip_serializing_if = "Vec::is_empty")]
180 pub accesses: Vec<Access>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub returned: Option<ReturnedBytes>,
187 #[serde(default, skip_serializing_if = "Option::is_none", with = "hex_bytes")]
190 pub out_bytes: Option<Vec<u8>>,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
195pub struct ReturnedBytes {
196 pub len: u64,
198 pub digest: u64,
201 pub truncated: bool,
204}
205
206pub const RETURNED_DIGEST_CAP: u64 = 64 * 1024;
208
209pub 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
218pub const RETURNED_DIGEST_SEED: u64 = 0xcbf2_9ce4_8422_2325;
220
221#[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 pub start: u64,
237 pub end: u64,
239}
240
241impl ByteRange {
242 pub fn overlaps(&self, other: &ByteRange) -> bool {
244 self.start < other.end && other.start < self.end
245 }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(tag = "resource", rename_all = "snake_case")]
252pub enum Access {
253 Fd {
255 slot: FdSlot,
257 open_file: Option<OpenFileId>,
261 op: FdOp,
263 },
264 OpenFile {
266 open_file: OpenFileId,
268 op: CursorOp,
270 },
271 FileContents {
273 inode: DetInode,
275 range: Option<ByteRange>,
278 op: DataOp,
280 },
281 Channel {
283 open_file: OpenFileId,
285 id: Option<ChannelId>,
291 channel: ChannelKind,
293 op: ChannelOp,
295 },
296 Futex {
298 futex: FutexID,
300 op: FutexOp,
302 },
303 Memory {
305 mm: MmId,
307 range: ByteRange,
309 op: MemoryOp,
311 },
312 Timer {
314 timer: TimerId,
316 op: TimerOp,
318 },
319 Lifetime {
321 target: Task,
323 op: LifetimeOp,
325 },
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
330#[serde(rename_all = "snake_case")]
331pub enum FdOp {
332 Use,
334 Open {
337 #[serde(default, skip_serializing_if = "Option::is_none")]
339 path: Option<String>,
340 },
341 Dup {
344 source: i32,
346 },
347 Close,
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
354#[serde(rename_all = "snake_case")]
355pub enum CursorOp {
356 Read,
358 Write,
360 Seek,
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
366#[serde(rename_all = "snake_case")]
367pub enum DataOp {
368 Read,
370 Write,
372}
373
374#[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 Pair(OpenFileId),
393 Inode(DetInode),
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
399#[serde(rename_all = "snake_case")]
400pub enum ChannelKind {
401 Pipe,
403 Fifo,
405 StreamSocket,
407 DatagramSocket,
409 SeqpacketSocket,
411 Socket,
413 Eventfd,
415}
416
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
419#[serde(rename_all = "snake_case")]
420pub enum ChannelOp {
421 Write,
423 Read,
425 Shutdown,
427 Close,
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
433#[serde(rename_all = "snake_case")]
434pub enum FutexOp {
435 Wait,
438 Wake,
441 RequeueTarget,
444 ModifyWake,
447 Lock,
450 Unlock,
452}
453
454#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
456#[serde(rename_all = "snake_case")]
457pub enum MemoryOp {
458 Map,
460 Unmap,
462 Protect,
464 Advise,
466 Brk,
468 SyscallRead,
470 SyscallWrite,
472}
473
474impl MemoryOp {
475 pub fn writes_or_remaps(self) -> bool {
478 self != MemoryOp::SyscallRead
479 }
480}
481
482#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
484#[serde(rename_all = "snake_case")]
485pub enum TimerId {
486 Posix {
488 process: DetPid,
490 id: i32,
492 },
493 Interval {
496 process: DetPid,
498 which: i32,
500 },
501 Fd {
503 open_file: OpenFileId,
505 },
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
510#[serde(rename_all = "snake_case")]
511pub enum TimerOp {
512 Create,
514 Set,
516 Get,
518 Delete,
520 Read,
522}
523
524#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
526#[serde(rename_all = "snake_case")]
527pub enum Task {
528 Thread(DetTid),
530 Process(DetPid),
532 ProcessGroup(DetPid),
538}
539
540#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
542#[serde(rename_all = "snake_case")]
543pub enum LifetimeOp {
544 Create,
546 Exit,
548 Exec,
550 Signal {
552 signo: i32,
554 },
555 Wait,
557}
558
559impl Access {
560 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 (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 (
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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
641#[serde(rename_all = "snake_case")]
642pub enum ObservedSignalDisposition {
643 Handler,
645 Ignored,
647 Default,
649 Unknown,
651}
652
653#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
655pub struct SignalSender {
656 pub pid: DetPid,
658 pub tid: Option<DetTid>,
660}
661
662#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
664pub struct SignalRecord {
665 pub event: EventIndex,
667 pub tid: DetTid,
669 pub signo: i32,
671 pub code: i32,
673 pub addr: Option<u64>,
676 pub sender: Option<SignalSender>,
678 pub observed_disposition: ObservedSignalDisposition,
681}
682
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
687#[serde(rename_all = "snake_case")]
688pub enum EdgeKind {
689 FutexWake,
692 FutexRequeue,
695 Create,
697 ExitJoin,
701 ChildExit,
704 Signal,
708}
709
710#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
712#[serde(rename_all = "snake_case")]
713pub enum SyncObject {
714 Futex(FutexID),
716 Thread(DetTid),
718 Process(DetPid),
720 Signal(i32),
722}
723
724#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
727pub struct EdgeRecord {
728 #[serde(rename = "edge")]
730 pub kind: EdgeKind,
731 pub from: EventRef,
733 pub to: EventRef,
735 pub object: SyncObject,
737}
738
739#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
741pub struct StackFrame {
742 pub ip: u64,
744 #[serde(default, skip_serializing_if = "Option::is_none")]
746 pub function: Option<String>,
747 #[serde(default, skip_serializing_if = "Option::is_none")]
749 pub file: Option<String>,
750 #[serde(default, skip_serializing_if = "Option::is_none")]
752 pub line: Option<u32>,
753}
754
755#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
757pub struct WatchHitRecord {
758 pub after: EventIndex,
761 pub tid: DetTid,
763 pub slot: u8,
765 pub addr: u64,
767 pub ip: u64,
769 #[serde(default, skip_serializing_if = "Vec::is_empty")]
771 pub stack: Vec<StackFrame>,
772}
773
774impl TraceRecord {
775 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 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#[derive(Debug, Clone, PartialEq, Eq)]
792pub struct AccessTrace {
793 pub header: TraceHeader,
795 pub records: Vec<TraceRecord>,
797 pub end: Option<TraceEnd>,
799}
800
801#[derive(Debug)]
803pub enum AccessTraceError {
804 Io(io::Error),
806 Empty,
808 BadLine {
810 line: usize,
812 message: String,
814 },
815 MissingHeader,
817 Unsupported {
819 format: String,
821 version: u32,
823 },
824 Misplaced {
826 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
866pub 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
910mod 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
930mod 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 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 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 #[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 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 #[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}