Skip to content

Compiled executable leaks embedded bun:ffi native temp files #30962

Closed as duplicate

Description

@Hona

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

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 -Force

Observed

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

  1. robobun commented on May 18, 2026

    @robobun
    Collaborator

    ✅ Reproduced — confirmed root cause in resolve_embedded_file_to_buf (src/runtime/jsc_hooks.rs:4451). 5× dlopen() → 5 leaked .so copies 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_path cache). 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.ts SIGKILL on 🐧 25.04 x64 — not touched by the diff). Tracking there to avoid a duplicate PR.

  2. Hona commented on May 18, 2026

    @Hona
    ContributorAuthor

    Update after pulling latest oven-sh/bun@9ecb985ad0 / current origin/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 fresh tmpname(...) file for each embedded bun:ffi load 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.ts only verifies embedded bun:ffi dlopen works after the Rust rewrite; it does not cover temp-file dedupe/cleanup.

    So #29587 remains the needed fix.

  3. changed the title [-]Compiled executable leaks embedded bun:ffi native temp files without Workers[/-] [+]Compiled executable leaks embedded bun:ffi native temp files[/+] on May 18, 2026
  4. WakaTaira commented on Jul 18, 2026

    @WakaTaira

    Confirming on Linux (WSL2, Arch): hunkdiff 0.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 polling hunk session every 2s filled a 16GB tmpfs overnight (~1,360 files). Same signature as reported: randomized name per run, no reuse, no cleanup on clean exit.

  5. Jarred-Sumner commented on Aug 13, 2026

    @Jarred-Sumner
    Collaborator

    #29585 tracks this same root cause, as you noted: resolve_embedded_file_to_buf extracts 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.)

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions