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