Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fefa4d5
Add DWARF/ELF debug info backend for UEFI/SMM source coverage (M1)
Wenzel Sep 14, 2026
7bb9105
Add offline DWARF fixture test for DwarfModule::intervals (M1)
Wenzel Sep 14, 2026
2dbca69
Fix SourceCache::new crashing when no live Simics session exists
Wenzel Sep 14, 2026
92a3eb5
Add UEFI module discovery parsing/resolution (UCOV-M2 milestone steps…
Wenzel Sep 16, 2026
cb5fc2b
fix: correct list-modules parsing to match confirmed live shape (UCOV…
Wenzel Sep 16, 2026
73bc670
Switch UEFI module discovery to tracker_obj->maps, not list-modules
Wenzel Sep 16, 2026
714b986
fix: handle real Nil pathless row in tracker_obj->maps parsing
Wenzel Sep 16, 2026
0c554f1
merge: DWARF source coverage (UCOV-M1)
Wenzel Sep 16, 2026
6695e19
merge: UEFI module discovery (UCOV-M2)
Wenzel Sep 16, 2026
a40b466
feat: wire UEFI module discovery into DWARF source coverage at HARNES…
Wenzel Sep 16, 2026
7148ce6
fix: skip Invalid top-level entries in tracker_obj->maps parsing
Wenzel Sep 16, 2026
50de300
fix: create symbolic_coverage_directory when it doesn't exist, not wh…
Wenzel Sep 16, 2026
ef8219b
fix: treat empty coverage as a no-op in save_symbolic_coverage, not a…
Wenzel Sep 16, 2026
d00936c
fix: read tracker_obj->maps via get_attribute, not run_command
Wenzel Sep 16, 2026
1905bc4
debug: add temporary diagnostic log for raw/parsed module row counts
Wenzel Sep 16, 2026
5ea59f6
fix: convert tracker_obj->maps rows via as_heterogeneous_list, not pl…
Wenzel Sep 16, 2026
448aad1
fix: skip unresolvable modules per-module, not fail the whole resolve…
Wenzel Sep 16, 2026
4f39b6d
debug: add temporary diagnostic logs for resolve/element counts
Wenzel Sep 16, 2026
defaff2
docs: remove temporary diagnostics, document debuginfo_source_directo…
Wenzel Sep 16, 2026
7ec6f40
fix: resolve DWARF subprogram names via DW_AT_abstract_origin/specifi…
Wenzel Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ sha2 = "0.10.8"
typed-path = "0.9.0"
thiserror = "1.0.63"
lcov2 = "0.1.0"
gimli = "0.34.0"
object = "0.40.0"

[dev-dependencies]
simics-test = "0.2.6"
Expand All @@ -111,6 +113,7 @@ command-ext = "0.1.2"
indoc = "2.0.5"
ispm-wrapper = "0.2.6"
versions = { version = "6.2.0", features = ["serde"] }
tempfile = "3.13.0"

[build-dependencies]
simics = "0.2.6"
Expand Down
429 changes: 429 additions & 0 deletions src/dwarf/mod.rs

Large diffs are not rendered by default.

75 changes: 74 additions & 1 deletion src/haps/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

//! Handlers for HAPs in the simulator

use std::time::SystemTime;
use std::{collections::HashSet, time::SystemTime};

use crate::{
arch::ArchitectureOperations,
Expand All @@ -13,6 +13,7 @@ use crate::{
ManualStartInfo, Tsffs,
};
use anyhow::{anyhow, bail, Result};
use intervaltree::IntervalTree;
use libafl::prelude::ExitKind;
use simics::{
api::{
Expand All @@ -39,6 +40,73 @@ enum SnapshotRestoreMode {
}

impl Tsffs {
/// Collect UEFI/SMM source coverage info, if `uefi` and `symbolic_coverage` are
/// both set. Called once, at `HARNESS_START` (the same three call sites Windows
/// uses for its own initial collection), rather than on a recurring trigger --
/// see `crate::uefi::collect_symbols`'s doc comment for why UEFI/SMM has no
/// CR3-write-equivalent refresh signal the way Windows does.
///
/// The resulting interval tree is stored in `self.windows_os_info`, alongside
/// Windows's own per-processor symbol lookup trees, keyed the same way (by
/// processor number). `self.uefi` and `self.windows` are mutually exclusive in
/// practice (a target is either a Windows kernel or a UEFI/SMM BIOS, not both),
/// so this reuses the exact same storage and the tracer's existing OS-agnostic
/// coverage lookup (`src/tracer/mod.rs`, the `self.coverage_enabled &&
/// self.symbolic_coverage` branch, which does not itself check `self.windows`)
/// rather than duplicating a second lookup path just for `uefi`.
fn collect_uefi_symbolic_coverage(&mut self, processor: *mut ConfObject) -> Result<()> {
if !(self.uefi && self.symbolic_coverage) {
return Ok(());
}

info!(
self.as_conf_object(),
"Collecting initial UEFI/SMM source coverage info"
);

let elements = crate::uefi::collect_symbols(
&self.uefi_tracker_object,
&self.uefi_debug_info_directory,
&self.source_file_cache,
)?;

let mut filtered_ranges = HashSet::new();

// Deduplicate elements by their range, mirroring
// `WindowsOsInfo::collect`'s own deduplication.
let elements = elements
.into_iter()
.filter(|e| filtered_ranges.insert(e.range.clone()))
.collect::<Vec<_>>();

// Populate elements into the coverage record set, mirroring
// `WindowsOsInfo::collect`'s own population of `user_debug_info.coverage`.
elements.iter().map(|e| &e.value).for_each(|si| {
if let Some(first) = si.lines.first() {
let record = self.coverage.get_or_insert_mut(&first.file_path);
record.add_function_if_not_exists(
first.start_line as usize,
si.lines.last().map(|l| l.end_line as usize),
&si.name,
);
si.lines.iter().for_each(|l| {
(l.start_line..=l.end_line).for_each(|line| {
record.add_line_if_not_exists(line as usize);
});
});
}
});

let processor_nr = get_processor_number(processor)?;

self.windows_os_info.symbol_lookup_trees.insert(
processor_nr,
elements.into_iter().collect::<IntervalTree<_, _>>(),
);

Ok(())
}

fn on_simulation_stopped_magic_start(&mut self, magic_number: MagicNumber) -> Result<()> {
if !self.have_initial_snapshot() {
self.start_fuzzer_thread()?;
Expand Down Expand Up @@ -86,6 +154,7 @@ impl Tsffs {
&self.source_file_cache,
)?;
}
self.collect_uefi_symbolic_coverage(start_processor_raw)?;
self.get_and_write_testcase()?;
self.post_timeout_event()?;
}
Expand Down Expand Up @@ -283,6 +352,8 @@ impl Tsffs {
)?;
}

self.collect_uefi_symbolic_coverage(processor)?;

self.get_and_write_testcase()?;

self.post_timeout_event()?;
Expand Down Expand Up @@ -325,6 +396,8 @@ impl Tsffs {
)?;
}

self.collect_uefi_symbolic_coverage(processor)?;

self.post_timeout_event()?;
}

Expand Down
81 changes: 72 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,36 @@ use typed_builder::TypedBuilder;
use versions::{Requirement, Versioning};

pub(crate) mod arch;
// `pub` (not `pub(crate)`): needed so the offline DWARF fixture test in
// `tests/dwarf_fixture.rs` (a separate cargo target/crate, since `[lib] test = false`
// means unit tests can't live inside this crate -- see that file's module doc) can
// reach `DwarfModule`/`DebugInfoModule`/`SourceCache` at all. `os` and `traits`
// stay `pub(crate)`; `dwarf::mod.rs` re-exports just the debug-info types
// (`SymbolInfo`/`LineInfo`) and the `DebugInfoModule` trait the test needs,
// instead of widening all of `os` (Windows kernel/PDB internals) or `traits`
// (which also holds the unrelated, and itself not-fully-public,
// `TracerDisassembler` trait).
pub mod dwarf;
pub(crate) mod fuzzer;
pub(crate) mod haps;
pub(crate) mod interfaces;
pub(crate) mod log;
pub(crate) mod magic;
pub(crate) mod os;
pub(crate) mod source_cov;
pub mod source_cov;
pub(crate) mod state;
pub(crate) mod tracer;
pub(crate) mod traits;
// `pub` (not `pub(crate)`): needed so the offline UEFI module discovery fixture
// test in `tests/uefi_module_discovery_fixture.rs` (UCOV-M2, milestone-scope steps
// 1-2 -- see that file's module doc) can reach `uefi::{parse_module_list,
// UefiOsInfo}` at all. This is a separate cargo target/crate (this crate's `[lib]`
// section sets `test = false`, so `#[cfg(test)]` code inside `src/` is never
// compiled by `cargo test`), so it only sees this crate's `pub` API -- the same
// reason the sibling DWARF milestone widened `dwarf`/`source_cov` similarly.
// `util` (which `uefi` itself depends on for `PathSuffixIndex`) stays
// `pub(crate)`, since the test doesn't need to reach it directly.
pub mod uefi;
pub(crate) mod util;

/// The class name used for all operations interfacing with SIMICS
Expand Down Expand Up @@ -445,6 +465,30 @@ pub(crate) struct Tsffs {
/// Directory in which source files are located. Source files do not need to be arranged in
/// the same directory structure as the compiled source, and are looked up by hash.
pub symbolic_coverage_directory: PathBuf,
#[class(attribute(optional, default = false))]
/// Whether UEFI/SMM is being run in the simulation. When set with
/// `symbolic_coverage`, TSFFS collects source coverage for UEFI/SMM modules at
/// `HARNESS_START` by querying `uefi_tracker_object`'s loaded module list and
/// resolving each module's DWARF debug info under `uefi_debug_info_directory`.
/// `debuginfo_source_directory` must also point at the real local source tree
/// (e.g. the EDK2 checkout) for any source *lines* to be resolved -- symbols
/// resolve independently of it, but every one of them will have zero lines
/// (and thus never contribute to the coverage report) without it, since DWARF
/// line entries are resolved against `debuginfo_source_directory`, not
/// `uefi_debug_info_directory` (which only locates each module's own `.debug`
/// file, not its original source).
pub uefi: bool,
#[class(attribute(optional, default = String::new()))]
/// The Simics object path of the UEFI/SMM module tracker to query for the loaded
/// module list (e.g. `board.software.tracker.tracker_obj`, queried via its
/// `->maps` attribute), used when `uefi` is set. Board-specific; there is no
/// default.
pub uefi_tracker_object: String,
#[class(attribute(optional, default = lookup_file("%simics%")?.join("uefi-debug-info")))]
/// Local build-output directory that UEFI/SMM modules' embedded build-machine
/// paths (reported by `uefi_tracker_object`) are resolved against, to locate
/// each module's local `.debug` DWARF sidecar file, used when `uefi` is set.
pub uefi_debug_info_directory: PathBuf,

/// Handle for the core simulation stopped hap
stop_hap_handle: HapHandle,
Expand Down Expand Up @@ -1004,7 +1048,7 @@ impl Tsffs {
}

pub fn save_symbolic_coverage(&mut self) -> Result<()> {
if self.symbolic_coverage_directory.is_dir() {
if !self.symbolic_coverage_directory.is_dir() {
create_dir_all(&self.symbolic_coverage_directory)?;
}

Expand All @@ -1014,13 +1058,32 @@ impl Tsffs {
self.symbolic_coverage_directory.display()
);

self.coverage.to_html(&self.symbolic_coverage_directory)?;

debug!(
self.as_conf_object(),
"Symbolic coverage saved to {}",
self.symbolic_coverage_directory.display()
);
// `Records::to_html` seeds its output tree from the records themselves, so
// if no source line was ever recorded (e.g. a short run whose covered code
// never lands inside a symbolicated module -- has been observed for real
// with UEFI/SMM coverage, whose HARNESS_START may fire before every module
// is loaded), it never creates a graph node for `output_directory` at all,
// and its own root-node lookup fails with `NodeNotFound` on that exact
// path. That's an empty-coverage outcome, not a real error, so it's
// reported and skipped rather than propagated as one.
match self.coverage.to_html(&self.symbolic_coverage_directory) {
Ok(()) => {
debug!(
self.as_conf_object(),
"Symbolic coverage saved to {}",
self.symbolic_coverage_directory.display()
);
}
Err(lcov2::error::Error::NodeNotFound { ref path })
if *path == self.symbolic_coverage_directory =>
{
debug!(
self.as_conf_object(),
"No symbolic coverage was recorded this run; skipping HTML report generation"
);
}
Err(e) => return Err(e.into()),
}

Ok(())
}
Expand Down
20 changes: 19 additions & 1 deletion src/os/windows/debug_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use windows_sys::Win32::System::{
SystemServices::{FILE_NOTIFY_FULL_INFORMATION, IMAGE_DOS_HEADER},
};

use crate::{os::DebugInfoConfig, source_cov::SourceCache};
use crate::{os::DebugInfoConfig, source_cov::SourceCache, traits::DebugInfoModule};

use super::{
pdb::{CvInfoPdb70, Export},
Expand Down Expand Up @@ -470,6 +470,15 @@ impl ProcessModule {
}
}

impl DebugInfoModule for ProcessModule {
/// Delegates to `ProcessModule::intervals` so PDB-backed process modules can be
/// used interchangeably with other `DebugInfoModule` implementations (e.g.
/// DWARF-backed modules).
fn intervals(&mut self, source_cache: &SourceCache) -> Result<Vec<Element<u64, SymbolInfo>>> {
ProcessModule::intervals(self, source_cache)
}
}

#[derive(Debug)]
/// A process
pub struct Process {
Expand Down Expand Up @@ -675,3 +684,12 @@ impl Module {
.collect())
}
}

impl DebugInfoModule for Module {
/// Delegates to `Module::intervals` so PDB-backed kernel modules can be used
/// interchangeably with other `DebugInfoModule` implementations (e.g. DWARF-backed
/// modules).
fn intervals(&mut self, source_cache: &SourceCache) -> Result<Vec<Element<u64, SymbolInfo>>> {
Module::intervals(self, source_cache)
}
}
Loading
Loading