From 840beed3f1a089636d645cc45cbbfa3de3cf79a3 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 9 Oct 2025 07:44:09 +0000 Subject: [PATCH] Fix shell cat panic on Windows when reader chunks arrive late MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a race condition where IO reader chunks could arrive after the cat builtin transitioned to terminal states (.done, .waiting_write_err, .idle), causing a panic with "Invalid state". This was particularly reproducible on Windows in package scripts. The fix handles these terminal states gracefully in onIOReaderChunk() by ignoring late-arriving chunks since the reader is already being cleaned up. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/shell/builtin/cat.zig | 5 +-- test/regression/issue/23370.test.ts | 54 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 test/regression/issue/23370.test.ts diff --git a/src/shell/builtin/cat.zig b/src/shell/builtin/cat.zig index 10e3d78566..303f842ab1 100644 --- a/src/shell/builtin/cat.zig +++ b/src/shell/builtin/cat.zig @@ -196,9 +196,10 @@ pub fn onIOReaderChunk(this: *Cat, chunk: []const u8, remove: *bool) Yield { _ = this.bltn().writeNoIO(.stdout, chunk); return .done; }, - else => @panic("Invalid state"), + // Race condition: chunks can arrive after state transitions (especially on Windows) + // Just ignore them - the reader is being cleaned up + .done, .waiting_write_err, .idle => return .done, } - return .done; } pub fn onIOReaderDone(this: *Cat, err: ?jsc.SystemError) Yield { diff --git a/test/regression/issue/23370.test.ts b/test/regression/issue/23370.test.ts new file mode 100644 index 0000000000..76930c9189 --- /dev/null +++ b/test/regression/issue/23370.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; + +test("cat should not panic on delayed reader chunks (issue #23370)", async () => { + const dir = tempDirWithFiles("cat-race", { + "file1.txt": "a".repeat(1024 * 1024), // 1MB + "file2.txt": "b".repeat(1024 * 1024), // 1MB + "file3.txt": "c".repeat(1024 * 1024), // 1MB + "test.js": ` + const { $ } = require("bun"); + await $\`cat file1.txt file2.txt file3.txt\`; + `, + }); + + const proc = Bun.spawn({ + cmd: [bunExe(), "test.js"], + cwd: dir, + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [exitCode, stderr] = await Promise.all([proc.exited, proc.stderr.text()]); + + expect(stderr).not.toContain("panic"); + expect(stderr).not.toContain("Invalid state"); + expect(exitCode).toBe(0); +}, 10000); + +test("cat in package script should not panic", async () => { + const dir = tempDirWithFiles("cat-pkgscript", { + "file.txt": "x".repeat(1024 * 1024), // 1MB + "package.json": JSON.stringify({ + name: "test", + scripts: { + test: "cat file.txt", + }, + }), + }); + + const proc = Bun.spawn({ + cmd: [bunExe(), "run", "test"], + cwd: dir, + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [exitCode, stderr] = await Promise.all([proc.exited, proc.stderr.text()]); + + expect(stderr).not.toContain("panic"); + expect(stderr).not.toContain("Invalid state"); + expect(exitCode).toBe(0); +}, 10000);