From 59db5c34b35447635986b9a41ff35b6c58319dd8 Mon Sep 17 00:00:00 2001 From: Juraj Kirchheim Date: Fri, 23 Oct 2026 23:01:18 +0100 Subject: [PATCH] Bun.write: fall back to read/write when copy_file_range fails with EBADF on an O_APPEND destination copy_file_range(2) returns EBADF for an O_APPEND destination; only EINVAL was handled. Co-Authored-By: Claude Sonnet 5.5 --- src/runtime/webcore/blob/copy_file.rs | 7 +++++++ test/js/bun/io/bun-write.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/runtime/webcore/blob/copy_file.rs b/src/runtime/webcore/blob/copy_file.rs index e41579856cdd..e0357f4286bd 100644 --- a/src/runtime/webcore/blob/copy_file.rs +++ b/src/runtime/webcore/blob/copy_file.rs @@ -428,6 +428,13 @@ impl CopyFile { return self.fallback_read_write(remain, unknown_size, &mut total_written); } + // EBADF: copy_file_range(2) fails with it when the destination was opened with + // O_APPEND (e.g. stdout redirected with `>>`). A read/write loop appends fine. + // (Any other bad fd fails there with its own error.) + bun_sys::E::EBADF if total_written == 0 => { + return self.fallback_read_write(remain, unknown_size, &mut total_written); + } + // EINVAL: eCryptfs and other filesystems may not support copy_file_range. // Also returned when the file descriptor is incompatible with the syscall. bun_sys::E::EINVAL => { diff --git a/test/js/bun/io/bun-write.test.js b/test/js/bun/io/bun-write.test.js index bcbd9deef142..83ab85808dc4 100644 --- a/test/js/bun/io/bun-write.test.js +++ b/test/js/bun/io/bun-write.test.js @@ -1784,3 +1784,28 @@ int posix_fadvise(int fd, off_t offset, off_t len, int advice) { expect(exitCode).toBe(0); }); }); + +describe.skipIf(isWindows)("Bun.write into a descriptor opened for appending", () => { + it("appends the file when stdout is redirected with >>", async () => { + using dir = tempDir("bun-write-append-fd", { + "src.txt": "hello world\n", + "log.txt": "before\n", + }); + const src = join(String(dir), "src.txt"); + const log = join(String(dir), "log.txt"); + const script = `process.stderr.write(String(await Bun.write(Bun.stdout, Bun.file(${JSON.stringify(src)}))))`; + await using proc = Bun.spawn({ + cmd: ["sh", "-c", `"$BUN" -e ${JSON.stringify(script)} >> ${JSON.stringify(log)}`], + env: { ...bunEnv, BUN: bunExe() }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, content: fs.readFileSync(log, "utf8") }).toEqual({ + stdout: "", + stderr: "12", + content: "before\nhello world\n", + }); + expect(exitCode).toBe(0); + }); +});