From 5eabf81b2525db96e5d283b9b62dc020f8885411 Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 14 Apr 2026 00:49:42 +0000 Subject: [PATCH 1/2] Fix integer overflow reading sliced non-regular file blobs Reading a sliced file blob backed by a pipe/fifo (e.g. Bun.stdin.slice(1).text()) would panic in debug builds with an integer overflow when computing the initial read buffer capacity. Slicing a blob with unknown size produces a max_length just below Blob.max_size, which was then used directly as the initial buffer size for non-regular files, and adding the 16-byte slack overflowed the u52 size type. Cap the initial buffer for non-regular files at 4096 (the read loop already grows it as needed) and use saturating addition for the slack bytes. --- src/bun.js/webcore/blob/read_file.zig | 14 +++----- test/js/bun/util/bun-stdin-slice.test.ts | 41 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 test/js/bun/util/bun-stdin-slice.test.ts diff --git a/src/bun.js/webcore/blob/read_file.zig b/src/bun.js/webcore/blob/read_file.zig index 55ac7d5d40b..c2b6e1bfe00 100644 --- a/src/bun.js/webcore/blob/read_file.zig +++ b/src/bun.js/webcore/blob/read_file.zig @@ -347,10 +347,7 @@ pub const ReadFile = struct { // read up to 4k at a time if // they didn't explicitly set a size and we're reading from something that's not a regular file } else if (stat.size == 0 and this.could_block) { - this.size = if (this.max_length == Blob.max_size) - 4096 - else - this.max_length; + this.size = @min(this.max_length, 4096); } if (this.offset > 0) { @@ -384,7 +381,7 @@ pub const ReadFile = struct { // add an extra 16 bytes to the buffer to avoid having to resize it for trailing extra data if (!this.could_block or (this.size > 0 and this.size != Blob.max_size)) - this.buffer = std.ArrayListUnmanaged(u8).initCapacity(bun.default_allocator, this.size + 16) catch |err| { + this.buffer = std.ArrayListUnmanaged(u8).initCapacity(bun.default_allocator, this.size +| 16) catch |err| { this.errno = err; this.onFinish(); return; @@ -669,10 +666,7 @@ pub const ReadFileUV = struct { } else if (stat.size == 0 and !this.is_regular_file) { // read up to 4k at a time if they didn't explicitly set a size and // we're reading from something that's not a regular file. - this.size = if (this.max_length == Blob.max_size) - 4096 - else - this.max_length; + this.size = @min(this.max_length, 4096); } if (this.offset > 0) { @@ -698,7 +692,7 @@ pub const ReadFileUV = struct { return; } // add an extra 16 bytes to the buffer to avoid having to resize it for trailing extra data - this.buffer.ensureTotalCapacityPrecise(this.byte_store.allocator, @min(this.size + 16, @as(usize, std.math.maxInt(bun.windows.ULONG)))) catch { + this.buffer.ensureTotalCapacityPrecise(this.byte_store.allocator, @min(this.size +| 16, @as(usize, std.math.maxInt(bun.windows.ULONG)))) catch { this.errno = error.OutOfMemory; this.system_error = bun.sys.Error.fromCode(bun.sys.E.NOMEM, .read).toSystemError(); this.onFinish(); diff --git a/test/js/bun/util/bun-stdin-slice.test.ts b/test/js/bun/util/bun-stdin-slice.test.ts new file mode 100644 index 00000000000..5aff6f0b292 --- /dev/null +++ b/test/js/bun/util/bun-stdin-slice.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +// Reading a sliced non-regular file blob (like stdin from a pipe) with a size +// close to Blob.max_size used to overflow when computing the initial read +// buffer capacity. +test("Bun.stdin.slice(1).text() does not crash when stdin is a pipe", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", `process.stdout.write(await Bun.stdin.slice(1).text());`], + env: bunEnv, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }); + + proc.stdin.write("hello world"); + await proc.stdin.end(); + + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + + expect(stdout).toBe("hello world"); + expect(exitCode).toBe(0); +}); + +test("Bun.stdin.slice(0, N).text() caps reads at N bytes", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", `process.stdout.write(await Bun.stdin.slice(0, 3).text());`], + env: bunEnv, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }); + + proc.stdin.write("0123456789"); + await proc.stdin.end(); + + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + + expect(stdout).toBe("012"); + expect(exitCode).toBe(0); +}); From 45a24256237bf292aac66955b7f7b84508c3c437 Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 14 Apr 2026 01:49:27 +0000 Subject: [PATCH 2/2] Skip Bun.stdin.slice() tests on Windows The integer overflow being tested is POSIX-only; on Windows, ReadFileUV already bailed with ENOMEM on size > ULONG_MAX before reaching the overflow site. Reading a sliced stdin pipe via uv_fs_read with a non-zero offset is pre-existing behavior unrelated to this fix and behaves inconsistently across Windows versions. --- test/js/bun/util/bun-stdin-slice.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/js/bun/util/bun-stdin-slice.test.ts b/test/js/bun/util/bun-stdin-slice.test.ts index 5aff6f0b292..8fad44a3d09 100644 --- a/test/js/bun/util/bun-stdin-slice.test.ts +++ b/test/js/bun/util/bun-stdin-slice.test.ts @@ -1,10 +1,11 @@ import { expect, test } from "bun:test"; -import { bunEnv, bunExe } from "harness"; +import { bunEnv, bunExe, isWindows } from "harness"; // Reading a sliced non-regular file blob (like stdin from a pipe) with a size // close to Blob.max_size used to overflow when computing the initial read -// buffer capacity. -test("Bun.stdin.slice(1).text() does not crash when stdin is a pipe", async () => { +// buffer capacity. The overflow was only reachable on POSIX; on Windows the +// ReadFileUV path already bailed on size > ULONG_MAX before the addition. +test.skipIf(isWindows)("Bun.stdin.slice(1).text() does not crash when stdin is a pipe", async () => { await using proc = Bun.spawn({ cmd: [bunExe(), "-e", `process.stdout.write(await Bun.stdin.slice(1).text());`], env: bunEnv, @@ -22,7 +23,7 @@ test("Bun.stdin.slice(1).text() does not crash when stdin is a pipe", async () = expect(exitCode).toBe(0); }); -test("Bun.stdin.slice(0, N).text() caps reads at N bytes", async () => { +test.skipIf(isWindows)("Bun.stdin.slice(0, N).text() caps reads at N bytes", async () => { await using proc = Bun.spawn({ cmd: [bunExe(), "-e", `process.stdout.write(await Bun.stdin.slice(0, 3).text());`], env: bunEnv,