Skip to content

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

@heavymio

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

  1. Run "freebuff" on the machine described below
  2. 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

  1. added
    type:bugA defect in the code with a reproducible failure
    on Sep 17, 2026
  2. heavymio commented on Sep 17, 2026

    @heavymio
    Author

    Workaround that confirms the diagnosis (for anyone hitting this):

    • snapshot consistent CPU info (N = cpu count in /proc/stat, first N blocks of /proc/cpuinfo, online range 0-(N-1))
    • unshare -m + bind-mount the snapshot over /proc/stat,
      /proc/cpuinfo, /sys/devices/system/cpu/online
    • run freebuff inside that namespace

    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.

  3. heavymio commented on Sep 17, 2026

    @heavymio
    Author

    closed by accident, issue still reproduces on 0.0.175

  4. added
    area:cliThe Codebuff/Freebuff terminal client
    bot:triagedClassified by the community triage bot
    on Sep 17, 2026
  5. added 2 commits that reference this issue on Sep 17, 2026
    1333802
    31878f4
  6. heavymio commented on Sep 17, 2026

    @heavymio
    Author

    PR #1377 is open with the fix:

    #1377

    Summary of the patch:

    • getCpuInfoSafe() wraps os.cpus() and systeminformation.cpu() in try/catch — returns empty values + logger.warn on fallback.
    • Replaces the bare systeminformationModule.cpu() call in getSystemInfo()'s Promise.all.
    • Fallback: fingerprint still generates (empty CPU fields or legacy path); process stays stable.

    Diagnosis confirmed via mount-namespace workaround (frozen CPU snapshot → CLI stays up; live skewed 9/8/9 → crash ~3s after start).

    Closes #1374

  7. changed the title [-]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)[/+] on Sep 27, 2026
  8. added 2 commits that reference this issue on Sep 27, 2026
  9. heavymio commented on Sep 27, 2026

    @heavymio
    Author

    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.so into /tmp and dlopens it without ever deleting it — one file per launch, ~10 MB, with a unique name so nothing is reused. On Android /tmp is 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-.so extraction — 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 after dlopen is missing.

    A TMPDIR=<scratch dir> freebuff wrapper is a working mitigation in the meantime; deleting the file mid-run is also safe, since the library is already mapped at that point.

  10. heavymio commented on Sep 30, 2026

    @heavymio
    Author

    fixed: d86b11d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:cliThe Codebuff/Freebuff terminal clientbot:triagedClassified by the community triage bottype:bugA defect in the code with a reproducible failure

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions