mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 12:29:07 +00:00
fix(compile): seek to start of file before EXDEV cross-device copy (#26883)
## What does this PR do? Fixes `bun build --compile` producing an all-zeros binary when the output directory is on a different filesystem than the temp directory. This is common in Docker containers, Gitea runners, and other environments using overlayfs. ## Problem When `inject()` finishes writing the modified executable to the temp file, the file descriptor's offset is at EOF. If the subsequent `renameat()` to the output path fails with `EXDEV` (cross-device — the temp file and output dir are on different filesystems), the code falls back to `copyFileZSlowWithHandle()`, which: 1. Calls `fallocate()` to pre-allocate the output file to the correct size (filled with zeros) 2. Calls `bun.copyFile(in_handle, out_handle)` — but `in_handle`'s offset is at EOF 3. `copy_file_range` / `sendfile` / `read` all use the current file offset (EOF), read 0 bytes, and return immediately 4. Result: output file is the correct size but entirely zeros This explains user reports of `bun build --compile --target=bun-darwin-arm64` producing invalid binaries that `file` identifies as "data" rather than a Mach-O executable. ## Fix Seek the input fd to offset 0 in `copyFileZSlowWithHandle` before calling `bun.copyFile`. ## How did you verify your code works? - `bun bd` compiles successfully - `bun bd test test/bundler/bun-build-compile.test.ts` — 6/6 pass - Added tests that verify compiled binaries have valid executable headers and produce correct output - Manually verified cross-compilation: `bun build --compile --target=bun-darwin-arm64` produces a valid Mach-O binary 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4092,6 +4092,12 @@ pub fn copyFileZSlowWithHandle(in_handle: bun.FileDescriptor, to_dir: bun.FileDe
|
||||
_ = std.os.linux.fallocate(out_handle.cast(), 0, 0, @intCast(stat_.size));
|
||||
}
|
||||
|
||||
// Seek input to beginning — the caller may have written to this fd,
|
||||
// leaving the file offset at EOF. copy_file_range / sendfile / read
|
||||
// all use the current offset when called with null offsets.
|
||||
// Ignore errors: the fd may be non-seekable (e.g. a pipe).
|
||||
_ = setFileOffset(in_handle, 0);
|
||||
|
||||
switch (bun.copyFile(in_handle, out_handle)) {
|
||||
.err => |e| return .{ .err = e },
|
||||
.result => {},
|
||||
|
||||
@@ -121,4 +121,71 @@ describe("Bun.build compile", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("compiled binary validity", () => {
|
||||
test("output binary has valid executable header", async () => {
|
||||
using dir = tempDir("build-compile-valid-header", {
|
||||
"app.js": `console.log("hello");`,
|
||||
});
|
||||
|
||||
const outfile = join(dir + "", "app-out");
|
||||
const result = await Bun.build({
|
||||
entrypoints: [join(dir + "", "app.js")],
|
||||
compile: {
|
||||
outfile,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
// Read the first 4 bytes and verify it's a valid executable magic number
|
||||
const file = Bun.file(result.outputs[0].path);
|
||||
const header = new Uint8Array(await file.slice(0, 4).arrayBuffer());
|
||||
|
||||
if (isMacOS) {
|
||||
// MachO magic: 0xCFFAEDFE (little-endian)
|
||||
expect(header[0]).toBe(0xcf);
|
||||
expect(header[1]).toBe(0xfa);
|
||||
expect(header[2]).toBe(0xed);
|
||||
expect(header[3]).toBe(0xfe);
|
||||
} else if (isLinux) {
|
||||
// ELF magic: 0x7F 'E' 'L' 'F'
|
||||
expect(header[0]).toBe(0x7f);
|
||||
expect(header[1]).toBe(0x45); // 'E'
|
||||
expect(header[2]).toBe(0x4c); // 'L'
|
||||
expect(header[3]).toBe(0x46); // 'F'
|
||||
} else if (isWindows) {
|
||||
// PE magic: 'M' 'Z'
|
||||
expect(header[0]).toBe(0x4d); // 'M'
|
||||
expect(header[1]).toBe(0x5a); // 'Z'
|
||||
}
|
||||
});
|
||||
|
||||
test("compiled binary runs and produces expected output", async () => {
|
||||
using dir = tempDir("build-compile-runs", {
|
||||
"app.js": `console.log("compile-test-output");`,
|
||||
});
|
||||
|
||||
const outfile = join(dir + "", "app-run");
|
||||
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.trim()).toBe("compile-test-output");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// file command test works well
|
||||
|
||||
Reference in New Issue
Block a user