Skip to content

V1 1.18.31: every opencode <script>.js / plugin-hook subprocess leaks a fresh 13.7 MB libopentui.so into $TMPDIR (17 GB in <2 h) #49283

Description

@diegovilar

Summary

On Linux x64 with opencode 1.18.31 (stable, Homebrew), every invocation of the compiled CLI that executes a JS script — e.g. plugin hook subprocesses spawned as opencode /path/hook.js — extracts a fresh 13,745,312-byte libopentui.so into $TMPDIR (/tmp) and never reuses or removes it. The embedded Bun runtime is v1.3.14, which predates the extraction-dedup fix (oven-sh/bun#29587, shipped in Bun 1.4.0).

In an agent setup where a plugin adapter spawns guard hooks with spawnSync(process.execPath, [hookPath]) on every tool/message event, this leaks ~13.7 MB per event. Observed accumulation: 1,332 files / 18,308,755,584 bytes (~17.05 GiB) in under 2 hours on a 32 GiB tmpfs /tmp, still growing (~12 files / 20 s during active tool use).

This is related to but distinct from #42700 / #28089 / #42880: this report pins the per-script-invocation / hook-subprocess amplification on current stable V1 and gives a deterministic, seconds-long repro that does not require a full TUI launch or a long-running server session.

Environment

  • opencode 1.18.31, Homebrew (linuxbrew): /home/linuxbrew/.linuxbrew/Cellar/opencode/1.18.31/bin/opencode, 185,030,784 bytes, sha256 f9dab32248695e9e…, built 2026-09-14
  • Embedded runtime: Bun v1.3.14 (0d9b296a) (confirmed via strings in the binary)
  • Embedded native lib asset: libopentui-h3hyjpa5.so
  • OS: Linux x86_64, /tmp is tmpfs (32 GiB), uid/gid 1000
  • Process topology: OpenChamber (@openchamber/web 1.23.2) → opencode serve (PID 1076) → per-event opencode ~/.config/opencode/hooks/*.js children
  • TMPDIR / BUN_TMPDIR unset for the server

Steps to reproduce (minimal, no TUI, no server)

D=$(mktemp -d /tmp/opencode-leak-repro.XXXXXX)
printf 'console.log("probe")\n' > /tmp/opencode-leak-probe.js

for i in 1 2 3; do
  TMPDIR="$D" BUN_TMPDIR="$D" timeout 30 \
    ./opencode /tmp/opencode-leak-probe.js </dev/null >/dev/null 2>&1
done

find "$D" -maxdepth 1 -type f -printf '%s %f\n'

Result (verified):

before: 0 files
after:  3 files, 41,235,936 bytes

13745312 .9adffbfbfae1ffbf-00000000.so
13745312 .9adffbfbf6e4eddf-00000000.so
13745312 .9adffbfbf3e1ffdf-00000000.so

Each file: sha256 ce73133a58d35e35610ef53353ddeeeb93fb29505dde0cf1854ce25facee241d, SONAME libopentui.so (x86-64 ELF, not stripped, debug info, Zig/clang 20.1.2 via zig-bootstrap).

Control: opencode --version with the same TMPDIR/BUN_TMPDIR leaves 0 files, so this is specific to the script/hook execution path, not all CLI commands.

Observed in normal use (hook amplification)

  • The plugin adapter runs each guard hook as a child process of the opencode binary: spawnSync(process.execPath, [hookPath]) (~/.config/opencode/plugins/gsd-core.js:230).
  • Sampling /proc/<serve-pid>/task/<tid>/children + /proc/<pid>/maps for 20 s caught 23 short-lived opencode .../hooks/*.js children, each mapping a distinct, freshly-created 13,745,312-byte /tmp/.*-00000000.so.
  • lsof on older copies shows none held open; files persist after the creating processes exit.
  • Natural accumulation: 1,033 → 1,332 files in ~45 min of inspection (rate scales with tool-call/message events); baseline idle ~1 file / 2 s.
  • All sampled copies are byte-identical to each other and to the hash above.

Expected behavior

Each process reuses a single extracted copy (e.g. a content-hashed path) or cleans up after itself. $TMPDIR must not grow without bound from process invocations.

Actual behavior

One new randomly-named 13.7 MB copy per opencode <script>.js invocation; no dedup, no unlink on clean exit.

Impact

  • /tmp on tmpfs consumes RAM: ~17 GiB already; ENOSPC is reachable within hours of agent use.
  • At ENOSPC, upstream reports opencode failing with a misleading Failed to open library "/$bunfs/root/libopentui-*.so" error (see tui: leaks ~21MB .so per launch into /tmp; fills tmpfs and breaks TUI startup #42700).
  • Hidden dotfiles can resemble malware and trigger incident response.
  • Any workflow that shells out to opencode <script> (plugin hooks, agent orchestrators, watchers) leaks proportionally to its event rate.

Root cause / attribution

  1. Bun 1.3.14 (root cause): extraction for bun:ffi dlopen() of embedded native libs in compiled binaries names each copy via tmpname(nanoTimestamp + per-VM counter) and never reuses/unlinks it. Fixed by Dedupe extracted embedded native modules in compiled binaries oven-sh/bun#29587 (content-hashed .bun-{uid}-{hash}.{ext} reuse, shipped in Bun 1.4.0); tracked in Compiled binary: Workers leak extracted native .so/.dylib files in /tmp (never cleaned up) oven-sh/bun#29585, fix(ui): avoid blocking on large file diffs #30962, fix(core): expand AWS_REGION in Bedrock Mantle base URL #40076.
  2. OpenCode V1 (amplifier): pins/embeds Bun 1.3.14 (packageManager in the root package.json). The upgrade is already tracked: [FEATURE]: Upgrade bundled Bun from 1.3.14 to 1.4.2 #44945 (open) and PR chore: bump embedded Bun to 1.4.2 #44946 (open). This report is direct evidence that the V1 pin is the remaining source of the leak on stable.
  3. OpenTUI (trigger): @opentui/core eagerly resolves and dlopen()s its embedded type: "file" native lib at module init, so merely importing the TUI code materializes the library — in a --compile binary that materialization goes through the buggy Bun path above.

Workaround

  • Contain extraction off tmpfs and purge periodically:
    BUN_TMPDIR="$HOME/.cache/opencode-bun-tmp" TMPDIR="$HOME/.cache/opencode-bun-tmp" opencode ...
  • Age-based cleanup (safe while running on Linux; mapped inodes survive unlink). Filter by size so the unrelated 5,576,816-byte .bcddef7ff5a7b7ee-00000000.so (a different embedded lib, held open by opencode serve) is not touched:
    find /tmp -maxdepth 1 -type f -regextype posix-extended \
      -regex '.*/\.[0-9a-f]{16}-00000000\.so' -size 13745312c -mmin +60 -delete
  • Permanent: land chore: bump embedded Bun to 1.4.2 #44946 (Bun ≥ 1.4.x). Once a fixed build is in use, the remaining stale files can be deleted after all older processes stop.

Related issues

Activity

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions