sandbox: the proof and microvm oracles read RAPL MSRs where powercap lists none; sandbox-tee attests rapl-msr as RAPL #651
5 changed files with 407 additions and 58 deletions
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue