Repository navigation
Description
Summary
bun build --compile executables leak extracted embedded native libraries loaded through bun:ffi.
A Worker is not required to reproduce this: each dlopen() of an embedded native file imported with with { type: "file" } materializes a new hidden temp file under BUN_TMPDIR / the OS temp dir and leaves it behind. Workers amplify the same bug because every Worker that calls dlopen() extracts another copy.
This is the same root cause as #29585, but this is the smaller repro with opencode/ripgrep removed from the equation. If you prefer to keep only #29585, feel free to close this as a duplicate; I wanted to capture the exact minimal shape.
Related
- Existing broader issue: Compiled binary: Workers leak extracted native .so/.dylib files in /tmp (never cleaned up) #29585
- Proposed fix: Dedupe extracted embedded native modules in compiled binaries #29587
- Older
.nodecleanup issue: Single-file executable doesn't clean up temporary .node files in /tmp when loading native addons #19550 — not sufficient for this case because this path isbun:ffiloading.so/.dylib/.dll, notprocess.dlopen()loading.node - Downstream opencode reports:
- [Bug] OpenCode leaks temporary .so files in /tmp, consuming hundreds of GB over time anomalyco/opencode#28089
- opencode serve leaks ~14GB/hour of .so files in /tmp due to non-pooled ripgrep Workers anomalyco/opencode#23804
opencode run --format jsonextracts libopentui.so to /tmp on every invocation, causing unbounded disk growth anomalyco/opencode#21427- Runaway /tmp .589*-00000000.so artifacts can exhaust disk during long-running usage anomalyco/opencode#16996
Repro: no Workers required
This is the minimal shape. On Linux, replace answer.dll with libanswer.so and build the library with cc -shared -fPIC.
// main-direct.ts
import { dlopen, FFIType } from "bun:ffi"
import libpath from "./answer.dll" with { type: "file" }
const count = Number(process.argv[2] ?? "5")
for (let i = 0; i < count; i++) {
const lib = dlopen(libpath, {
answer: { args: [], returns: FFIType.u32 },
})
if (lib.symbols.answer() !== 42) throw new Error("bad result")
lib.close()
}
console.log(`dlopens=${count}`)// answer.zig
export fn answer() u32 {
return 42;
}zig build-lib -dynamic -O ReleaseSmall "-femit-bin=answer.dll" answer.zig
bun build --compile --outfile direct.exe main-direct.ts
$env:BUN_TMPDIR = "$PWD/tmp"
New-Item -ItemType Directory -Force tmp | Out-Null
.\direct.exe 5
Get-ChildItem tmp -ForceObserved
With Bun 1.3.14 on Windows x64, one process doing five dlopen() calls leaves five extracted native temp files:
dlopens=5
count=5
.fef3cbbf9eefafea-00000002.dll 4608
.fef3cbbf9efcfeea-00000003.dll 4608
.fef3cbbf9effbffe-00000001.dll 4608
.fef3cbbf9effe7fa-00000000.dll 4608
.fef3cbbf9fedf6ea-00000004.dll 4608
Running the same compiled executable again accumulates more files instead of reusing or cleaning the previous extraction.
Worker amplification repro
// worker.ts
import { dlopen, FFIType } from "bun:ffi"
import libpath from "./answer.dll" with { type: "file" }
const lib = dlopen(libpath, {
answer: { args: [], returns: FFIType.u32 },
})
postMessage(lib.symbols.answer())
lib.close()// main-worker.ts
const count = Number(process.argv[2] ?? "5")
for (let i = 0; i < count; i++) {
await new Promise<void>((resolve, reject) => {
const worker = new Worker("./worker.ts")
const timeout = setTimeout(() => reject(new Error("worker timeout")), 10_000)
worker.onmessage = (event) => {
clearTimeout(timeout)
if (event.data !== 42) reject(new Error(`bad result ${event.data}`))
worker.terminate()
resolve()
}
worker.onerror = (event) => {
clearTimeout(timeout)
reject(event.error ?? new Error(event.message))
}
})
}
console.log(`workers=${count}`)Observed with the same embedded DLL:
workers=5
.fef3cbbab4eeeefe-00000000.dll 4608
.fef3cbbabdedfefe-00000001.dll 4608
.fef3cbbabeeeafea-00000002.dll 4608
.fef3cbbabfedb7ea-00000003.dll 4608
.fef3cbbabffcbfee-00000004.dll 4608
A second run produced ten files total.
Expected
Either:
- reuse a deterministic extracted path per embedded native file, or
- clean up the extracted file after use / process exit.
The #29587 design of content-hashed extraction in a per-user temp subdir matches the expected behavior.
Why this matters for opencode
Current opencode no longer appears to use the old ripgrep Worker-per-search path, so that specific amplification vector is gone in current source.
However, opencode can still trigger this Bun bug because @opentui/core uses bun:ffi to load libopentui.so / .dylib / .dll. In current opencode, some headless commands can still import OpenTUI unnecessarily through eager import("./run/runtime"), which causes native extraction even outside the TUI path. TUI/interactive paths also legitimately load OpenTUI.
So the downstream status is: the old ripgrep Worker amplification is gone, but the underlying Bun bun:ffi compiled-executable extraction leak is still relevant until Bun dedupes/cleans embedded native library extraction.
Activity
robobun commentedon May 18, 2026
✅ Reproduced — confirmed root cause in
resolve_embedded_file_to_buf(src/runtime/jsc_hooks.rs:4451). 5×dlopen()→ 5 leaked.socopies under$BUN_TMPDIR.This is the minimal-repro form of #29585. Fix is already open as #29587 (content-hashed filename + reuse on-disk + per-
File.extracted_pathcache). That branch has been ported forward to the Rust runtime and is green on every Buildkite lane except one unrelated flake (test/js/bun/util/v8-heap-snapshot.test.tsSIGKILL on 🐧 25.04 x64 — not touched by the diff). Tracking there to avoid a duplicate PR.Hona commentedon May 18, 2026
Update after pulling latest
oven-sh/bun@9ecb985ad0/ currentorigin/main: the issue still reproduces in the Rust runtime path.The active path is
src/runtime/ffi/ffi_body.rs->src/runtime/jsc_hooks.rs:resolve_embedded_file_to_buf, which still creates a freshtmpname(...)file for each embeddedbun:ffiload and does not cache/dedupe/cleanup it.The older Zig implementation still exists side-by-side, but the Rust path is the relevant current path.
test/regression/issue/30717.test.tsonly verifies embeddedbun:ffidlopen works after the Rust rewrite; it does not cover temp-file dedupe/cleanup.So #29587 remains the needed fix.
[-]Compiled executable leaks embedded bun:ffi native temp files without Workers[/-][+]Compiled executable leaks embedded bun:ffi native temp files[/+]WakaTaira commentedon Jul 18, 2026
Confirming on Linux (WSL2, Arch):
hunkdiff0.17.1 (npm), which ships as a Bun single-file executable, leaks one ~13MB hidden.so(/tmp/.{hex}-00000000.so, OpenTUI's native lib) per invocation. An agent workflow pollinghunk sessionevery 2s filled a 16GB tmpfs overnight (~1,360 files). Same signature as reported: randomized name per run, no reuse, no cleanup on clean exit.fix(cli): sweep stale Bun-extracted temp libraries at startup
Jarred-Sumner commentedon Aug 13, 2026
#29585 tracks this same root cause, as you noted:
resolve_embedded_file_to_bufextracts a fresh temp file on every dlopen of an embedded library and never reuses or removes it, and the fix is being tracked there via #29587. Closing as a duplicate; thanks for the no-Worker minimal repro, which is linked from that thread.Duplicate of #29585
Please add a 👍 or any extra details over there so it all stays in one place. If this is actually a different problem, say so here and we'll reopen.
(This comment was written by Claude, on behalf of the Bun team.)