Update — root cause identified, and it is not a hotplug window.
proot-distro 5.9.0 replaces /proc/stat with a hardcoded eight-core
file. sysdata.py:66-83 defines _FAKE_STAT containing cpu0..cpu7 as
a fixed string, and fake_sysdata_bindings() (sysdata.py:485-493) binds it
over /proc/stat whenever the real path cannot be opened — which is always
the case on Android. /proc/cpuinfo and /sys/devices/system/cpu/* are not
masked and stay live.
So on an 8-core device the counts agree and nothing is visible. On any device
with a different core count — mine is 9 (4×Cortex-A510 + 4×Cortex-A715 +
1×Cortex-X3, online=0-8) — the guest sees /proc/stat reporting 8 while
/proc/cpuinfo reports 9, permanently. There is no transient skew to wait
out; /proc/stat is pinned at 8 for the life of the container. The crash is
deterministic, not intermittent: exit 1 at ~3s on every run on the same
hardware.
The fake is also internally inconsistent — its aggregate line never equalled
the sum of its own per-CPU lines (93280 vs 93276).
One-liner to confirm (mine prints stat=8 cpuinfo=9 nproc=9):
printf 'stat=%s cpuinfo=%s nproc=%s\n' \
"$(grep -c '^cpu[0-9][0-9]* ' /proc/stat)" \
"$(grep -c ^processor /proc/cpuinfo)" "$(nproc)"
Hardware correction: this is a Pixel 8 Pro (Tensor G3), not a Snapdragon
as the log below says — 1xCortex-X3 + 4xCortex-A715 + 4xCortex-A510, which is
Tensor G3's configuration. The bug is unaffected, but the SoC in the pasted
environment line is wrong.
Still reproducible on 0.0.204, not only on 0.0.175.
Filed upstream as it is a proot-distro bug:
termux/proot-distro#717
What happened
freebuff CLI crashes ~3 seconds after start with:
Unhandled rejection: Error: Failed to get CPU information
at cpus (unknown)
at populate (node:os:18:25)
at model (node:os:27:21)
at (../node_modules/systeminformation/lib/cpu.js:956:41)
at processTicksAndRejections (native:7:39)
Exit code 1. The TUI renders (project picker), then the process dies.
Steps to reproduce
- Run "freebuff" on the machine described below
- Wait ~3 seconds
Where does this happen?
CLI (terminal client)
Operating system
Linux
Version
0.0.175
Model
No response
Logs or screenshots
Environment: freebuff 0.0.175 (target linux-arm64)
under Debian container (proot-distro) on Termux (Android), aarch64, Snapdragon 8 Gen 2
TERM=xterm-256color, SHELL=/bin/bash
Host is ARM big.LITTLE with CPU hotplug, running inside Termux/PRoot
where /proc and /sys are emulated. At crash time the kernel sources
disagree:
- /sys/devices/system/cpu/online: 0-8 (9 CPUs)
- /proc/cpuinfo: 9 processor blocks
- /proc/stat: only cpu0..cpu7 (8 CPUs)
Bun "os.cpus()" throws "Failed to get CPU information" in this
state (Node's "os.cpus()" tolerates it and returns 8 entries), and
the rejection from "systeminformation" "cpu()" (lib/cpu.js:956,
"os.cpus()\[0\].model") is unhandled, killing the app. Making the
three sources consistent (8 CPUs everywhere) stops the crash, so the
request: please wrap the "os.cpus()" / "cpu()" call in try/catch
(or handle the rejection) so a transient hotplug skew doesn't kill
the CLI.
Additional isolation done:
- `freebuff --help` / `-v` / `login` work; only interactive startup crashes
- direct `~/.config/manicode/freebuff` binary crashes the same way, so it is not a launcher issue
- freezing a consistent snapshot (8 CPUs in cpuinfo, stat and online via a mount namespace) stops the crash; the live skewed state (9/8/9) crashes deterministically ~3s after start
- Node v22 `os.cpus()` on the same machine returns 8 entries; only the bundled Bun runtime (systeminformation 5.33.1, lib/cpu.js:956) throws
This looks like the Linux variant of the startup-crash class fixed in issue 1091.
Happy to test an arm64 canary if you publish one.
Workaround (no code change required)
Rewrite the fake so it carries one line per real CPU and an aggregate line
equal to their sum. The aggregate is the part that is easy to miss: appending
cpu8 while leaving the aggregate alone still crashes at ~3s.
#!/bin/sh
# proot-distro ships an 8-core /proc/stat; make it agree with the real CPUs.
set -e
S=$(awk '$5=="/proc/stat"{for(i=7;i<=NF;i++)if($i=="bind"){print $(i+1);exit}}' /proc/self/mountinfo)
N=$(grep -c '^processor' /proc/cpuinfo)
awk -v n=$N '
/^cpu[0-9]+ /{i=$1;sub("cpu","",i);if(i<n){p[i]=$0;h[i]=1};next}
/^cpu /{next}
{o[++k]=$0}
END{for(i=0;i<n;i++)if(h[i]){split(p[i],f);for(j=2;j<=11;j++)t[j]+=f[j];s+=f[5];c++}
d=c?int(s/c):0
for(i=0;i<n;i++)if(!h[i]){p[i]="cpu"i" 0 0 0 "d" 0 0 0 0 0 0";t[4]+=d}
l="cpu";for(j=2;j<=11;j++)l=l" "t[j];print l
for(i=0;i<n;i++)print p[i]
for(i=1;i<=k;i++)print o[i]}' "$S" > "$S.n"
[ -e "$S.orig" ] || cp "$S" "$S.orig"
cat "$S.n" > "$S" && rm -f "$S.n"
Verified on 0.0.204 / proot-distro 5.9.0: with the fake corrected the CLI stays
up (survived 35s under timeout, exit 124, project picker renders); with the
stock 8-core fake it exits 1 at ~3s. The generated file is byte-identical to
one produced by building _FAKE_STAT from the kernel's CPU count, and the
script is idempotent. Note that _write_if_missing() never rewrites an
existing entry, so a stale sysdata/stat must be deleted once for a patched
generator to take effect.
What happened
freebuff CLI crashes ~3 seconds after start with:
Unhandled rejection: Error: Failed to get CPU information
at cpus (unknown)
at populate (node:os:18:25)
at model (node:os:27:21)
at (../node_modules/systeminformation/lib/cpu.js:956:41)
at processTicksAndRejections (native:7:39)
Exit code 1. The TUI renders (project picker), then the process dies.
Steps to reproduce
Where does this happen?
CLI (terminal client)
Operating system
Linux
Version
0.0.175
Model
No response
Logs or screenshots
Workaround (no code change required)
Rewrite the fake so it carries one line per real CPU and an aggregate line
equal to their sum. The aggregate is the part that is easy to miss: appending
cpu8while leaving the aggregate alone still crashes at ~3s.Verified on 0.0.204 / proot-distro 5.9.0: with the fake corrected the CLI stays
up (survived 35s under
timeout, exit 124, project picker renders); with thestock 8-core fake it exits 1 at ~3s. The generated file is byte-identical to
one produced by building
_FAKE_STATfrom the kernel's CPU count, and thescript is idempotent. Note that
_write_if_missing()never rewrites anexisting entry, so a stale
sysdata/statmust be deleted once for a patchedgenerator to take effect.