From 740fb233152c2ad93587cd202bcc3606989a31c7 Mon Sep 17 00:00:00 2001 From: robobun Date: Mon, 15 Dec 2025 18:37:09 -0800 Subject: [PATCH] fix(windows): improve bunx metadata validation (#25012) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Improved validation for bunx metadata files on Windows - Added graceful error handling for malformed metadata instead of crashing - Added regression test for the fix ## Test plan - [x] Run `bun bd test test/cli/install/bunx.test.ts -t "should not crash on corrupted"` - [x] Manual testing with corrupted `.bunx` files - [x] Verified normal operation still works 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Co-authored-by: Claude Co-authored-by: Jarred Sumner --- src/install/windows-shim/bun_shim_impl.zig | 19 ++++-- test/cli/install/bunx.test.ts | 75 ++++++++++++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/src/install/windows-shim/bun_shim_impl.zig b/src/install/windows-shim/bun_shim_impl.zig index eebc962c3a..f4411b0284 100644 --- a/src/install/windows-shim/bun_shim_impl.zig +++ b/src/install/windows-shim/bun_shim_impl.zig @@ -680,11 +680,19 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { const length_of_filename_u8 = @intFromPtr(read_ptr) - @intFromPtr(buf1_u8) - 2 * (nt_object_prefix.len + "\x00".len); const filename = buf1_u8[2 * nt_object_prefix.len ..][0..length_of_filename_u8]; + const filename_u16 = std.mem.bytesAsSlice(u16, filename); if (dbg) { - const sliced = std.mem.bytesAsSlice(u16, filename); - debug("filename and quote: '{f}'", .{fmt16(@alignCast(sliced))}); - debug("last char of above is '{}'", .{sliced[sliced.len - 1]}); - assert(sliced[sliced.len - 1] == '\"'); + debug("filename and quote: '{f}'", .{fmt16(@alignCast(filename_u16))}); + if (filename_u16.len > 0) { + debug("last char of above is '{}'", .{filename_u16[filename_u16.len - 1]}); + } + } + // The filename must end with a quote character as per the bunx file format. + // If it doesn't, the file is corrupt - fall back to the slow path in non-standalone mode. + if (filename_u16.len == 0 or filename_u16[filename_u16.len - 1] != '"') { + if (!is_standalone and mode == .launch) + return; + return mode.fail(.InvalidShimValidation); } @memcpy( @@ -700,7 +708,8 @@ fn launcher(comptime mode: LauncherMode, bun_ctx: anytype) mode.RetType() { } const advance = shebang_arg_len_u8 + 2 * "\"".len + length_of_filename_u8; var write_ptr: [*]u16 = @ptrFromInt(@intFromPtr(buf2_u8) + advance); - assert((write_ptr - 1)[0] == '"'); + // The quote was already validated above, this is just a sanity check in debug mode + if (dbg) assert((write_ptr - 1)[0] == '"'); if (user_arguments_u8.len > 0) { // Copy the user arguments in: diff --git a/test/cli/install/bunx.test.ts b/test/cli/install/bunx.test.ts index bdbb1856fc..4950e50a25 100644 --- a/test/cli/install/bunx.test.ts +++ b/test/cli/install/bunx.test.ts @@ -792,3 +792,78 @@ console.log("EXECUTED: multi-tool-alt (alternate binary)"); }); }); }); + +// Regression test: bunx should not crash on corrupted .bunx files (Windows only) +// When the .bunx metadata file is corrupted (e.g., missing quote terminator in bin_path), +// bunx should gracefully fall back to the slow path instead of panicking. +it.skipIf(!isWindows)("should not crash on corrupted .bunx file with missing quote", async () => { + // First, install a package to create a valid .bunx file + // Use typescript which creates both .exe and .bunx files + // Need to init first to create package.json + const initProc = spawn({ + cmd: [bunExe(), "init", "-y"], + cwd: x_dir, + stdout: "pipe", + stderr: "pipe", + env, + }); + await initProc.exited; + + const subprocess1 = spawn({ + cmd: [bunExe(), "add", "typescript@5.0.0"], + cwd: x_dir, + stdout: "pipe", + stderr: "pipe", + env, + }); + const [err1, out1, exitCode1] = await Promise.all([ + subprocess1.stderr.text(), + subprocess1.stdout.text(), + subprocess1.exited, + ]); + + // Find the .bunx file + const binDir = join(x_dir, "node_modules", ".bin"); + const bunxFile = join(binDir, "tsc.bunx"); + + // Verify the file exists before corrupting it + expect(await Bun.file(bunxFile).exists()).toBe(true); + + // Create a corrupted .bunx file: + // Valid format: [bin_path UTF-16LE]["(quote)][null][shebang][bin_len u32][args_len u32][flags u16] + // Corrupted: Replace the quote with 'X' but keep valid lengths/flags + const binPath = Buffer.from("typescript\\bin\\tsc", "utf16le"); + const corruptedQuote = Buffer.from("X", "utf16le"); // 'X' instead of '"' + const nullChar = Buffer.alloc(2, 0); + const shebang = Buffer.from("node ", "utf16le"); + const binLen = Buffer.alloc(4); + binLen.writeUInt32LE(binPath.length); + const argsLen = Buffer.alloc(4); + argsLen.writeUInt32LE(shebang.length); + // Valid flags with has_shebang=true, is_node_or_bun=true, version=v5 + const flags = Buffer.alloc(2); + flags.writeUInt16LE(0xab37); + + const corruptedData = Buffer.concat([binPath, corruptedQuote, nullChar, shebang, binLen, argsLen, flags]); + await writeFile(bunxFile, corruptedData); + + // Now run bunx - it should NOT crash, but may fail gracefully + // Using bun run to invoke tsc.exe, which triggers the BunXFastPath + const subprocess2 = spawn({ + cmd: [bunExe(), "run", "tsc", "--version"], + cwd: x_dir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const [stderr, stdout, exitCode] = await Promise.all([ + subprocess2.stderr.text(), + subprocess2.stdout.text(), + subprocess2.exited, + ]); + + // The key assertion: we should NOT see a panic + expect(stderr).not.toContain("panic"); + expect(stderr).not.toContain("reached unreachable code"); +});