Skip to content

fix(standalone): support explicit ld.so invocation via BUN_SELF_EXE#26753

Open
robobun wants to merge 1 commit intomainfrom
claude/fix-explicit-ldso-standalone-exec
Open

fix(standalone): support explicit ld.so invocation via BUN_SELF_EXE#26753
robobun wants to merge 1 commit intomainfrom
claude/fix-explicit-ldso-standalone-exec

Conversation

@robobun
Copy link
Collaborator

@robobun robobun commented Feb 5, 2026

Summary

  • Adds BUN_SELF_EXE environment variable to override /proc/self/exe path detection for standalone executables
  • Fixes compiled executables failing when invoked via explicit dynamic linker (e.g., /lib64/ld-linux-x86-64.so.2 ./my-app)

Problem

When a Bun compiled executable is invoked via explicit dynamic linker, the kernel sets /proc/self/exe to point to the linker instead of the actual binary. This causes:

  1. Bun opens the wrong file (the dynamic linker)
  2. Looks for the ---- Bun! ---- trailer at the end
  3. Trailer not found since the linker doesn't have it
  4. fromExecutable() returns null
  5. Bun falls back to CLI help mode instead of running the embedded JS

This affects:

  • Termux/Android users (glibc binaries require explicit ld.so)
  • Container/chroot environments with non-standard library paths
  • Any system requiring explicit dynamic linker invocation

Solution

Add BUN_SELF_EXE environment variable that allows users to specify the correct path to the executable, which is checked before falling back to /proc/self/exe.

Usage:

BUN_SELF_EXE=./my-app /lib64/ld-linux-x86-64.so.2 ./my-app

This follows the existing pattern used by BUN_NEEDS_PROC_SELF_WORKAROUND.

Test plan

  • Added regression test in test/regression/issue/26752.test.ts
  • Test verifies compiled executable works with BUN_SELF_EXE override when invoked via explicit ld.so
  • Test passes with debug build and fails with system bun (confirming the fix works)

Fixes #26752

🤖 Generated with Claude Code

When a Bun compiled executable is invoked via explicit dynamic linker
(e.g., /lib64/ld-linux-x86-64.so.2 ./my-app), /proc/self/exe points to
the linker, not the actual binary. This causes the embedded bytecode
detection to fail.

Add BUN_SELF_EXE environment variable to override /proc/self/exe path
detection, allowing compiled executables to work in Termux/Android and
other environments requiring explicit ld.so invocation.

Fixes #26752

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions github-actions bot added the claude label Feb 5, 2026
@robobun
Copy link
Collaborator Author

robobun commented Feb 5, 2026

Updated 4:33 AM PT - Feb 5th, 2026

❌ Your commit 89e49224 has 4 failures in Build #36584 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 26753

That installs a local version of the PR into your bun-26753 executable, so you can run:

bun-26753 --bun

@claude
Copy link
Contributor

claude bot commented Feb 5, 2026

Code Review

No issues found. Checked for bugs and CLAUDE.md compliance.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 5, 2026

Walkthrough

Adds support for overriding the self-executable path via the BUN_SELF_EXE environment variable. This enables bundled executables to detect their embedded JavaScript when invoked through an explicit dynamic linker, addressing compatibility issues in environments like Termux/Android.

Changes

Cohort / File(s) Summary
Self-executable path override
src/StandaloneModuleGraph.zig, src/env_var.zig
Adds BUN_SELF_EXE environment variable declaration and logic to check it before /proc/self/exe in self-executable detection, enabling binaries to run via explicit dynamic linker invocation.
Regression test
test/regression/issue/26752.test.ts
Linux-only test verifying compilation to standalone executable and execution both directly and via explicit dynamic linker with BUN_SELF_EXE override.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding support for explicit ld.so invocation via BUN_SELF_EXE environment variable, which is the primary objective of this PR.
Description check ✅ Passed The description thoroughly covers the problem, solution, and test plan, addressing both required template sections with detailed context about the bug and how it was verified.
Linked Issues check ✅ Passed All coding requirements from issue #26752 are met: BUN_SELF_EXE environment variable added to override /proc/self/exe detection, implementation follows existing patterns, and regression test validates the fix for explicit ld.so invocation scenarios.
Out of Scope Changes check ✅ Passed All changes directly address the linked issue: environment variable declaration, self-executable detection override, and targeted regression test. No extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/StandaloneModuleGraph.zig`:
- Around line 1412-1418: The code opens the BUN_SELF_EXE path with
std.fs.cwd().openFile(), which uses the CWD and can fail for relative paths;
change this to resolve the BUN_SELF_EXE value to an absolute path (e.g., using
std.fs.path.resolve or equivalent) before opening and then call openFile on the
resolved path so that .fromStdFile receives a file handle for the absolute path;
update the logic surrounding bun.env_var.BUN_SELF_EXE.get(),
std.fs.cwd().openFile(path, .{}), and the .fromStdFile return path to use the
resolved absolute path.

In `@test/regression/issue/26752.test.ts`:
- Around line 10-17: Replace the single-file fixture write and the build
invocation that references "hello.js" by inlining the script via the "-e" flag:
remove creating "hello.js" in tempDir (variable dir) and change the Bun.spawn
call (compileProc) to call bunExe() with args ["build", "--compile", "-e",
'console.log("Hello from compiled Bun!");', "--outfile", String(dir) + "/hello"]
(or equivalent path assembly) while keeping cwd as String(dir); this keeps the
temp directory for the output binary but avoids the file fixture.

Comment on lines +1412 to +1418
// Check for override first (for Termux/explicit ld.so scenarios)
// When invoked via explicit dynamic linker (e.g., /lib64/ld-linux-x86-64.so.2 ./app),
// /proc/self/exe points to the linker, not the actual binary.
if (bun.env_var.BUN_SELF_EXE.get()) |path| {
if (std.fs.cwd().openFile(path, .{})) |file| {
return .fromStdFile(file);
} else |_| {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/StandaloneModuleGraph.zig | sed -n '1405,1425p'

Repository: oven-sh/bun

Length of output: 1160


🏁 Script executed:

# Check what BUN_SELF_EXE is and how it's used elsewhere
rg "BUN_SELF_EXE" --type zig -B 3 -A 3

Repository: oven-sh/bun

Length of output: 1433


🏁 Script executed:

# Check for similar openFile patterns with environment variables in Zig files
rg "env_var.*\.get\(\)" src/ --type zig -B 2 -A 5

Repository: oven-sh/bun

Length of output: 50367


🏁 Script executed:

# Verify absolute path usage patterns in this file
rg "openFileAbsolute|std\.fs\.path\.isAbsolute" src/StandaloneModuleGraph.zig -B 2 -A 2

Repository: oven-sh/bun

Length of output: 720


🏁 Script executed:

# Check bun.path.joinAbsStringBuf usage for context and correctness
rg "bun\.path\.joinAbsStringBuf" src/ --type zig -B 1 -A 5 | head -60

Repository: oven-sh/bun

Length of output: 3355


🏁 Script executed:

head -50 src/StandaloneModuleGraph.zig | grep -E "^(const|var|@import|pub)"

Repository: oven-sh/bun

Length of output: 97


🏁 Script executed:

# Check imports and module structure
rg "^const|^var|@import" src/StandaloneModuleGraph.zig | head -30

Repository: oven-sh/bun

Length of output: 466


🏁 Script executed:

# Verify std.fs.openFileAbsolute is standard in Zig
rg "std\.fs\.openFileAbsolute" src/ --type zig | head -20

Repository: oven-sh/bun

Length of output: 1323


Use absolute paths when opening BUN_SELF_EXE.

The current code opens the path from BUN_SELF_EXE using std.fs.cwd().openFile(), which makes the behavior dependent on the current working directory. Since the path may be relative or absolute, resolve it to an absolute path first before opening, following the absolute-path requirement for file operations.

🔧 Proposed fix
-                if (bun.env_var.BUN_SELF_EXE.get()) |path| {
-                    if (std.fs.cwd().openFile(path, .{})) |file| {
-                        return .fromStdFile(file);
-                    } else |_| {}
-                }
+                if (bun.env_var.BUN_SELF_EXE.get()) |path| {
+                    var abs_buf: bun.PathBuffer = undefined;
+                    var cwd_buf: bun.PathBuffer = undefined;
+                    const abs_path = if (std.fs.path.isAbsolute(path))
+                        path
+                    else
+                        bun.path.joinAbsStringBuf(
+                            bun.getcwd(&cwd_buf) catch path,
+                            &abs_buf,
+                            &.{path},
+                            .auto,
+                        );
+                    if (std.fs.openFileAbsolute(abs_path, .{})) |file| {
+                        return .fromStdFile(file);
+                    } else |_| {}
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check for override first (for Termux/explicit ld.so scenarios)
// When invoked via explicit dynamic linker (e.g., /lib64/ld-linux-x86-64.so.2 ./app),
// /proc/self/exe points to the linker, not the actual binary.
if (bun.env_var.BUN_SELF_EXE.get()) |path| {
if (std.fs.cwd().openFile(path, .{})) |file| {
return .fromStdFile(file);
} else |_| {}
// Check for override first (for Termux/explicit ld.so scenarios)
// When invoked via explicit dynamic linker (e.g., /lib64/ld-linux-x86-64.so.2 ./app),
// /proc/self/exe points to the linker, not the actual binary.
if (bun.env_var.BUN_SELF_EXE.get()) |path| {
var abs_buf: bun.PathBuffer = undefined;
var cwd_buf: bun.PathBuffer = undefined;
const abs_path = if (std.fs.path.isAbsolute(path))
path
else
bun.path.joinAbsStringBuf(
bun.getcwd(&cwd_buf) catch path,
&abs_buf,
&.{path},
.auto,
);
if (std.fs.openFileAbsolute(abs_path, .{})) |file| {
return .fromStdFile(file);
} else |_| {}
}
🤖 Prompt for AI Agents
In `@src/StandaloneModuleGraph.zig` around lines 1412 - 1418, The code opens the
BUN_SELF_EXE path with std.fs.cwd().openFile(), which uses the CWD and can fail
for relative paths; change this to resolve the BUN_SELF_EXE value to an absolute
path (e.g., using std.fs.path.resolve or equivalent) before opening and then
call openFile on the resolved path so that .fromStdFile receives a file handle
for the absolute path; update the logic surrounding
bun.env_var.BUN_SELF_EXE.get(), std.fs.cwd().openFile(path, .{}), and the
.fromStdFile return path to use the resolved absolute path.

Comment on lines +10 to +17
using dir = tempDir("issue-26752", {
"hello.js": `console.log("Hello from compiled Bun!");`,
});

// Compile the script into an executable
await using compileProc = Bun.spawn({
cmd: [bunExe(), "build", "--compile", "hello.js", "--outfile", "hello"],
cwd: String(dir),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Inline the single-file script with -e instead of writing hello.js.

This is a single-file test; inlining avoids the file fixture while keeping the temp directory for the output binary.

♻️ Suggested refactor
-  using dir = tempDir("issue-26752", {
-    "hello.js": `console.log("Hello from compiled Bun!");`,
-  });
+  using dir = tempDir("issue-26752", {});
@@
-  await using compileProc = Bun.spawn({
-    cmd: [bunExe(), "build", "--compile", "hello.js", "--outfile", "hello"],
+  await using compileProc = Bun.spawn({
+    cmd: [bunExe(), "build", "--compile", "-e", `console.log("Hello from compiled Bun!");`, "--outfile", "hello"],

As per coding guidelines: For single-file tests, prefer -e flag over tempDir when spawning Bun processes.

🤖 Prompt for AI Agents
In `@test/regression/issue/26752.test.ts` around lines 10 - 17, Replace the
single-file fixture write and the build invocation that references "hello.js" by
inlining the script via the "-e" flag: remove creating "hello.js" in tempDir
(variable dir) and change the Bun.spawn call (compileProc) to call bunExe() with
args ["build", "--compile", "-e", 'console.log("Hello from compiled Bun!");',
"--outfile", String(dir) + "/hello"] (or equivalent path assembly) while keeping
cwd as String(dir); this keeps the temp directory for the output binary but
avoids the file fixture.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bun build --compile binaries fail when executed via explicit ld.so invocation (Termux/Android)

1 participant