diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 531bae6..4c7247c 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -103,7 +103,7 @@ jobs: sudo apt-get update -q sudo apt-get install -qy --no-install-recommends gcc-14 flex bison bc libelf-dev libssl-dev \ cpio "$QEMU" busybox-static libcap-dev libnuma-dev rsync libaio-dev libacl1-dev \ - libkeyutils-dev + libkeyutils-dev dwarves env: QEMU: ${{ inputs.row == 'A64' && 'qemu-system-arm' || 'qemu-system-x86' }} - name: let the runner user open /dev/kvm @@ -212,6 +212,16 @@ jobs: > "$RUNNER_TEMP/checks/frames.md"; then verdict=clean; else verdict=findings; fi echo "$verdict" > "$RUNNER_TEMP/checks/frames.status" cat "$RUNNER_TEMP/checks/frames.md" + - name: rk btf + if: always() && steps.test.outcome != 'skipped' + run: | + mkdir -p "$RUNNER_TEMP/checks" + rc=0 + target/release/rk btf --reference "$RUNNER_TEMP/gcc" --other "$RUNNER_TEMP/rucc" \ + > "$RUNNER_TEMP/checks/btf.md" || rc=$? + case $rc in 0) verdict=clean ;; 1) verdict=findings ;; *) verdict="not run" ;; esac + echo "$verdict" > "$RUNNER_TEMP/checks/btf.status" + cat "$RUNNER_TEMP/checks/btf.md" - name: rk mixed on the first failed unit if: failure() && steps.test.outcome == 'failure' timeout-minutes: 180 @@ -265,23 +275,23 @@ jobs: run: | import json, os, pathlib row = os.environ["ROW"] - lines = [f"### nightly, {row}", "", "| version | graded | failed | splats | verdict | objtool | frames |", "|---|---|---|---|---|---|---|"] + lines = [f"### nightly, {row}", "", "| version | graded | failed | splats | verdict | objtool | frames | btf |", "|---|---|---|---|---|---|---|---|"] for record in sorted(pathlib.Path("records").iterdir()): version = record.name.removeprefix("nightly-").removesuffix(f"-{row}") path = record / "test" / "test.json" checks = [] - for name in ("objtool", "frames"): + for name in ("objtool", "frames", "btf"): status = record / "checks" / f"{name}.status" checks.append(status.read_text().strip() if status.exists() else "not run") if not path.exists(): - lines.append(f"| {version} | | | | not run | {checks[0]} | {checks[1]} |") + lines.append(f"| {version} | | | | not run | {checks[0]} | {checks[1]} | {checks[2]} |") continue t = json.loads(path.read_text()) graded = [u for u in t["units"] if u["graded"]] failed = [u["name"] for u in graded if not u["passed"]] splats = len(t["splats"].get("other-only", [])) verdict = "green" if not failed and not splats else "red" - lines.append(f"| {version} | {len(graded)} | {len(failed)} | {splats} | {verdict} | {checks[0]} | {checks[1]} |") + lines.append(f"| {version} | {len(graded)} | {len(failed)} | {splats} | {verdict} | {checks[0]} | {checks[1]} | {checks[2]} |") for record in sorted(pathlib.Path("records").iterdir()): summary = record / "mixed" / "summary.md" if summary.exists(): diff --git a/README.md b/README.md index 2cbee10..53f9f65 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,9 @@ The reference compilers live in era containers built from `provision/eras` and p `rk config-diff`, `rk probes`, `rk flags-diff`, `rk demands` and `rk asm-inventory` read build directories and write markdown tables: the `.config` differences, the compiler probes the two compilers answered differently, the flags each unit was compiled with on one side only (read from the `.cmd` files kbuild writes), the failed units by error, and the instructions the kernel writes itself. `rk config-diff --why` also names the Kconfig expressions behind each difference and the probe that decided them, and needs the kernel tree, which it finds through `build.json` or `--source`. `rk syntax --build DIR --cc rucc` replays every unit of a reference build through rucc's front end with the reference's own command lines, `-fsyntax-only` for C and `-E` for assembly, and writes the failing units grouped by their first error. Units listed with an issue in `syntax-known.toml` are known failures, so the command fails only on new ones. -`rk sections-diff`, `rk symvers-diff`, `rk vec-audit`, `rk modules-audit` and `rk objtool-report` look at what the compilers produced. `rk sections-diff` compares the section names of every object two builds share, the size and relocation count of each kernel table (exports, jump labels, alternatives, exception fixups, initcalls and the rest), the call-site lists per function, and the `.modinfo` and `__ksymtab_strings` strings. A difference that `sections-divergences.toml` explains, such as a constant pool only gcc builds or a WARN check only one compiler could prove dead, is listed apart with its reason and does not fail the run. It lists the first 60 differences of each kind, and every one with `--all`. `rk symvers-diff` compares `Module.symvers`, CRCs and namespaces included, and the global symbols of `System.map`. `rk vec-audit` decodes the x86-64 code of every unit built with `-mno-sse` or `-mgeneral-regs-only` and lists the vector and x87 instructions it finds, only those above the reference's count when given `--reference`. `rk modules-audit` fails on any module relocation the loader rejects, a wrong `vermagic` or an imported CRC that does not match `Module.symvers`. `rk objtool-report` buckets the objtool warnings of two build logs by message and function shape. `rk frames` compares the stack frame of every function in two builds made with `rk build --stack-usage`, which passes `KCFLAGS=-fstack-usage` so that each compiler writes a `.su` file next to every object. It fails on a function over `CONFIG_FRAME_WARN` that the reference keeps under it, and on a run time stack high water mark more than 10% above the reference's when both build directories have a `boot.log` from a kernel with `CONFIG_DEBUG_STACK_USAGE`. `rk sections-diff`, `rk vec-audit` and `rk frames` take `--save FILE` to keep what they read as JSON, and accept such a file in place of a build directory, which is how CI compares builds without moving their objects. +`rk sections-diff`, `rk symvers-diff`, `rk vec-audit`, `rk modules-audit` and `rk objtool-report` look at what the compilers produced. `rk sections-diff` compares the section names of every object two builds share, the size and relocation count of each kernel table (exports, jump labels, alternatives, exception fixups, initcalls and the rest), the call-site lists per function, and the `.modinfo` and `__ksymtab_strings` strings. A difference that `sections-divergences.toml` explains, such as a constant pool only gcc builds or a WARN check only one compiler could prove dead, is listed apart with its reason and does not fail the run. It lists the first 60 differences of each kind, and every one with `--all`. `rk symvers-diff` compares `Module.symvers`, CRCs and namespaces included, and the global symbols of `System.map`. `rk vec-audit` decodes the x86-64 code of every unit built with `-mno-sse` or `-mgeneral-regs-only` and lists the vector and x87 instructions it finds, only those above the reference's count when given `--reference`. `rk modules-audit` fails on any module relocation the loader rejects, a wrong `vermagic` or an imported CRC that does not match `Module.symvers`. `rk objtool-report` buckets the objtool warnings of two build logs by message and function shape. `rk frames` compares the stack frame of every function in two builds made with `rk build --stack-usage`, which passes `KCFLAGS=-fstack-usage` so that each compiler writes a `.su` file next to every object. It fails on a function over `CONFIG_FRAME_WARN` that the reference keeps under it, and on a run time stack high water mark more than 10% above the reference's when both build directories have a `boot.log` from a kernel with `CONFIG_DEBUG_STACK_USAGE`. `rk btf` reads the `.BTF` section pahole put in each build's vmlinux and writes every named type and function prototype out by name, so that type IDs do not matter. It fails on a type the two builds describe differently or only one has, and on a function both have with different prototypes, and it lists functions only one build has without failing, since the compilers inline differently. `rk sections-diff`, `rk vec-audit`, `rk frames` and `rk btf` take `--save FILE` to keep what they read as JSON, and accept such a file in place of a build directory, which is how CI compares builds without moving their objects. -The nightly workflow builds every pin in the current set on X64 with gcc-14 and with rucc, from `tinyconfig` and the test fragment, and runs `rk test` on boot, smoke and KUnit. When the rucc kernel fails a unit, `rk mixed` runs on the first one in the same job. The run's summary has a line per version, green or red, whether `rk objtool-report` and `rk frames` came out clean, and what `rk mixed` found. A manual run takes a branch or commit of tamnd/rucc, another configuration target, the versions, the row and the QEMU CPU, so `row=A64 cpu=cortex-a57 config=debian-13-arm64` runs every test kind on arm64 under TCG on the arm runner. +The nightly workflow builds every pin in the current set on X64 with gcc-14 and with rucc, from `tinyconfig` and the test fragment, and runs `rk test` on boot, smoke and KUnit. When the rucc kernel fails a unit, `rk mixed` runs on the first one in the same job. The run's summary has a line per version, green or red, whether `rk objtool-report`, `rk frames` and `rk btf` came out clean (`rk btf` is not run when the configuration has no BTF, as `tinyconfig` does not), and what `rk mixed` found. A manual run takes a branch or commit of tamnd/rucc, another configuration target, the versions, the row and the QEMU CPU, so `row=A64 cpu=cortex-a57 config=debian-13-arm64` runs every test kind on arm64 under TCG on the arm runner. The mixed workflow is run by hand. It builds the rucc kernel with `-fenable-inject-fault=time64_to_tm`, checks that the KUnit case for that function fails, and passes only when `rk mixed` traces the failure back to the `inject-fault` pass in that function. diff --git a/crates/rk/src/btf.rs b/crates/rk/src/btf.rs new file mode 100644 index 0000000..e2067d0 --- /dev/null +++ b/crates/rk/src/btf.rs @@ -0,0 +1,549 @@ +//! `rk btf`: the BTF of two builds' vmlinux, compared. +//! +//! With `CONFIG_DEBUG_INFO_BTF` the kernel build runs pahole over the DWARF of vmlinux and links +//! what it says into a `.BTF` section, which BPF programs, `bpftool` and the verifier read. A +//! compiler whose DWARF pahole reads differently gives a kernel whose BTF describes other types, +//! and BPF programs built against the reference then fail to load. The plan's check (09, K5) is +//! that both builds describe the same types and the same functions, and that functions only one +//! of them has, because the compilers inlined differently, are listed and not failed. +//! +//! Type IDs differ between any two builds, so each type is written out by name instead: a named +//! struct, union, enum or typedef is referred to by its name, and an anonymous one is written in +//! full where it is used. Every named type then has a description, and the same name may have more +//! than one, since two files may define different structs of one name. Both builds must give each +//! name the same descriptions, and each function the same prototype. + +use object::{Object as _, ObjectSection as _}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::Write as _; +use std::path::Path; + +/// How many lines of each list the report shows. +const SHOWN: usize = 40; + +/// How deep a description goes before it stops, against a type that refers to itself without a +/// name in between, which well formed BTF does not have. +const DEEPEST: usize = 64; + +/// What a build's BTF says, which `--save` keeps as JSON. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Btf { + /// Each named type's descriptions, by kind and name, such as `struct file`. + pub types: BTreeMap>, + /// Each function's prototypes, by name. + pub functions: BTreeMap>, +} + +/// One type as it is in the section, with the words that follow it. +#[derive(Debug, Clone)] +struct Raw { + name: String, + kind: u32, + vlen: usize, + kind_flag: bool, + size_or_type: u32, + extra: Vec, +} + +const INT: u32 = 1; +const PTR: u32 = 2; +const ARRAY: u32 = 3; +const STRUCT: u32 = 4; +const UNION: u32 = 5; +const ENUM: u32 = 6; +const FWD: u32 = 7; +const TYPEDEF: u32 = 8; +const VOLATILE: u32 = 9; +const CONST: u32 = 10; +const RESTRICT: u32 = 11; +const FUNC: u32 = 12; +const FUNC_PROTO: u32 = 13; +const VAR: u32 = 14; +const DATASEC: u32 = 15; +const FLOAT: u32 = 16; +const DECL_TAG: u32 = 17; +const TYPE_TAG: u32 = 18; +const ENUM64: u32 = 19; + +/// Read the `.BTF` section of a vmlinux, or of the vmlinux in a build directory. +pub fn scan(path: &Path) -> Result { + let file = if path.is_dir() { + path.join("vmlinux") + } else { + path.to_path_buf() + }; + let data = std::fs::read(&file).map_err(|e| format!("reading {}: {e}", file.display()))?; + let elf = object::File::parse(&*data).map_err(|e| format!("{}: {e}", file.display()))?; + let section = elf + .section_by_name(".BTF") + .ok_or_else(|| format!("{} has no .BTF section", file.display()))?; + let bytes = section.data().map_err(|e| e.to_string())?; + parse(bytes).map_err(|e| format!("{}: {e}", file.display())) +} + +/// Read a `.BTF` section. +pub fn parse(bytes: &[u8]) -> Result { + let little = match bytes.get(..2) { + Some([0x9f, 0xeb]) => true, + Some([0xeb, 0x9f]) => false, + _ => return Err("the .BTF section does not start with the BTF magic".to_string()), + }; + let word = |at: usize| -> Result { + let b: [u8; 4] = bytes + .get(at..at + 4) + .and_then(|b| b.try_into().ok()) + .ok_or("the .BTF section is cut short")?; + Ok(if little { + u32::from_le_bytes(b) + } else { + u32::from_be_bytes(b) + }) + }; + let header = word(4)? as usize; + let (type_off, type_len) = (word(8)? as usize, word(12)? as usize); + let (str_off, str_len) = (word(16)? as usize, word(20)? as usize); + let strings = bytes + .get(header + str_off..header + str_off + str_len) + .ok_or("the string table is outside the section")?; + let name = |off: u32| -> String { + let rest = strings.get(off as usize..).unwrap_or_default(); + let end = rest.iter().position(|&b| b == 0).unwrap_or(rest.len()); + String::from_utf8_lossy(&rest[..end]).into_owned() + }; + let mut types = Vec::new(); + let (mut at, end) = (header + type_off, header + type_off + type_len); + while at < end { + let info = word(at + 4)?; + let kind = (info >> 24) & 0x1f; + let vlen = (info & 0xffff) as usize; + let words = match kind { + INT | VAR | DECL_TAG => 1, + ARRAY => 3, + STRUCT | UNION | DATASEC | ENUM64 => 3 * vlen, + ENUM | FUNC_PROTO => 2 * vlen, + PTR | FWD | TYPEDEF | VOLATILE | CONST | RESTRICT | FUNC | FLOAT | TYPE_TAG => 0, + _ => { + return Err(format!( + "type {} has the unknown kind {kind}", + types.len() + 1 + )); + } + }; + let extra = (0..words) + .map(|i| word(at + 12 + 4 * i)) + .collect::, _>>()?; + types.push(Raw { + name: name(word(at)?), + kind, + vlen, + kind_flag: info >> 31 == 1, + size_or_type: word(at + 8)?, + extra, + }); + at += 12 + 4 * words; + } + Ok(Table { types, name: &name }.describe()) +} + +/// The types of a section, numbered from 1, as type 0 is `void`. +struct Table<'a> { + types: Vec, + name: &'a dyn Fn(u32) -> String, +} + +impl Table<'_> { + fn get(&self, id: u32) -> Option<&Raw> { + self.types.get((id as usize).checked_sub(1)?) + } + + /// How a use of a type is written: by name when it has one, in full when not. + fn refer(&self, id: u32, depth: usize) -> String { + if id == 0 { + return "void".to_string(); + } + if depth > DEEPEST { + return "...".to_string(); + } + let Some(t) = self.get(id) else { + return format!(""); + }; + let next = |id| self.refer(id, depth + 1); + match t.kind { + PTR => format!("*{}", next(t.size_or_type)), + ARRAY => format!("[{}]{}", t.extra[2], next(t.extra[0])), + CONST => format!("const {}", next(t.size_or_type)), + VOLATILE => format!("volatile {}", next(t.size_or_type)), + RESTRICT => format!("restrict {}", next(t.size_or_type)), + TYPE_TAG => format!("tag({}) {}", t.name, next(t.size_or_type)), + FUNC_PROTO => self.proto(t, depth), + STRUCT | UNION | ENUM | ENUM64 if t.name.is_empty() => self.body(t, depth), + STRUCT | ENUM | ENUM64 | UNION | FWD => format!("{} {}", Self::tag(t), t.name), + _ => t.name.clone(), + } + } + + /// The word a struct, union, enum or forward declaration is named with. + fn tag(t: &Raw) -> &'static str { + match t.kind { + UNION => "union", + FWD if t.kind_flag => "union", + ENUM | ENUM64 => "enum", + _ => "struct", + } + } + + /// A struct, union or enum written in full. + fn body(&self, t: &Raw, depth: usize) -> String { + let mut s = format!("{} size {} {{", Self::tag(t), t.size_or_type); + match t.kind { + STRUCT | UNION => { + for m in t.extra.chunks(3) { + let (bits, at) = if t.kind_flag { + (m[2] >> 24, m[2] & 0x00ff_ffff) + } else { + (0, m[2]) + }; + let _ = write!( + s, + " {}: {} @{at}", + (self.name)(m[0]), + self.refer(m[1], depth + 1) + ); + if bits > 0 { + let _ = write!(s, ":{bits}"); + } + s.push(';'); + } + } + ENUM => { + for v in t.extra.chunks(2) { + let value = if t.kind_flag { + i64::from(v[1].cast_signed()).to_string() + } else { + v[1].to_string() + }; + let _ = write!(s, " {} = {value};", (self.name)(v[0])); + } + } + _ => { + for v in t.extra.chunks(3) { + let value = u64::from(v[2]) << 32 | u64::from(v[1]); + let value = if t.kind_flag { + value.cast_signed().to_string() + } else { + value.to_string() + }; + let _ = write!(s, " {} = {value};", (self.name)(v[0])); + } + } + } + s.push_str(" }"); + s + } + + /// A function prototype, with its parameters' names, which come from the DWARF. + fn proto(&self, t: &Raw, depth: usize) -> String { + let params: Vec = t + .extra + .chunks(2) + .map(|p| match (p[1], (self.name)(p[0])) { + (0, _) => "...".to_string(), + (ty, name) if name.is_empty() => self.refer(ty, depth + 1), + (ty, name) => format!("{name}: {}", self.refer(ty, depth + 1)), + }) + .collect(); + format!( + "fn({}) -> {}", + params.join(", "), + self.refer(t.size_or_type, depth + 1) + ) + } + + /// Every named type and function, described. + fn describe(&self) -> Btf { + let mut btf = Btf::default(); + for t in &self.types { + if t.name.is_empty() { + continue; + } + let (key, description) = match t.kind { + STRUCT | UNION | ENUM | ENUM64 => { + (format!("{} {}", Self::tag(t), t.name), self.body(t, 0)) + } + TYPEDEF => (format!("typedef {}", t.name), self.refer(t.size_or_type, 0)), + INT => { + let e = t.extra[0]; + let sign = match e >> 24 { + 1 => " signed", + 2 => " char", + 4 => " bool", + _ => "", + }; + let shape = format!("size {}{sign} bits {}", t.size_or_type, e & 0xff); + (format!("int {}", t.name), shape) + } + FLOAT => ( + format!("float {}", t.name), + format!("size {}", t.size_or_type), + ), + VAR => ( + format!("var {}", t.name), + format!("{} linkage {}", self.refer(t.size_or_type, 0), t.extra[0]), + ), + DECL_TAG => ( + format!("decl_tag {}", t.name), + format!( + "{} component {}", + self.refer(t.size_or_type, 0), + t.extra[0].cast_signed() + ), + ), + FUNC => { + let proto = self.refer(t.size_or_type, 0); + let linkage = ["static", "global", "extern"] + .get(t.vlen) + .unwrap_or(&"linkage?"); + btf.functions + .entry(t.name.clone()) + .or_default() + .insert(format!("{linkage} {proto}")); + continue; + } + _ => continue, + }; + btf.types.entry(key).or_default().insert(description); + } + btf + } +} + +/// What differs between two builds' BTF. +#[derive(Debug, Default)] +pub struct Comparison { + /// Named types both builds have and describe differently, with each build's descriptions. + pub differ: Vec<(String, BTreeSet, BTreeSet)>, + /// Named types only the reference has. + pub reference_only: Vec, + /// Named types only the other build has. + pub other_only: Vec, + /// Functions both builds have with different prototypes. + pub prototypes: Vec<(String, BTreeSet, BTreeSet)>, + /// Functions only the reference has, which is not a finding. + pub functions_reference_only: Vec, + /// Functions only the other build has, which is not a finding. + pub functions_other_only: Vec, + /// Named types and functions in each build. + pub counts: ((usize, usize), (usize, usize)), +} + +impl Comparison { + /// Whether both builds describe the same types and give shared functions the same prototype. + #[must_use] + pub fn clean(&self) -> bool { + self.differ.is_empty() + && self.reference_only.is_empty() + && self.other_only.is_empty() + && self.prototypes.is_empty() + } +} + +type Differ = Vec<(String, BTreeSet, BTreeSet)>; + +fn split( + reference: &BTreeMap>, + other: &BTreeMap>, +) -> (Differ, Vec, Vec) { + let mut differ = Vec::new(); + let mut reference_only = Vec::new(); + for (name, r) in reference { + match other.get(name) { + Some(o) if o != r => differ.push((name.clone(), r.clone(), o.clone())), + Some(_) => {} + None => reference_only.push(name.clone()), + } + } + let other_only = other + .keys() + .filter(|name| !reference.contains_key(*name)) + .cloned() + .collect(); + (differ, reference_only, other_only) +} + +/// Compare the other build's BTF with the reference's. +#[must_use] +pub fn compare(reference: &Btf, other: &Btf) -> Comparison { + let (differ, reference_only, other_only) = split(&reference.types, &other.types); + let (prototypes, functions_reference_only, functions_other_only) = + split(&reference.functions, &other.functions); + Comparison { + differ, + reference_only, + other_only, + prototypes, + functions_reference_only, + functions_other_only, + counts: ( + (reference.types.len(), reference.functions.len()), + (other.types.len(), other.functions.len()), + ), + } +} + +fn list(s: &mut String, title: &str, names: &[String]) { + if names.is_empty() { + return; + } + let _ = writeln!(s, "\n{title} ({}):\n", names.len()); + for name in names.iter().take(SHOWN) { + let _ = writeln!(s, "- `{name}`"); + } + if names.len() > SHOWN { + let _ = writeln!(s, "- and {} more", names.len() - SHOWN); + } +} + +fn pairs(s: &mut String, title: &str, differ: &Differ) { + if differ.is_empty() { + return; + } + let _ = writeln!(s, "\n{title} ({}):\n", differ.len()); + for (name, r, o) in differ.iter().take(SHOWN) { + let _ = writeln!(s, "- `{name}`"); + for d in r.difference(o) { + let _ = writeln!(s, " - reference: `{d}`"); + } + for d in o.difference(r) { + let _ = writeln!(s, " - other: `{d}`"); + } + } + if differ.len() > SHOWN { + let _ = writeln!(s, "- and {} more", differ.len() - SHOWN); + } +} + +/// The comparison as Markdown. +#[must_use] +pub fn report(c: &Comparison) -> String { + let mut s = String::from("### btf\n\n"); + let ((rt, rf), (ot, of)) = c.counts; + let _ = writeln!( + s, + "The reference describes {rt} named types and {rf} functions, the other build {ot} and {of}." + ); + pairs(&mut s, "Types described differently", &c.differ); + list(&mut s, "Types only the reference has", &c.reference_only); + list(&mut s, "Types only the other build has", &c.other_only); + pairs(&mut s, "Functions with different prototypes", &c.prototypes); + list( + &mut s, + "Functions only the reference has, not failed since inlining differs", + &c.functions_reference_only, + ); + list( + &mut s, + "Functions only the other build has, not failed since inlining differs", + &c.functions_other_only, + ); + if c.clean() { + s.push_str("\nBoth builds describe the same types, and shared functions the same way.\n"); + } + s +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A section built by hand from types given as words and a string table. + fn section(types: &[u32], strings: &[u8]) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(&0xeb9f_u16.to_le_bytes()); + out.extend_from_slice(&[1, 0]); + let type_len = u32::try_from(types.len() * 4).unwrap(); + let str_len = u32::try_from(strings.len()).unwrap(); + for w in [24, 0, type_len, type_len, str_len] { + out.extend_from_slice(&w.to_le_bytes()); + } + for w in types { + out.extend_from_slice(&w.to_le_bytes()); + } + out.extend_from_slice(strings); + out + } + + fn info(kind: u32, vlen: u32) -> u32 { + kind << 24 | vlen + } + + /// Strings: 1 int, 5 point, 11 x, 13 y, 15 f, 17 p, 19 u. + const STRINGS: &[u8] = b"\0int\0point\0x\0y\0f\0p\0u\0"; + + /// `int`, `struct point { int x; int y; }`, `struct point *`, `int f(struct point *p)`, and a + /// typedef `u` of an anonymous struct holding a point, laid out in the order given. + fn sample(order_swapped: bool) -> Vec { + let int = [1, info(INT, 0), 4, 1 << 24 | 32]; + let (point, ptr) = if order_swapped { (3, 2) } else { (2, 3) }; + let point_words = [5, info(STRUCT, 2), 8, 11, 1, 0, 13, 1, 32]; + let ptr_words = [0, info(PTR, 0), point]; + let mut words = int.to_vec(); + if order_swapped { + words.extend_from_slice(&ptr_words); + words.extend_from_slice(&point_words); + } else { + words.extend_from_slice(&point_words); + words.extend_from_slice(&ptr_words); + } + words.extend_from_slice(&[0, info(FUNC_PROTO, 1), 1, 17, ptr]); + words.extend_from_slice(&[15, info(FUNC, 1), 4]); + words.extend_from_slice(&[0, info(STRUCT, 1), 8, 17, point, 0]); + words.extend_from_slice(&[19, info(TYPEDEF, 0), 6]); + section(&words, STRINGS) + } + + #[test] + fn types_are_described_by_name_whatever_their_ids() { + let a = parse(&sample(false)).unwrap(); + let b = parse(&sample(true)).unwrap(); + assert_eq!(a, b); + assert_eq!( + a.types["struct point"].iter().next().unwrap(), + "struct size 8 { x: int @0; y: int @32; }" + ); + assert_eq!( + a.functions["f"].iter().next().unwrap(), + "global fn(p: *struct point) -> int" + ); + assert_eq!( + a.types["typedef u"].iter().next().unwrap(), + "struct size 8 { p: struct point @0; }" + ); + assert!(compare(&a, &b).clean()); + } + + #[test] + fn a_member_moved_is_a_finding_and_a_function_missing_is_not() { + let a = parse(&sample(false)).unwrap(); + let mut b = a.clone(); + b.types.insert( + "struct point".to_string(), + BTreeSet::from(["struct size 8 { x: int @0; y: int @16; }".to_string()]), + ); + b.functions.remove("f"); + let c = compare(&a, &b); + assert!(!c.clean()); + assert_eq!(c.differ.len(), 1); + assert_eq!(c.functions_reference_only, ["f"]); + let mut d = a.clone(); + d.functions.remove("f"); + let c = compare(&a, &d); + assert!(c.clean(), "{}", report(&c)); + assert!(report(&c).contains("- `f`")); + } + + #[test] + fn a_section_without_the_magic_is_refused() { + assert!(parse(b"\0\0\0\0").is_err()); + } +} diff --git a/crates/rk/src/cli.rs b/crates/rk/src/cli.rs index d9ebed3..2c53a27 100644 --- a/crates/rk/src/cli.rs +++ b/crates/rk/src/cli.rs @@ -63,7 +63,7 @@ fn accepted(command: &str) -> Option<(bool, &'static [&'static str])> { (false, &["reference", "other"]) } "sections-diff" => (false, &["reference", "other", "save", "all"]), - "frames" => (false, &["reference", "other", "save"]), + "frames" | "btf" => (false, &["reference", "other", "save"]), "vec-audit" => (false, &["build", "reference", "save"]), "modules-audit" => (false, &["build"]), "syntax" => (false, &["build", "cc", "allow", "jobs"]), @@ -172,6 +172,7 @@ usage: rk modules-audit --build DIR rk objtool-report --reference DIR|LOG --other DIR|LOG rk frames --reference DIR|FILE [--other DIR|FILE] [--save FILE] + rk btf --reference DIR|VMLINUX|FILE [--other DIR|VMLINUX|FILE] [--save FILE] rk syntax --build DIR [--cc rucc] [--allow FILE] [--jobs N] rk boot --build DIR [--row X64] [--cpu NAME] [--initramfs FILE | --busybox PATH] [--timeout 300] [--append WORDS] diff --git a/crates/rk/src/main.rs b/crates/rk/src/main.rs index bae2462..8b57465 100644 --- a/crates/rk/src/main.rs +++ b/crates/rk/src/main.rs @@ -4,6 +4,7 @@ mod asm; mod baseline; mod boot; +mod btf; mod build; mod cli; mod cross; @@ -71,6 +72,7 @@ fn main() -> ExitCode { "modules-audit" => modules_audit(&args), "objtool-report" => objtool_report(&args), "frames" => frames_command(&args), + "btf" => btf_command(&args), "demands" => demands_command(&args), "boot" => boot_command(&repo, &args), "test" => test_command(&repo, &args), @@ -615,6 +617,35 @@ fn frames_command(args: &Args) -> Result { Ok(verdict(comparison.clean())) } +/// Compare the BTF of two builds' vmlinux. +fn btf_command(args: &Args) -> Result { + let read = |path: &str| { + let path = std::path::Path::new(path); + let elf = std::fs::read(path).is_ok_and(|b| b.starts_with(b"\x7fELF")); + if elf { + btf::scan(path) + } else { + objects::load_or_scan(path, btf::scan) + } + }; + let reference = read(args.get("reference").ok_or("rk btf needs --reference")?)?; + if let Some(path) = args.get("save") { + objects::save(std::path::Path::new(path), &reference)?; + eprintln!("saved {} types to {path}", reference.types.len()); + } + let Some(other) = args.get("other") else { + return if args.get("save").is_some() { + Ok(ExitCode::SUCCESS) + } else { + Err("rk btf needs --other, or --save to keep the reference".to_string()) + }; + }; + let other = read(other)?; + let comparison = btf::compare(&reference, &other); + print!("{}", btf::report(&comparison)); + Ok(verdict(comparison.clean())) +} + /// Replay every unit of a reference build through another compiler's front end. fn syntax_command(repo: &Repo, args: &Args) -> Result { let build = std::path::PathBuf::from(args.get("build").ok_or("rk syntax needs --build")?);