sandbox: the proof and microvm oracles read RAPL MSRs where powercap lists none; sandbox-tee attests rapl-msr as RAPL #651

Merged
dcharlot merged 1 commit from sandbox/proof-tee-msr into main 2026-11-06 02:40:02 -05:00
5 changed files with 407 additions and 58 deletions

View file

@ -37,26 +37,31 @@ pub trait PowerMeter: Send + Sync {
/// Linux RAPL CPU energy through the powercap interface
/// (`/sys/class/powercap/intel-rapl:<n>/energy_uj`, Intel's or AMD's): the
/// sum of every top-level zone whose `name` is `package-<n>`, each
/// sum of every top-level zone whose `name` is `package-<n>` (or
/// `package-<n>-die-<m>`, 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/<n>/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<Mutex<RaplPackages>, 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())

View file

@ -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-<n>` 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-<n>`, or
//! `package-<n>-die-<m>` 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-<n>`.
/// A package zone: top-level, named `package-<n>`, or `package-<n>-die-<m>`
/// (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<String> = 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<String>) -> 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<String>, mut read: impl FnMut() -> Option<Vec<u64>>) -> 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<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))
}
/// 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<Vec<Domain>> {
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::<u32>().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<u64> {
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<Vec<u64>> {
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<M>(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),

View file

@ -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-<n>` 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<u64>, Vec<u64>)> {
let mut zones: Vec<std::path::PathBuf> = 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-<n>` (or `package-<n>-die-<m>`, 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/<n>/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<u64>, Vec<u64>)> {
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::path::PathBuf> = 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::<u64>().ok();
if !zones.is_empty() {
let energy: Option<Vec<u64>> = zones.iter().map(|z| number(z.join("energy_uj"))).collect();
let range: Option<Vec<u64>> = 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::<u32>().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<Vec<(u64, u64)>> = 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::<u64>().ok();
let energy: Option<Vec<u64>> = zones.iter().map(|z| read(z, "energy_uj")).collect();
let range: Option<Vec<u64>> = 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

View file

@ -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<CounterClass> {
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,
})

View file

@ -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