diff --git a/aldermon/Makefile b/aldermon/Makefile
index 958e459..75738f1 100644
--- a/aldermon/Makefile
+++ b/aldermon/Makefile
@@ -5,8 +5,9 @@ SYSCONF ?= /etc
DATADIR ?= /usr/share
BIN := $(DESTDIR)$(PREFIX)/bin/aldermon
+MSRBIN := $(DESTDIR)$(PREFIX)/bin/aldermon-msr
CONF := $(DESTDIR)$(SYSCONF)/aldermon/aldermon.conf
-HELPER := $(DESTDIR)$(DATADIR)/aldermon/powercap-pl-rules.sh
+SETUP := $(DESTDIR)$(DATADIR)/aldermon/aldermon-setup.sh
.PHONY: all build check install uninstall clean
@@ -22,13 +23,21 @@ check:
install: build
install -Dm755 target/release/aldermon $(BIN)
+ install -Dm755 target/release/aldermon-msr $(MSRBIN)
install -Dm644 aldermon.conf $(CONF)
- install -Dm755 powercap-pl-rules.sh $(HELPER)
+ install -Dm755 aldermon-setup.sh $(SETUP)
+ifeq ($(DESTDIR),)
+ # VID needs the cap half of a two-gate check (msr nodes also go 0440 adm
+ # via aldermon-setup.sh); grant cap_sys_rawio to the read-only helper
+ # only. Re-applied on every install since rebuilding drops file caps.
+ setcap cap_sys_rawio+ep $(MSRBIN)
+endif
uninstall:
- rm -f $(BIN) $(CONF) $(HELPER)
+ rm -f $(BIN) $(MSRBIN) $(CONF) $(SETUP)
+ rm -f $(DESTDIR)$(DATADIR)/aldermon/powercap-pl-rules.sh
rmdir -p --ignore-fail-on-non-empty $(CONF) 2>/dev/null || true
- rmdir -p --ignore-fail-on-non-empty $(HELPER) 2>/dev/null || true
+ rmdir -p --ignore-fail-on-non-empty $(SETUP) 2>/dev/null || true
clean:
cargo clean
diff --git a/aldermon/README.md b/aldermon/README.md
index 8f06b95..f0ac840 100644
--- a/aldermon/README.md
+++ b/aldermon/README.md
@@ -7,7 +7,7 @@ voltage for overclock-safety work.
```sh
aldermon --tui # TUI (default workhorse)
-aldermon --vid # VID spike tool (debug, needs root + msr module)
+aldermon --vid # VID spike tool (debug; needs MSR access, see below)
aldermon --log # append CSV samples to ./aldermon-vid.log
aldermon # one-shot sensor dump
aldermon --help # options
@@ -15,8 +15,10 @@ aldermon --help # options
The TUI always shows the delivered vCore (SIO in0), the CPU's requested SVID
setpoint (IA32_PERF_STATUS), and the delta (delivered − requested; negative =
-VRM droop, positive = LLC overshoot). The VID read needs root +
-`/dev/cpu/*/msr`; without it the panel shows `VID n/a (needs root)`.
+VRM droop, positive = LLC overshoot). VID needs MSR access: run under root,
+or install once (below) — the setcap'd `aldermon-msr` helper holds
+`CAP_SYS_RAWIO`, the app itself stays unprivileged. Without access the panel
+shows `VID n/a (no msr access)`.
`--log` appends one CSV row per poll to `./aldermon-vid.log` (header +
per-CPU VID / frequency / raw MSR columns). With `--tui` it logs live; with
@@ -70,21 +72,28 @@ make check # fmt + clippy + tests
doas make install # PREFIX=/usr/local by default
```
-Installs `aldermon` to `$(PREFIX)/bin`, this repo's `aldermon.conf` to
-`/etc/aldermon/aldermon.conf` (system default — override per-user via
-`~/.config/aldermon/`), and the udev helper to `/usr/share/aldermon/`.
-`make uninstall` reverses it (leaves the system conf dir if non-empty).
+Installs `aldermon` + `aldermon-msr` (VID helper) to `$(PREFIX)/bin`, this
+repo's `aldermon.conf` to `/etc/aldermon/aldermon.conf` (system default —
+override per-user via `~/.config/aldermon/`), and the setup script to
+`/usr/share/aldermon/`. `make install` also setcaps the helper (skipped for
+staged `DESTDIR=` installs — `aldermon-setup.sh` applies it). `make
+uninstall` reverses it (leaves the system conf dir if non-empty).
-## Package power (RAPL) — one-time setup
-
-`energy_uj` is root-only on this kernel. To read watts as a normal user,
-install the udev rule once as root (comes from `make install`):
+## Privileged setup — one-time (RAPL watts + VID)
```sh
-doas /usr/share/aldermon/powercap-pl-rules.sh
+doas /usr/share/aldermon/aldermon-setup.sh
doas usermod -aG adm $USER # then re-login
```
+1. RAPL: `energy_uj` is root-only, so the script installs a udev rule that
+ chgrps it 0440 to group `adm` — watts then work unprivileged.
+2. VID: opening `/dev/cpu/*/msr` passes two gates — the file-mode check
+ (node is 0600; `CAP_SYS_RAWIO` does NOT override DAC) and `msr_open()`'s
+ capability check. The script installs a udev rule making the nodes
+ `0440 adm` and sets `cap_sys_rawio+ep` on `aldermon-msr`, a read-only
+ helper that touches exactly one MSR; the app never holds the capability.
+
## Status
Working: TUI, config (+ system /etc default), RAPL power. Remaining
diff --git a/aldermon/aldermon-setup.sh b/aldermon/aldermon-setup.sh
new file mode 100755
index 0000000..39bb1c5
--- /dev/null
+++ b/aldermon/aldermon-setup.sh
@@ -0,0 +1,69 @@
+#!/bin/sh
+# aldermon-setup.sh - one-shot privileged setup for aldermon (run once as
+# root; idempotent, re-running overwrites installed files):
+#
+# 1. RAPL watts: energy_uj is 0400 root-only. Installs a udev rule + RUN
+# helper chgrp'ing it 0440 to group `adm` so the TUI reads watts
+# unprivileged.
+# 2. VID: opening /dev/cpu/N/msr must pass TWO gates: the DAC file-mode
+# check (node is 0600 root:root — CAP_SYS_RAWIO does NOT override DAC),
+# then msr_open()'s capable(CAP_SYS_RAWIO). So: udev puts the nodes at
+# 0440 group `adm` (gate 1), and cap_sys_rawio+ep goes on the minimal
+# read-only helper aldermon-msr (gate 2, reads only 0x198). Neither
+# alone works; the app itself never holds the capability.
+#
+# doas ./aldermon-setup.sh (from the source tree)
+# doas /usr/share/aldermon/aldermon-setup.sh (after make install)
+
+set -eu
+
+PREFIX=${PREFIX:-/usr/local}
+MSRBIN=$PREFIX/bin/aldermon-msr
+HELPER=$PREFIX/sbin/aldermon-powercap-chmod
+RULE=/etc/udev/rules.d/60-aldermon-powercap.rules
+
+# Helper: relax energy_uj to 0440, group adm, on every powercap dir.
+cat > "$HELPER" <<'EOF'
+#!/bin/sh
+for d in /sys/class/powercap/intel-rapl*; do
+ [ -e "$d/energy_uj" ] && chgrp adm "$d/energy_uj" && chmod 0440 "$d/energy_uj"
+done
+exit 0
+EOF
+chmod 0755 "$HELPER"
+
+# RUN+= (not MODE=/GROUP=) - powercap has no devnode, so udev's standard
+# MODE/GROUP assignment doesn't apply; we chmod in-process instead.
+cat > "$RULE" <<EOF
+SUBSYSTEM=="powercap", ACTION=="add", RUN+="$HELPER"
+EOF
+
+# Apply immediately so a reboot isn't required.
+"$HELPER"
+
+# VID gate 1: unlike powercap, msr IS a real devnode (SUBSYSTEM=="msr",
+# KERNEL=="msr0..N", DEVNAME=/dev/cpu/N/msr), so udev MODE/GROUP works
+# directly — put the nodes 0440 group adm.
+MSRRULE=/etc/udev/rules.d/60-aldermon-msr.rules
+cat > "$MSRRULE" <<'EOF'
+KERNEL=="msr[0-9]*", SUBSYSTEM=="msr", MODE="0440", GROUP="adm"
+EOF
+udevadm control --reload-rules
+udevadm trigger --subsystem-match=msr
+
+# VID gate 2: hand the capability to the helper only. Fail loudly if it's
+# missing — without the cap the helper is inert and the TUI shows "VID n/a".
+if [ -x "$MSRBIN" ]; then
+ setcap cap_sys_rawio+ep "$MSRBIN"
+ echo " setcap cap_sys_rawio+ep $MSRBIN"
+else
+ echo "aldermon-setup: $MSRBIN not found - run 'doas make install' first," >&2
+ echo " or use aldermon --vid under root." >&2
+ exit 1
+fi
+
+echo "Installed:"
+echo " $HELPER"
+echo " $RULE"
+echo " $MSRRULE"
+echo "Add yourself to adm if not already: doas usermod -aG adm \$USER"
\ No newline at end of file
diff --git a/aldermon/powercap-pl-rules.sh b/aldermon/powercap-pl-rules.sh
deleted file mode 100755
index c852ef8..0000000
--- a/aldermon/powercap-pl-rules.sh
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/bin/sh
-# powercap-pl-rules.sh - one-shot installer for RAPL access (aldermon).
-#
-# RAPL energy_uj is 0400 root-only on this kernel, which gates the power
-# panel. This installs a udev rule + helper so any user in group `adm`
-# can read energy counters. Run once as root:
-#
-# doas ./powercap-pl-rules.sh
-#
-# Idempotent: re-running overwrites installed files.
-
-set -eu
-
-HELPER=/usr/local/sbin/aldermon-powercap-chmod
-RULE=/etc/udev/rules.d/60-aldermon-powercap.rules
-
-# Helper: relax energy_uj to 0440, group adm, on every powercap dir.
-cat > "$HELPER" <<'EOF'
-#!/bin/sh
-for d in /sys/class/powercap/intel-rapl*; do
- [ -e "$d/energy_uj" ] && chgrp adm "$d/energy_uj" && chmod 0440 "$d/energy_uj"
-done
-exit 0
-EOF
-chmod 0755 "$HELPER"
-
-# RUN+= (not MODE=/GROUP=) - powercap has no devnode, so udev's standard
-# MODE/GROUP assignment doesn't apply; we chmod in-process instead.
-cat > "$RULE" <<EOF
-SUBSYSTEM=="powercap", ACTION=="add", RUN+="$HELPER"
-EOF
-
-# Apply immediately so a reboot isn't required.
-"$HELPER"
-
-echo "Installed:"
-echo " $HELPER"
-echo " $RULE"
-echo "Add yourself to adm if not already: doas usermod -aG adm \$USER"
\ No newline at end of file
diff --git a/aldermon/src/bin/aldermon-msr.rs b/aldermon/src/bin/aldermon-msr.rs
new file mode 100644
index 0000000..08a1e73
--- /dev/null
+++ b/aldermon/src/bin/aldermon-msr.rs
@@ -0,0 +1,53 @@
+//! aldermon-msr — minimal setcap'd helper for VID reads.
+//!
+//! The kernel gates /dev/cpu/N/msr twice: file mode (0600 root:root;
+//! CAP_SYS_RAWIO does NOT override DAC) AND msr_open()'s capable check —
+//! so access needs BOTH a 0440 adm node (udev rule) AND the capability,
+//! which file permissions can never grant. This helper is the only binary
+//! that ever holds the capability: it reads exactly one hard-coded MSR
+//! (IA32_PERF_STATUS 0x198), read-only, and prints one line per CPU.
+//! Everything else — the app, its deps, its config writes — stays unprivileged.
+//!
+//! usage: aldermon-msr CPU [CPU...] -> "CPU 0x<hex>" per cpu, "CPU -" on miss
+
+use std::io::{Read, Seek, Write};
+
+const PERF_STATUS: u64 = 0x198;
+const CPU_MAX: u32 = 4096;
+
+fn read_perf_status(cpu: u32) -> Option<u64> {
+ let path = format!("/dev/cpu/{cpu}/msr");
+ let mut f = std::fs::File::open(path).ok()?;
+ f.seek(std::io::SeekFrom::Start(PERF_STATUS)).ok()?;
+ let mut buf = [0u8; 8];
+ f.read_exact(&mut buf).ok()?;
+ Some(u64::from_le_bytes(buf))
+}
+
+fn main() {
+ let mut cpus = Vec::new();
+ for arg in std::env::args().skip(1) {
+ match arg.parse::<u32>() {
+ Ok(c) if c <= CPU_MAX => cpus.push(c),
+ _ => {
+ eprintln!("usage: aldermon-msr CPU [CPU...]");
+ std::process::exit(2);
+ }
+ }
+ }
+ if cpus.is_empty() {
+ eprintln!("usage: aldermon-msr CPU [CPU...]");
+ std::process::exit(2);
+ }
+ let out = std::io::stdout();
+ let mut out = out.lock();
+ for cpu in cpus {
+ let line = match read_perf_status(cpu) {
+ Some(v) => format!("{cpu} {v:#x}\n"),
+ None => format!("{cpu} -\n"),
+ };
+ if out.write_all(line.as_bytes()).is_err() {
+ std::process::exit(1);
+ }
+ }
+}
diff --git a/aldermon/src/main.rs b/aldermon/src/main.rs
index 43d4c16..a2088b8 100644
--- a/aldermon/src/main.rs
+++ b/aldermon/src/main.rs
@@ -23,12 +23,14 @@ usage: aldermon [OPTION]
(no option) one-shot sensor dump to stdout
--tui live monitoring TUI (q/Esc quit, F2 settings)
- --vid VID vs SIO in0 cross-check spike (needs root + msr module)
+ --vid VID vs SIO in0 cross-check spike (needs MSR access, see below)
--log append CSV samples to ./aldermon-vid.log
--help show this help
-The TUI always shows the delivered vCore, the requested VID and the delta
-(needs root + /dev/cpu/*/msr; shows 'VID n/a (needs root)' otherwise).
+The TUI always shows the delivered vCore, the requested VID and the delta.
+VID needs MSR access: run as root, or install the setcap'd aldermon-msr
+helper (doas make install + aldermon-setup.sh); otherwise it shows
+'VID n/a (no msr access)'.
--log appends one CSV row per poll; with --tui it logs live, with --vid it
logs continuously until interrupted (otherwise --vid runs a 5-sample spike).
@@ -136,11 +138,13 @@ fn dump() {
}
/// VID cross-check: compare SIO in0 (delivered rail) with the CPU's requested
-/// SVID setpoint (IA32_PERF_STATUS) per sample. Requires root + msr module.
+/// SVID setpoint (IA32_PERF_STATUS) per sample. Needs MSR access (root or
+/// the setcap'd aldermon-msr helper).
/// Without `log` this is a bounded 5-sample spike; with `log` it runs until
/// interrupted, appending CSV rows to ./aldermon-vid.log.
fn vid_spike(log: bool) {
let cpus = sensors::cpu_count();
+ let access = sensors::probe_msr();
let mut logger = if log {
match log::VidLogger::open(Path::new(LOG_PATH), cpus) {
Ok(l) => {
@@ -157,8 +161,11 @@ fn vid_spike(log: bool) {
};
if logger.is_none() {
- println!("== VID cross-check (needs root: /dev/cpu/*/msr) ==");
+ println!("== VID cross-check (MSR access: root, or setcap'd aldermon-msr) ==");
println!("(hint: --log appends to ./{LOG_PATH} continuously)");
+ if !access.usable() {
+ println!("(no MSR access — run under root, or install + setup the helper)");
+ }
}
// Package-energy counter for the logged watts (None until the second
// sample; the first delta has no baseline).
@@ -169,7 +176,7 @@ fn vid_spike(log: bool) {
let rounds: u64 = if logger.is_some() { u64::MAX } else { 5 };
for round in 0..rounds {
let vcore = sensors::sio_vcore();
- let msrs = sensors::vid_msrs(cpus);
+ let msrs = sensors::vid_msrs(access, cpus);
let vmax = sensors::vid_max(&msrs);
let freqs = cpu_frequencies();
diff --git a/aldermon/src/sensors.rs b/aldermon/src/sensors.rs
index c6349d6..207cf97 100644
--- a/aldermon/src/sensors.rs
+++ b/aldermon/src/sensors.rs
@@ -237,10 +237,84 @@ pub fn vid_from_msr(msr: u64) -> f64 {
((msr >> 32) & 0xffff) as f64 / (1u32 << 13) as f64
}
+/// How we can reach /dev/cpu/N/msr: opening it takes BOTH file read access
+/// (udev puts the nodes 0440 group adm) and CAP_SYS_RAWIO (msr_open), which
+/// only setcap or root can grant — so the unprivileged route is the setcap'd
+/// `aldermon-msr` helper. Probed once at startup; re-probing per poll would
+/// fork the helper on every tick.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MsrAccess {
+ Direct,
+ Helper,
+ None,
+}
+
+impl MsrAccess {
+ pub fn usable(self) -> bool {
+ self != MsrAccess::None
+ }
+}
+
+/// Locate the helper: sibling of the running exe first (installed layout and
+/// `cargo run` target/debug both put them side by side), else PATH.
+fn helper_path() -> std::path::PathBuf {
+ std::env::current_exe()
+ .ok()
+ .and_then(|exe| {
+ let sib = exe.parent()?.join("aldermon-msr");
+ sib.exists().then_some(sib)
+ })
+ .unwrap_or_else(|| std::path::PathBuf::from("aldermon-msr"))
+}
+
+/// Parse helper stdout: "CPU 0x<hex>" lines, "CPU -" for misses. Short or
+/// malformed output fills None slots so the result always covers all cpus.
+fn parse_helper_stdout(s: &str, n: usize) -> Vec<Option<u64>> {
+ let mut out = vec![None; n];
+ for (slot, line) in out.iter_mut().zip(s.lines()) {
+ if let Some(val) = line.split_once(' ').map(|(_, v)| v) {
+ if val != "-" {
+ *slot = u64::from_str_radix(val.trim_start_matches("0x"), 16).ok();
+ }
+ }
+ }
+ out
+}
+
+/// One helper exec for all cpus (a 50 ms poll = one fork+exec, not 16).
+fn helper_perf_status(n: usize) -> Option<Vec<Option<u64>>> {
+ let out = std::process::Command::new(helper_path())
+ .args((0..n).map(|c| c.to_string()).collect::<Vec<_>>())
+ .output()
+ .ok()?;
+ if !out.status.success() {
+ return None;
+ }
+ Some(parse_helper_stdout(
+ &String::from_utf8_lossy(&out.stdout),
+ n,
+ ))
+}
+
+/// Probe once which MSR route works (cpu 0 is always online).
+pub fn probe_msr() -> MsrAccess {
+ if perf_status_msr(0).is_some() {
+ return MsrAccess::Direct;
+ }
+ match helper_perf_status(1).map(|v| v[0]) {
+ Some(Some(_)) => MsrAccess::Helper,
+ _ => MsrAccess::None,
+ }
+}
+
/// Read the raw IA32_PERF_STATUS of every logical cpu `0..n` (None where the
-/// read fails, e.g. an offline cpu).
-pub fn vid_msrs(n: usize) -> Vec<Option<u64>> {
- (0..n).map(perf_status_msr).collect()
+/// read fails, e.g. an offline cpu), via the probed access route.
+pub fn vid_msrs(access: MsrAccess, n: usize) -> Vec<Option<u64>> {
+ match access {
+ MsrAccess::Direct => (0..n).map(perf_status_msr).collect(),
+ MsrAccess::Helper => helper_perf_status(n).unwrap_or_else(|| vec![None; n]),
+ MsrAccess::None => vec![None; n],
+ }
}
/// Highest requested VID across all logical CPUs (the core asking the VRM
@@ -296,8 +370,23 @@ mod tests {
Some(8000u64 << 32),
];
let m = vid_max(&msrs).unwrap();
- assert!((m - 9830.0 / 8192.0).abs() < 1e-9);
+ assert!((m - 9830.0 / 8192.0).abs() < 1e-12);
assert_eq!(vid_max(&[None, None]), None);
assert_eq!(vid_max(&[]), None);
}
+
+ #[test]
+ fn parse_helper_stdout_maps_values_misses_and_gaps() {
+ let got = parse_helper_stdout("0 0x266600000000\n1 -\n2 0x1f4000000000", 3);
+ assert_eq!(
+ got,
+ vec![Some(0x2666_0000_0000), None, Some(0x1f40_0000_0000)]
+ );
+ // fewer lines than cpus → trailing None; junk line → that slot None
+ let got = parse_helper_stdout("0 0xff\n", 3);
+ assert_eq!(got, vec![Some(0xff), None, None]);
+ let got = parse_helper_stdout("0 zz\n", 1);
+ assert_eq!(got, vec![None]);
+ assert_eq!(parse_helper_stdout("", 2), vec![None, None]);
+ }
}
diff --git a/aldermon/src/ui.rs b/aldermon/src/ui.rs
index a47244f..70c5528 100644
--- a/aldermon/src/ui.rs
+++ b/aldermon/src/ui.rs
@@ -73,9 +73,9 @@ pub struct App {
edit_buf: String,
/// Last settings-input error (inline, cleared on next keypress).
settings_error: Option<String>,
- /// MSR is readable (root + /dev/cpu/*/msr), probed once at startup. When
- /// false the VID read is skipped each poll and the panel shows "VID n/a".
- msr_readable: bool,
+ /// MSR access route (root direct read, or setcap'd aldermon-msr helper),
+ /// probed once at startup. None = VID skipped each poll, panel "VID n/a".
+ msr_access: sensors::MsrAccess,
/// Requested SVID setpoint this poll, max across logical CPUs (V).
vid: Option<f64>,
/// Per-logical-cpu raw IA32_PERF_STATUS, for the CSV row.
@@ -258,9 +258,9 @@ impl App {
pub fn new(logger: Option<VidLogger>, n_cpus: usize) -> App {
let cfg = config::load();
let cap = ring_capacity(&cfg);
- // Probe MSR once: a successful read of cpu0's IA32_PERF_STATUS means
- // we have root + /dev/cpu/0/msr. Avoids a stat syscall every poll.
- let msr_readable = sensors::perf_status_msr(0).is_some();
+ // Probe MSR once: root direct read, or the setcap'd aldermon-msr
+ // helper. Avoids a syscall + fork/exec every poll.
+ let msr_access = sensors::probe_msr();
App {
cfg,
vcore: None,
@@ -284,7 +284,7 @@ impl App {
settings_row: 0,
edit_buf: String::new(),
settings_error: None,
- msr_readable,
+ msr_access,
vid: None,
vid_msr: Vec::new(),
logger,
@@ -306,8 +306,8 @@ impl App {
fn poll(&mut self) {
self.vcore = sensors::sio_vcore();
- if self.msr_readable {
- self.vid_msr = sensors::vid_msrs(self.n_cpus);
+ if self.msr_access.usable() {
+ self.vid_msr = sensors::vid_msrs(self.msr_access, self.n_cpus);
self.vid = sensors::vid_max(&self.vid_msr);
}
if let Some(v) = self.vcore {
@@ -548,8 +548,8 @@ fn meter_line(
/// vCore panel text: delivered (SIO in0), requested SVID setpoint, and the
/// delta (delivered − requested; negative = VRM droop, positive = LLC
-/// overshoot). VID needs root + /dev/cpu/*/msr; `msr_readable` distinguishes
-/// "not root" from a transient read miss.
+/// overshoot). VID needs MSR access (root, or the setcap'd aldermon-msr
+/// helper); `msr_readable` distinguishes "no access" from a transient miss.
fn hero_text(vcore: Option<f64>, vid: Option<f64>, msr_readable: bool) -> String {
let base = match vcore {
Some(v) => format!("{v:.3} V"),
@@ -559,7 +559,7 @@ fn hero_text(vcore: Option<f64>, vid: Option<f64>, msr_readable: bool) -> String
(Some(ask), Some(got)) => format!("{base} VID {ask:.3} Δ{:+.3}", got - ask),
(Some(ask), None) => format!("{base} VID {ask:.3}"),
(None, _) if msr_readable => format!("{base} VID n/a"),
- (None, _) => format!("{base} VID n/a (needs root)"),
+ (None, _) => format!("{base} VID n/a (no msr access)"),
}
}
@@ -829,7 +829,7 @@ pub fn draw(f: &mut Frame, app: &App) {
None => Color::DarkGray,
};
- let hero_text = hero_text(app.vcore, app.vid, app.msr_readable);
+ let hero_text = hero_text(app.vcore, app.vid, app.msr_access.usable());
let hero_peak = match app.vcore_peak {
Some(p) => format!("peak {p:.3}"),
None => String::new(),
@@ -1379,7 +1379,7 @@ mod tests {
fn hero_distinguishes_not_root_from_transient_miss() {
assert_eq!(
hero_text(Some(1.0), None, false),
- "1.000 V VID n/a (needs root)"
+ "1.000 V VID n/a (no msr access)"
);
assert_eq!(hero_text(Some(1.0), None, true), "1.000 V VID n/a");
}
@@ -1387,6 +1387,6 @@ mod tests {
#[test]
fn hero_handles_missing_vcore() {
assert_eq!(hero_text(None, Some(1.2), true), "n/a VID 1.200");
- assert_eq!(hero_text(None, None, false), "n/a VID n/a (needs root)");
+ assert_eq!(hero_text(None, None, false), "n/a VID n/a (no msr access)");
}
}
VID unprivileged: setcap'd minimal helper + two-gate setup script
- aldermon-msr (new bin target): the ONLY holder of cap_sys_rawio+ep;
argv = cpu ids (<=4096), reads hard-coded MSR 0x198, prints per-cpu
hex/"-"; app + deps stay unprivileged
- sensors.rs: MsrAccess{Direct,Helper,None} probed once; vid_msrs(access,n)
batches all cpus into ONE helper exec per poll; exe-sibling then PATH
lookup; parse_helper_stdout pads short/garbled output
- ui.rs/main.rs: App.msr_access replaces msr_readable; "VID n/a (no msr
access)" replaces "(needs root)"; --vid prints no-access hint
- powercap-pl-rules.sh -> aldermon-setup.sh: now installs BOTH rules —
60-aldermon-powercap (RUN chmod, no devnode) AND 60-aldermon-msr
(MODE/GROUP 0440 adm; msr IS a devnode). UAT finding: open(/dev/cpu/N/msr)
checks DAC BEFORE msr_open's capable() — setcap alone is inert on 0600
nodes; capability alone never grants DAC
- Makefile: install aldermon-msr + setup script; setcap in install when
DESTDIR empty (re-applied every install, rebuilds drop file caps);
uninstall removes helper + legacy rule script
- README: privileged-setup section rewritten (two gates, adm group)
- 28 tests, clippy -D/fmt clean; non-root degradation pty-verified 30x110;
UAT-CONFIRMED LIVE: regular user sees blue VID overlay via setcap'd
helper (no root)
7b77cafa6402fb1d9d01c4ae47caf2a9b0d87724
josie <josie@example.com> · 2026-10-08T19:46 ·
browse files at this commit
parents:
e719f77