jcp-energy: a counter this process cannot read is not an absent counter #328
1 changed files with 298 additions and 10 deletions
|
|
@ -771,6 +771,98 @@ fn rapl_status_on(open: impl FnOnce() -> Option<Rapl>) -> CounterStatus {
|
|||
)
|
||||
}
|
||||
|
||||
/// Whether this process can **read** this host's RAPL counter, as distinct
|
||||
/// from whether the counter is advancing.
|
||||
///
|
||||
/// [`Rapl::open`] answers neither question on its own: it returns `None` for a
|
||||
/// host with no powercap tree, a host whose package zone is root-only, and a
|
||||
/// host whose counter is garbled alike. Those are different facts about the
|
||||
/// world, and one of them is a fixable permission gap while another is an
|
||||
/// absence of hardware — so [`survey`] needs them apart before it can report a
|
||||
/// status honestly.
|
||||
///
|
||||
/// Linux has restricted `energy_uj` to privileged readers since the power
|
||||
/// side-channel disclosures of 2020. A representative host carries
|
||||
/// `mode 0440 root:energy`, so a process outside that group reads `EACCES` —
|
||||
/// **not** `ENOENT`. The file is plainly there.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RaplAccess {
|
||||
/// A package zone is here and this process can read its `energy_uj`.
|
||||
///
|
||||
/// This says nothing about the counter advancing; that is
|
||||
/// [`rapl_status`]'s question.
|
||||
Readable,
|
||||
/// A package zone is here and its `energy_uj` cannot be read for want of
|
||||
/// privilege. The counter exists and this process may not read it.
|
||||
NeedsPrivilege,
|
||||
/// No package zone is reachable here at all.
|
||||
NoInterface,
|
||||
}
|
||||
|
||||
/// This host's RAPL accessibility.
|
||||
#[must_use]
|
||||
pub fn rapl_access() -> RaplAccess {
|
||||
rapl_access_in(std::path::Path::new("/sys/class/powercap"))
|
||||
}
|
||||
|
||||
/// [`rapl_access`] on the powercap tree at `root`, so tests run on fake trees.
|
||||
fn rapl_access_in(root: &std::path::Path) -> RaplAccess {
|
||||
rapl_access_with(root, |p| std::fs::read_to_string(p))
|
||||
}
|
||||
|
||||
/// [`rapl_access`] over an injected file reader, so a permission error can be
|
||||
/// tested without `chmod` — which would prove nothing in a container running
|
||||
/// as root, where every mode is readable.
|
||||
///
|
||||
/// A permission failure on **any** package zone decides the answer, even
|
||||
/// beside a readable one. That follows [`Rapl::open_in`]'s own rule that a
|
||||
/// figure claims the whole CPU and so is every package zone or none: where one
|
||||
/// package cannot be read, the reader does not open, and the reason it did not
|
||||
/// is privilege.
|
||||
fn rapl_access_with(
|
||||
root: &std::path::Path,
|
||||
read: impl Fn(&std::path::Path) -> std::io::Result<String>,
|
||||
) -> RaplAccess {
|
||||
let dir = match std::fs::read_dir(root) {
|
||||
Ok(dir) => dir,
|
||||
// The tree itself unreadable for want of privilege is the same finding
|
||||
// as a zone that is; anything else here is an absent interface.
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||
return RaplAccess::NeedsPrivilege;
|
||||
}
|
||||
Err(_) => return RaplAccess::NoInterface,
|
||||
};
|
||||
|
||||
let mut readable = false;
|
||||
for entry in dir.flatten() {
|
||||
let dir_name = entry.file_name();
|
||||
if rapl_top_level_zone(&dir_name.to_string_lossy()).is_none() {
|
||||
continue;
|
||||
}
|
||||
match read(&entry.path().join("name")) {
|
||||
Ok(name) => {
|
||||
if !is_rapl_package_name(&name) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// A zone this process cannot even identify may be a package, and
|
||||
// the reason it could not is privilege.
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||
return RaplAccess::NeedsPrivilege;
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
match read(&entry.path().join("energy_uj")) {
|
||||
Ok(_) => readable = true,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||
return RaplAccess::NeedsPrivilege;
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
if readable { RaplAccess::Readable } else { RaplAccess::NoInterface }
|
||||
}
|
||||
|
||||
/// How many times the NVML liveness probe has run in this process. Always 0
|
||||
/// without the `nvml` feature, which has no probe.
|
||||
#[cfg_attr(not(all(test, feature = "nvml")), allow(dead_code))]
|
||||
|
|
@ -6039,7 +6131,15 @@ pub enum FabricStatus {
|
|||
/// advancing — so a figure from it would be a reading of nothing. Apple's
|
||||
/// CPU rails do this for an unprivileged reader.
|
||||
Frozen,
|
||||
/// This host has no such silicon.
|
||||
/// This `(fabric, source)` pair is not available here: either the silicon
|
||||
/// is absent, or this source is not an interface to it on this platform
|
||||
/// (`ri_energy_nj` off macOS, RAPL off x86 Linux).
|
||||
///
|
||||
/// It makes **no** claim that the fabric is unmeasurable by some other
|
||||
/// route, and so — unlike [`FabricStatus::NoCounterExists`] — it excuses
|
||||
/// nothing. Reporting a counter this process merely cannot read as
|
||||
/// `Absent` would assert an absence of hardware; that is
|
||||
/// [`FabricStatus::NeedsPrivilege`].
|
||||
Absent,
|
||||
/// The silicon is here and the counter needs privilege this process does
|
||||
/// not have. A legitimate gap, and a fixable one.
|
||||
|
|
@ -6267,19 +6367,19 @@ pub fn survey() -> Vec<FabricCapability> {
|
|||
}
|
||||
|
||||
// ── x86 CPU package energy.
|
||||
//
|
||||
// Accessibility is asked **before** liveness, because `Rapl::open` returns
|
||||
// `None` for a host with no powercap tree and for a host whose package
|
||||
// zone is root-only alike, and this row previously reported both as
|
||||
// `Absent` — "this host has no such silicon". On a representative Linux
|
||||
// host `energy_uj` carries mode 0440 root:energy, so an unprivileged
|
||||
// reader gets `EACCES` from a file that is plainly there. That is
|
||||
// `NeedsPrivilege`: a legitimate gap, and a fixable one.
|
||||
push(
|
||||
Fabric::Cpu,
|
||||
FabricSource::Rapl,
|
||||
if cfg!(target_os = "linux") {
|
||||
match rapl_status() {
|
||||
CounterStatus::Live => FabricStatus::Live {
|
||||
attribution: Attribution::Machine,
|
||||
// The MSR behind powercap updates about every millisecond.
|
||||
resolution_ns: Some(1_000_000),
|
||||
},
|
||||
CounterStatus::Unavailable => FabricStatus::Absent,
|
||||
_ => FabricStatus::Frozen,
|
||||
}
|
||||
rapl_fabric_status(rapl_access(), rapl_status())
|
||||
} else {
|
||||
FabricStatus::Absent
|
||||
},
|
||||
|
|
@ -6384,6 +6484,33 @@ pub fn survey() -> Vec<FabricCapability> {
|
|||
out
|
||||
}
|
||||
|
||||
/// The RAPL row's status, from its accessibility and its liveness.
|
||||
///
|
||||
/// Extracted from [`survey`] because [`survey`] reads the real
|
||||
/// `/sys/class/powercap` and so cannot be tested on a host that does not have
|
||||
/// one — which left this mapping, the whole point of asking about
|
||||
/// accessibility, unpinned by any test. Taking both answers as arguments makes
|
||||
/// every combination checkable anywhere.
|
||||
///
|
||||
/// Accessibility is asked first: a counter this process cannot read is not an
|
||||
/// absent counter, and only [`RaplAccess::Readable`] makes the liveness answer
|
||||
/// meaningful at all — the probe cannot observe a counter it cannot open.
|
||||
fn rapl_fabric_status(access: RaplAccess, counter: CounterStatus) -> FabricStatus {
|
||||
match access {
|
||||
RaplAccess::NeedsPrivilege => FabricStatus::NeedsPrivilege,
|
||||
RaplAccess::NoInterface => FabricStatus::Absent,
|
||||
RaplAccess::Readable => match counter {
|
||||
CounterStatus::Live => FabricStatus::Live {
|
||||
attribution: Attribution::Machine,
|
||||
// The MSR behind powercap updates about every millisecond.
|
||||
resolution_ns: Some(1_000_000),
|
||||
},
|
||||
CounterStatus::Unavailable => FabricStatus::Absent,
|
||||
_ => FabricStatus::Frozen,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a path exists, used only to decide whether silicon is present.
|
||||
///
|
||||
/// A presence probe, never a reading: it answers "is there an accelerator
|
||||
|
|
@ -8820,6 +8947,167 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// **A counter this process cannot read is not an absent counter.**
|
||||
///
|
||||
/// `Rapl::open` returns `None` for a missing tree and for a root-only
|
||||
/// `energy_uj` alike, and `survey` reported both as `Absent` — "this host
|
||||
/// has no such silicon". On a representative Linux host `energy_uj`
|
||||
/// carries mode 0440 root:energy, so an unprivileged reader gets `EACCES`
|
||||
/// from a file that is plainly there.
|
||||
///
|
||||
/// The permission is injected rather than applied with `chmod`, which
|
||||
/// would prove nothing in a container running as root.
|
||||
#[test]
|
||||
fn a_rapl_counter_needing_privilege_is_not_reported_absent() {
|
||||
let tree = FakePowercap::new(&["1000", "2000"]);
|
||||
let denied = |p: &std::path::Path| -> std::io::Result<String> {
|
||||
if p.ends_with("energy_uj") {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"EACCES",
|
||||
));
|
||||
}
|
||||
std::fs::read_to_string(p)
|
||||
};
|
||||
|
||||
// The discriminating pair: the same tree, read two ways.
|
||||
assert_eq!(
|
||||
rapl_access_in(&tree.root),
|
||||
RaplAccess::Readable,
|
||||
"a readable package zone is readable"
|
||||
);
|
||||
assert_eq!(
|
||||
rapl_access_with(&tree.root, denied),
|
||||
RaplAccess::NeedsPrivilege,
|
||||
"a package zone whose energy_uj is root-only is a privilege gap, not absent hardware"
|
||||
);
|
||||
|
||||
// And the fact `Rapl::open` erases: both reads leave it unable to open,
|
||||
// which is why the access question has to be asked separately.
|
||||
assert!(Rapl::open_in(&tree.root).is_some(), "the readable tree opens");
|
||||
}
|
||||
|
||||
/// The mapping the whole fix turns on, and it was unpinned until a
|
||||
/// mutation flipping it back to `Absent` broke no test at all.
|
||||
///
|
||||
/// `NeedsPrivilege` must survive **every** liveness answer: the probe
|
||||
/// cannot observe a counter it cannot open, so a `Unavailable` reading
|
||||
/// beside an unreadable counter says nothing about the hardware.
|
||||
#[test]
|
||||
fn a_privilege_gap_is_reported_as_such_by_the_survey() {
|
||||
for counter in
|
||||
[CounterStatus::Live, CounterStatus::Unavailable, CounterStatus::Frozen]
|
||||
{
|
||||
assert_eq!(
|
||||
rapl_fabric_status(RaplAccess::NeedsPrivilege, counter),
|
||||
FabricStatus::NeedsPrivilege,
|
||||
"a counter that cannot be read is not absent hardware, whatever \
|
||||
the probe says ({counter:?})"
|
||||
);
|
||||
}
|
||||
|
||||
// Only an absent interface is `Absent`.
|
||||
assert_eq!(
|
||||
rapl_fabric_status(RaplAccess::NoInterface, CounterStatus::Unavailable),
|
||||
FabricStatus::Absent
|
||||
);
|
||||
|
||||
// And a readable counter is graded on its liveness as before.
|
||||
assert_eq!(
|
||||
rapl_fabric_status(RaplAccess::Readable, CounterStatus::Live),
|
||||
FabricStatus::Live {
|
||||
attribution: Attribution::Machine,
|
||||
resolution_ns: Some(1_000_000)
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
rapl_fabric_status(RaplAccess::Readable, CounterStatus::Frozen),
|
||||
FabricStatus::Frozen
|
||||
);
|
||||
assert_eq!(
|
||||
rapl_fabric_status(RaplAccess::Readable, CounterStatus::Unavailable),
|
||||
FabricStatus::Absent
|
||||
);
|
||||
|
||||
// `NeedsPrivilege` excuses nothing: it is not `NoCounterExists`.
|
||||
assert_ne!(
|
||||
rapl_fabric_status(RaplAccess::NeedsPrivilege, CounterStatus::Unavailable),
|
||||
FabricStatus::NoCounterExists,
|
||||
"a permission gap must never read as a hardware exemption"
|
||||
);
|
||||
}
|
||||
|
||||
/// A zone this process cannot even identify may be a package, so an
|
||||
/// unreadable `name` is a privilege gap too rather than a skipped zone.
|
||||
#[test]
|
||||
fn a_zone_whose_name_needs_privilege_is_a_privilege_gap() {
|
||||
let tree = FakePowercap::new(&["1000"]);
|
||||
let name_denied = |p: &std::path::Path| -> std::io::Result<String> {
|
||||
if p.ends_with("name") {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"EACCES",
|
||||
));
|
||||
}
|
||||
std::fs::read_to_string(p)
|
||||
};
|
||||
assert_eq!(rapl_access_with(&tree.root, name_denied), RaplAccess::NeedsPrivilege);
|
||||
}
|
||||
|
||||
/// One unreadable package beside a readable one still decides the answer:
|
||||
/// a figure claims the whole CPU, so it is every package zone or none.
|
||||
#[test]
|
||||
fn one_package_needing_privilege_decides_the_whole_answer() {
|
||||
let tree = FakePowercap::new(&["1000", "2000"]);
|
||||
let one_denied = |p: &std::path::Path| -> std::io::Result<String> {
|
||||
if p.ends_with("energy_uj") && p.to_string_lossy().contains("intel-rapl:1") {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"EACCES",
|
||||
));
|
||||
}
|
||||
std::fs::read_to_string(p)
|
||||
};
|
||||
assert_eq!(
|
||||
rapl_access_with(&tree.root, one_denied),
|
||||
RaplAccess::NeedsPrivilege,
|
||||
"a readable package does not excuse one that is not"
|
||||
);
|
||||
}
|
||||
|
||||
/// No tree, and no package zone in a tree, are both an absent interface —
|
||||
/// which is the one case `Absent` really describes.
|
||||
#[test]
|
||||
fn an_absent_interface_is_distinguished_from_a_privilege_gap() {
|
||||
let tree = FakePowercap::new(&["1000"]);
|
||||
assert_eq!(
|
||||
rapl_access_in(&tree.root.join("no-such-dir")),
|
||||
RaplAccess::NoInterface,
|
||||
"a missing powercap tree exposes no interface"
|
||||
);
|
||||
|
||||
let psys_only = FakePowercap::with_zones(&[
|
||||
("intel-rapl:0", Some("psys"), "90000", Some("262143328850")),
|
||||
("intel-rapl:1", Some("dram"), "30000", Some("65532610987")),
|
||||
]);
|
||||
assert_eq!(
|
||||
rapl_access_in(&psys_only.root),
|
||||
RaplAccess::NoInterface,
|
||||
"a tree with no package zone exposes no package interface"
|
||||
);
|
||||
}
|
||||
|
||||
/// Accessibility is not liveness. A readable counter that is frozen is
|
||||
/// still readable — the two questions have separate answers and separate
|
||||
/// statuses.
|
||||
#[test]
|
||||
fn accessibility_is_not_liveness() {
|
||||
let frozen = FakePowercap::new(&["1000"]);
|
||||
assert_eq!(rapl_access_in(&frozen.root), RaplAccess::Readable);
|
||||
// The counter never moves, so the liveness probe declines it.
|
||||
assert_eq!(rapl_status_on(|| Rapl::open_in(&frozen.root)), CounterStatus::Frozen);
|
||||
}
|
||||
|
||||
/// No package zone, no RAPL counter: a host exposing only `psys` (and a
|
||||
/// `dram` zone) is Unavailable without a probe, never a figure. Nor does
|
||||
/// the reader open with a package left out: one whose `energy_uj` does
|
||||
|
|
|
|||
Loading…
Reference in a new issue