Skip to content

OverlayDevice fresh-inode allocation intermittently wedges the runtime (TypeError reading 'a1', exit code 71; cx.run promise never settles) #222

@konard

Description

@konard

Summary

On a CheerpX OverlayDevice mount (read-only ext2 base + IDBDevice writable layer), a cx.run that allocates a brand-new inode on the writable overlay non-deterministically throws

TypeError: Cannot read properties of undefined (reading 'a1')
CheerpException: Program exited with code 71

The exception surfaces via pageerror, but the JS-level cx.run promise never settles — the whole CheerpX runtime then wedges, and every subsequent cx.run fails with function signature mismatch. The repro is racy and strongly biased toward fresh inode allocation (mkdir/touch/> newfile); overwriting an existing inode is reliable.

This has been stable across CheerpX 1.3.01.3.3 (the 1.3.1–1.3.3 changelog has no OverlayDevice fix).

Environment

  • CheerpX 1.3.01.3.3 (vendored from the official CDN).
  • Page is cross-origin isolated: crossOriginIsolated === true, SharedArrayBuffer present (COOP same-origin + COEP require-corp). So this is not a SAB/isolation problem — Linux boots fully and prints its banner, which already requires SAB.
  • Disk: a small i386 Alpine ext2 (read-only base) + IDBDevice (writable) combined via OverlayDevice.
  • Reproduces headless on desktop Chromium; the wedge rate rises on iPad Safari (memory pressure / GC timing shift the race), where it makes an interactive bash --login hang before printing a prompt.

Reproduction

Boot the OverlayDevice disk, then run a script that allocates a fresh inode:

const CheerpX = await loadCheerpX();           // 1.3.0–1.3.3
const handle  = await bootLinux({ CheerpX });  // ext2 base + IDBDevice + OverlayDevice
await handle.dataDevice.writeFile('/test.sh', 'set -eu\nmkdir /workspace/newdir\nexit 0\n');

const result = await Promise.race([
  handle.cx.run('/bin/sh', ['/data/test.sh'], {
    env: ['PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'],
    cwd: '/root', uid: 0, gid: 0,
  }).then((exit) => ({ ok: true, exit })),
  new Promise((r) => setTimeout(() => r({ ok: false, error: 'TIMEOUT' }), 15000)),
]);
// Intermittently: pageerror "TypeError: ...reading 'a1'", exit code 71,
// and the cx.run promise above never resolves -> TIMEOUT.
// Re-running the SAME script that overwrites an EXISTING path is reliable.

A self-contained variant (touch vs mkdir vs > vs heredoc, in /tmp vs /workspace) is here: https://github.com/link-foundation/rust-web-box/blob/main/experiments/cx-130-alpine-narrow5.mjs

Real-world triggers we hit

Any normal workflow that writes new files into the overlay:

  1. Seeding a workspace at boot (mkdir -p, printf > newfile).
  2. cargo run (debug build writes hundreds of fresh inodes under target/debug/{build,deps,.fingerprint}/).
  3. Interactive bash --login first start (allocates ~/.bash_history and readline temp paths) — this is the iPad "terminal doesn't work" symptom.

Impact

Because the failed cx.run promise never settles, a single fresh-inode wedge takes down the entire runtime — not just the one command. There is no way for the caller to recover other than reloading the page.

Suggested fixes

  1. Primary: make fresh-inode allocation on OverlayDevice deterministic. The 'a1' access reads an undefined object — it looks like a missing/late-initialised entry in the overlay inode table under a particular allocation ordering.
  2. Defensive: when this internal error fires, reject the cx.run promise instead of leaving it permanently pending, so callers can respawn / surface an error rather than wedging the whole runtime.

Workarounds we currently ship (all recoverable once fixed)

  • Pre-bake all seed paths in the disk image so we only ever overwrite existing inodes.
  • CARGO_INCREMENTAL=0 + pre-baked debug and release artifacts so cargo run re-uses inodes.
  • HISTFILE=/dev/null so interactive bash stops allocating a fresh history inode.
  • Promise.race timeouts so a wedge bounds boot instead of hanging forever.

Filed from the rust-web-box project (in-browser VS Code + CheerpX). Happy to provide more traces or test patches.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions