From 96bd3eb83645b59e154926f7f1d005428b88fd90 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 5 Nov 2026 10:23:03 -0500 Subject: [PATCH] sandbox: RAPL read through the MSRs is attested as RAPL, and the hardware tests' oracles read the MSRs where powercap lists no package Since sandbox-meter's RAPL probe falls back to the energy-status MSRs where powercap lists no package zone, everything built on it reads such a host: PowerMeter (`rapl-msr`), and sandbox-exec-microvm's RaplMeter, which is that probe and which sandbox-proof's S5 proof meters with. Three places still assumed powercap. - sandbox-tee's attested_efficiency example named only `rapl-powercap` as a CPU counter, so on an MSR host it attested the reading as `Estimator`. Both probes are the same hardware counter: `rapl-msr` now attests as CounterClass::Rapl and seals HwShunt, as `rapl-powercap` does. Its test accepts either probe for the class, and a new test pins every probe name to its class. - sandbox-proof's accelerator test read its oracle from powercap only, so where the meter read the MSRs the oracle had no package and the test panicked. The oracle (oracle_package_uj_at, independent of the crate) now reads the powercap package zones where any is listed (package-, or package--die-), and otherwise each RAPL domain's energy-status MSR on its first CPU: a die of a package on Intel, a package on AMD and Hygon, its 32-bit count at its own unit, wrapping at 2^32 counts. A new test drives it over fake files: two dies of one Intel package, an AMD package whose CPUs list several die_ids, a zero unit, and a powercap die zone preferred. - sandbox-exec-microvm's rapl_hardware oracle watched only powercap, so where RaplMeter opened on the MSRs the oracle called the host Unreadable; and the MSR meter's test bracketed its figure with powercap zones a host without powercap does not have. The oracle now watches and brackets with its own MSR reads there, scoped the same way, and accepts die zones as packages. RaplMeter's docs say what it reads. On two bare-metal servers with no powercap zone (a one-socket Xeon E-2278G, two Xeon Silver 4215R sockets), as root: the accelerator span read rapl-msr at a ratio of 1.000 and 0.999 to the oracle around the call; both microvm hardware tests sat inside the oracle's MSR bracket (RaplMeter 8.9 J and 23.5 J over a 300 ms burn); the example attested `rapl` and sealed HwShunt. As an unprivileged user every counter is Unavailable, the figures are estimates, and every test passes; as that user with the msr group and CAP_SYS_RAWIO (systemd-run), the results match root's. Linux x86_64: sandbox-proof, sandbox-tee (default and amd-kds,intel-pcs) and sandbox-exec-microvm tests pass, `cargo check --workspace --all-targets` is clean, clippy -D warnings is clean for the three crates. macOS: the CI step for sandbox-proof passes with its grep, sandbox-tee passes, clippy is clean for sandbox-meter, sandbox-proof, sandbox-tee and sandbox-exec-microvm. Co-Authored-By: Claude Opus 5.5 --- .../crates/sandbox-exec-microvm/src/power.rs | 29 ++- .../tests/rapl_hardware.rs | 215 ++++++++++++++++-- .../crates/sandbox-proof/src/lib.rs | 169 ++++++++++++-- .../examples/attested_efficiency.rs | 11 +- .../tests/attested_efficiency_example.rs | 41 +++- 5 files changed, 407 insertions(+), 58 deletions(-) diff --git a/sandbox/sandbox-rs/crates/sandbox-exec-microvm/src/power.rs b/sandbox/sandbox-rs/crates/sandbox-exec-microvm/src/power.rs index 0f90f6e02..a275ae68c 100644 --- a/sandbox/sandbox-rs/crates/sandbox-exec-microvm/src/power.rs +++ b/sandbox/sandbox-rs/crates/sandbox-exec-microvm/src/power.rs @@ -37,26 +37,31 @@ pub trait PowerMeter: Send + Sync { /// Linux RAPL CPU energy through the powercap interface /// (`/sys/class/powercap/intel-rapl:/energy_uj`, Intel's or AMD's): the -/// sum of every top-level zone whose `name` is `package-`, each +/// sum of every top-level zone whose `name` is `package-` (or +/// `package--die-`, one zone per die of a multi-die package), each /// wrap-corrected at its own `max_energy_range_uj`, as a cumulative counter -/// from when the meter opened. It is sandbox-meter's own RAPL probe -/// ([`RaplPackages`]): not `psys`, `dram`, a package's subzones or the -/// `intel-rapl-mmio` interface to the same packages; every package or none (a -/// package whose counter or range does not read, or a top-level zone whose -/// `name` does not, leaves the meter unopened); a package with no readable, -/// non-zero range never opens, and a fall from above a package's range breaks -/// the count for good, so that read and every later one sample `None`. It -/// opens only once every package counter has proved live. +/// from when the meter opened. Where powercap lists no package (a kernel +/// without the powercap RAPL driver), the same counters through the +/// energy-status MSRs (`/dev/cpu//msr`, which needs `CAP_SYS_RAWIO`): each +/// die of a package on Intel, each package on AMD and Hygon. It is +/// sandbox-meter's own RAPL probe ([`RaplPackages`]): not `psys`, `dram`, a +/// package's subzones or the `intel-rapl-mmio` interface to the same packages; +/// every package or none (a package whose counter or range does not read, or a +/// top-level zone whose `name` does not, leaves the meter unopened); a package +/// with no readable, non-zero range never opens, and a fall from above a +/// package's range breaks the count for good, so that read and every later one +/// sample `None`. It opens only once every package counter has proved live. pub struct RaplMeter { /// The package counters, or why there are none. packages: Result, String>, } impl RaplMeter { - /// This host's package zones, read only when sandbox-meter's cached + /// This host's package zones, or its energy-status MSRs where powercap + /// lists no package, read only when sandbox-meter's cached /// [`rapl_status`](sandbox_meter::rapl_status) is `Live` (probed once per - /// process: about a second of CPU on the first caller's thread where - /// powercap reads). Otherwise a meter that samples `None` and says why + /// process: about a second of CPU on the first caller's thread where the + /// counters read). Otherwise a meter that samples `None` and says why /// ([`unavailable`](Self::unavailable)); always so off Linux. pub fn host() -> RaplMeter { Self::opened(RaplPackages::host()) diff --git a/sandbox/sandbox-rs/crates/sandbox-exec-microvm/tests/rapl_hardware.rs b/sandbox/sandbox-rs/crates/sandbox-exec-microvm/tests/rapl_hardware.rs index 42bc66680..e306bccdc 100644 --- a/sandbox/sandbox-rs/crates/sandbox-exec-microvm/tests/rapl_hardware.rs +++ b/sandbox/sandbox-rs/crates/sandbox-exec-microvm/tests/rapl_hardware.rs @@ -5,12 +5,15 @@ //! In a test binary of its own, so the probes run in this process, and the //! tests take one lock: each burns CPU and reads machine-wide counters. The //! oracle reads `/sys/class/powercap` itself and applies the sustained- -//! advance rule to the top-level `package-` zones on its own; for the -//! MSRs it tries to read `MSR_PKG_ENERGY_STATUS` from `/dev/cpu/0/msr` -//! itself. None of these tests passes without asserting: where a counter -//! cannot be read the meter must say so and the oracle agree; where it is -//! live, the meter's figure over a CPU burn must sit inside the oracle's -//! package-only bracket. +//! advance rule to the top-level package zones (`package-`, or +//! `package--die-` on a multi-die package) on its own. Where powercap +//! lists no package (a kernel without the powercap RAPL driver), it reads the +//! RAPL energy-status MSRs itself (`oracle::msr`), each domain on its first +//! CPU: a die of a package on Intel, a package on AMD and Hygon — the domains +//! `RaplMeter` reads there too, through sandbox-meter's probe. None of these +//! tests passes without asserting: where a counter cannot be read the meter +//! must say so and the oracle agree; where it is live, the meter's figure over +//! a CPU burn must sit inside the oracle's package-only bracket. #![cfg(target_os = "linux")] @@ -115,9 +118,14 @@ mod oracle { z.dir.strip_prefix("intel-rapl:").is_some_and(digits) } - /// A package zone: top-level, named `package-`. + /// A package zone: top-level, named `package-`, or `package--die-` + /// (one zone per die of a multi-die Intel package). pub fn is_package(z: &Zone) -> bool { - top_level(z) && z.name.as_deref().and_then(|n| n.strip_prefix("package-")).is_some_and(digits) + top_level(z) + && z.name.as_deref().and_then(|n| n.strip_prefix("package-")).is_some_and(|n| match n.split_once("-die-") { + Some((package, die)) => digits(package) && digits(die), + None => digits(n), + }) } /// The platform zone. @@ -187,23 +195,53 @@ mod oracle { } } - /// Sample every package's `energy_uj`, burning 25 ms of this thread's CPU - /// between samples (capped at 500 ms of wall time), until every package - /// has advanced in at least five intervals, at least 80% of those + /// Sample every package's `energy_uj` under `root`, burning 25 ms of this + /// thread's CPU between samples (capped at 500 ms of wall time), until every + /// package has advanced in at least five intervals, at least 80% of those /// observed, over at least 300 ms; or one second has passed with at least /// five intervals; or five seconds have. pub fn watch(root: &Path) -> Watch { let all = zones(root); let packages: Vec<&Zone> = all.iter().filter(|z| is_package(z)).collect(); let names: Vec = packages.iter().map(|z| z.dir.clone()).collect(); - let none = |verdict| Watch { verdict, packages: names.clone(), intervals: 0, advances: Vec::new(), window: Duration::ZERO }; if packages.is_empty() { let readable = all.iter().any(|z| top_level(z) && z.energy_uj.is_some()); - return none(if readable { Verdict::NoPackage } else { Verdict::Unreadable }); + return Watch::none(if readable { Verdict::NoPackage } else { Verdict::Unreadable }, names); } + watch_with(names, || read(root, &packages)) + } + + /// Whether the host's powercap lists a package zone. + pub fn powercap_lists_a_package() -> bool { + zones(Path::new(POWERCAP)).iter().any(is_package) + } + + /// [`watch`] over this host: its powercap package zones, or, where powercap + /// lists none, its RAPL MSRs ([`msr::domains`]), sampled the same way. + pub fn watch_host() -> Watch { + if powercap_lists_a_package() { + return watch(Path::new(POWERCAP)); + } + match msr::domains() { + Some(domains) => { + let names = domains.iter().map(|d| d.name.clone()).collect(); + watch_with(names, || msr::read_all(&domains)) + } + None => watch(Path::new(POWERCAP)), + } + } + + impl Watch { + fn none(verdict: Verdict, packages: Vec) -> Watch { + Watch { verdict, packages, intervals: 0, advances: Vec::new(), window: Duration::ZERO } + } + } + + /// The sustained-advance run over the counters `read` returns, one per name. + fn watch_with(names: Vec, mut read: impl FnMut() -> Option>) -> Watch { let started = Instant::now(); - let Some(mut prev) = read(root, &packages) else { return none(Verdict::Unreadable) }; - let mut advances = vec![0u32; packages.len()]; + let Some(mut prev) = read() else { return Watch::none(Verdict::Unreadable, names) }; + let mut advances = vec![0u32; names.len()]; let mut intervals = 0u32; let live = |adv: &[u32], n: u32, w: Duration| { w >= Duration::from_millis(300) && adv.iter().all(|&a| a >= 5 && u64::from(a) * 5 >= u64::from(n) * 4) @@ -214,7 +252,7 @@ mod oracle { break; } burn(Duration::from_millis(25), Duration::from_millis(500)); - if let Some(now) = read(root, &packages) { + if let Some(now) = read() { intervals += 1; for ((a, n), p) in advances.iter_mut().zip(&now).zip(&prev) { *a += u32::from(n > p); @@ -227,6 +265,111 @@ mod oracle { Watch { verdict, packages: names, intervals, advances, window } } + /// The oracle's own reading of the RAPL energy-status MSRs, for a host + /// whose powercap lists no package: each domain's count through the `msr` + /// driver's device file of its first CPU. A domain is a die of a package on + /// Intel (its RAPL MSRs are die-scope) and a package on AMD and Hygon (a + /// per-package counter). + pub mod msr { + use super::number; + use std::os::unix::fs::FileExt; + use std::path::{Path, PathBuf}; + + /// One RAPL domain's energy-status MSR. + pub struct Domain { + pub name: String, + device: PathBuf, + register: u64, + shift: u32, + } + + fn read(device: &Path, register: u64) -> Option { + let file = std::fs::File::open(device).ok()?; + let mut raw = [0u8; 8]; + file.read_exact_at(&mut raw, register).ok()?; + Some(u64::from_le_bytes(raw)) + } + + /// Every domain whose power-unit and energy-status MSRs read with a + /// non-zero unit; `None` when the CPU's MSRs are not known here, no + /// topology is listed, or any domain cannot be read. + pub fn domains() -> Option> { + let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").ok()?; + let field = |key: &str| { + cpuinfo.lines().find_map(|l| l.split_once(':').filter(|(k, _)| k.trim() == key).map(|(_, v)| v.trim())) + }; + let family: u32 = field("cpu family")?.parse().ok()?; + let (unit, energy, per_die) = match field("vendor_id")? { + "GenuineIntel" if family == 6 => (0x606, 0x611, true), + "AuthenticAMD" | "HygonGenuine" if family >= 0x17 => (0xC001_0299, 0xC001_029B, false), + _ => return None, + }; + let mut first = std::collections::BTreeMap::<(u64, u64), u32>::new(); + for entry in std::fs::read_dir("/sys/devices/system/cpu").ok()?.flatten() { + let Some(cpu) = entry.file_name().to_str().and_then(|n| n.strip_prefix("cpu")?.parse::().ok()) else { + continue; + }; + let topology = entry.path().join("topology"); + let Some(package) = number(topology.join("physical_package_id")) else { continue }; + let die = if per_die { number(topology.join("die_id")).unwrap_or(0) } else { 0 }; + let lowest = first.entry((package, die)).or_insert(cpu); + *lowest = (*lowest).min(cpu); + } + if first.is_empty() { + return None; + } + first + .into_iter() + .map(|((package, die), cpu)| { + let device = PathBuf::from(format!("/dev/cpu/{cpu}/msr")); + let shift = ((read(&device, unit)? >> 8) & 0x1F) as u32; + if shift == 0 { + return None; + } + read(&device, energy)?; + Some(Domain { name: format!("msr package-{package}-die-{die} (cpu {cpu})"), device, register: energy, shift }) + }) + .collect() + } + + /// One domain's count, in µJ. + fn uj(d: &Domain) -> Option { + read(&d.device, d.register).map(|raw| ((u128::from(raw & 0xFFFF_FFFF) * 1_000_000) >> d.shift) as u64) + } + + /// Every domain's count, in µJ; `None` if one fails. + pub fn read_all(domains: &[Domain]) -> Option> { + domains.iter().map(uj).collect() + } + + /// The domains' summed energy between two reads, each modulo its 2^32 counts. + pub fn energy(domains: &[Domain], before: &[u64], after: &[u64]) -> u64 { + domains + .iter() + .zip(before.iter().zip(after)) + .map(|(d, (&b, &a))| { + let range = (((1u128 << 32) * 1_000_000) >> d.shift) as u64; + super::delta(b, a, Some(range)).expect("an MSR count above its range") + }) + .sum() + } + + /// Read the domains outer and inner around `meter_read` (called twice) + /// and `work`: the domains' energy over the inner and the outer window, + /// and the meter's two readings. + pub fn around(domains: &[Domain], mut meter_read: impl FnMut() -> M, work: impl FnOnce()) -> (u64, u64, M, M) { + let read = || read_all(domains).expect("an oracle MSR read"); + let o0 = read(); + let m0 = meter_read(); + let o1 = read(); + work(); + let o2 = read(); + let m1 = meter_read(); + let o3 = read(); + (energy(domains, &o1, &o2), energy(domains, &o0, &o3), m0, m1) + } + } + /// The zones under `root`, as one line for the log. pub fn listing(root: &Path) -> String { zones(root) @@ -377,7 +520,7 @@ use oracle::Verdict; fn the_powercap_meter_opens_where_the_packages_are_live_and_reads_them() { let _one = serial(); let meter = RaplMeter::host(); - let watch = oracle::watch(Path::new(oracle::POWERCAP)); + let watch = oracle::watch_host(); eprintln!( "rapl hardware: {}; oracle {}; RaplMeter unavailable={:?}", oracle::listing(Path::new(oracle::POWERCAP)), @@ -389,6 +532,26 @@ fn the_powercap_meter_opens_where_the_packages_are_live_and_reads_them() { assert_eq!(meter.sample_uj(), None, "a meter that did not open sampled"); return; } + if !oracle::powercap_lists_a_package() { + // No powercap package: the meter reads the MSRs (sandbox-meter's probe + // falls back to them), and so does the oracle. + let domains = oracle::msr::domains().expect("the meter reads the MSRs, so the oracle can"); + let mut cpu = Duration::ZERO; + let (inner, outer, m0, m1) = oracle::msr::around( + &domains, + || meter.sample_uj().expect("an open meter samples"), + || cpu = oracle::burn(Duration::from_millis(300), Duration::from_secs(10)), + ); + let uj = m1 - m0; + let names: Vec<&str> = domains.iter().map(|d| d.name.as_str()).collect(); + eprintln!("rapl burn (MSR {names:?}): {cpu:?} of thread CPU; meter {uj} µJ; oracle inner {inner} outer {outer} µJ"); + assert!(inner > 0, "the domains did not advance over the burn"); + // One count of the energy unit (2^-14 J, ~61 µJ) per domain either side. + let slack = 123 * domains.len() as u64; + assert!(inner.saturating_sub(slack) <= uj && uj <= outer + slack, "{uj} µJ outside [{inner}, {outer}]"); + assert!(cpu >= Duration::from_millis(300), "the burn got only {cpu:?} of CPU"); + return; + } let all = oracle::zones(Path::new(oracle::POWERCAP)); let mut cpu = Duration::ZERO; let (b, m0, m1) = oracle::around( @@ -430,6 +593,24 @@ fn the_msr_meter_opens_only_where_the_msrs_read() { return; }; let meter = meter.expect("the MSRs read, yet the MSR meter did not open"); + if !oracle::powercap_lists_a_package() { + // No powercap package to bracket with (the host this meter is for): + // the oracle's own MSR reads bracket it instead. + let domains = oracle::msr::domains().expect("the MSRs read, so the oracle's domains do"); + let (inner, outer, m0, m1) = oracle::msr::around( + &domains, + || meter.sample_uj().expect("an open MSR meter samples"), + || { + oracle::burn(Duration::from_millis(300), Duration::from_secs(10)); + }, + ); + let uj = m1 - m0; + eprintln!("rapl msr burn (no powercap): meter {uj} µJ; oracle MSR inner {inner} outer {outer} µJ"); + assert!(inner > 0, "the domains did not advance over the burn"); + let slack = 123 * domains.len() as u64; + assert!(inner.saturating_sub(slack) <= uj && uj <= outer + slack, "{uj} µJ outside [{inner}, {outer}]"); + return; + } let all = oracle::zones(Path::new(oracle::POWERCAP)); let (b, m0, m1) = oracle::around( oracle::bracket_set(&all), diff --git a/sandbox/sandbox-rs/crates/sandbox-proof/src/lib.rs b/sandbox/sandbox-rs/crates/sandbox-proof/src/lib.rs index 69284d685..9ddb3b91a 100644 --- a/sandbox/sandbox-rs/crates/sandbox-proof/src/lib.rs +++ b/sandbox/sandbox-rs/crates/sandbox-proof/src/lib.rs @@ -2561,8 +2561,9 @@ mod tests { /// On Linux, the host's own counters over a real run of a couple of /// seconds: RAPL's package counters for `Cpu` where every package counter - /// is live, read against the package zones' `energy_uj` read here, around - /// the call, wrap included (the span is inside that window, so its figure + /// is live, read against the packages' counts read here around the call + /// (the powercap zones' `energy_uj`, or each domain's energy-status MSR + /// where powercap lists no package: [`oracle_package_uj_at`]), wrap included (the span is inside that window, so its figure /// is at most the oracle's and, the window being the span plus a spawn, /// most of it); NVML for `Gpu` where its counter is live and advanced /// over the run, else `nvidia-smi`'s power × the run's time. Where neither GPU figure exists @@ -2646,24 +2647,160 @@ mod tests { } } - /// Each RAPL `package-` zone's `energy_uj` and `max_energy_range_uj`, - /// read from sysfs here; `None` if there is none or one is unreadable. + /// This host's RAPL packages' counts and wraps, in µJ, read here + /// ([`oracle_package_uj_at`] over sysfs and `/`). #[cfg(target_os = "linux")] fn oracle_package_uj() -> Option<(Vec, Vec)> { - let mut zones: Vec = std::fs::read_dir("/sys/class/powercap") - .ok()? - .filter_map(|e| e.ok().map(|e| e.path())) - .filter(|p| { - std::fs::read_to_string(p.join("name")).is_ok_and(|n| n.trim().starts_with("package-")) - && !p.to_string_lossy().contains("mmio") + oracle_package_uj_at(std::path::Path::new("/sys/class/powercap"), std::path::Path::new("/")) + } + + /// Each RAPL package's count and the value it wraps at, in µJ, read here, + /// nothing from the crate under test: the powercap zones under `powercap` + /// named `package-` (or `package--die-`, one zone per die of a + /// multi-die package), their `energy_uj` and `max_energy_range_uj`; or, + /// where powercap lists no package (a kernel without its RAPL driver), + /// each RAPL domain's energy-status MSR under the filesystem root `root`, + /// read on the domain's first CPU through `dev/cpu//msr`: a die of a + /// package on Intel (0x611, its RAPL MSRs being die-scope), a package on + /// AMD and Hygon (0xC001029B, a per-package counter), its 32-bit count at + /// the unit in bits 12:8 of its power-unit MSR, wrapping at 2^32 counts. + /// `None` if there is none, one is unreadable, or a unit reads zero. + #[cfg(unix)] + fn oracle_package_uj_at(powercap: &std::path::Path, root: &std::path::Path) -> Option<(Vec, Vec)> { + use std::os::unix::fs::FileExt; + let digits = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()); + let is_package = |name: &str| { + name.trim().strip_prefix("package-").is_some_and(|n| match n.split_once("-die-") { + Some((package, die)) => digits(package) && digits(die), + None => digits(n), + }) + }; + let mut zones: Vec = std::fs::read_dir(powercap) + .map(|d| d.filter_map(|e| e.ok().map(|e| e.path())).collect()) + .unwrap_or_default(); + zones.retain(|p| { + p.file_name().and_then(|n| n.to_str()).and_then(|n| n.strip_prefix("intel-rapl:")).is_some_and(digits) + && std::fs::read_to_string(p.join("name")).is_ok_and(|n| is_package(&n)) + }); + zones.sort(); + let number = |p: std::path::PathBuf| std::fs::read_to_string(p).ok()?.trim().parse::().ok(); + if !zones.is_empty() { + let energy: Option> = zones.iter().map(|z| number(z.join("energy_uj"))).collect(); + let range: Option> = zones.iter().map(|z| number(z.join("max_energy_range_uj"))).collect(); + return Some((energy?, range?)); + } + // No powercap package: the MSRs. + let cpuinfo = std::fs::read_to_string(root.join("proc/cpuinfo")).ok()?; + let field = |key: &str| { + cpuinfo.lines().find_map(|l| l.split_once(':').filter(|(k, _)| k.trim() == key).map(|(_, v)| v.trim())) + }; + let family: u32 = field("cpu family")?.parse().ok()?; + let (unit_register, energy_register, per_die) = match field("vendor_id")? { + "GenuineIntel" if family == 6 => (0x606, 0x611, true), + "AuthenticAMD" | "HygonGenuine" if family >= 0x17 => (0xC001_0299, 0xC001_029B, false), + _ => return None, + }; + let mut first = std::collections::BTreeMap::<(u64, u64), u32>::new(); + for entry in std::fs::read_dir(root.join("sys/devices/system/cpu")).ok()?.flatten() { + let Some(cpu) = entry.file_name().to_str().and_then(|n| n.strip_prefix("cpu")?.parse::().ok()) else { + continue; + }; + let topology = entry.path().join("topology"); + let Some(package) = number(topology.join("physical_package_id")) else { continue }; + let die = if per_die { number(topology.join("die_id")).unwrap_or(0) } else { 0 }; + let lowest = first.entry((package, die)).or_insert(cpu); + *lowest = (*lowest).min(cpu); + } + let msr = |device: &std::path::Path, register: u64| { + let file = std::fs::File::open(device).ok()?; + let mut raw = [0u8; 8]; + file.read_exact_at(&mut raw, register).ok()?; + Some(u64::from_le_bytes(raw)) + }; + let read: Option> = first + .values() + .map(|cpu| { + let device = root.join(format!("dev/cpu/{cpu}/msr")); + let shift = ((msr(&device, unit_register)? >> 8) & 0x1F) as u32; + if shift == 0 { + return None; + } + let count = msr(&device, energy_register)? & 0xFFFF_FFFF; + Some((((u128::from(count) * 1_000_000) >> shift) as u64, (((1u128 << 32) * 1_000_000) >> shift) as u64)) }) .collect(); - zones.sort(); - let read = |p: &std::path::Path, f: &str| std::fs::read_to_string(p.join(f)).ok()?.trim().parse::().ok(); - let energy: Option> = zones.iter().map(|z| read(z, "energy_uj")).collect(); - let range: Option> = zones.iter().map(|z| read(z, "max_energy_range_uj")).collect(); - let (energy, range) = (energy?, range?); - (!energy.is_empty()).then_some((energy, range)) + let read = read?; + (!read.is_empty()).then(|| read.into_iter().unzip()) + } + + /// The oracle over fake files: a powercap package zone is read where one is + /// listed (a die zone included), and the MSRs only where none is; on Intel + /// each die of a package is a domain read on its own first CPU, on AMD the + /// package is one domain whatever its CPUs' `die_id`; a zero unit is no + /// counter. + #[cfg(unix)] + #[test] + fn the_oracle_reads_powercap_where_it_lists_a_package_else_each_domains_msr() { + use std::os::unix::fs::FileExt; + let root = std::env::temp_dir().join(format!("sandbox-proof-oracle-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&root); + let put = |rel: &str, text: &str| { + let p = root.join(rel); + std::fs::create_dir_all(p.parent().unwrap()).unwrap(); + std::fs::write(p, text).unwrap(); + }; + let set_msr = |cpu: u32, register: u64, value: u64| { + let p = root.join(format!("dev/cpu/{cpu}/msr")); + std::fs::create_dir_all(p.parent().unwrap()).unwrap(); + let f = std::fs::OpenOptions::new().create(true).truncate(false).write(true).open(p).unwrap(); + f.write_all_at(&value.to_le_bytes(), register).unwrap(); + }; + let topology = |places: &[(u32, u32)]| { + for (cpu, (package, die)) in places.iter().enumerate() { + put(&format!("sys/devices/system/cpu/cpu{cpu}/topology/physical_package_id"), &format!("{package}\n")); + put(&format!("sys/devices/system/cpu/cpu{cpu}/topology/die_id"), &format!("{die}\n")); + } + }; + let powercap = root.join("sys/class/powercap"); + std::fs::create_dir_all(&powercap).unwrap(); + const RANGE_14: u64 = 262_144_000_000; // 2^32 counts of 2^-14 J + + // Intel, one package of two dies: two domains, each on its first CPU. + put("proc/cpuinfo", "vendor_id\t: GenuineIntel\ncpu family\t: 6\n"); + topology(&[(0, 0), (0, 0), (0, 1), (0, 1)]); + for (cpu, count) in [(0, 1_000), (2, 2_000)] { + set_msr(cpu, 0x606, 0xA_0E03); + set_msr(cpu, 0x611, 0xABCD_0000_0000_0000 | count); + } + assert_eq!( + oracle_package_uj_at(&powercap, &root), + Some((vec![61_035, 122_070], vec![RANGE_14, RANGE_14])), + "each die, the bits above the 32-bit count ignored" + ); + + // A zero unit is no counter. + set_msr(2, 0x606, 0); + assert_eq!(oracle_package_uj_at(&powercap, &root), None, "a zero unit"); + set_msr(2, 0x606, 0xA_0E03); + + // Powercap lists a package (a die zone): it is read, not the MSRs. + put("sys/class/powercap/intel-rapl:0/name", "package-0-die-0\n"); + put("sys/class/powercap/intel-rapl:0/energy_uj", "5000\n"); + put("sys/class/powercap/intel-rapl:0/max_energy_range_uj", "262143328850\n"); + put("sys/class/powercap/intel-rapl:1/name", "psys\n"); + put("sys/class/powercap/intel-rapl:1/energy_uj", "9000\n"); + assert_eq!(oracle_package_uj_at(&powercap, &root), Some((vec![5_000], vec![262_143_328_850]))); + std::fs::remove_dir_all(&powercap).unwrap(); + std::fs::create_dir_all(&powercap).unwrap(); + + // AMD: one domain per package, whatever its CPUs' die_id. + put("proc/cpuinfo", "vendor_id\t: AuthenticAMD\ncpu family\t: 26\n"); + topology(&[(0, 0), (0, 1), (0, 2), (0, 3)]); + set_msr(0, 0xC001_0299, 0x10 << 8); + set_msr(0, 0xC001_029B, 65_536); + assert_eq!(oracle_package_uj_at(&powercap, &root), Some((vec![1_000_000], vec![65_536_000_000])), "2^-16 J units"); + + let _ = std::fs::remove_dir_all(&root); } /// The engine's `ri_energy_nj`: `proc_pid_rusage(pid, 6)` into a raw diff --git a/sandbox/sandbox-rs/crates/sandbox-tee/examples/attested_efficiency.rs b/sandbox/sandbox-rs/crates/sandbox-tee/examples/attested_efficiency.rs index b24ddfc5b..5662f4205 100644 --- a/sandbox/sandbox-rs/crates/sandbox-tee/examples/attested_efficiency.rs +++ b/sandbox/sandbox-rs/crates/sandbox-tee/examples/attested_efficiency.rs @@ -13,7 +13,10 @@ //! - where a vendor counter read this process's CPU work over the span (the //! kernel's per-process count on macOS, a RAPL package on Linux, IOReport's //! CPU rails on Apple Silicon where they are live), the statement names that -//! counter's class and the receipt seals the reading, `HwShunt`; +//! counter's class and the receipt seals the reading, `HwShunt`. RAPL is one +//! class however it is read: through powercap (`rapl-powercap`) or, on a +//! kernel without the powercap RAPL driver, through the energy-status MSRs +//! (`rapl-msr`) — the same hardware counter, a reading while it is live; //! - where none did (no live counter, or one that did not advance over the //! span), the figure is the meter's estimate: attested as //! [`CounterClass::Estimator`] and sealed `Estimator`. @@ -54,11 +57,13 @@ pub fn task() -> [u8; 64] { /// The counter class of the probe among `sources` (a [`PowerMeter`]'s live /// hardware probes) that reads CPU energy: only a known one is named. `None` -/// for a probe set with no CPU counter this example can name. +/// for a probe set with no CPU counter this example can name. RAPL read +/// through powercap and RAPL read through the MSRs are the same counter, so +/// both are [`CounterClass::Rapl`]. pub fn cpu_counter(sources: &[&str]) -> Option { sources.iter().find_map(|s| match *s { "process-energy" => Some(CounterClass::ProcessEnergy), - "rapl-powercap" => Some(CounterClass::Rapl), + "rapl-powercap" | "rapl-msr" => Some(CounterClass::Rapl), "ioreport-energy-model" => Some(CounterClass::IoReport), _ => None, }) diff --git a/sandbox/sandbox-rs/crates/sandbox-tee/tests/attested_efficiency_example.rs b/sandbox/sandbox-rs/crates/sandbox-tee/tests/attested_efficiency_example.rs index 46e2463c5..2725780e8 100644 --- a/sandbox/sandbox-rs/crates/sandbox-tee/tests/attested_efficiency_example.rs +++ b/sandbox/sandbox-rs/crates/sandbox-tee/tests/attested_efficiency_example.rs @@ -15,14 +15,35 @@ mod example; use sandbox_tee::energy::{AttestTrust, CounterClass}; -/// The probe name a class's reading comes from, for the classes the example -/// can attest a reading under. -fn source_of(class: CounterClass) -> Option<&'static str> { +/// The probe names a class's reading can come from, for the classes the +/// example can attest a reading under: RAPL through powercap, or through the +/// energy-status MSRs where powercap lists no package. +fn sources_of(class: CounterClass) -> &'static [&'static str] { match class { - CounterClass::ProcessEnergy => Some("process-energy"), - CounterClass::Rapl => Some("rapl-powercap"), - CounterClass::IoReport => Some("ioreport-energy-model"), - _ => None, + CounterClass::ProcessEnergy => &["process-energy"], + CounterClass::Rapl => &["rapl-powercap", "rapl-msr"], + CounterClass::IoReport => &["ioreport-energy-model"], + _ => &[], + } +} + +/// Every probe the example names maps to its class, both RAPL probes to +/// `Rapl`; a probe it does not know names no class. +#[test] +fn the_example_names_each_cpu_probe_by_its_counter_class() { + for (sources, class) in [ + (&["process-energy"][..], Some(CounterClass::ProcessEnergy)), + (&["rapl-powercap"][..], Some(CounterClass::Rapl)), + (&["rapl-msr"][..], Some(CounterClass::Rapl)), + (&["nvml", "rapl-msr"][..], Some(CounterClass::Rapl)), + (&["ioreport-energy-model"][..], Some(CounterClass::IoReport)), + (&["nvml"][..], None), + (&[][..], None), + ] { + assert_eq!(example::cpu_counter(sources), class, "{sources:?}"); + if let Some(class) = class { + assert!(sources.iter().any(|s| sources_of(class).contains(s)), "{sources:?} → {}", class.tag()); + } } } @@ -51,10 +72,10 @@ fn the_example_attests_and_seals_what_the_meter_read() { assert_eq!(r.receipt.provenance, "Estimator", "an estimate sealed as something else"); } reading => { - let source = source_of(reading) - .unwrap_or_else(|| panic!("the example attested a reading under `{}`, a class it cannot read", reading.tag())); + let sources = sources_of(reading); + assert!(!sources.is_empty(), "the example attested a reading under `{}`, a class it cannot read", reading.tag()); assert!( - r.sources.contains(&source), + r.sources.iter().any(|s| sources.contains(s)), "attested `{}` but the meter's live probes are {:?}: a counter nothing read", reading.tag(), r.sources -- 2.54.0 (Apple Git-157)