jcp-energy: a counter this process cannot read is not an absent counter #328

Merged
dcharlot merged 1 commit from fix/rapl-permission-vs-absent into main 2026-10-19 11:12:29 -04:00
Owner

survey() exists to tell apart the ways an energy counter can be missing, because they are not the same kind of gap. NoCounterExists is a property of the hardware and is the status that makes a disclosure exemption legitimate; NeedsPrivilege is, in the enum's own words, "a legitimate gap, and a fixable one".

The RAPL row could not produce NeedsPrivilege at all.

Rapl::open_in maps every failure to None — its own doc says so, "non-Linux, locked-down sysfs, no Intel/AMD RAPL" — and survey() mapped that single None to FabricStatus::Absent, documented as "This host has no such silicon."

This is not hypothetical

Linux has restricted energy_uj to privileged readers since the power side-channel disclosures of 2020. Checked on a Linux host in this fleet:

440 root:energy  /sys/class/powercap/intel-rapl:0/energy_uj
440 root:energy  /sys/class/powercap/intel-rapl:0:0/energy_uj

Mode 0440 root:energy. A process outside that group reads EACCES from a file that is plainly there, and the survey reported that as an absence of hardware. In a disclosure context the direction matters: Absent understates by asserting there is nothing to meter, where the truth is a permission a sysadmin can grant.

The fix

RaplAccess asks the access question separately from the liveness question — Readable, NeedsPrivilege, NoInterface — and rapl_fabric_status maps the two answers to a status.

Accessibility is consulted first, because the probe cannot observe a counter it cannot open: a Unavailable liveness answer beside an unreadable counter says nothing about the hardware, so it must not be allowed to produce Absent.

A permission failure on any one package zone decides the answer even beside a readable one. That follows Rapl::open_in's existing 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.

This is a consistency repair, not a new idea

probe_path's doc already states the rule:

A presence probe, never a reading: it answers "is there an accelerator here", so the survey can say NotImplemented — our gap — instead of Absent, which would be a false claim about the host.

Every vendor accelerator row follows it. The RAPL row was the one that did not.

Absent now says what it asserts

It is used for (fabric, source) pairs unavailable here, including a source that is not an interface to the fabric on this platform — ri_energy_nj off macOS, RAPL off x86 Linux — where the silicon is plainly present. So its doc no longer claims "no such silicon", states that it makes no claim about the fabric being unmeasurable by another route, and says that unlike NoCounterExists it excuses nothing.

What falsification caught in my own work

mutation result
A1 a permission error on energy_uj is an absent interface again a_rapl_counter_needing_privilege_is_not_reported_absent + 1 FAILED
A2 an unreadable zone name is silently skipped only a_zone_whose_name_needs_privilege_is_a_privilege_gap FAILED
A3 a readable package excuses an unreadable sibling only one_package_needing_privilege_decides_the_whole_answer FAILED
A4 survey reports Absent where the counter needs privilege only a_privilege_gap_is_reported_as_such_by_the_survey FAILED
A5 accessibility ignored — liveness alone decides (the exact pre-fix behaviour) only a_privilege_gap_is_reported_as_such_by_the_survey FAILED

A4 initially survived, and that was the real finding. The mapping was written inline in survey(), which reads the real /sys/class/powercap and so cannot be tested on a host without one — leaving the entire point of asking about accessibility unpinned by any test. Flipping NeedsPrivilege back to Absent broke nothing at all. Extracting rapl_fabric_status to take both answers as arguments makes every combination checkable anywhere, and A5 confirms the closure: a mutation restoring the exact pre-fix behaviour now fails.

Two of my mutations (A3's first form, A5's first form) were also wrong — one semantically a no-op, one not compiling. A mutation that does not change behaviour or does not build tests nothing, and both were rewritten before any result was read from them.

Permissions are injected in tests rather than applied with chmod, which would prove nothing in a container running as root where every mode is readable. The headline test is the discriminating pair: the same fake tree read two ways, Readable and NeedsPrivilege.

Verification

  • cargo check --workspace --all-targets --locked (jcp-rs) — exit 0
  • cargo test --workspace --locked — exit 0, 64 test binaries green, jcp-energy 116 passed
  • cargo check -p jcp-energy --features nvml --all-targets --locked — exit 0 (the feature leg CI runs separately)
  • cargo clippy -p jcp-energy -p jcp-gateway --all-targets --no-deps --locked -- -D warnings — clean
  • cargo metadata --locked — exit 0
  • check-spec-refs 1626 citations / 15 standards, check-lockfiles 39 current, check-tests-in-ci clean, check-comparative-prose clean

jcp-energy is a path dependency only within jcp/jcp-rs (unlike jcp-efficiency, which spans three workspaces), so this workspace is the whole blast radius — verified rather than assumed.

🤖 Generated with Claude Code

`survey()` exists to tell apart the ways an energy counter can be missing, because they are not the same kind of gap. `NoCounterExists` is a property of the hardware and is the status that makes a disclosure exemption legitimate; `NeedsPrivilege` is, in the enum's own words, *"a legitimate gap, and a fixable one"*. **The RAPL row could not produce `NeedsPrivilege` at all.** `Rapl::open_in` maps every failure to `None` — its own doc says so, *"non-Linux, locked-down sysfs, no Intel/AMD RAPL"* — and `survey()` mapped that single `None` to `FabricStatus::Absent`, documented as **"This host has no such silicon."** ## This is not hypothetical Linux has restricted `energy_uj` to privileged readers since the power side-channel disclosures of 2020. Checked on a Linux host in this fleet: ``` 440 root:energy /sys/class/powercap/intel-rapl:0/energy_uj 440 root:energy /sys/class/powercap/intel-rapl:0:0/energy_uj ``` Mode `0440 root:energy`. A process outside that group reads `EACCES` from a file that is **plainly there**, and the survey reported that as an absence of hardware. In a disclosure context the direction matters: `Absent` understates by asserting there is nothing to meter, where the truth is a permission a sysadmin can grant. ## The fix `RaplAccess` asks the *access* question separately from the *liveness* question — `Readable`, `NeedsPrivilege`, `NoInterface` — and `rapl_fabric_status` maps the two answers to a status. Accessibility is consulted **first**, because the probe cannot observe a counter it cannot open: a `Unavailable` liveness answer beside an unreadable counter says nothing about the hardware, so it must not be allowed to produce `Absent`. A permission failure on **any one** package zone decides the answer even beside a readable one. That follows `Rapl::open_in`'s existing 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. ### This is a consistency repair, not a new idea `probe_path`'s doc already states the rule: > *A presence probe, never a reading: it answers "is there an accelerator here", so the survey can say `NotImplemented` — our gap — instead of `Absent`, which would be a false claim about the host.* Every vendor accelerator row follows it. The RAPL row was the one that did not. ### `Absent` now says what it asserts It is used for `(fabric, source)` pairs unavailable here, **including a source that is not an interface to the fabric on this platform** — `ri_energy_nj` off macOS, RAPL off x86 Linux — where the silicon is plainly present. So its doc no longer claims "no such silicon", states that it makes no claim about the fabric being unmeasurable by another route, and says that unlike `NoCounterExists` it **excuses nothing**. ## What falsification caught in my own work | mutation | result | |---|---| | **A1** a permission error on `energy_uj` is an absent interface again | `a_rapl_counter_needing_privilege_is_not_reported_absent` + 1 FAILED | | **A2** an unreadable zone `name` is silently skipped | **only** `a_zone_whose_name_needs_privilege_is_a_privilege_gap` FAILED | | **A3** a readable package excuses an unreadable sibling | **only** `one_package_needing_privilege_decides_the_whole_answer` FAILED | | **A4** `survey` reports `Absent` where the counter needs privilege | **only** `a_privilege_gap_is_reported_as_such_by_the_survey` FAILED | | **A5** accessibility ignored — liveness alone decides (**the exact pre-fix behaviour**) | **only** `a_privilege_gap_is_reported_as_such_by_the_survey` FAILED | **A4 initially survived, and that was the real finding.** The mapping was written inline in `survey()`, which reads the real `/sys/class/powercap` and so cannot be tested on a host without one — leaving *the entire point of asking about accessibility* unpinned by any test. Flipping `NeedsPrivilege` back to `Absent` broke nothing at all. Extracting `rapl_fabric_status` to take both answers as arguments makes every combination checkable anywhere, and **A5 confirms the closure**: a mutation restoring the exact pre-fix behaviour now fails. Two of my mutations (A3's first form, A5's first form) were also wrong — one semantically a no-op, one not compiling. A mutation that does not change behaviour or does not build tests nothing, and both were rewritten before any result was read from them. Permissions are **injected** in tests rather than applied with `chmod`, which would prove nothing in a container running as root where every mode is readable. The headline test is the discriminating pair: the same fake tree read two ways, `Readable` and `NeedsPrivilege`. ## Verification - `cargo check --workspace --all-targets --locked` (`jcp-rs`) — exit **0** - `cargo test --workspace --locked` — exit **0**, **64** test binaries green, `jcp-energy` **116** passed - `cargo check -p jcp-energy --features nvml --all-targets --locked` — exit **0** (the feature leg CI runs separately) - `cargo clippy -p jcp-energy -p jcp-gateway --all-targets --no-deps --locked -- -D warnings` — clean - `cargo metadata --locked` — exit **0** - `check-spec-refs` 1626 citations / 15 standards, `check-lockfiles` 39 current, `check-tests-in-ci` clean, `check-comparative-prose` clean `jcp-energy` is a path dependency only within `jcp/jcp-rs` (unlike `jcp-efficiency`, which spans three workspaces), so this workspace is the whole blast radius — verified rather than assumed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jcp-energy: a counter this process cannot read is not an absent counter
All checks were successful
energy meters on real hardware / RAPL and NVML on jetson-hub (pull_request) Successful in 47s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 7s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 58s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m54s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m14s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m12s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m16s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 2m55s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 3m58s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 1m7s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 6m50s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 2m29s
3b1cb556c0
`survey()` exists to distinguish the ways an energy counter can be missing,
because they are not the same kind of gap: `NoCounterExists` is a property of
the hardware and is the status that makes a disclosure exemption legitimate,
while `NeedsPrivilege` is "a legitimate gap, and a fixable one".

The RAPL row could not produce `NeedsPrivilege` at all. `Rapl::open_in` maps
every failure to `None` — its own doc says so, "non-Linux, locked-down sysfs,
no Intel/AMD RAPL" — and `survey()` mapped that single `None` to
`FabricStatus::Absent`, documented as "This host has no such silicon".

Linux has restricted `energy_uj` to privileged readers since the power
side-channel disclosures of 2020. Checked on a Linux host in this fleet:

    440 root:energy  /sys/class/powercap/intel-rapl:0/energy_uj
    440 root:energy  /sys/class/powercap/intel-rapl:0:0/energy_uj

Mode 0440 root:energy, so a process outside that group reads `EACCES` from a
file that is plainly there. The survey called that an absence of hardware.

`RaplAccess` asks the access question separately from the liveness question:
`Readable`, `NeedsPrivilege`, or `NoInterface`. `rapl_fabric_status` maps the
two answers to a status, consulting accessibility first — the probe cannot
observe a counter it cannot open, so a liveness answer beside an unreadable
counter says nothing about the hardware. A permission failure on any one
package zone decides the answer even beside a readable one, which follows
`Rapl::open_in`'s existing rule that a figure claims the whole CPU and so is
every package zone or none.

This is a consistency repair, not a new idea: `probe_path`'s doc already states
the rule — "a presence probe, never a reading... so the survey can say
NotImplemented — our gap — instead of Absent, which would be a false claim
about the host" — and every vendor accelerator row follows it. The RAPL row was
the one that did not.

`FabricStatus::Absent` now says what it actually asserts. It is used for
`(fabric, source)` pairs that are not available here, including a source that
is not an interface to the fabric on this platform (`ri_energy_nj` off macOS,
RAPL off x86 Linux) — where the silicon is plainly present. So it makes no
claim that the fabric is unmeasurable by another route and, unlike
`NoCounterExists`, excuses nothing.

The status mapping is a free function rather than inline in `survey()` because
`survey()` reads the real `/sys/class/powercap` and so cannot be tested on a
host without one. That left the mapping — the entire point of asking about
accessibility — unpinned: falsification caught it, because a mutation flipping
`NeedsPrivilege` back to `Absent` broke no test whatsoever. Taking both answers
as arguments makes every combination checkable anywhere, and a mutation
restoring the exact pre-fix behaviour now fails.

Permissions are injected in tests rather than applied with `chmod`, which would
prove nothing in a container running as root where every mode is readable. The
headline test is the discriminating pair: the same fake tree read two ways,
`Readable` and `NeedsPrivilege`.

Co-Authored-By: Claude Opus 5 <[email protected]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!328
No description provided.