Repository navigation
CLI crashes ~3s after start: Failed to get CPU information — proot-distro binds a hardcoded 8-core /proc/stat that disagrees with live /proc/cpuinfo (Termux) #1374
Description
Update — root cause identified, and it is not a hotplug window.
proot-distro 5.9.0 replaces
/proc/statwith a hardcoded eight-core
file.sysdata.py:66-83defines_FAKE_STATcontainingcpu0..cpu7as
a fixed string, andfake_sysdata_bindings()(sysdata.py:485-493) binds it
over/proc/statwhenever the real path cannot be opened — which is always
the case on Android./proc/cpuinfoand/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/statreporting 8 while
/proc/cpuinforeports 9, permanently. There is no transient skew to wait
out;/proc/statis 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 (93280vs93276).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.
Activity
heavymio commentedon Sep 17, 2026
Workaround that confirms the diagnosis (for anyone hitting this):
unshare -m+ bind-mount the snapshot over /proc/stat,/proc/cpuinfo, /sys/devices/system/cpu/online
With the frozen consistent view the CLI starts and stays up; with the live skewed view (9/8/9) it dies ~3s after start. So the crash condition is exactly the cpuinfo/stat/online disagreement, and any handling (try/catch + fallback) only needs to cover that window.
P.S.
Exact spot in code: "cli/src/utils/fingerprint.ts:47" calls "systeminformationModule.cpu()" for the startup fingerprint.
Note: the try/catch around it (fingerprint.ts:41-76) cannot catch this crash. In systeminformation@5.33.1, "cpu()" defers via "process.nextTick", and Bun's "os.cpus()" throws there (lib/cpu.js:956, "os.cpus()[0].model"). A throw inside a nextTick callback is an uncaught exception, not a promise rejection, so it bypasses your try/catch AND the legacy-fingerprint fallback in "calculateFingerprint()" — the process just dies (matches the "at processTicksAndRejections" frame in the stack above).
Minimal fix: probe "os.cpus()" in a try/catch before calling
"si.cpu()"; on throw, use the empty-value fallback you already have.
heavymio commentedon Sep 17, 2026
closed by accident, issue still reproduces on 0.0.175
fix(cli): guard si.cpu() against uncatchable os.cpus() throw
fix(cli): guard systeminformation.cpu() against uncatchable os.cpus()…
heavymio commentedon Sep 17, 2026
PR #1377 is open with the fix:
#1377
Summary of the patch:
getCpuInfoSafe()wrapsos.cpus()andsysteminformation.cpu()in try/catch — returns empty values +logger.warnon fallback.systeminformationModule.cpu()call ingetSystemInfo()'sPromise.all.Diagnosis confirmed via mount-namespace workaround (frozen CPU snapshot → CLI stays up; live skewed 9/8/9 → crash ~3s after start).
Closes #1374
fix(cli): guard the second os.cpus() call site in fingerprinting
test(cli): cover the CodebuffAI#1374 os.cpus() guard at the public en…
[-]CLI crashes ~3s after start: unhandled `Failed to get CPU information` from systeminformation on ARM Linux with CPU hotplug skew (Termux/PRoot)[/-][+]CLI crashes ~3s after start: `Failed to get CPU information` — proot-distro binds a hardcoded 8-core /proc/stat that disagrees with live /proc/cpuinfo (Termux)[/+]test(cli): restore mocked modules so the CodebuffAI#1374 guard test d…
test(cli): make the CodebuffAI#1374 guard test order-independent
heavymio commentedon Sep 27, 2026
One more finding from the same investigation, filed separately since it's a resource leak rather than the startup crash: #1443
Every launch unpacks the bundled
libopentui.sointo/tmpanddlopens it without ever deleting it — one file per launch, ~10 MB, with a unique name so nothing is reused. On Android/tmpis a tmpfs, so it is RAM rather than disk: 35 launches in one session left 342 MB resident.It is not Bun's built-in embedded-
.soextraction — that one is deduplicated by name, verified on stock Bun 1.2.21 and 1.4.2. The per-launch unique name here means the CLI unpacks it itself and the unlink afterdlopenis missing.A
TMPDIR=<scratch dir> freebuffwrapper is a working mitigation in the meantime; deleting the file mid-run is also safe, since the library is already mapped at that point.heavymio commentedon Sep 30, 2026
fixed: d86b11d