1use std::borrow::Cow;
23use std::collections::BTreeMap;
24use std::path::Path;
25
26use addr2line::gimli;
27use anyhow::Context as _;
28use detcore_model::happens_before::Anchor;
29use detcore_model::happens_before::CodeLocation;
30use detcore_model::happens_before::HappensBeforeProgram;
31use detcore_model::happens_before::HappensBeforeSpec;
32use detcore_model::happens_before::Position;
33use detcore_model::happens_before::syscall_never_reaches_tracer;
34use object::Object;
35use object::ObjectSection;
36use object::ObjectSymbol;
37use object::SymbolKind;
38
39#[derive(Clone, Copy, Debug)]
41struct FuncExtent {
42 addr: u64,
44 size: u64,
46}
47
48impl FuncExtent {
49 fn contains(&self, addr: u64) -> bool {
52 if self.size == 0 {
53 addr == self.addr
54 } else {
55 addr >= self.addr && addr < self.addr + self.size
56 }
57 }
58}
59
60#[derive(Clone, Debug)]
62struct LineRow {
63 addr: u64,
64 file: Option<String>,
65 line: u32,
66}
67
68#[derive(Clone, Debug, Default, PartialEq, Eq)]
70pub struct ResolvedLocation {
71 pub function: Option<String>,
73 pub file: Option<String>,
75 pub line: Option<u32>,
77}
78
79pub struct DebugInfoResolver {
81 functions: BTreeMap<String, FuncExtent>,
83 lines: Vec<LineRow>,
85}
86
87impl DebugInfoResolver {
88 pub fn open(path: &Path) -> anyhow::Result<Self> {
90 let data = std::fs::read(path)
91 .with_context(|| format!("reading binary for debug info: {}", path.display()))?;
92 Self::from_bytes(&data)
93 .with_context(|| format!("parsing debug info from {}", path.display()))
94 }
95
96 pub fn from_bytes(data: &[u8]) -> anyhow::Result<Self> {
98 let object = object::File::parse(data)?;
99
100 let mut functions: BTreeMap<String, FuncExtent> = BTreeMap::new();
102 for sym in object.symbols() {
103 if sym.kind() != SymbolKind::Text {
104 continue;
105 }
106 let addr = sym.address();
107 if addr == 0 {
108 continue;
109 }
110 if let Ok(name) = sym.name() {
111 if name.is_empty() {
112 continue;
113 }
114 functions
117 .entry(name.to_string())
118 .and_modify(|e| {
119 if e.size == 0 && sym.size() > 0 {
120 *e = FuncExtent {
121 addr,
122 size: sym.size(),
123 };
124 }
125 })
126 .or_insert(FuncExtent {
127 addr,
128 size: sym.size(),
129 });
130 }
131 }
132
133 let lines = Self::load_line_rows(&object).unwrap_or_default();
135
136 Ok(Self { functions, lines })
137 }
138
139 fn load_line_rows(object: &object::File<'_>) -> anyhow::Result<Vec<LineRow>> {
141 let endian = if object.is_little_endian() {
142 gimli::RunTimeEndian::Little
143 } else {
144 gimli::RunTimeEndian::Big
145 };
146
147 let load_section = |id: gimli::SectionId| -> Result<Cow<'_, [u8]>, gimli::Error> {
148 match object.section_by_name(id.name()) {
149 Some(section) => Ok(section
150 .uncompressed_data()
151 .unwrap_or(Cow::Borrowed(&[][..]))),
152 None => Ok(Cow::Borrowed(&[][..])),
153 }
154 };
155
156 let dwarf_sections = gimli::DwarfSections::load(load_section)?;
157 let dwarf = dwarf_sections.borrow(|section| gimli::EndianSlice::new(section, endian));
158
159 let mut rows: Vec<LineRow> = Vec::new();
160 let mut units = dwarf.units();
161 while let Some(header) = units.next()? {
162 let unit = dwarf.unit(header)?;
163 let Some(program) = unit.line_program.clone() else {
164 continue;
165 };
166 let mut state = program.rows();
167 while let Some((header, row)) = state.next_row()? {
168 if row.end_sequence() {
169 continue;
170 }
171 let Some(line) = row.line() else {
172 continue;
173 };
174 let file = row.file(header).and_then(|file_entry| {
175 dwarf
176 .attr_string(&unit, file_entry.path_name())
177 .ok()
178 .and_then(|r| r.to_string().ok().map(basename))
179 });
180 rows.push(LineRow {
181 addr: row.address(),
182 file,
183 line: line.get() as u32,
184 });
185 }
186 }
187 rows.sort_by_key(|r| r.addr);
188 Ok(rows)
189 }
190
191 pub fn is_empty(&self) -> bool {
193 self.functions.is_empty() && self.lines.is_empty()
194 }
195
196 pub fn function_addr(&self, name: &str) -> Option<u64> {
198 self.functions.get(name).map(|e| e.addr)
199 }
200
201 pub fn line_addr(
205 &self,
206 file: Option<&str>,
207 line: u32,
208 within: Option<(u64, u64)>,
209 ) -> Option<u64> {
210 let file_base = file.map(basename);
211 let extent = within.map(|(addr, size)| FuncExtent { addr, size });
212 self.lines
213 .iter()
214 .filter(|r| r.line == line)
215 .filter(|r| match (&file_base, &r.file) {
216 (Some(want), Some(have)) => want == have,
217 (Some(_), None) => false,
218 (None, _) => true,
219 })
220 .filter(|r| extent.map(|f| f.contains(r.addr)).unwrap_or(true))
221 .map(|r| r.addr)
222 .min()
223 }
224
225 pub fn resolve_location(&self, loc: &CodeLocation) -> Option<u64> {
229 match (&loc.function, loc.line) {
230 (Some(func), Some(line)) => {
231 let extent = self.functions.get(func).copied();
232 self.line_addr(loc.file.as_deref(), line, extent.map(|e| (e.addr, e.size)))
233 .or(extent.map(|e| e.addr))
236 }
237 (Some(func), None) => self.function_addr(func),
238 (None, Some(line)) => self.line_addr(loc.file.as_deref(), line, None),
239 (None, None) => None,
240 }
241 }
242
243 pub fn describe(&self, addr: u64) -> ResolvedLocation {
246 let function = self
249 .functions
250 .iter()
251 .filter(|(_, e)| e.contains(addr) || e.addr <= addr)
252 .max_by_key(|(_, e)| e.addr)
253 .map(|(name, _)| name.clone());
254
255 let idx = self.lines.partition_point(|r| r.addr <= addr);
257 let line_row = if idx > 0 {
258 self.lines.get(idx - 1)
259 } else {
260 None
261 };
262
263 ResolvedLocation {
264 function,
265 file: line_row.and_then(|r| r.file.clone()),
266 line: line_row.map(|r| r.line),
267 }
268 }
269}
270
271pub fn resolve_program(
274 program: &mut HappensBeforeProgram,
275 resolver: &DebugInfoResolver,
276) -> Vec<String> {
277 let mut unresolved = Vec::new();
278 for (name, anchor) in program.anchors.iter_mut() {
279 if let Position::Rip { addr: None, nth } = &anchor.position {
280 let nth = *nth;
281 if let Some(resolved) = resolver.resolve_location(&anchor.location) {
282 anchor.position = Position::Rip {
283 addr: Some(resolved),
284 nth,
285 };
286 } else {
287 unresolved.push(name.clone());
288 }
289 }
290 }
291 unresolved
292}
293
294pub fn load_program(path: &Path) -> anyhow::Result<HappensBeforeProgram> {
298 let json = std::fs::read_to_string(path)
299 .with_context(|| format!("reading happens-before spec: {}", path.display()))?;
300 let spec = HappensBeforeSpec::from_json(&json)
301 .with_context(|| format!("parsing happens-before spec: {}", path.display()))?;
302 let program = spec
303 .normalize()
304 .with_context(|| format!("normalizing happens-before spec: {}", path.display()))?;
305 Ok(program)
306}
307
308#[derive(Debug, Clone, Copy)]
311pub struct HbLaunch<'a> {
312 pub backend: &'a str,
314 pub ptrace: bool,
318 pub namespace_only: bool,
321 pub passthru_opt: bool,
323 pub preemption: bool,
326 pub replay_schedule: bool,
329}
330
331pub fn refuse_unenforceable_anchors(
354 program: &HappensBeforeProgram,
355 path: &Path,
356 launch: HbLaunch<'_>,
357) -> anyhow::Result<()> {
358 for anchor in program.anchors.values() {
359 let refusal = match &anchor.position {
360 Position::Syscall { .. } if !anchor.is_syscall_occurrence() => {
361 "names the polling phase, which is not enforced; syscall anchors fire at the \
362 prehook or, with \"phase\": \"posthook\", when the syscall completes"
363 .to_owned()
364 }
365 Position::Syscall { .. } if anchor.fires_at_posthook() && launch.replay_schedule => {
366 "fires at the posthook, which is not supported with --replay-schedule-from: \
367 its gate adds a scheduler turn the recorded schedule does not have"
368 .to_owned()
369 }
370 Position::Syscall { sysno, .. } if syscall_never_reaches_tracer(*sysno) => format!(
371 "names {}, which never reaches Hermit's tracer: it runs untraced so the \
372 signal frame is restored safely",
373 sysno.name()
374 ),
375 Position::Syscall { .. } | Position::SyscallCount(_) => {
376 let kind = if anchor.is_syscall_occurrence() {
377 "syscall-occurrence anchor"
378 } else {
379 "count anchor"
380 };
381 if launch.namespace_only {
382 format!(
383 "is a {kind}, but --namespace-only runs the guest without interception \
384 or a scheduler"
385 )
386 } else if !launch.ptrace {
387 format!(
388 "is a {kind}, which is enforced only on the ptrace backend; this run \
389 selected the {} backend",
390 launch.backend
391 )
392 } else if launch.passthru_opt && anchor.is_syscall_occurrence() {
393 format!(
397 "is a {kind}, which needs every syscall intercepted; --passthru-opt \
398 leaves some unintercepted"
399 )
400 } else {
401 continue;
402 }
403 }
404 _ => continue,
405 };
406 anyhow::bail!(
407 "happens-before spec {}: anchor '{}' ({}) {}",
408 path.display(),
409 anchor.name,
410 anchor.position,
411 refusal
412 );
413 }
414 if !launch.preemption {
415 refuse_hold_without_preemption(
416 program,
417 path,
418 "--max-timeslice disabled turns the preemption timer off",
419 )?;
420 }
421 Ok(())
422}
423
424pub fn refuse_hold_without_preemption(
434 program: &HappensBeforeProgram,
435 path: &Path,
436 reason: &str,
437) -> anyhow::Result<()> {
438 if !program.is_version_2() {
439 return Ok(());
440 }
441 anyhow::bail!(
442 "HERMIT_HB_HOLD_WITHOUT_PREEMPTION: happens-before spec {} is version {}, which bounds \
443 every hold by max_hold_ns and needs the preemption timer to charge it, but {}",
444 path.display(),
445 program.version,
446 reason
447 )
448}
449
450pub fn describe_anchor(anchor: &Anchor, resolver: Option<&DebugInfoResolver>) -> String {
453 let mut out = format!("{}", anchor);
454 if let (Position::Rip { addr: Some(a), .. }, Some(r)) = (&anchor.position, resolver) {
455 let d = r.describe(*a);
456 let where_ = match (&d.function, &d.file, d.line) {
457 (Some(f), _, Some(l)) => format!(" -> {:#x} ({}:{})", a, f, l),
458 (Some(f), _, None) => format!(" -> {:#x} ({})", a, f),
459 _ => format!(" -> {:#x}", a),
460 };
461 out.push_str(&where_);
462 }
463 out
464}
465
466fn basename(s: &str) -> String {
468 match s.rsplit_once('/') {
469 Some((_, base)) => base.to_string(),
470 None => s.to_string(),
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use std::process::Command;
477
478 use detcore_model::happens_before::HappensBeforeSpec;
479
480 use super::*;
481
482 fn compile_fixture() -> Option<std::path::PathBuf> {
485 use std::sync::atomic::AtomicUsize;
486 use std::sync::atomic::Ordering;
487 static COUNTER: AtomicUsize = AtomicUsize::new(0);
488
489 let cc = ["cc", "gcc", "clang"]
490 .into_iter()
491 .find(|c| Command::new(c).arg("--version").output().is_ok())?;
492
493 let uniq = COUNTER.fetch_add(1, Ordering::Relaxed);
494 let dir = std::env::temp_dir().join(format!("hb_fixture_{}_{}", std::process::id(), uniq));
495 std::fs::create_dir_all(&dir).ok()?;
496 let src = dir.join("fixture.c");
497 let bin = dir.join("fixture");
498 std::fs::write(
499 &src,
500 "int free_buffer(int x) { return x + 1; }\n\
501 int read_buffer(int x) { return x * 2; }\n\
502 int main(void) { return free_buffer(read_buffer(3)); }\n",
503 )
504 .ok()?;
505 let status = Command::new(cc)
506 .args(["-g", "-O0", "-o"])
507 .arg(&bin)
508 .arg(&src)
509 .status()
510 .ok()?;
511 if status.success() { Some(bin) } else { None }
512 }
513
514 #[test]
515 fn resolves_function_and_line_from_dwarf() {
516 let Some(bin) = compile_fixture() else {
517 eprintln!("skipping: no C compiler available");
518 return;
519 };
520 let resolver = DebugInfoResolver::open(&bin).unwrap();
521 assert!(!resolver.is_empty(), "expected symbols/lines in -g binary");
522
523 let free_addr = resolver.function_addr("free_buffer");
525 assert!(free_addr.is_some(), "free_buffer should resolve");
526 assert_ne!(free_addr.unwrap(), 0);
527
528 let loc = CodeLocation {
531 function: Some("free_buffer".to_string()),
532 file: None,
533 line: Some(1),
534 };
535 let addr = resolver.resolve_location(&loc);
536 assert!(addr.is_some(), "func+line should resolve");
537
538 let d = resolver.describe(free_addr.unwrap());
540 assert_eq!(d.function.as_deref(), Some("free_buffer"));
541
542 let _ = std::fs::remove_dir_all(bin.parent().unwrap());
543 }
544
545 #[test]
546 fn resolve_program_fills_rips() {
547 let Some(bin) = compile_fixture() else {
548 eprintln!("skipping: no C compiler available");
549 return;
550 };
551 let resolver = DebugInfoResolver::open(&bin).unwrap();
552
553 let json = r#"{
554 "version": 1,
555 "threads": { "t": {"label": "t"} },
556 "events": {
557 "A": {"thread": "t", "func": "free_buffer"},
558 "B": {"thread": "t", "func": "read_buffer", "line": 2}
559 },
560 "edges": [ {"before": "A", "after": "B"} ]
561 }"#;
562 let mut prog = HappensBeforeSpec::from_json(json)
563 .unwrap()
564 .normalize()
565 .unwrap();
566 assert_eq!(prog.unresolved_locations().count(), 2);
567
568 let missing = resolve_program(&mut prog, &resolver);
569 assert!(
570 missing.is_empty(),
571 "all locations should resolve: {:?}",
572 missing
573 );
574 assert_eq!(prog.unresolved_locations().count(), 0);
575
576 for anchor in prog.anchors.values() {
577 match &anchor.position {
578 Position::Rip { addr: Some(a), .. } => assert_ne!(*a, 0),
579 other => panic!("expected resolved RIP, got {:?}", other),
580 }
581 }
582
583 let _ = std::fs::remove_dir_all(bin.parent().unwrap());
584 }
585
586 #[test]
587 fn missing_function_reported() {
588 let Some(bin) = compile_fixture() else {
589 eprintln!("skipping: no C compiler available");
590 return;
591 };
592 let resolver = DebugInfoResolver::open(&bin).unwrap();
593 let json = r#"{
594 "version": 1,
595 "events": { "A": {"thread": "1", "func": "no_such_function"} },
596 "edges": []
597 }"#;
598 let mut prog = HappensBeforeSpec::from_json(json)
599 .unwrap()
600 .normalize()
601 .unwrap();
602 let missing = resolve_program(&mut prog, &resolver);
603 assert_eq!(missing, vec!["A".to_string()]);
604
605 let _ = std::fs::remove_dir_all(bin.parent().unwrap());
606 }
607
608 #[test]
615 fn launch_refuses_every_anchor_it_cannot_enforce() {
616 let dir = tempfile::tempdir().unwrap();
617 let spec = dir.path().join("spec.json");
618 let ptrace = HbLaunch {
619 backend: "Ptrace",
620 ptrace: true,
621 namespace_only: false,
622 passthru_opt: false,
623 preemption: true,
624 replay_schedule: false,
625 };
626 let check = |json: &str, launch: HbLaunch<'_>| -> Result<(), String> {
627 std::fs::write(&spec, json).unwrap();
628 let program = load_program(&spec).expect("every spec here loads for preview");
629 refuse_unenforceable_anchors(&program, &spec, launch).map_err(|e| e.to_string())
630 };
631 let fd_anchor = r#"{"version": 1,
632 "events": {"w": {"thread": "3", "syscall": "write", "fd": 1}}}"#;
633 let err = check(
634 r#"{"version": 1,
635 "events": {"closed": {"thread": "3", "syscall": "close", "fd": 4, "phase": "polling"}}}"#,
636 ptrace,
637 )
638 .unwrap_err();
639 assert!(
640 err.contains("anchor 'closed'")
641 && err.contains("names the polling phase, which is not enforced"),
642 "{err}"
643 );
644 let err = check(
645 r#"{"version": 1, "events": {"ret": {"thread": "3", "syscall": "rt_sigreturn"}}}"#,
646 ptrace,
647 )
648 .unwrap_err();
649 assert!(
650 err.contains("anchor 'ret'") && err.contains("never reaches Hermit's tracer"),
651 "{err}"
652 );
653 let err = check(
654 fd_anchor,
655 HbLaunch {
656 namespace_only: true,
657 ..ptrace
658 },
659 )
660 .unwrap_err();
661 assert!(
662 err.contains("anchor 'w'") && err.contains("--namespace-only"),
663 "{err}"
664 );
665 let err = check(
666 fd_anchor,
667 HbLaunch {
668 backend: "Kvm",
669 ptrace: false,
670 ..ptrace
671 },
672 )
673 .unwrap_err();
674 assert!(
675 err.contains("anchor 'w'") && err.contains("the Kvm backend"),
676 "{err}"
677 );
678 let err = check(
679 fd_anchor,
680 HbLaunch {
681 passthru_opt: true,
682 ..ptrace
683 },
684 )
685 .unwrap_err();
686 assert!(
687 err.contains("anchor 'w'") && err.contains("--passthru-opt"),
688 "{err}"
689 );
690 assert!(check(fd_anchor, ptrace).is_ok());
691 let posthook = r#"{"version": 1,
696 "events": {"p": {"thread": "3", "syscall": "write", "fd": 1, "phase": "posthook"}}}"#;
697 assert!(check(posthook, ptrace).is_ok());
698 let replay = HbLaunch {
699 replay_schedule: true,
700 ..ptrace
701 };
702 let err = check(posthook, replay).unwrap_err();
703 assert!(
704 err.contains("anchor 'p'") && err.contains("not supported with --replay-schedule-from"),
705 "{err}"
706 );
707 assert!(check(fd_anchor, replay).is_ok());
708 assert!(
709 check(
710 r#"{"version": 1,
711 "events": {"closed": {"thread": "3", "syscall": "close", "fd": 4, "phase": "pre"}}}"#,
712 ptrace
713 )
714 .is_ok()
715 );
716 let count_only = r#"{"version": 1, "events": {"c": {"thread": "3", "syscalls": 5}}}"#;
720 let err = check(
721 count_only,
722 HbLaunch {
723 namespace_only: true,
724 ..ptrace
725 },
726 )
727 .unwrap_err();
728 assert!(
729 err.contains("anchor 'c' (after 5 syscalls) is a count anchor, but --namespace-only"),
730 "{err}"
731 );
732 let err = check(
733 count_only,
734 HbLaunch {
735 backend: "Sabre",
736 ptrace: false,
737 ..ptrace
738 },
739 )
740 .unwrap_err();
741 assert!(
742 err.contains("is a count anchor, which is enforced only on the ptrace backend")
743 && err.contains("the Sabre backend"),
744 "{err}"
745 );
746 assert!(
747 check(
748 count_only,
749 HbLaunch {
750 passthru_opt: true,
751 ..ptrace
752 }
753 )
754 .is_ok()
755 );
756 assert!(check(count_only, ptrace).is_ok());
757
758 let v2 = r#"{"version": 2, "events": {"w": {"thread": "3", "syscall": "write", "fd": 1}}}"#;
761 let no_timer = HbLaunch {
762 preemption: false,
763 ..ptrace
764 };
765 let err = check(v2, no_timer).unwrap_err();
766 assert!(
767 err.starts_with("HERMIT_HB_HOLD_WITHOUT_PREEMPTION: happens-before spec ")
768 && err.contains("is version 2")
769 && err.ends_with("but --max-timeslice disabled turns the preemption timer off"),
770 "{err}"
771 );
772 assert!(check(v2, ptrace).is_ok());
773 assert!(check(fd_anchor, no_timer).is_ok());
774 }
775}