Summary
On the live browser machines, execve of a large binary can fail with -ENOMEM late in a memory-heavy session, even though the same binary runs fine in isolation and under headless run-machine.mjs. Observed as sh: tcpdump: Cannot allocate memory while running an on-target test suite that exercises many tools in one session.
Root cause
Browser machines boot each process with a 2 MiB initial user_memory (physRam.ts → userInitBytes ?? 2 * 1024 * 1024). execve re-instantiates the new image against that same per-process SAB, so ensureUserMemoryFitsImage() (in src/worker.mjs, added for hardwarejs#6) must grow() the user memory to the new binary's declared memory-import minimum. When grow() throws, the code returns -ENOMEM:
hardwarejs#6 <label>: exec image declares a <N>-page (<X> MiB) user_memory minimum but this process's Memory holds <M> pages and grow() refused (…; maximum too low — raise userMaxBytes). Failing execve with -ENOMEM (never-silent).
Note the message's "maximum too low — raise userMaxBytes" is a guess and is not the operative cause here: tcpdump's declared minimum is only 153 pages / 9.6 MiB, far under userMaxBytes (256 MiB). The grow() fails because the browser tab cannot commit the pages — the shared kernel SAB is 256 MiB (init==max, eagerly committed on Chrome) and per-process user_memory SABs accumulate committed/reserved memory across a long session, so by the time a large binary needs to grow, there is no room. Smaller binaries (musl, or glibc tools under the ceiling) don't trip it.
Reproduction
- Boot a glibc image with large static binaries in the browser (e.g.
alex/core-image-brintos-emcraft-glibc;tcpdumpis an 18 MB wasm module, 9.6 MiB declared memory minimum). - In the in-browser terminal run the full on-target suite:
brintos-selftest. - Late in the run:
BST not ok net/tcpdump : rc=126 … sh: tcpdump: Cannot allocate memory.
Evidence it is memory-budget, not the binary
| Scenario | user_memory init | tcpdump result |
|---|---|---|
Browser, tcpdump --version first command |
2 MiB (grow-to-fit) | works |
Browser, full brintos-selftest (tcpdump late) |
2 MiB (grow-to-fit) | -ENOMEM at execve |
Headless run-machine.mjs, full suite |
64 MiB (--user-ram 64) |
BST ok net/tcpdump |
| Browser, musl image, full suite | 2 MiB | ok (binaries smaller, stay under ceiling) |
So the binary is correct; the failure is the browser per-process memory budget under sustained load, surfaced by the larger glibc binaries. tcpdump --version reports libpcap version 1.10.4 when it runs.
Candidate directions (a design tradeoff for the maintainer — no patch attached)
- Raise the browser default
userInitBytestoward the headless value (headless boots 64 MiB init and never trips this).physRam.tsdocuments 2 MiB as a deliberate per-process-footprint choice, so this trades footprint for headroom — maintainer's call on the balance. - Reclaim exited-process
user_memorySABs promptly so a long session doesn't accumulate committed/reserved memory — this is the root of the "late in the run" behavior and would fix it without raising the per-process floor. - Make grow-to-fit resilient: on
grow()failure, trigger reclaim and retry before returning-ENOMEM.
Relates to hardwarejs#6 (the grow-to-fit path this exercises). Change is confined to brintos/hardwarejs — no distro submodule pins moved.