fix(compile): use ELF section for standalone binaries on Linux - #26923
Conversation
|
🤖 Claude is monitoring this PR I'll review code, respond to comments, and investigate CI failures.
|
|
Updated 10:42 PM PT - Mar 19th, 2026
❌ @Jarred-Sumner, your commit 77ca147 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 26923That installs a local version of the PR into your bun-26923 --bun |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds ELF support for embedding/extracting the standalone module graph: new elf module and ElfFile API, public ELF accessor in StandaloneModuleGraph, C bindings exposing Mach-O/ELF/PE blob hooks, Linux ELF-targeted tests, and small test-config updates. Changes
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 📝 Coding Plan
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@src/bun.zig`:
- Line 3707: The new top-level import "pub const elf = `@import`(\"./elf.zig\");"
was added near line 3707; move this `@import` into the file's existing bottom
import block so all `@import` statements are consolidated per repository Zig
conventions—locate the declaration "pub const elf" and cut it from its current
spot and paste it into the module's import section at the end of the file
(preserve the pub const name and string path exactly so references to elf
continue to work).
In `@src/elf.zig`:
- Around line 12-35: The Managed buffer created as "data" isn't freed if later
steps fail (e.g., allocator.create), so add an errdefer to release it: after
creating and populating data (the variable from
std.array_list.Managed(u8).initCapacity and try data.appendSlice), add "errdefer
data.deinit();" before calling allocator.create(ElfFile) so the buffer is
cleaned on any error; keep the existing errdefer allocator.destroy(self) for the
created ElfFile and rely on errdefer semantics (runs only on error) so
successful init transfers ownership into self.* without leaking.
- Around line 53-121: The writeBunSection function is using u64 values as slice
indices and for buffer capacity calls which require usize; fix by casting the
named u64 values to usize where they are used as slice indices or passed to
ensureTotalCapacity: convert new_file_offset, new_shdr_offset, shdr_table_size,
total_new_size (argument to self.data.ensureTotalCapacity), padding_start, and
bun_section_offset to usize before any slice like self.data.items[...],
memmove/memcpy/memset ranges, and before calling std.mem.writeInt or other
functions that take slice ranges; keep the variables as u64 for arithmetic but
create usize locals (e.g., new_file_offset_usize) and use those in memmove,
`@memcpy`, `@memset`, self.data.items.len assignments, and the ensureTotalCapacity
call in writeBunSection.
In `@src/StandaloneModuleGraph.zig`:
- Around line 904-953: You added a Linux-specific ELF injection path in
StandaloneModuleGraph.zig (the .linux branch that uses bun.elf.ElfFile,
elf_file.writeBunSection, Syscall.setFileOffset, Syscall.ftruncate and
bun.c.fchmod); run the full cross-platform Zig build check by executing `bun run
zig:check-all`, fix any compile or platform-conditional issues reported (e.g.,
missing imports, comptime guards around Environment.isWindows usage, incorrect
types or unreachable branches), and iterate until `zig:check-all` completes
cleanly so the new Linux code compiles on all target platforms.
In `@test/bundler/bun-build-compile.test.ts`:
- Around line 208-209: Replace the dynamic require of chmodSync with a
module-level import: add an import (or const { chmodSync } = require("fs")) at
the top of the test module and remove the inline require calls currently present
where chmodSync is used (references in the test near usages around
result.outputs[0].path and the later occurrence at the other use site). Update
the test to call the module-scoped chmodSync directly so no runtime require
remains and ensure both occurrences (the one shown and the other later call) are
deleted.
|
|
||
| pub const macho = @import("./macho.zig"); | ||
| pub const pe = @import("./pe.zig"); | ||
| pub const elf = @import("./elf.zig"); |
There was a problem hiding this comment.
Move the new @import into the bottom import block.
This keeps imports consolidated per the repository’s Zig conventions.
As per coding guidelines: Place @import statements at the bottom of the file in Zig (auto formatter will handle positioning).
🤖 Prompt for AI Agents
In `@src/bun.zig` at line 3707, The new top-level import "pub const elf =
`@import`(\"./elf.zig\");" was added near line 3707; move this `@import` into the
file's existing bottom import block so all `@import` statements are consolidated
per repository Zig conventions—locate the declaration "pub const elf" and cut it
from its current spot and paste it into the module's import section at the end
of the file (preserve the pub const name and string path exactly so references
to elf continue to work).
| pub fn writeBunSection(self: *ElfFile, payload: []const u8) !void { | ||
| const ehdr = readEhdr(self.data.items); | ||
| const bun_section_offset = try self.findBunSection(ehdr); | ||
| const page_size = pageSize(ehdr); | ||
|
|
||
| const header_size: u64 = @sizeOf(u64); | ||
| const new_content_size: u64 = header_size + payload.len; | ||
| const aligned_new_size = alignUp(new_content_size, page_size); | ||
|
|
||
| // Find the highest virtual address across all PT_LOAD segments | ||
| var max_vaddr_end: u64 = 0; | ||
| const phdr_size = @sizeOf(Elf64_Phdr); | ||
| for (0..ehdr.e_phnum) |i| { | ||
| const phdr_offset = @as(usize, @intCast(ehdr.e_phoff)) + i * phdr_size; | ||
| const phdr = std.mem.bytesAsValue(Elf64_Phdr, self.data.items[phdr_offset..][0..phdr_size]).*; | ||
| if (phdr.p_type == elf.PT_LOAD) { | ||
| const vaddr_end = phdr.p_vaddr + phdr.p_memsz; | ||
| if (vaddr_end > max_vaddr_end) { | ||
| max_vaddr_end = vaddr_end; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // The new segment's virtual address: after all existing mappings, page-aligned | ||
| const new_vaddr = alignUp(max_vaddr_end, page_size); | ||
|
|
||
| // The new data goes at the end of the file, page-aligned | ||
| const new_file_offset = alignUp(self.data.items.len, page_size); | ||
|
|
||
| // Grow the buffer to hold the new data + section header table after it | ||
| const shdr_table_size = @as(u64, ehdr.e_shnum) * @sizeOf(Elf64_Shdr); | ||
| const new_shdr_offset = new_file_offset + aligned_new_size; | ||
| const total_new_size = new_shdr_offset + shdr_table_size; | ||
|
|
||
| const old_file_size = self.data.items.len; | ||
| try self.data.ensureTotalCapacity(total_new_size); | ||
| self.data.items.len = total_new_size; | ||
|
|
||
| // Zero the gap between old file end and new data (alignment padding). | ||
| // Without this, uninitialized allocator memory would leak into the output. | ||
| if (new_file_offset > old_file_size) { | ||
| @memset(self.data.items[old_file_size..new_file_offset], 0); | ||
| } | ||
|
|
||
| // Copy the section header table to its new location | ||
| const old_shdr_offset = ehdr.e_shoff; | ||
| bun.memmove( | ||
| self.data.items[new_shdr_offset..][0..shdr_table_size], | ||
| self.data.items[old_shdr_offset..][0..shdr_table_size], | ||
| ); | ||
|
|
||
| // Update e_shoff to the new section header table location | ||
| self.writeEhdrShoff(new_shdr_offset); | ||
|
|
||
| // Write the payload at the new location: [u64 LE size][data][zero padding] | ||
| std.mem.writeInt(u64, self.data.items[new_file_offset..][0..8], @intCast(payload.len), .little); | ||
| @memcpy(self.data.items[new_file_offset + header_size ..][0..payload.len], payload); | ||
|
|
||
| // Zero the padding between payload end and section header table | ||
| const padding_start = new_file_offset + new_content_size; | ||
| if (new_shdr_offset > padding_start) { | ||
| @memset(self.data.items[padding_start..new_shdr_offset], 0); | ||
| } | ||
|
|
||
| // Write the vaddr of the appended data at the ORIGINAL .bun section location | ||
| // (where BUN_COMPILED symbol points). At runtime, BUN_COMPILED.size will be | ||
| // this vaddr (always non-zero), which the runtime dereferences as a pointer. | ||
| // Non-standalone binaries have BUN_COMPILED.size = 0, so 0 means "no data". | ||
| std.mem.writeInt(u64, self.data.items[bun_section_offset..][0..8], new_vaddr, .little); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check if the file exists and read the relevant section
wc -l src/elf.zigRepository: oven-sh/bun
Length of output: 70
🏁 Script executed:
# Read the section mentioned in the review (lines 53-121)
sed -n '53,121p' src/elf.zigRepository: oven-sh/bun
Length of output: 3512
🏁 Script executed:
# Also get some context around this function to understand the full signature
sed -n '45,130p' src/elf.zigRepository: oven-sh/bun
Length of output: 4555
🏁 Script executed:
# Find alignUp and pageSize function definitions
rg -A 5 "fn alignUp|fn pageSize" src/elf.zigRepository: oven-sh/bun
Length of output: 392
🏁 Script executed:
# Check what types alignUp returns and how it's used elsewhere
rg "alignUp" src/elf.zig | head -20Repository: oven-sh/bun
Length of output: 304
🏁 Script executed:
# Check the ElfFile struct to understand self.data type
rg -B 5 -A 10 "struct ElfFile|pub const ElfFile" src/elf.zig | head -40Repository: oven-sh/bun
Length of output: 747
🏁 Script executed:
# Check the Managed type to confirm ensureTotalCapacity signature
rg -A 3 "pub fn ensureTotalCapacity" src/elf.zigRepository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Get the complete picture - check what happens around lines 107-114 in context
sed -n '105,120p' src/elf.zigRepository: oven-sh/bun
Length of output: 996
🏁 Script executed:
# Let's verify by checking if there are similar patterns in the codebase that handle this correctly
rg -B 2 -A 2 "ensureTotalCapacity.*@intCast|\.items\.len.*@intCast" src/ | head -30Repository: oven-sh/bun
Length of output: 1825
🏁 Script executed:
# Check if the file compiles - let's look for any build configuration
ls -la src/elf.zig && head -30 src/elf.zigRepository: oven-sh/bun
Length of output: 1370
🏁 Script executed:
# Let's verify the actual type issue by checking what Zig std.ArrayList.Managed expects
# Check how similar operations are handled elsewhere in the codebase
rg "\.items\.len.*alignUp|alignUp.*\.items\.len" src/ -A 2 -B 2Repository: oven-sh/bun
Length of output: 335
🏁 Script executed:
# Check if there are other places where u64 values are used directly as slice indices
rg "@memset.*\.\.\|@memcpy.*\.\." src/elf.zigRepository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Let's verify the actual Zig compilation error by checking nearby working patterns
sed -n '65,72p' src/elf.zigRepository: oven-sh/bun
Length of output: 504
🏁 Script executed:
# Check the actual signature of ensureTotalCapacity by looking at standard library usage patterns
rg -B 2 -A 2 'ensureTotalCapacity\(' src/elf.zig src/macho.zig | head -40Repository: oven-sh/bun
Length of output: 272
🏁 Script executed:
# Check if there's a recent commit that might have introduced this code
git log --oneline src/elf.zig | head -5Repository: oven-sh/bun
Length of output: 98
🏁 Script executed:
# Let's check if Zig has implicit coercion between u64 and usize for alignUp call
# by searching for similar patterns in the codebase
rg "alignUp\(" src/ -B 1 -A 1 | grep -E "alignUp|items\.len|u64" | head -30Repository: oven-sh/bun
Length of output: 631
🏁 Script executed:
# Most importantly, let's verify by checking if this file has compilation issues
# by looking at the build.zig or checking for error reports
git log --all --grep="elf.zig" --oneline | head -5Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Let's check what the current state of the repo is - are there any compilation issues?
# Check if there are open issues or recent PRs related to elf.zig
git log --all --oneline src/elf.zig | head -10Repository: oven-sh/bun
Length of output: 98
🏁 Script executed:
# Most importantly, let's verify the exact type issue by checking slice operations
# In Zig, slice indices MUST be usize. Let's see if the code has actual type errors
# by examining what happens when u64 is used as a slice index
# Line 93: `@memset`(self.data.items[old_file_size..new_file_offset], 0)
# where old_file_size is usize but new_file_offset is u64
# Let's trace through the types more carefully
sed -n '85,100p' src/elf.zig | cat -nRepository: oven-sh/bun
Length of output: 880
🏁 Script executed:
# Check if implicit coercion happens or if there's a compilation error
# by looking at the actual types at each line
python3 << 'EOF'
code = """
const header_size: u64 = `@sizeOf`(u64);
const new_content_size: u64 = header_size + payload.len; // payload.len is usize
const aligned_new_size = alignUp(new_content_size, page_size); // alignUp returns u64
const new_file_offset = alignUp(self.data.items.len, page_size); // self.data.items.len is usize passed to u64 param, result is u64
const shdr_table_size = `@as`(u64, ehdr.e_shnum) * `@sizeOf`(Elf64_Shdr); // explicitly u64
const new_shdr_offset = new_file_offset + aligned_new_size; // u64 + u64 = u64
const total_new_size = new_shdr_offset + shdr_table_size; // u64 + u64 = u64
const old_file_size = self.data.items.len; // usize
try self.data.ensureTotalCapacity(total_new_size); // passing u64 where usize expected - ERROR
self.data.items.len = total_new_size; // assigning u64 to usize - ERROR
`@memset`(self.data.items[old_file_size..new_file_offset], 0); // slice indices must be usize, new_file_offset is u64 - ERROR
"""
lines = code.split('\n')
for i, line in enumerate(lines, 1):
if 'ERROR' in line:
print(f"Line {i}: {line.strip()}")
EOFRepository: oven-sh/bun
Length of output: 375
🏁 Script executed:
# Verify the exact error by checking if the code compiles
# Let's see if there are recent CI failures or if this is a known issue
git log --all --oneline --grep="type" -- src/elf.zig | head -10Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Check the actual recent history to see if this code is new or if it's been fixed
git show b9ac0a5b:src/elf.zig 2>/dev/null | sed -n '85,100p' || echo "Commit may not have this file"Repository: oven-sh/bun
Length of output: 768
🏁 Script executed:
# Let's verify what the actual compilation behavior would be by checking Zig's type coercion rules
# Zig does NOT implicitly coerce u64 to usize in most contexts, especially for slice indices
# This is a real error. Let's see if there's a way the code could compile
# Check if there's any wrapper function that might be accepting u64
rg "fn ensureTotalCapacity" --type zig | head -5Repository: oven-sh/bun
Length of output: 581
🏁 Script executed:
# Let's verify the Managed type definition for ArrayList
rg -B 5 -A 10 "pub const Managed" src/collections/array_list.zig | head -30Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Let's also check if the code we're looking at is actually in a compiled path
# by verifying if this function is actually called/used
rg "writeBunSection" src/ --type zigRepository: oven-sh/bun
Length of output: 230
🏁 Script executed:
# Check if this code is in a conditional compilation block or if it's newly added
git log -p --all -- src/elf.zig | head -100Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Let's verify if the code is actually being compiled by checking build configuration
find . -name "build.zig" -o -name "build.zig.zon" | head -5Repository: oven-sh/bun
Length of output: 126
🏁 Script executed:
# Check if there's a way to actually compile and test this
rg "elf_file|ElfFile" src/StandaloneModuleGraph.zig -B 5 -A 5 | head -40Repository: oven-sh/bun
Length of output: 1880
🏁 Script executed:
# Verify the exact line numbers and check if there's a recent fix or if this is still broken
git diff HEAD src/elf.zig | head -50Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Check the current status - does the code compile without errors?
# by checking if there are any recent fixes
git log --oneline -20 src/elf.zigRepository: oven-sh/bun
Length of output: 98
🏁 Script executed:
# Let's verify one more thing - check if there are other slice operations using u64 values
sed -n '105,120p' src/elf.zigRepository: oven-sh/bun
Length of output: 996
🏁 Script executed:
# Let's do a final comprehensive check on the actual slice operations with u64 indices
python3 << 'EOF'
import re
code = """ pub fn writeBunSection(self: *ElfFile, payload: []const u8) !void {
const ehdr = readEhdr(self.data.items);
const bun_section_offset = try self.findBunSection(ehdr);
const page_size = pageSize(ehdr);
const header_size: u64 = `@sizeOf`(u64);
const new_content_size: u64 = header_size + payload.len;
const aligned_new_size = alignUp(new_content_size, page_size);
// Find the highest virtual address across all PT_LOAD segments
var max_vaddr_end: u64 = 0;
const phdr_size = `@sizeOf`(Elf64_Phdr);
for (0..ehdr.e_phnum) |i| {
const phdr_offset = `@as`(usize, `@intCast`(ehdr.e_phoff)) + i * phdr_size;
const phdr = std.mem.bytesAsValue(Elf64_Phdr, self.data.items[phdr_offset..][0..phdr_size]).*;
if (phdr.p_type == elf.PT_LOAD) {
const vaddr_end = phdr.p_vaddr + phdr.p_memsz;
if (vaddr_end > max_vaddr_end) {
max_vaddr_end = vaddr_end;
}
}
}
// The new segment's virtual address: after all existing mappings, page-aligned
const new_vaddr = alignUp(max_vaddr_end, page_size);
// The new data goes at the end of the file, page-aligned
const new_file_offset = alignUp(self.data.items.len, page_size);
// Grow the buffer to hold the new data + section header table after it
const shdr_table_size = `@as`(u64, ehdr.e_shnum) * `@sizeOf`(Elf64_Shdr);
const new_shdr_offset = new_file_offset + aligned_new_size;
const total_new_size = new_shdr_offset + shdr_table_size;
const old_file_size = self.data.items.len;
try self.data.ensureTotalCapacity(total_new_size);
self.data.items.len = total_new_size;
// Zero the gap between old file end and new data (alignment padding).
// Without this, uninitialized allocator memory would leak into the output.
if (new_file_offset > old_file_size) {
`@memset`(self.data.items[old_file_size..new_file_offset], 0);
}
// Copy the section header table to its new location
const old_shdr_offset = ehdr.e_shoff;
bun.memmove(
self.data.items[new_shdr_offset..][0..shdr_table_size],
self.data.items[old_shdr_offset..][0..shdr_table_size],
);
// Update e_shoff to the new section header table location
self.writeEhdrShoff(new_shdr_offset);
// Write the payload at the new location: [u64 LE size][data][zero padding]
std.mem.writeInt(u64, self.data.items[new_file_offset..][0..8], `@intCast`(payload.len), .little);
`@memcpy`(self.data.items[new_file_offset + header_size ..][0..payload.len], payload);
// Zero the padding between payload end and section header table
const padding_start = new_file_offset + new_content_size;
if (new_shdr_offset > padding_start) {
`@memset`(self.data.items[padding_start..new_shdr_offset], 0);
}
// Write the vaddr of the appended data at the ORIGINAL .bun section location
// (where BUN_COMPILED symbol points). At runtime, BUN_COMPILED.size will be
// this vaddr (always non-zero), which the runtime dereferences as a pointer.
// Non-standalone binaries have BUN_COMPILED.size = 0, so 0 means "no data".
std.mem.writeInt(u64, self.data.items[bun_section_offset..][0..8], new_vaddr, .little);
}"""
# Find all slice operations
slice_pattern = r'\.items\[([^\]]+)\.\.\]'
matches = re.finditer(slice_pattern, code)
print("Slice operations with potential u64 indices:")
for match in matches:
expr = match.group(1)
print(f" - .items[{expr}..] - needs verification")
# Type tracking
print("\nType assignments:")
assignments = [
("new_file_offset", "alignUp(self.data.items.len, page_size) → u64"),
("new_shdr_offset", "new_file_offset + aligned_new_size → u64"),
("shdr_table_size", "@as(u64, ...) → u64"),
("total_new_size", "new_shdr_offset + shdr_table_size → u64"),
("padding_start", "new_file_offset + new_content_size → u64"),
]
for var, typ in assignments:
print(f" - {var}: {typ}")
EOFRepository: oven-sh/bun
Length of output: 728
🏁 Script executed:
# Final verification - check if this code was recently added and hasn't been compiled yet
# or if it's in a Linux-only path
rg "linux|linux_x64|aarch64" src/elf.zigRepository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Check if the code is wrapped in a condition that might not be compiled by default
head -50 src/elf.zig | grep -E "comptime|if.*Environment"Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Most importantly - verify the actual compilation status by looking at CI config
find . -name ".github" -o -name ".cirrus.yml" -o -name "Makefile" | head -5Repository: oven-sh/bun
Length of output: 107
🏁 Script executed:
# Let me check the GitHub workflows to understand compilation
ls -la .github/workflows/ 2>/dev/null | head -20Repository: oven-sh/bun
Length of output: 1330
🏁 Script executed:
# Verify if there are any known Zig compiler compatibility issues with this code
rg "error|compile|fail" .github/ 2>/dev/null | grep -i elf | head -10Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Final check - verify the exact types involved by checking if there are type annotations nearby
sed -n '86,92p' src/elf.zig | cat -nRepository: oven-sh/bun
Length of output: 424
🏁 Script executed:
# Let me check the findBunSection return type to complete the picture
rg -A 10 "fn findBunSection" src/elf.zigRepository: oven-sh/bun
Length of output: 577
🏁 Script executed:
# Also verify the exact behavior of Zig type coercion with this specific pattern
# Check if there are any recent changes or PRs related to this code
git log --all --since="2026-01-01" --oneline -- src/elf.zigRepository: oven-sh/bun
Length of output: 98
🏁 Script executed:
# Verify that the code would fail to compile by checking Zig's type system
# Let me also check if there are any implicit coercion rules that might apply
python3 << 'EOF'
# In Zig, slice indices MUST be usize
# The type system is strict about this - there is NO implicit coercion from u64 to usize
# This is a safety feature to prevent out-of-bounds access
# Let's document the errors:
errors = {
"Line 87": "ensureTotalCapacity(total_new_size: u64) expects usize",
"Line 88": "items.len = total_new_size tries to assign u64 to usize",
"Line 92": "@memset(.items[old_file_size..new_file_offset], 0) - new_file_offset is u64",
"Line 99": ".items[new_shdr_offset..][0..shdr_table_size] - new_shdr_offset is u64",
"Line 108": ".items[new_file_offset..][0..8] - new_file_offset is u64",
"Line 109": ".items[new_file_offset + header_size ..] - index is u64",
"Line 113": "@memset(.items[padding_start..new_shdr_offset]) - both are u64",
"Line 119": ".items[bun_section_offset..][0..8] - bun_section_offset is u64",
}
print("Expected compilation errors:")
for line, err in errors.items():
print(f" {line}: {err}")
EOFRepository: oven-sh/bun
Length of output: 672
Fix u64/usize type mismatches in slice operations and buffer capacity calls.
Zig requires usize for all slice indices and buffer capacity operations. Multiple slice operations use u64 values directly (lines 87–88, 92, 99, 108–109, 113, 119), which will fail to compile. Cast new_file_offset, new_shdr_offset, shdr_table_size, total_new_size, padding_start, and bun_section_offset to usize before using them as slice indices or in ensureTotalCapacity().
Suggested fix
+ const total_new_size_usize: usize = `@intCast`(total_new_size);
+ const new_file_offset_usize: usize = `@intCast`(new_file_offset);
+ const new_shdr_offset_usize: usize = `@intCast`(new_shdr_offset);
+ const shdr_table_size_usize: usize = `@intCast`(shdr_table_size);
+
const old_file_size = self.data.items.len;
- try self.data.ensureTotalCapacity(total_new_size);
- self.data.items.len = total_new_size;
+ try self.data.ensureTotalCapacity(total_new_size_usize);
+ self.data.items.len = total_new_size_usize;
if (new_file_offset > old_file_size) {
- `@memset`(self.data.items[old_file_size..new_file_offset], 0);
+ `@memset`(self.data.items[old_file_size..new_file_offset_usize], 0);
}
- self.data.items[new_shdr_offset..][0..shdr_table_size],
- self.data.items[old_shdr_offset..][0..shdr_table_size],
+ self.data.items[new_shdr_offset_usize..][0..shdr_table_size_usize],
+ self.data.items[old_shdr_offset..][0..shdr_table_size_usize],
- std.mem.writeInt(u64, self.data.items[new_file_offset..][0..8], `@intCast`(payload.len), .little);
- `@memcpy`(self.data.items[new_file_offset + header_size ..][0..payload.len], payload);
+ std.mem.writeInt(u64, self.data.items[new_file_offset_usize..][0..8], `@intCast`(payload.len), .little);
+ `@memcpy`(self.data.items[new_file_offset_usize + header_size ..][0..payload.len], payload);
+ const padding_start_usize: usize = `@intCast`(padding_start);
if (new_shdr_offset > padding_start) {
- `@memset`(self.data.items[padding_start..new_shdr_offset], 0);
+ `@memset`(self.data.items[padding_start_usize..new_shdr_offset_usize], 0);
}
- std.mem.writeInt(u64, self.data.items[bun_section_offset..][0..8], new_vaddr, .little);
+ const bun_section_offset_usize: usize = `@intCast`(bun_section_offset);
+ std.mem.writeInt(u64, self.data.items[bun_section_offset_usize..][0..8], new_vaddr, .little);🤖 Prompt for AI Agents
In `@src/elf.zig` around lines 53 - 121, The writeBunSection function is using u64
values as slice indices and for buffer capacity calls which require usize; fix
by casting the named u64 values to usize where they are used as slice indices or
passed to ensureTotalCapacity: convert new_file_offset, new_shdr_offset,
shdr_table_size, total_new_size (argument to self.data.ensureTotalCapacity),
padding_start, and bun_section_offset to usize before any slice like
self.data.items[...], memmove/memcpy/memset ranges, and before calling
std.mem.writeInt or other functions that take slice ranges; keep the variables
as u64 for arithmetic but create usize locals (e.g., new_file_offset_usize) and
use those in memmove, `@memcpy`, `@memset`, self.data.items.len assignments, and the
ensureTotalCapacity call in writeBunSection.
| .linux => { | ||
| // ELF section approach: find .bun section and expand it | ||
| const input_result = bun.sys.File.readToEnd(.{ .handle = cloned_executable_fd }, bun.default_allocator); | ||
| if (input_result.err) |err| { | ||
| Output.prettyErrorln("Error reading executable: {f}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| } | ||
|
|
||
| const elf_file = bun.elf.ElfFile.init(bun.default_allocator, input_result.bytes.items) catch |err| { | ||
| Output.prettyErrorln("Error initializing ELF file: {}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| }; | ||
| defer elf_file.deinit(); | ||
|
|
||
| elf_file.writeBunSection(bytes) catch |err| { | ||
| Output.prettyErrorln("Error writing .bun section to ELF: {}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| }; | ||
| input_result.bytes.deinit(); | ||
|
|
||
| switch (Syscall.setFileOffset(cloned_executable_fd, 0)) { | ||
| .err => |err| { | ||
| Output.prettyErrorln("Error seeking to start of temporary file: {f}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| }, | ||
| else => {}, | ||
| } | ||
|
|
||
| // Write the modified ELF data back to the file | ||
| const write_file = bun.sys.File{ .handle = cloned_executable_fd }; | ||
| switch (write_file.writeAll(elf_file.data.items)) { | ||
| .err => |err| { | ||
| Output.prettyErrorln("Error writing ELF file: {f}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| }, | ||
| .result => {}, | ||
| } | ||
| // Truncate the file to the exact size of the modified ELF | ||
| _ = Syscall.ftruncate(cloned_executable_fd, @intCast(elf_file.data.items.len)); | ||
|
|
||
| if (comptime !Environment.isWindows) { | ||
| _ = bun.c.fchmod(cloned_executable_fd.native(), 0o777); | ||
| } | ||
| return cloned_executable_fd; | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Run zig:check-all for the new Linux ELF injection path.
Please run bun run zig:check-all to validate cross-platform Zig builds after this platform-specific change.
As per coding guidelines: When making platform-specific changes, run bun run zig:check-all to compile the Zig code on all platforms.
🤖 Prompt for AI Agents
In `@src/StandaloneModuleGraph.zig` around lines 904 - 953, You added a
Linux-specific ELF injection path in StandaloneModuleGraph.zig (the .linux
branch that uses bun.elf.ElfFile, elf_file.writeBunSection,
Syscall.setFileOffset, Syscall.ftruncate and bun.c.fchmod); run the full
cross-platform Zig build check by executing `bun run zig:check-all`, fix any
compile or platform-conditional issues reported (e.g., missing imports, comptime
guards around Environment.isWindows usage, incorrect types or unreachable
branches), and iterate until `zig:check-all` completes cleanly so the new Linux
code compiles on all target platforms.
Code ReviewNewest first 🟡
|
|
(Superseded by updated review below) |
Code ReviewNewest first ✅ ✅ 🟡
|
| @@ -1,5 +1,6 @@ | |||
| import { describe, expect, test } from "bun:test"; | |||
| import { isArm64, isLinux, isMacOS, isMusl, isWindows, tempDir } from "harness"; | |||
There was a problem hiding this comment.
Fixed in 24d6f5e — moved to module-scope import { chmodSync } from "node:fs" at the top of the file.
|
Newest first ✅ 5268a — Looks good! Reviewed 6 files across Previous reviews✅ 52c68 — Looks good! Reviewed 6 files across Previous reviews🟡 bf03e — 1 issue(s) found
Previous reviews |
|
|
||
| fn pageSize(ehdr: Elf64_Ehdr) u64 { | ||
| return switch (ehdr.e_machine) { | ||
| .AARCH64, .PPC64 => 0x10000, // 64KB |
There was a problem hiding this comment.
Note that our current executable is still 4 KB pages on ARM64 iirc?
Standalone executables on Linux previously read their embedded module
graph from /proc/self/exe at startup, which fails when the binary has
execute-only permissions (chmod 111). This mirrors the approach already
used on macOS (__BUN,__bun section) and Windows (.bun PE section).
Build time: places a BUN_COMPILED symbol in a .bun ELF section via
__attribute__((section(".bun"))). At bun build --compile time, appends
the module graph to the end of the file, converts PT_GNU_STACK into a
PT_LOAD segment to map it, and stores the new vaddr at the original
BUN_COMPILED location.
Runtime: BUN_COMPILED.size holds either 0 (not standalone) or the vaddr
of the appended data. The kernel maps it via PT_LOAD during execve, so
the runtime just dereferences a pointer — zero file I/O, no read
permission needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MSVC limits alignment to 8192 bytes. BlobHeader has 16KB alignment which is only needed on macOS and Linux (for the section-based standalone approach). Windows uses a different PE section strategy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…equire
Replace dynamic `require("fs")` inside test functions with a module-scope
`import { chmodSync } from "node:fs"` per test/CLAUDE.md guidelines.
https://claude.ai/code/session_01UujMs6n1JkfK5Sr18JM6du
bf03ea4 to
52c68b8
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/elf.zig`:
- Line 18: Replace uses of std.mem.eql for ELF magic and other short string
comparisons with the SIMD-accelerated bun.strings.eql utility: locate the
comparison using std.mem.eql(u8, ehdr.e_ident[0..4], "\x7fELF") and change it to
call bun.strings.eql for the same byte slices, and likewise update the
comparisons referenced around the section-name checks (the occurrences noted at
the block near the code handling section names around the lines mentioned).
Ensure you import or reference bun.strings if not already available and keep the
same types (u8 slices) so the equality checks remain equivalent.
- Around line 194-197: The slice indexing in readShdr uses offset (a u64) which
is invalid for slice indices; convert offset to usize before slicing. In
function readShdr, compute const offset_usize = `@intCast`(usize, offset) (or cast
when indexing) and use self.data.items[offset_usize..][0..@sizeOf(Elf64_Shdr)]
so the slice expression accepts a usize; keep the rest of the logic and types
(table_offset, index, Elf64_Shdr) unchanged.
- Around line 175-176: The slice operations use u64 values from shstrtab_shdr
(shstrtab_shdr.sh_offset and shstrtab_shdr.sh_size) as indices, which must be
cast to usize; change the variables used in the bounds check and slice
(strtab_offset and strtab_size) to usize (e.g., set strtab_offset =
`@intCast`(usize, shstrtab_shdr.sh_offset) and strtab_size = `@intCast`(usize,
shstrtab_shdr.sh_size)) before comparing to self.data.items.len and constructing
the slice so the bounds check and
self.data.items[strtab_offset..][0..strtab_size] compile correctly.
In `@src/StandaloneModuleGraph.zig`:
- Around line 904-952: The Syscall.ftruncate call at the end of the ELF write
block is currently ignored; change it to handle the Maybe(void) result (from
Syscall.ftruncate(cloned_executable_fd, `@intCast`(elf_file.data.items.len)))
using a switch or .catch to detect errors, log a descriptive error via
Output.prettyErrorln (including the error value), call cleanup(zname,
cloned_executable_fd), and return bun.invalid_fd on failure so the temporary
file is not left in a partially-updated state; keep the successful path
unchanged and preserve the existing permission-setting and return of
cloned_executable_fd.
In `@test/bundler/bun-build-compile.test.ts`:
- Around line 192-341: The bundler tests in the describe("ELF section", ...)
block use test(...) but should use the itBundled helper for bundler/transpiler
tests; update each test invocation (the three tests named "compiled binary runs
with execute-only permissions", "compiled binary with large payload runs
correctly", "compiled binary with large payload runs with execute-only
permissions", and "compiled binary has .bun ELF section") to call itBundled(...)
with the same async function body and arguments so the tests run under the
bundler harness; keep the describe block and test bodies unchanged except for
replacing the test keyword with itBundled.
---
Duplicate comments:
In `@src/bun.zig`:
- Around line 3705-3708: The three imports macho, pe, elf and the new valkey
import are placed too early; move the new `@import`("./valkey/index.zig") into the
existing bottom import block alongside the other module imports (macho, pe, elf)
so all `@import` statements are grouped at the file bottom following the
repository Zig import convention, ensuring the formatter/auto-ordering keeps
them together.
In `@src/elf.zig`:
- Around line 87-121: Several u64 variables are being used as slice indices and
in ensureTotalCapacity, which require usize; cast each such value to usize when
used for slicing or capacity calls (e.g., call
self.data.ensureTotalCapacity(`@intCast`(usize, total_new_size)), use
`@intCast`(usize, new_file_offset) / new_shdr_offset / shdr_table_size /
padding_start / bun_section_offset where passed to bun.memmove, slice
expressions, `@memset`, `@memcpy`, and std.mem.writeInt targets). Update all uses
around self.data.ensureTotalCapacity, bun.memmove, the slice ranges for
memset/memcpy, the padding calculation, and the final std.mem.writeInt call to
perform `@intCast`(usize, ...) so indices and lengths are the correct type.
- Around line 26-29: Add an errdefer to ensure the Managed(u8) buffer is freed
if subsequent allocation fails: after creating and filling `data` (the
Managed(u8) instance used to hold `elf_data`) and before calling
`allocator.create(ElfFile)`, add `errdefer data.deinit()` so that if
`allocator.create(ElfFile)` errors the `data` buffer is deinitialized and not
leaked; keep the call placement around the `data` lifetime (created/appended to
`data` then `errdefer data.deinit()` before `const self = try
allocator.create(ElfFile)`) so normal success paths are unaffected.
| if (strtab_offset + strtab_size > self.data.items.len) return error.InvalidElfFile; | ||
| const strtab = self.data.items[strtab_offset..][0..strtab_size]; |
There was a problem hiding this comment.
Cast strtab_offset and strtab_size to usize for slice operations.
shstrtab_shdr.sh_offset and shstrtab_shdr.sh_size are u64 (from Elf64_Shdr), but Zig requires usize for slice indices. This will fail to compile.
Proposed fix
- if (strtab_offset + strtab_size > self.data.items.len) return error.InvalidElfFile;
- const strtab = self.data.items[strtab_offset..][0..strtab_size];
+ const strtab_offset_usize: usize = `@intCast`(strtab_offset);
+ const strtab_size_usize: usize = `@intCast`(strtab_size);
+ if (strtab_offset + strtab_size > self.data.items.len) return error.InvalidElfFile;
+ const strtab = self.data.items[strtab_offset_usize..][0..strtab_size_usize];📝 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.
| if (strtab_offset + strtab_size > self.data.items.len) return error.InvalidElfFile; | |
| const strtab = self.data.items[strtab_offset..][0..strtab_size]; | |
| const strtab_offset_usize: usize = `@intCast`(strtab_offset); | |
| const strtab_size_usize: usize = `@intCast`(strtab_size); | |
| if (strtab_offset + strtab_size > self.data.items.len) return error.InvalidElfFile; | |
| const strtab = self.data.items[strtab_offset_usize..][0..strtab_size_usize]; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/elf.zig` around lines 175 - 176, The slice operations use u64 values from
shstrtab_shdr (shstrtab_shdr.sh_offset and shstrtab_shdr.sh_size) as indices,
which must be cast to usize; change the variables used in the bounds check and
slice (strtab_offset and strtab_size) to usize (e.g., set strtab_offset =
`@intCast`(usize, shstrtab_shdr.sh_offset) and strtab_size = `@intCast`(usize,
shstrtab_shdr.sh_size)) before comparing to self.data.items.len and constructing
the slice so the bounds check and
self.data.items[strtab_offset..][0..strtab_size] compile correctly.
| fn readShdr(self: *const ElfFile, table_offset: u64, index: u16) Elf64_Shdr { | ||
| const offset = table_offset + @as(u64, index) * @sizeOf(Elf64_Shdr); | ||
| return std.mem.bytesAsValue(Elf64_Shdr, self.data.items[offset..][0..@sizeOf(Elf64_Shdr)]).*; | ||
| } |
There was a problem hiding this comment.
Cast offset to usize for slice indexing.
offset is computed as u64 but used as a slice index, which requires usize.
Proposed fix
fn readShdr(self: *const ElfFile, table_offset: u64, index: u16) Elf64_Shdr {
const offset = table_offset + `@as`(u64, index) * `@sizeOf`(Elf64_Shdr);
- return std.mem.bytesAsValue(Elf64_Shdr, self.data.items[offset..][0..@sizeOf(Elf64_Shdr)]).*;
+ const offset_usize: usize = `@intCast`(offset);
+ return std.mem.bytesAsValue(Elf64_Shdr, self.data.items[offset_usize..][0..@sizeOf(Elf64_Shdr)]).*;
}📝 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.
| fn readShdr(self: *const ElfFile, table_offset: u64, index: u16) Elf64_Shdr { | |
| const offset = table_offset + @as(u64, index) * @sizeOf(Elf64_Shdr); | |
| return std.mem.bytesAsValue(Elf64_Shdr, self.data.items[offset..][0..@sizeOf(Elf64_Shdr)]).*; | |
| } | |
| fn readShdr(self: *const ElfFile, table_offset: u64, index: u16) Elf64_Shdr { | |
| const offset = table_offset + `@as`(u64, index) * `@sizeOf`(Elf64_Shdr); | |
| const offset_usize: usize = `@intCast`(offset); | |
| return std.mem.bytesAsValue(Elf64_Shdr, self.data.items[offset_usize..][0..@sizeOf(Elf64_Shdr)]).*; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/elf.zig` around lines 194 - 197, The slice indexing in readShdr uses
offset (a u64) which is invalid for slice indices; convert offset to usize
before slicing. In function readShdr, compute const offset_usize =
`@intCast`(usize, offset) (or cast when indexing) and use
self.data.items[offset_usize..][0..@sizeOf(Elf64_Shdr)] so the slice expression
accepts a usize; keep the rest of the logic and types (table_offset, index,
Elf64_Shdr) unchanged.
| .linux => { | ||
| // ELF section approach: find .bun section and expand it | ||
| const input_result = bun.sys.File.readToEnd(.{ .handle = cloned_executable_fd }, bun.default_allocator); | ||
| if (input_result.err) |err| { | ||
| Output.prettyErrorln("Error reading executable: {f}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| } | ||
|
|
||
| const elf_file = bun.elf.ElfFile.init(bun.default_allocator, input_result.bytes.items) catch |err| { | ||
| Output.prettyErrorln("Error initializing ELF file: {}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| }; | ||
| defer elf_file.deinit(); | ||
|
|
||
| elf_file.writeBunSection(bytes) catch |err| { | ||
| Output.prettyErrorln("Error writing .bun section to ELF: {}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| }; | ||
| input_result.bytes.deinit(); | ||
|
|
||
| switch (Syscall.setFileOffset(cloned_executable_fd, 0)) { | ||
| .err => |err| { | ||
| Output.prettyErrorln("Error seeking to start of temporary file: {f}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| }, | ||
| else => {}, | ||
| } | ||
|
|
||
| // Write the modified ELF data back to the file | ||
| const write_file = bun.sys.File{ .handle = cloned_executable_fd }; | ||
| switch (write_file.writeAll(elf_file.data.items)) { | ||
| .err => |err| { | ||
| Output.prettyErrorln("Error writing ELF file: {f}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| }, | ||
| .result => {}, | ||
| } | ||
| // Truncate the file to the exact size of the modified ELF | ||
| _ = Syscall.ftruncate(cloned_executable_fd, @intCast(elf_file.data.items.len)); | ||
|
|
||
| if (comptime !Environment.isWindows) { | ||
| _ = bun.c.fchmod(cloned_executable_fd.native(), 0o777); | ||
| } | ||
| return cloned_executable_fd; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and examine the file
cd /root/repo && fd -t f "StandaloneModuleGraph.zig"Repository: oven-sh/bun
Length of output: 115
🏁 Script executed:
# Check the file size and read the relevant lines
wc -l /root/repo/src/StandaloneModuleGraph.zigRepository: oven-sh/bun
Length of output: 126
🏁 Script executed:
# Read lines around 904-952
sed -n '900,955p' /root/repo/src/StandaloneModuleGraph.zigRepository: oven-sh/bun
Length of output: 138
🏁 Script executed:
# Search for Syscall.ftruncate to understand its return type
rg "fn ftruncate|Syscall\.ftruncate" -A 3 -B 1Repository: oven-sh/bun
Length of output: 3665
Handle Syscall.ftruncate failures.
The ftruncate result is currently ignored; if it fails, the output binary can be left in a partially updated state. Per the coding guidelines, Maybe(void) results must be handled with switch statements or .unwrap().
Suggested fix
- _ = Syscall.ftruncate(cloned_executable_fd, `@intCast`(elf_file.data.items.len));
+ switch (Syscall.ftruncate(cloned_executable_fd, `@intCast`(elf_file.data.items.len))) {
+ .err => |err| {
+ Output.prettyErrorln("Error truncating ELF file: {f}", .{err});
+ cleanup(zname, cloned_executable_fd);
+ return bun.invalid_fd;
+ },
+ .result => {},
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/StandaloneModuleGraph.zig` around lines 904 - 952, The Syscall.ftruncate
call at the end of the ELF write block is currently ignored; change it to handle
the Maybe(void) result (from Syscall.ftruncate(cloned_executable_fd,
`@intCast`(elf_file.data.items.len))) using a switch or .catch to detect errors,
log a descriptive error via Output.prettyErrorln (including the error value),
call cleanup(zname, cloned_executable_fd), and return bun.invalid_fd on failure
so the temporary file is not left in a partially-updated state; keep the
successful path unchanged and preserve the existing permission-setting and
return of cloned_executable_fd.
| if (isLinux) { | ||
| describe("ELF section", () => { | ||
| test("compiled binary runs with execute-only permissions", async () => { | ||
| using dir = tempDir("build-compile-exec-only", { | ||
| "app.js": `console.log("exec-only-output");`, | ||
| }); | ||
|
|
||
| const outfile = join(dir + "", "app-exec-only"); | ||
| const result = await Bun.build({ | ||
| entrypoints: [join(dir + "", "app.js")], | ||
| compile: { | ||
| outfile, | ||
| }, | ||
| }); | ||
|
|
||
| expect(result.success).toBe(true); | ||
|
|
||
| chmodSync(result.outputs[0].path, 0o111); | ||
|
|
||
| await using proc = Bun.spawn({ | ||
| cmd: [result.outputs[0].path], | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
|
|
||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
|
|
||
| expect(stdout.trim()).toBe("exec-only-output"); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
|
|
||
| test("compiled binary with large payload runs correctly", async () => { | ||
| // Generate a string payload >16KB to exceed the initial .bun section allocation | ||
| // (BUN_COMPILED is aligned to 16KB). This forces the expansion path in elf.zig | ||
| // which appends data to the end of the file and converts PT_GNU_STACK to PT_LOAD. | ||
| const largeString = Buffer.alloc(20000, "x").toString(); | ||
| using dir = tempDir("build-compile-large-payload", { | ||
| "app.js": `const data = "${largeString}"; console.log("large-payload-" + data.length);`, | ||
| }); | ||
|
|
||
| const outfile = join(dir + "", "app-large"); | ||
| const result = await Bun.build({ | ||
| entrypoints: [join(dir + "", "app.js")], | ||
| compile: { | ||
| outfile, | ||
| }, | ||
| }); | ||
|
|
||
| expect(result.success).toBe(true); | ||
|
|
||
| await using proc = Bun.spawn({ | ||
| cmd: [result.outputs[0].path], | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
|
|
||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
|
|
||
| expect(stdout).toContain("large-payload-20000"); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
|
|
||
| test("compiled binary with large payload runs with execute-only permissions", async () => { | ||
| // Same as above but also verifies execute-only works with the expansion path | ||
| const largeString = Buffer.alloc(20000, "y").toString(); | ||
| using dir = tempDir("build-compile-large-exec-only", { | ||
| "app.js": `const data = "${largeString}"; console.log("large-exec-only-" + data.length);`, | ||
| }); | ||
|
|
||
| const outfile = join(dir + "", "app-large-exec-only"); | ||
| const result = await Bun.build({ | ||
| entrypoints: [join(dir + "", "app.js")], | ||
| compile: { | ||
| outfile, | ||
| }, | ||
| }); | ||
|
|
||
| expect(result.success).toBe(true); | ||
|
|
||
| chmodSync(result.outputs[0].path, 0o111); | ||
|
|
||
| await using proc = Bun.spawn({ | ||
| cmd: [result.outputs[0].path], | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
|
|
||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
|
|
||
| expect(stdout).toContain("large-exec-only-20000"); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
|
|
||
| test("compiled binary has .bun ELF section", async () => { | ||
| using dir = tempDir("build-compile-elf-section", { | ||
| "app.js": `console.log("elf-section-test");`, | ||
| }); | ||
|
|
||
| const outfile = join(dir + "", "app-elf-section"); | ||
| const result = await Bun.build({ | ||
| entrypoints: [join(dir + "", "app.js")], | ||
| compile: { | ||
| outfile, | ||
| }, | ||
| }); | ||
|
|
||
| expect(result.success).toBe(true); | ||
|
|
||
| // Verify .bun ELF section exists by reading section headers | ||
| const file = Bun.file(result.outputs[0].path); | ||
| const bytes = new Uint8Array(await file.arrayBuffer()); | ||
|
|
||
| // Parse ELF header to find section headers | ||
| const view = new DataView(bytes.buffer); | ||
| // e_shoff at offset 40 (little-endian u64) | ||
| const shoff = Number(view.getBigUint64(40, true)); | ||
| // e_shentsize at offset 58 | ||
| const shentsize = view.getUint16(58, true); | ||
| // e_shnum at offset 60 | ||
| const shnum = view.getUint16(60, true); | ||
| // e_shstrndx at offset 62 | ||
| const shstrndx = view.getUint16(62, true); | ||
|
|
||
| // Read .shstrtab section header to get string table | ||
| const strtabOff = shoff + shstrndx * shentsize; | ||
| const strtabFileOffset = Number(view.getBigUint64(strtabOff + 24, true)); | ||
| const strtabSize = Number(view.getBigUint64(strtabOff + 32, true)); | ||
|
|
||
| const decoder = new TextDecoder(); | ||
| let foundBunSection = false; | ||
| for (let i = 0; i < shnum; i++) { | ||
| const hdrOff = shoff + i * shentsize; | ||
| const nameIdx = view.getUint32(hdrOff, true); | ||
| if (nameIdx < strtabSize) { | ||
| // Read null-terminated string from strtab | ||
| let end = strtabFileOffset + nameIdx; | ||
| while (end < bytes.length && bytes[end] !== 0) end++; | ||
| const name = decoder.decode(bytes.slice(strtabFileOffset + nameIdx, end)); | ||
| if (name === ".bun") { | ||
| foundBunSection = true; | ||
| // Verify the section has non-zero size | ||
| const shSize = Number(view.getBigUint64(hdrOff + 32, true)); | ||
| expect(shSize).toBeGreaterThan(0); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| expect(foundBunSection).toBe(true); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Use itBundled for the new bundler tests.
These are bundler tests under test/bundler/, so they should be expressed with the itBundled helper rather than test(...).
As per coding guidelines: For bundler and transpiler tests, place tests in test/bundler/ and use itBundled helper.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/bundler/bun-build-compile.test.ts` around lines 192 - 341, The bundler
tests in the describe("ELF section", ...) block use test(...) but should use the
itBundled helper for bundler/transpiler tests; update each test invocation (the
three tests named "compiled binary runs with execute-only permissions",
"compiled binary with large payload runs correctly", "compiled binary with large
payload runs with execute-only permissions", and "compiled binary has .bun ELF
section") to call itBundled(...) with the same async function body and arguments
so the tests run under the bundler harness; keep the describe block and test
bodies unchanged except for replacing the test keyword with itBundled.
There was a problem hiding this comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/elf.zig`:
- Around line 60-135: In writeBunSection, several offsets/sizes are u64 but are
later used as slice indices and passed to self.data.ensureTotalCapacity which
require usize; change by creating usize versions of the computed u64 values
(e.g., new_file_offset_usize = `@as`(usize, new_file_offset),
new_shdr_offset_usize = `@as`(usize, new_shdr_offset), total_new_size_usize =
`@as`(usize, total_new_size), shdr_table_size_usize = `@as`(usize, shdr_table_size),
old_shdr_offset_usize = `@as`(usize, old_shdr_offset), bun_section_offset_usize =
`@as`(usize, bun_section_offset), shdr_offset_usize = `@as`(usize, shdr_offset),
padding_start_usize = `@as`(usize, padding_start)) and use those usize variables
for ensureTotalCapacity, setting self.data.items.len, all slice indexing,
memmove/memcpy/memset, and std.mem.writeInt calls so the code compiles and
avoids truncation on 32-bit hosts.
- Around line 182-221: In findBunSection and readShdr, cast all ELF
offsets/counts to usize before using them for bounds checks or slicing: create
local usize variables for ehdr.e_shoff (shdr_table_offset), `@sizeOf`(Elf64_Shdr)
(shdr_size if needed), shdr.sh_offset, shdr.sh_size, shdr.sh_name and the
computed offset in readShdr; use `@intCast/`@as(usize, ...) to convert ei
e_shoff/u64 and sh_* u32/u64 values and then use those usize locals for
comparisons, slicing (strtab = self.data.items[strtab_offset..][0..strtab_size])
and for computing the table entry address in readShdr so the code compiles and
avoids mixing integer widths.
| }, | ||
| .linux => { | ||
| // ELF section approach: find .bun section and expand it | ||
| const input_result = bun.sys.File.readToEnd(.{ .handle = cloned_executable_fd }, bun.default_allocator); | ||
| if (input_result.err) |err| { | ||
| Output.prettyErrorln("Error reading executable: {f}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| } | ||
|
|
||
| const elf_file = bun.elf.ElfFile.init(bun.default_allocator, input_result.bytes.items) catch |err| { | ||
| Output.prettyErrorln("Error initializing ELF file: {}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| }; | ||
| defer elf_file.deinit(); | ||
|
|
||
| elf_file.writeBunSection(bytes) catch |err| { | ||
| Output.prettyErrorln("Error writing .bun section to ELF: {}", .{err}); | ||
| cleanup(zname, cloned_executable_fd); | ||
| return bun.invalid_fd; | ||
| }; | ||
| input_result.bytes.deinit(); |
There was a problem hiding this comment.
🟣 input_result.bytes is leaked if ElfFile.init (line 913) or writeBunSection (line 920) fails, since .deinit() on line 925 is only reached on success. This is a pre-existing pattern — the macOS path (lines 820-831) and Windows path (lines 862-880) have the identical leak. Adding defer input_result.bytes.deinit() after line 911 would fix all error paths here.
Extended reasoning...
Memory leak of input_result.bytes on error paths
In the new Linux ELF injection path, input_result.bytes is allocated by bun.sys.File.readToEnd() on line 906 and holds the entire bun executable (typically ~100MB). The .deinit() call that frees this buffer is on line 925, which is only reached when both ElfFile.init and writeBunSection succeed.
Concrete error path walkthrough
- Line 906:
input_result = bun.sys.File.readToEnd(...)succeeds, allocating ~100MB ininput_result.bytes - Line 913:
bun.elf.ElfFile.init(...)is called withinput_result.bytes.items - If
initfails (e.g., invalid ELF magic, not 64-bit, not little-endian), thecatchblock on lines 914-916 executes - The catch block calls
cleanup(zname, cloned_executable_fd)and returnsbun.invalid_fd input_result.bytes.deinit()on line 925 is never reached — the ~100MB buffer is leaked
The same leak occurs if writeBunSection fails on line 920: the catch block on lines 921-923 returns early before line 925.
Pre-existing pattern
This is not unique to the Linux path introduced in this PR. The macOS path (lines 820-831) has the identical structure: MachoFile.init or writeSection failing causes an early return that skips input_result.bytes.deinit(). The Windows path (lines 862-880) follows the same pattern. The new Linux code faithfully copies this established convention.
Impact
The practical impact is low. These error paths only trigger during bun build --compile when the executable is malformed or when ELF manipulation fails. In these cases, the build fails and the process exits shortly after, at which point the OS reclaims all memory. The leak is at most ~100MB and occurs at most once per process.
Fix
The cleanest fix for all three platforms would be to add defer input_result.bytes.deinit() immediately after the error check on line 911 (and equivalently in the macOS/Windows paths). Since ElfFile.init copies the data into its own managed buffer, the original input_result.bytes can safely be freed at any point after init succeeds. Using defer would ensure it is freed on all paths — success and error alike.
The Linux bundler_compile.test.ts hang is a pre-existing issue from PR #26923 (ELF section for standalone binaries), not from the WebKit upgrade. That PR added describe.concurrent to bundler_compile.test.ts and changed the Linux inject path from a simple append to reading and rewriting the entire executable. With 20 concurrent compiles of a ~500MB profile binary, this causes resource exhaustion. The ELF PR's own CI (build #40193) shows the same failures.
PR #26923 changed this to describe.concurrent and also changed the Linux ELF inject path from a simple append to reading + rewriting the entire executable. With profile builds at ~500MB, running 20 concurrent compiles exhausts CI resources. Sequential execution avoids this resource contention. The ELF PR's own CI (build #40193) shows identical timeouts on all Linux x64 targets.
…sh#26923) ## Summary - Standalone executables on Linux previously read their embedded module graph from `/proc/self/exe` at startup, which fails when the binary has execute-only permissions (`chmod 111`) - Now uses an ELF section approach (`.bun` section with `BUN_COMPILED` linker symbol), matching the existing macOS (`__BUN,__bun`) and Windows (`.bun` PE section) implementations - At runtime, the kernel maps the data via `PT_LOAD` during `execve` — zero file I/O, no read permission needed ### How it works **Build time** (`src/elf.zig`): Appends the module graph to the end of the ELF file, converts `PT_GNU_STACK` into a `PT_LOAD` segment to map it, and stores the new virtual address at the original `BUN_COMPILED` location. **Runtime** (`StandaloneModuleGraph.zig`): `BUN_COMPILED.size` holds either `0` (not standalone) or the vaddr of the appended data. Just dereferences a pointer to get the module graph. ### Files changed | File | Change | |---|---| | `src/elf.zig` | New — ELF manipulation module (229 lines) | | `src/bun.zig` | Added `elf` import | | `src/bun.js/bindings/c-bindings.cpp` | Added Linux `BUN_COMPILED` in `.bun` section | | `src/StandaloneModuleGraph.zig` | Added `ELF` struct, Linux inject/read paths, removed `/proc/self/exe` reading | | `test/bundler/bun-build-compile.test.ts` | 4 new Linux-only tests | ## Test plan - [x] `bun bd test test/bundler/bun-build-compile.test.ts` — 10/10 pass - [x] `bun bd test test/bundler/bun-build-compile-sourcemap.test.ts` — 5/5 pass - [x] `bun bd test test/bundler/compile-argv.test.ts` — 10/10 pass - [x] Manual: small payload + `chmod 111` — works - [x] Manual: large payload (>16KB, forces append path) + `chmod 111` — works 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
…NU_STACK (#29967) Fixes #29963. ## Reproduction On WSL1, Linux executables produced by `bun build --compile` since 1.3.13 fail at kernel `execve` with `ENOEXEC (Exec format error)` before any userland code runs: ``` $ ./hello-1.3.13 bash: ./hello-1.3.13: cannot execute binary file: Exec format error $ strace -f ./hello-1.3.13 2>&1 | head -1 execve("./hello-1.3.13", …) = -1 ENOEXEC (Exec format error) ``` Invoking the binary through the loader works (`/lib64/ld-linux-x86-64.so.2 ./hello-1.3.13`), which isolates the failure to WSL1's kernel-side ELF header parsing. The issue author binary-patched the appended phdr's `p_type` field from `PT_LOAD` back to `PT_GNU_STACK` and confirmed WSL1 then accepts the binary — proving the trigger is specifically the late `PT_LOAD`. ## Cause #26923 introduced the "append .bun payload, convert PT_GNU_STACK into PT_LOAD" approach in `src/elf.zig`. That produces an ELF with 4 PT_LOAD segments, the last one mapped only by the ex-PT_GNU_STACK slot: ``` LOAD … 0x4000 LOAD … 0x4000 LOAD … 0x4000 LOAD … .bun … 0x1000 ← WSL1 execve rejects this layout ``` Opencode users tracked this regression (opencode 1.14.20 → 1.14.21 = Bun 1.3.11 → 1.3.13), and WSL1 has been unable to run any recent opencode / compiled-bun binary. ## Fix Grow the existing writable PT_LOAD to cover the appended payload — the layout a linker would produce natively. Leave PT_GNU_STACK alone. `src/elf.zig` `writeBunSection`: - Find the `PF_W` PT_LOAD (where `.bun` already lives as a BlobHeader-aligned PROGBITS section). - Place the payload at `rw.p_offset + alignUp(rw.p_memsz, rw.p_align)` in file and the matching vaddr — the equal offsets automatically satisfy `p_vaddr mod p_align == p_offset mod p_align`, the ELF constraint for extending a PT_LOAD. - Grow `p_filesz` and `p_memsz` to cover the new tail. Equal values are fine — the extension is entirely file-backed, no new BSS gap. - Relocate the non-ALLOC sections (`.comment`, `.symtab`, `.strtab`, `.shstrtab`, debug info) and the section header table past the new payload, since their old file range is now inside the extended PT_LOAD and would otherwise get mapped into what was previously BSS. Zero-fill that range so zero-initialized statics stay zeroed. - Update every moved section header's `sh_offset`. ## Verification **On this (non-WSL1) Linux host:** ``` $ readelf -l hello-new | grep -E 'LOAD|GNU_STACK' LOAD ... 0x0000000019f1b1a0 ... RW 0x4000 ← grown writable PT_LOAD, now covers .bun LOAD ... 0x0000000000000000 ... R LOAD ... 0x0000000007fd3200 ... R E GNU_STACK ... ← preserved $ readelf -S hello-new | grep '\.bun' [31] .bun PROGBITS 000000001e6df1a0 ... ``` The `.bun` vaddr (`0x1e6df1a0`) falls inside the RW segment's `[p_vaddr, p_vaddr + p_memsz)` range. Only 3 PT_LOADs. PT_GNU_STACK intact. Execute-only permissions (`chmod 111`) still work — the kernel maps `.bun` at `execve`, no `/proc/self/exe` read needed. **Test** (`test/bundler/bun-build-compile.test.ts`): new test walks the compiled ELF's program headers and asserts `PT_GNU_STACK` is present, the writable PT_LOAD's vaddr range covers `.bun`, and the LOAD count stays at 3. Gates the shape directly so we don't need a WSL1 host to validate the fix. Fails on main (no PT_GNU_STACK), passes with this change. ## Related - #26923 — introduced the `.bun` ELF section approach (replaces `/proc/self/exe` trailer read; still preserved here). - #24742, #29290 — PT_INTERP normalization for Nix/Guix hosts. Untouched.
…sh#26923) ## Summary - Standalone executables on Linux previously read their embedded module graph from `/proc/self/exe` at startup, which fails when the binary has execute-only permissions (`chmod 111`) - Now uses an ELF section approach (`.bun` section with `BUN_COMPILED` linker symbol), matching the existing macOS (`__BUN,__bun`) and Windows (`.bun` PE section) implementations - At runtime, the kernel maps the data via `PT_LOAD` during `execve` — zero file I/O, no read permission needed ### How it works **Build time** (`src/elf.zig`): Appends the module graph to the end of the ELF file, converts `PT_GNU_STACK` into a `PT_LOAD` segment to map it, and stores the new virtual address at the original `BUN_COMPILED` location. **Runtime** (`StandaloneModuleGraph.zig`): `BUN_COMPILED.size` holds either `0` (not standalone) or the vaddr of the appended data. Just dereferences a pointer to get the module graph. ### Files changed | File | Change | |---|---| | `src/elf.zig` | New — ELF manipulation module (229 lines) | | `src/bun.zig` | Added `elf` import | | `src/bun.js/bindings/c-bindings.cpp` | Added Linux `BUN_COMPILED` in `.bun` section | | `src/StandaloneModuleGraph.zig` | Added `ELF` struct, Linux inject/read paths, removed `/proc/self/exe` reading | | `test/bundler/bun-build-compile.test.ts` | 4 new Linux-only tests | ## Test plan - [x] `bun bd test test/bundler/bun-build-compile.test.ts` — 10/10 pass - [x] `bun bd test test/bundler/bun-build-compile-sourcemap.test.ts` — 5/5 pass - [x] `bun bd test test/bundler/compile-argv.test.ts` — 10/10 pass - [x] Manual: small payload + `chmod 111` — works - [x] Manual: large payload (>16KB, forces append path) + `chmod 111` — works 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
…NU_STACK (oven-sh#29967) Fixes oven-sh#29963. ## Reproduction On WSL1, Linux executables produced by `bun build --compile` since 1.3.13 fail at kernel `execve` with `ENOEXEC (Exec format error)` before any userland code runs: ``` $ ./hello-1.3.13 bash: ./hello-1.3.13: cannot execute binary file: Exec format error $ strace -f ./hello-1.3.13 2>&1 | head -1 execve("./hello-1.3.13", …) = -1 ENOEXEC (Exec format error) ``` Invoking the binary through the loader works (`/lib64/ld-linux-x86-64.so.2 ./hello-1.3.13`), which isolates the failure to WSL1's kernel-side ELF header parsing. The issue author binary-patched the appended phdr's `p_type` field from `PT_LOAD` back to `PT_GNU_STACK` and confirmed WSL1 then accepts the binary — proving the trigger is specifically the late `PT_LOAD`. ## Cause oven-sh#26923 introduced the "append .bun payload, convert PT_GNU_STACK into PT_LOAD" approach in `src/elf.zig`. That produces an ELF with 4 PT_LOAD segments, the last one mapped only by the ex-PT_GNU_STACK slot: ``` LOAD … 0x4000 LOAD … 0x4000 LOAD … 0x4000 LOAD … .bun … 0x1000 ← WSL1 execve rejects this layout ``` Opencode users tracked this regression (opencode 1.14.20 → 1.14.21 = Bun 1.3.11 → 1.3.13), and WSL1 has been unable to run any recent opencode / compiled-bun binary. ## Fix Grow the existing writable PT_LOAD to cover the appended payload — the layout a linker would produce natively. Leave PT_GNU_STACK alone. `src/elf.zig` `writeBunSection`: - Find the `PF_W` PT_LOAD (where `.bun` already lives as a BlobHeader-aligned PROGBITS section). - Place the payload at `rw.p_offset + alignUp(rw.p_memsz, rw.p_align)` in file and the matching vaddr — the equal offsets automatically satisfy `p_vaddr mod p_align == p_offset mod p_align`, the ELF constraint for extending a PT_LOAD. - Grow `p_filesz` and `p_memsz` to cover the new tail. Equal values are fine — the extension is entirely file-backed, no new BSS gap. - Relocate the non-ALLOC sections (`.comment`, `.symtab`, `.strtab`, `.shstrtab`, debug info) and the section header table past the new payload, since their old file range is now inside the extended PT_LOAD and would otherwise get mapped into what was previously BSS. Zero-fill that range so zero-initialized statics stay zeroed. - Update every moved section header's `sh_offset`. ## Verification **On this (non-WSL1) Linux host:** ``` $ readelf -l hello-new | grep -E 'LOAD|GNU_STACK' LOAD ... 0x0000000019f1b1a0 ... RW 0x4000 ← grown writable PT_LOAD, now covers .bun LOAD ... 0x0000000000000000 ... R LOAD ... 0x0000000007fd3200 ... R E GNU_STACK ... ← preserved $ readelf -S hello-new | grep '\.bun' [31] .bun PROGBITS 000000001e6df1a0 ... ``` The `.bun` vaddr (`0x1e6df1a0`) falls inside the RW segment's `[p_vaddr, p_vaddr + p_memsz)` range. Only 3 PT_LOADs. PT_GNU_STACK intact. Execute-only permissions (`chmod 111`) still work — the kernel maps `.bun` at `execve`, no `/proc/self/exe` read needed. **Test** (`test/bundler/bun-build-compile.test.ts`): new test walks the compiled ELF's program headers and asserts `PT_GNU_STACK` is present, the writable PT_LOAD's vaddr range covers `.bun`, and the LOAD count stays at 3. Gates the shape directly so we don't need a WSL1 host to validate the fix. Fails on main (no PT_GNU_STACK), passes with this change. ## Related - oven-sh#26923 — introduced the `.bun` ELF section approach (replaces `/proc/self/exe` trailer read; still preserved here). - oven-sh#24742, oven-sh#29290 — PT_INTERP normalization for Nix/Guix hosts. Untouched.
githunk idles at ~91 MB RSS against lazygit's ~20 MB. The Bun runtime is not the cause: a compiled `console.log` idles at 14 MB, below lazygit's Go runtime. Records the measured ladder, the mechanism (`--compile` maps the embedded module graph as a PT_LOAD segment, so bundled-but-unimported code is charged to RSS unconditionally — oven-sh/bun#26923), and the approaches tried and rejected, so the negative results are not re-derived later. Notably: a dynamic import alone changes nothing without `--splitting`, `bundledLanguages` cannot be tree-shaken because it is a map of stored import thunks, and a `tsconfig` `paths` alias does not apply to a bare specifier imported from inside node_modules. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--compile` maps the embedded module graph as a PT_LOAD segment during execve (oven-sh/bun#26923), so bundled-but-unimported code is charged to RSS whether or not it runs. Without `--splitting`, converting an import to `await import()` produced a byte-identical binary and identical RSS; with it, deferred chunks stay uninstantiated at startup. `--splitting` is supported alongside `--compile` (https://bun.com/docs/bundler/executables). This lands the flags first so the lazy Branch Review chunk that follows has something to defer into; on its own the flag pair is already a small win. Measured on the compiled binary, idle in this repository under a pty: binary 107.5 MB -> 105.3 MB; idle RSS 91 MB -> 88/91/86 MB over three runs against a 91 MB baseline reference. Cross-run variance is roughly +/-5 MB, so the flags alone are worth a few MB; the larger win is the deferred chunk. `--bytecode` is deliberately not set yet: it needs `--format=esm` under `--compile` because @opentui/core uses top-level await, and it grows the binary to ~127 MB for startup time rather than memory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
React, @opentui/react, the workspace components and the syntax highlighter behind them are reachable only from Branch Review, but a static import puts them in the repository screen's startup image: `--compile` maps the embedded module graph as a PT_LOAD segment during execve (oven-sh/bun#26923). Routing the host through a dynamic import lets `--splitting` keep that chunk uninstantiated until the screen is actually opened. `createReviewView` now returns a promise, which `AppScreenController` awaits. Shutdown gets a per-instance `reviewViewCreated` guard: the module cache in `react-review-host-lazy` is process-wide, so it cannot decide whether *this* controller ever built a view, and disposing unconditionally would pull the chunk back in at teardown. Measured on the compiled binary, idle in this repository under a pty, medians of three runs in one batch: 92 MB -> 87 MB. Binary 107.5 MB -> 105.3 MB. Full suite green across eight consecutive runs (1794 pass, 0 fail). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: spec the memory footprint reduction work githunk idles at ~91 MB RSS against lazygit's ~20 MB. The Bun runtime is not the cause: a compiled `console.log` idles at 14 MB, below lazygit's Go runtime. Records the measured ladder, the mechanism (`--compile` maps the embedded module graph as a PT_LOAD segment, so bundled-but-unimported code is charged to RSS unconditionally — oven-sh/bun#26923), and the approaches tried and rejected, so the negative results are not re-derived later. Notably: a dynamic import alone changes nothing without `--splitting`, `bundledLanguages` cannot be tree-shaken because it is a map of stored import thunks, and a `tsconfig` `paths` alias does not apply to a bare specifier imported from inside node_modules. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf: compile the binary with --splitting and --minify `--compile` maps the embedded module graph as a PT_LOAD segment during execve (oven-sh/bun#26923), so bundled-but-unimported code is charged to RSS whether or not it runs. Without `--splitting`, converting an import to `await import()` produced a byte-identical binary and identical RSS; with it, deferred chunks stay uninstantiated at startup. `--splitting` is supported alongside `--compile` (https://bun.com/docs/bundler/executables). This lands the flags first so the lazy Branch Review chunk that follows has something to defer into; on its own the flag pair is already a small win. Measured on the compiled binary, idle in this repository under a pty: binary 107.5 MB -> 105.3 MB; idle RSS 91 MB -> 88/91/86 MB over three runs against a 91 MB baseline reference. Cross-run variance is roughly +/-5 MB, so the flags alone are worth a few MB; the larger win is the deferred chunk. `--bytecode` is deliberately not set yet: it needs `--format=esm` under `--compile` because @opentui/core uses top-level await, and it grows the binary to ~127 MB for startup time rather than memory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf: load the Branch Review React tree only when the screen opens React, @opentui/react, the workspace components and the syntax highlighter behind them are reachable only from Branch Review, but a static import puts them in the repository screen's startup image: `--compile` maps the embedded module graph as a PT_LOAD segment during execve (oven-sh/bun#26923). Routing the host through a dynamic import lets `--splitting` keep that chunk uninstantiated until the screen is actually opened. `createReviewView` now returns a promise, which `AppScreenController` awaits. Shutdown gets a per-instance `reviewViewCreated` guard: the module cache in `react-review-host-lazy` is process-wide, so it cannot decide whether *this* controller ever built a view, and disposing unconditionally would pull the chunk back in at teardown. Measured on the compiled binary, idle in this repository under a pty, medians of three runs in one batch: 92 MB -> 87 MB. Binary 107.5 MB -> 105.3 MB. Full suite green across eight consecutive runs (1794 pass, 0 fail). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: record the Shiki stub as a measured negative result Phase 2 aliased the `shiki` barrel to a stub whose `bundledLanguages` held 60 curated grammars instead of 346, so the binary would stop carrying the rest. It was built, and highlighting was verified in a compiled binary against a real-Shiki build of the same probe — identical tokens for typescript and ruby. It bought 5 MB of binary and no memory: 88 MB against phase 1's 87 MB, medians of three runs in one batch. Phase 1 had already solved it. Shiki's grammar entries are dynamic imports to begin with; what made them expensive was `--compile` without `--splitting` mapping the entire graph into the process image. Narrowing 346 deferred chunks to 60 deferred chunks changes nothing that is resident. Reverted, because the stub's price is a language allowlist: any grammar outside the curated set silently loses highlighting. That is a real regression for zero progress on the goal. Also records where the remaining 87 MB sits. `import @opentui/core` alone is +39 MB, and in its own binary that import takes 16 MB -> 50 MB with 36.3 MB of it binary file pages. OpenTUI already embeds its tree-sitter grammars with `with { type: "file" }`, so there is no payload to strip; the cost is its own JavaScript plus extracting the 6.1 MB libopentui.so at import. The ~70 MB target is not reachable from githunk's own code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: format rebased additions * fix: release closed branch review session * test: stabilize review session gc regression --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
/proc/self/exeat startup, which fails when the binary has execute-only permissions (chmod 111).bunsection withBUN_COMPILEDlinker symbol), matching the existing macOS (__BUN,__bun) and Windows (.bunPE section) implementationsPT_LOADduringexecve— zero file I/O, no read permission neededHow it works
Build time (
src/elf.zig): Appends the module graph to the end of the ELF file, convertsPT_GNU_STACKinto aPT_LOADsegment to map it, and stores the new virtual address at the originalBUN_COMPILEDlocation.Runtime (
StandaloneModuleGraph.zig):BUN_COMPILED.sizeholds either0(not standalone) or the vaddr of the appended data. Just dereferences a pointer to get the module graph.Files changed
src/elf.zigsrc/bun.zigelfimportsrc/bun.js/bindings/c-bindings.cppBUN_COMPILEDin.bunsectionsrc/StandaloneModuleGraph.zigELFstruct, Linux inject/read paths, removed/proc/self/exereadingtest/bundler/bun-build-compile.test.tsTest plan
bun bd test test/bundler/bun-build-compile.test.ts— 10/10 passbun bd test test/bundler/bun-build-compile-sourcemap.test.ts— 5/5 passbun bd test test/bundler/compile-argv.test.ts— 10/10 passchmod 111— workschmod 111— works🤖 Generated with Claude Code