OXYGENHUB

A runner reads its load and kernel without /proc (refs andymccall/oxygenhub-android#8) 337afb8f6043

sysinfo and uname give the same numbers; an Android app may not read
/proc/loadavg or /proc/sys/kernel/osrelease, so the phone runner was
refused, and the refusal logged, at every claim.
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key c2bbadac80df
When
· 2026-10-25 16:55 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
337afb8f604306f39990537f04b0259892164b1bf205bb374ff7b6d110b4e267
Depends on
4f5e2af75816 62829d15ef09 8f211e20bea5 c3db750ed96e ea56c56eaf90
Depended on by
f4dfd742b30d 3554afefb6aa 06a4c472b385
In bookmarks
main
In tags
v0.4.0 v0.5.0 v0.5.1 v0.5.2 v0.5.3
Edits
9 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified CHANGELOG.md +1 −0
…
3 unchanged lines
4 4
5 5
## Unreleased
6 6
7 +
- **A runner reads its load and kernel without /proc:** `sysinfo` and `uname`, the same numbers; an Android app may not read `/proc/loadavg` or `/proc/sys/kernel/osrelease`, so the phone runner was refused, and logged, at every claim (oxygenhub-android#8).
7 8
- **Built on oxygen's main with #140 and #141:** the runner's store puts objects in place where hard links are refused (Android, so the phone runner can clone), and the `deps/oxygen` pin conflict two replacements in one change left on main is settled with `oxy resolve deps/oxygen`.
8 9
- **The WASM tier (#75):** `runtime "wasm"` runs WASI preview 1 modules with wasmtime 49 on any runner (`flow tiers` offers `wasm` everywhere): no rootfs, no network, the job's files at `/src`, its mounts at their targets, the memory limit enforced by wasmtime. `exec` names a module or a glob of them (`target/wasm32-wasip1/debug/deps/*.wasm` runs each test binary in turn); the receipt names wasmtime's version and every module's hash. The tier phones and browsers will run (docs/handover.md). Secrets and identities are not offered in it yet.
9 10
- **cargo-nextest in the Rust image (#81):** for JUnit test reports beside `cargo llvm-cov`'s coverage.
…
87 unchanged lines
modified crates/flow/src/facts.rs +28 −6
…
16 unchanged lines
17 17
        load_x100: load_x100(),
18 18
        speed,
19 19
        speed_class: speed.map(Facts::class_of),
20 -
        kernel: std::fs::read_to_string("/proc/sys/kernel/osrelease").ok().map(|s| s.trim().to_string()),
20 +
        kernel: kernel(),
21 21
    }
22 22
}
23 23
24 +
/// The kernel's release, as uname says. Not read from /proc: an Android
25 +
/// app may not read it there, and is refused (and logged) every claim.
26 +
pub fn kernel() -> Option<String> {
27 +
    let mut u: libc::utsname = unsafe { std::mem::zeroed() };
28 +
    // SAFETY: `u` is a writable utsname.
29 +
    if unsafe { libc::uname(&mut u) } != 0 {
30 +
        return None;
31 +
    }
32 +
    // SAFETY: uname writes NUL-terminated strings.
33 +
    let release = unsafe { std::ffi::CStr::from_ptr(u.release.as_ptr()) };
34 +
    Some(release.to_string_lossy().into_owned())
35 +
}
36 +
24 37
/// Free space for an unprivileged writer on the file system holding `path`, in MiB.
25 38
pub fn free_disk_mib(path: &std::path::Path) -> Option<u64> {
26 39
    use std::os::unix::ffi::OsStrExt;
…
32 unchanged lines
59 72
    Some(kib / 1024)
60 73
}
61 74
62 -
/// The 1-minute load average from /proc/loadavg, times 100.
75 +
/// The 1-minute load average, times 100, from sysinfo (the kernel's own
76 +
/// numbers behind /proc/loadavg, which an Android app may not read).
63 77
pub fn load_x100() -> Option<u32> {
64 -
    let text = std::fs::read_to_string("/proc/loadavg").ok()?;
65 -
    let first: f64 = text.split_whitespace().next()?.parse().ok()?;
66 -
    Some((first * 100.0).round() as u32)
78 +
    let mut i: libc::sysinfo = unsafe { std::mem::zeroed() };
79 +
    // SAFETY: `i` is a writable sysinfo.
80 +
    if unsafe { libc::sysinfo(&mut i) } != 0 {
81 +
        return None;
82 +
    }
83 +
    // Fixed point with 16 fraction bits (SI_LOAD_SHIFT).
84 +
    Some(((i.loads[0] as f64) / 65536.0 * 100.0).round() as u32)
67 85
}
68 86
69 87
/// The benchmark, one thread: BLAKE3 over 256 MiB (spec §11.1), and a sort
…
30 unchanged lines
100 118
    #[test]
101 119
    fn the_cheap_facts_are_read_and_classes_double() {
102 120
        assert!(free_disk_mib(std::path::Path::new("/")).is_some());
103 -
        assert!(load_x100().is_some());
121 +
        // The same numbers /proc gives, read without it (an Android app may not).
122 +
        let proc_load: f64 = std::fs::read_to_string("/proc/loadavg").unwrap().split_whitespace().next().unwrap().parse().unwrap();
123 +
        let load = load_x100().unwrap() as f64 / 100.0;
124 +
        assert!((load - proc_load).abs() < 1.0 + proc_load / 4.0, "sysinfo {load}, /proc {proc_load}");
125 +
        assert_eq!(kernel().unwrap(), std::fs::read_to_string("/proc/sys/kernel/osrelease").unwrap().trim());
104 126
        assert!(peak_rss_mib(std::process::id()).is_some_and(|m| m > 0));
105 127
        let dir = tempfile::tempdir().unwrap();
106 128
        let f = std::fs::File::create(dir.path().join("sparse")).unwrap();
…
8 unchanged lines
modified crates/flow/src/syscall.rs +1 −1
…
59 unchanged lines
60 60
}
61 61
62 62
fn kernel_release() -> String {
63 -
    std::fs::read_to_string("/proc/sys/kernel/osrelease").map(|s| s.trim().to_string()).unwrap_or_else(|_| "unknown".into())
63 +
    crate::facts::kernel().unwrap_or_else(|| "unknown".into())
64 64
}
65 65
66 66
/// Whether this machine can run the tier now: a cgroup this runner may
…
799 unchanged lines