Compare commits

..

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
054e5b2e72 [autofix.ci] apply automated fixes 2025-10-13 19:41:25 +00:00
Claude Bot
e7c624e773 Add missing module polling in watch mode
When a script tries to import a file that doesn't exist in watch mode,
Bun now polls for the missing module instead of exiting with an error.
This allows developers to create the file and have Bun automatically
reload once it appears.

## Implementation

- Created `MissingModulePollTimer.zig` that polls for missing files
  using exponential backoff (2ms to 100ms intervals)
- Added timer integration to EventLoopTimer enum and fire handler
- Integrated polling detection in `VirtualMachine.uncaughtException()`
  and `handlePendingInternalPromiseRejection()`
- Detects MODULE_NOT_FOUND and ERR_MODULE_NOT_FOUND error codes
- Triggers hot reload when the missing file appears

## Testing

Added tests that verify the module polling works correctly when
a missing module is created after the process starts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-13 19:38:21 +00:00
14 changed files with 420 additions and 1124 deletions

View File

@@ -644,6 +644,51 @@ pub fn handledPromise(this: *jsc.VirtualMachine, globalObject: *JSGlobalObject,
return Bun__emitHandledPromiseEvent(globalObject, promise);
}
/// Check if an error is a module-not-found error and start polling for it in watch mode
fn tryHandleMissingModuleInWatchMode(this: *jsc.VirtualMachine, globalObject: *JSGlobalObject, err: JSValue) void {
// Only poll in watch mode
if (this.hot_reload != .watch) return;
if (!err.isObject()) return;
const error_obj = err.uncheckedPtrCast(jsc.JSObject);
// Get the error code without triggering getters
const code_value = error_obj.getCodePropertyVMInquiry(globalObject) orelse return;
if (!code_value.isString()) return;
const code_str = code_value.toBunString(globalObject) catch return;
defer code_str.deref();
// Check if it's a module-not-found error code
const is_module_not_found = code_str.eqlComptime("ERR_MODULE_NOT_FOUND") or
code_str.eqlComptime("MODULE_NOT_FOUND");
if (!is_module_not_found) return;
// Try to extract the specifier
const specifier_value = (err.get(globalObject, "specifier") catch return) orelse return;
if (!specifier_value.isString()) return;
const specifier = specifier_value.toBunString(globalObject) catch return;
defer specifier.deref();
// Convert to a slice
const specifier_slice = specifier.toUTF8(bun.default_allocator);
defer specifier_slice.deinit();
// Start polling for this path
this.timer.missing_module_poll_timer.startPolling(this, specifier_slice.slice()) catch {
// If we can't start polling, just continue normally
return;
};
Output.prettyErrorln("<r><yellow>Waiting for module to exist:<r> {s}", .{specifier_slice.slice()});
Output.flush();
}
pub fn uncaughtException(this: *jsc.VirtualMachine, globalObject: *JSGlobalObject, err: JSValue, is_rejection: bool) bool {
if (this.isShuttingDown()) {
return true;
@@ -665,6 +710,10 @@ pub fn uncaughtException(this: *jsc.VirtualMachine, globalObject: *JSGlobalObjec
bun.api.node.process.exit(globalObject, 1);
@panic("made it past Bun__Process__exit");
}
// Try to handle missing module in watch mode before exiting
this.tryHandleMissingModuleInWatchMode(globalObject, err);
this.is_handling_uncaught_exception = true;
defer this.is_handling_uncaught_exception = false;
const handled = Bun__handleUncaughtException(globalObject, err.toError() orelse err, if (is_rejection) 1 else 0) > 0;
@@ -680,7 +729,12 @@ pub fn uncaughtException(this: *jsc.VirtualMachine, globalObject: *JSGlobalObjec
pub fn handlePendingInternalPromiseRejection(this: *jsc.VirtualMachine) void {
var promise = this.pending_internal_promise.?;
if (promise.status(this.global.vm()) == .rejected and !promise.isHandled(this.global.vm())) {
this.unhandledRejection(this.global, promise.result(this.global.vm()), promise.asValue());
const rejection_value = promise.result(this.global.vm());
// Try to handle missing module in watch mode
this.tryHandleMissingModuleInWatchMode(this.global, rejection_value);
this.unhandledRejection(this.global, rejection_value, promise.asValue());
promise.setHandled(this.global.vm());
}
}

View File

@@ -52,6 +52,9 @@ pub const All = struct {
/// Updates the "Date" header.
date_header_timer: DateHeaderTimer = .{},
/// Polls for missing modules in watch mode
missing_module_poll_timer: MissingModulePollTimer = MissingModulePollTimer.init(),
pub fn init() @This() {
return .{
.thread_id = std.Thread.getCurrentId(),
@@ -602,6 +605,8 @@ pub const DateHeaderTimer = @import("./Timer/DateHeaderTimer.zig");
pub const EventLoopDelayMonitor = @import("./Timer/EventLoopDelayMonitor.zig");
pub const MissingModulePollTimer = @import("./Timer/MissingModulePollTimer.zig");
pub const internal_bindings = struct {
/// Node.js has some tests that check whether timers fire at the right time. They check this
/// with the internal binding `getLibuvNow()`, which returns an integer in milliseconds. This

View File

@@ -69,6 +69,7 @@ pub const Tag = enum {
DateHeaderTimer,
BunTest,
EventLoopDelayMonitor,
MissingModulePollTimer,
pub fn Type(comptime T: Tag) type {
return switch (T) {
@@ -94,6 +95,7 @@ pub const Tag = enum {
.DateHeaderTimer => jsc.API.Timer.DateHeaderTimer,
.BunTest => jsc.Jest.bun_test.BunTest,
.EventLoopDelayMonitor => jsc.API.Timer.EventLoopDelayMonitor,
.MissingModulePollTimer => jsc.API.Timer.MissingModulePollTimer,
};
}
};
@@ -188,6 +190,10 @@ pub fn fire(self: *Self, now: *const timespec, vm: *VirtualMachine) Arm {
monitor.onFire(vm, now);
return .disarm;
},
.MissingModulePollTimer => {
const poll_timer = @as(*jsc.API.Timer.MissingModulePollTimer, @fieldParentPtr("event_loop_timer", self));
return poll_timer.onTimeout(vm);
},
inline else => |t| {
if (@FieldType(t.Type(), "event_loop_timer") != Self) {
@compileError(@typeName(t.Type()) ++ " has wrong type for 'event_loop_timer'");

View File

@@ -0,0 +1,146 @@
/// MissingModulePollTimer manages polling for missing modules in watch mode.
///
/// When a script tries to import a file that doesn't exist, Bun's watch mode should
/// wait for the file to be created rather than exiting with an error. This timer
/// implements exponential backoff polling to check if the missing file has appeared.
///
/// Behavior:
/// - Starts at 2ms intervals, exponentially backs off to max 100ms
/// - Checks if the missing file exists on each poll
/// - Triggers a process reload when the file appears
/// - Stops polling if watch mode is disabled or VM is shutting down
const MissingModulePollTimer = @This();
event_loop_timer: jsc.API.Timer.EventLoopTimer = .{
.tag = .MissingModulePollTimer,
.next = .epoch,
},
/// The path to the missing module that we're polling for
missing_path: []const u8 = &.{},
/// Current polling interval in milliseconds
current_interval_ms: u32 = 2,
/// Minimum polling interval (2ms)
min_interval_ms: u32 = 2,
/// Maximum polling interval (100ms)
max_interval_ms: u32 = 100,
/// Whether the timer is actively polling
is_polling: bool = false,
pub fn init() MissingModulePollTimer {
return .{};
}
/// Start polling for a missing module
pub fn startPolling(this: *MissingModulePollTimer, vm: *VirtualMachine, missing_path: []const u8) !void {
// If already polling, stop the current timer first
if (this.is_polling) {
this.stopPolling(vm);
}
// Store a copy of the path
if (this.missing_path.len > 0) {
bun.default_allocator.free(this.missing_path);
}
this.missing_path = try bun.default_allocator.dupe(u8, missing_path);
// Reset interval to minimum
this.current_interval_ms = this.min_interval_ms;
this.is_polling = true;
// Schedule the first poll
this.scheduleNextPoll(vm);
log("Started polling for missing module: {s} (interval: {}ms)", .{ this.missing_path, this.current_interval_ms });
}
/// Stop polling for the missing module
pub fn stopPolling(this: *MissingModulePollTimer, vm: *VirtualMachine) void {
if (!this.is_polling) return;
if (this.event_loop_timer.state == .ACTIVE) {
vm.timer.remove(&this.event_loop_timer);
}
this.is_polling = false;
this.current_interval_ms = this.min_interval_ms;
log("Stopped polling for missing module: {s}", .{this.missing_path});
}
/// Schedule the next poll with the current interval
fn scheduleNextPoll(this: *MissingModulePollTimer, vm: *VirtualMachine) void {
this.event_loop_timer.next = bun.timespec.msFromNow(@intCast(this.current_interval_ms));
vm.timer.insert(&this.event_loop_timer);
}
/// Timer callback that checks if the missing file exists
pub fn onTimeout(this: *MissingModulePollTimer, vm: *VirtualMachine) jsc.API.Timer.EventLoopTimer.Arm {
this.event_loop_timer.state = .FIRED;
if (!this.is_polling) {
return .disarm;
}
// Check if the file exists
const file_exists = this.checkFileExists();
if (file_exists) {
log("Missing module found: {s}. Triggering reload.", .{this.missing_path});
// Stop polling
this.is_polling = false;
// Trigger a hot reload by calling reload directly
const HotReloader = jsc.hot_reloader.HotReloader;
var task = HotReloader.Task.initEmpty(undefined);
vm.reload(&task);
return .disarm;
}
// File still doesn't exist, increase interval with exponential backoff
this.current_interval_ms = @min(this.current_interval_ms * 2, this.max_interval_ms);
log("Missing module not found yet: {s}. Next poll in {}ms", .{ this.missing_path, this.current_interval_ms });
// Schedule next poll
this.scheduleNextPoll(vm);
return .disarm;
}
/// Check if the file exists
fn checkFileExists(this: *MissingModulePollTimer) bool {
if (this.missing_path.len == 0) return false;
// Use stat to check if the file exists
const stat_result = std.fs.cwd().statFile(this.missing_path) catch return false;
return stat_result.kind == .file;
}
/// Cleanup timer resources
pub fn deinit(this: *MissingModulePollTimer, vm: *VirtualMachine) void {
if (this.event_loop_timer.state == .ACTIVE) {
vm.timer.remove(&this.event_loop_timer);
}
if (this.missing_path.len > 0) {
bun.default_allocator.free(this.missing_path);
this.missing_path = &.{};
}
this.is_polling = false;
}
const log = bun.Output.scoped(.MissingModulePollTimer, .hidden);
const bun = @import("bun");
const std = @import("std");
const jsc = bun.jsc;
const VirtualMachine = jsc.VirtualMachine;

View File

@@ -944,6 +944,9 @@ fn getArgv0(globalThis: *jsc.JSGlobalObject, PATH: []const u8, cwd: []const u8,
fn getArgv(globalThis: *jsc.JSGlobalObject, args: JSValue, PATH: []const u8, cwd: []const u8, argv0: *?[*:0]const u8, allocator: std.mem.Allocator, argv: *std.ArrayList(?[*:0]const u8)) bun.JSError!void {
var cmds_array = try args.arrayIterator(globalThis);
// + 1 for argv0
// + 1 for null terminator
argv.* = try @TypeOf(argv.*).initCapacity(allocator, cmds_array.len + 2);
if (args.isEmptyOrUndefinedOrNull()) {
return globalThis.throwInvalidArguments("cmd must be an array of strings", .{});
@@ -953,18 +956,6 @@ fn getArgv(globalThis: *jsc.JSGlobalObject, args: JSValue, PATH: []const u8, cwd
return globalThis.throwInvalidArguments("cmd must not be empty", .{});
}
// Check for integer overflow when adding 2 (for argv0 and null terminator)
// Also enforce a reasonable limit to prevent excessive memory allocation
const max_args = 1024 * 1024; // 1 million args should be more than enough
if (cmds_array.len > max_args) {
return globalThis.throwInvalidArguments("cmd array is too large (max {d} arguments)", .{max_args});
}
// + 1 for argv0
// + 1 for null terminator
// We've already checked that cmds_array.len + 2 won't overflow
argv.* = try @TypeOf(argv.*).initCapacity(allocator, cmds_array.len + 2);
const argv0_result = try getArgv0(globalThis, PATH, cwd, argv0.*, (try cmds_array.next()).?, allocator);
argv0.* = argv0_result.argv0.ptr;

View File

@@ -0,0 +1,50 @@
import { expect, test } from "bun:test";
import { writeFileSync } from "fs";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
test("watch mode should poll for missing module and reload when it appears", async () => {
using dir = tempDir("watch-missing-external", {
"entry.js": `
console.log("Starting...");
require("./missing.js");
console.log("This should not print on first run");
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "--watch", "entry.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
// Wait a bit for the process to start and hit the error
await new Promise(resolve => setTimeout(resolve, 500));
// Now write the missing file from outside the process
writeFileSync(join(String(dir), "missing.js"), "console.log('Module loaded!'); process.exit(0);");
const timeout = setTimeout(() => {
proc.kill();
}, 5000);
try {
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
clearTimeout(timeout);
console.log("STDOUT:", stdout);
console.log("STDERR:", stderr);
console.log("Exit code:", exitCode);
// The test passes if the process exits with code 0
expect(exitCode).toBe(0);
expect(stdout).toContain("Module loaded!");
} catch (err) {
clearTimeout(timeout);
proc.kill();
throw err;
}
}, 10000);

View File

@@ -0,0 +1,56 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("watch mode should detect missing module error", async () => {
using dir = tempDir("watch-missing-simple", {
"entry.js": `
console.log("Starting...");
try {
require("./missing.js");
} catch (err) {
console.log("ERROR:", err.code);
console.log("Caught error for missing module");
const fs = require("fs");
const path = require("path");
// Write the missing file after a short delay
setTimeout(() => {
console.log("Writing missing.js");
fs.writeFileSync(path.join(__dirname, "missing.js"), "console.log('Module loaded!'); process.exit(0);");
}, 100);
// Re-throw so watch mode sees it
throw err;
}
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "--watch", "entry.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const timeout = setTimeout(() => {
proc.kill();
}, 5000);
try {
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
clearTimeout(timeout);
console.log("STDOUT:", stdout);
console.log("STDERR:", stderr);
console.log("Exit code:", exitCode);
// The test passes if the process exits with code 0
expect(exitCode).toBe(0);
} catch (err) {
clearTimeout(timeout);
proc.kill();
throw err;
}
}, 10000);

View File

@@ -0,0 +1,99 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("watch mode should poll for missing modules", async () => {
using dir = tempDir("watch-missing-module", {
"entry.js": `
try {
require("./missing.js");
} catch (err) {
// Write the missing file
const fs = require("fs");
const path = require("path");
fs.writeFileSync(path.join(__dirname, "missing.js"), "process.exit(0);");
// Re-throw so watch mode sees the error and starts polling
throw err;
}
// If we get here without the file existing, something went wrong
setTimeout(() => {
process.exit(1);
}, 5000);
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "--watch", "entry.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const timeout = setTimeout(() => {
proc.kill();
}, 10000);
try {
const exitCode = await proc.exited;
clearTimeout(timeout);
// The test passes if the process exits with code 0
// This means the missing file was created and executed successfully
expect(exitCode).toBe(0);
} catch (err) {
clearTimeout(timeout);
proc.kill();
throw err;
}
}, 15000);
test("watch mode should poll for missing modules with import", async () => {
using dir = tempDir("watch-missing-module-import", {
"entry.mjs": `
try {
await import("./missing.mjs");
} catch (err) {
// Write the missing file
const fs = await import("fs");
const path = await import("path");
const url = await import("url");
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
fs.writeFileSync(path.join(__dirname, "missing.mjs"), "process.exit(0);");
// Re-throw so watch mode sees the error and starts polling
throw err;
}
// If we get here without the file existing, something went wrong
setTimeout(() => {
process.exit(1);
}, 5000);
`,
});
const proc = Bun.spawn({
cmd: [bunExe(), "--watch", "entry.mjs"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const timeout = setTimeout(() => {
proc.kill();
}, 10000);
try {
const exitCode = await proc.exited;
clearTimeout(timeout);
// The test passes if the process exits with code 0
expect(exitCode).toBe(0);
} catch (err) {
clearTimeout(timeout);
proc.kill();
throw err;
}
}, 15000);

View File

@@ -1,337 +0,0 @@
import { spawn, spawnSync } from "bun";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, gcTick } from "harness";
// Continuing fuzz testing - avoiding known crash cases to find more bugs
// This version skips the integer overflow case to let us find other issues
describe("Bun.spawn continued fuzz test", () => {
test("fuzz spawn with controlled edge cases", async () => {
const iterations = 100;
let crashCount = 0;
// Controlled edge cases that won't immediately hit known bugs
const edgeCaseStrings = ["", " ", "\n", "\t", "\u0000", "\uFFFD", "../etc/passwd", ".", "..", "🚀"];
const stdioOptions = ["pipe", "inherit", "ignore", null, undefined];
for (let i = 0; i < iterations; i++) {
try {
const testType = i % 8;
switch (testType) {
case 0: // Invalid cwd
try {
spawn({
cmd: [bunExe(), "--version"],
cwd: edgeCaseStrings[i % edgeCaseStrings.length],
stdout: "pipe",
stderr: "pipe",
});
} catch (e) {}
break;
case 1: // Null bytes in env
try {
const proc = spawn({
cmd: [bunExe(), "-e", "console.log(process.env.TEST)"],
env: { TEST: "value\u0000test", ...bunEnv },
stdout: "pipe",
stderr: "pipe",
});
await proc.exited;
} catch (e) {}
break;
case 2: // Invalid stdin types
try {
const proc = spawn({
cmd: [bunExe(), "--version"],
stdin: edgeCaseStrings[i % edgeCaseStrings.length] as any,
stdout: "pipe",
});
await proc.exited;
} catch (e) {}
break;
case 3: // Rapid kill after spawn
try {
const proc = spawn({
cmd: [bunExe(), "-e", "await Bun.sleep(1000)"],
stdout: "ignore",
});
proc.kill();
proc.kill(); // Double kill
await proc.exited;
} catch (e) {}
break;
case 4: // Stream operations in weird order
try {
const proc = spawn({
cmd: [bunExe(), "-e", "console.log('test')"],
stdout: "pipe",
});
proc.stdout.cancel();
proc.kill();
await proc.exited;
} catch (e) {}
break;
case 5: // Multiple ref/unref
try {
const proc = spawn({
cmd: [bunExe(), "--version"],
stdout: "ignore",
});
proc.ref();
proc.unref();
proc.ref();
proc.unref();
await proc.exited;
} catch (e) {}
break;
case 6: // spawnSync with weird options
try {
spawnSync({
cmd: [bunExe(), "--version"],
env: { "": "empty key", ...bunEnv },
});
} catch (e) {}
break;
case 7: // Invalid command with various stdio
try {
spawn({
cmd: ["\u0000"],
stdin: stdioOptions[i % stdioOptions.length] as any,
stdout: stdioOptions[i % stdioOptions.length] as any,
stderr: stdioOptions[i % stdioOptions.length] as any,
});
} catch (e) {}
break;
}
if (i % 20 === 0) {
gcTick();
}
} catch (e) {
console.error("Unexpected outer error:", e);
crashCount++;
}
}
expect(crashCount).toBe(0);
}, 60000);
test("fuzz with file descriptor edge cases", async () => {
// Test boundary conditions for file descriptors
const fds = [3, 4, 10, 100, 255];
for (const fd of fds) {
try {
const proc = spawn({
cmd: [bunExe(), "--version"],
stdin: fd,
stdout: "pipe",
});
await proc.exited;
} catch (e) {
// Expected - these should error gracefully
}
}
gcTick();
});
test("fuzz with concurrent spawns and kills", async () => {
const procs = [];
// Spawn 20 processes
for (let i = 0; i < 20; i++) {
try {
const proc = spawn({
cmd: [bunExe(), "-e", "await Bun.sleep(100)"],
stdout: "ignore",
stderr: "ignore",
});
procs.push(proc);
} catch (e) {}
}
// Kill them in random order
for (let i = 0; i < procs.length; i++) {
const idx = Math.floor(Math.random() * procs.length);
try {
procs[idx]?.kill();
} catch (e) {}
}
// Wait for all
await Promise.allSettled(procs.map(p => p?.exited));
gcTick();
});
test("fuzz with stdin write operations", async () => {
const sizes = [0, 1, 100, 1000, 10000];
for (const size of sizes) {
try {
const data = new Uint8Array(size).fill(65);
const proc = spawn({
cmd: [bunExe(), "-e", "await Bun.sleep(10)"],
stdin: "pipe",
stdout: "ignore",
});
try {
proc.stdin.write(data);
proc.stdin.write(data); // Write twice
proc.stdin.end();
proc.stdin.end(); // End twice
} catch (e) {
// Expected
}
await proc.exited;
} catch (e) {
// Expected
}
}
gcTick();
});
test("fuzz with process properties access", async () => {
for (let i = 0; i < 20; i++) {
try {
const proc = spawn({
cmd: [bunExe(), "-e", "await Bun.sleep(10)"],
stdout: "pipe",
stderr: "pipe",
});
// Access properties in various orders
const _ = proc.pid;
const __ = proc.exitCode;
const ___ = proc.killed;
const ____ = proc.signalCode;
// Try to read from streams immediately
try {
const reader = proc.stdout.getReader();
reader.releaseLock();
} catch (e) {}
proc.kill();
await proc.exited;
// Access after exit
const _____ = proc.exitCode;
const ______ = proc.killed;
try {
proc.resourceUsage();
} catch (e) {}
} catch (e) {
// Expected
}
}
gcTick();
});
test("fuzz spawnSync with various stdin", () => {
const inputs = [
new Uint8Array(0),
new Uint8Array(1).fill(0),
new Uint8Array(100).fill(65),
new Uint8Array(10000).fill(65),
Buffer.from("test"),
Buffer.from("\u0000"),
Buffer.from("test\u0000test"),
];
for (const input of inputs) {
try {
const result = spawnSync({
cmd: [bunExe(), "-e", "console.log('ok')"],
stdin: input,
});
result.stdout?.toString();
result.stderr?.toString();
} catch (e) {
// Expected
}
}
gcTick();
});
test("fuzz with env edge cases", async () => {
const envTests = [
{ "": "empty key" },
{ "KEY": "" },
{ "KEY": "\u0000" },
{ "KEY\u0000": "value" },
{ "KEY": "value\u0000value" },
{ "🚀": "rocket" },
{ "KEY": "🚀" },
Object.fromEntries(
Array(100)
.fill(0)
.map((_, i) => [`K${i}`, `V${i}`]),
),
];
for (const env of envTests) {
try {
const proc = spawn({
cmd: [bunExe(), "-e", "console.log('ok')"],
env: { ...bunEnv, ...env },
stdout: "pipe",
});
await proc.exited;
} catch (e) {
// Expected
}
}
gcTick();
});
test("fuzz with cwd edge cases", async () => {
const cwds = [
"/nonexistent/path",
"/tmp/../tmp/../tmp",
".",
"..",
"",
"\u0000",
"/\u0000/test",
"relative/path",
"./././././",
];
for (const cwd of cwds) {
try {
const proc = spawn({
cmd: [bunExe(), "--version"],
cwd: cwd,
stdout: "ignore",
});
await proc.exited;
} catch (e) {
// Expected - most should error
}
}
gcTick();
});
});

View File

@@ -1,568 +0,0 @@
import { spawn, spawnSync } from "bun";
import { describe, expect, test } from "bun:test";
import { bunExe, gcTick } from "harness";
// This fuzz test tries many edge case combinations to find panics, segfaults, and assertion failures
// We're NOT looking for thrown errors - those are expected and handled properly
// We're looking for crashes, panics, and undefined behavior
describe("Bun.spawn fuzz test", () => {
test("fuzz spawn with random invalid/edge case inputs", async () => {
const iterations = 500;
let crashCount = 0;
// Generate various edge case values
const edgeCaseStrings = [
"",
" ",
"\0",
"\n",
"\r\n",
"\t",
"a".repeat(10000), // very long string
"a".repeat(100000), // extremely long string
"\u0000",
"\uFFFD", // replacement character
String.fromCharCode(0xd800), // unpaired surrogate
"../../../etc/passwd",
".",
"..",
"/",
"\\",
"C:\\",
"//",
"\\\\",
"./.",
"./../",
"con", // Windows reserved name
"nul",
"prn",
String.fromCharCode(...Array(100).fill(0)),
"🚀",
"test\x00test",
"|",
"&",
";",
"`",
"$",
"$(echo test)",
"`echo test`",
];
const edgeCaseNumbers = [-1, 0, 1, 2, 999, 1000, 65535, 65536, 2147483647, -2147483648, NaN, Infinity, -Infinity];
const edgeCaseArrays = [
[],
[""],
[" "],
["a".repeat(10000)],
Array(100).fill("test"),
Array(1000).fill("a"),
["\0"],
["test", "\0", "arg"],
...edgeCaseStrings.map(s => [bunExe(), "-e", `console.log("${s}")`]),
[bunExe(), ...Array(50).fill("-e")],
];
const edgeCaseBuffers = [
new Uint8Array(0),
new Uint8Array(1),
new Uint8Array(10000),
new Uint8Array(1000000), // 1MB
new Uint8Array([0]),
new Uint8Array(Array(100).fill(0)),
new Uint8Array(Array(100).fill(255)),
Buffer.from(""),
Buffer.from("\0"),
Buffer.from("test\0test"),
];
const stdioOptions = ["pipe", "inherit", "ignore", null, undefined, 0, 1, 2, 999, -1];
// Random helper functions
const randomElement = <T>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];
const randomInt = (max: number) => Math.floor(Math.random() * max);
const randomBool = () => Math.random() > 0.5;
for (let i = 0; i < iterations; i++) {
try {
// Randomly choose what to fuzz
const fuzzType = randomInt(10);
let options: any = {};
let cmdArray: any = [bunExe(), "--version"];
// Fuzz different aspects
switch (fuzzType) {
case 0: // Fuzz cmd array
if (randomBool()) {
cmdArray = randomElement(edgeCaseArrays);
} else {
cmdArray = [
randomElement(edgeCaseStrings),
...Array(randomInt(10))
.fill(0)
.map(() => randomElement(edgeCaseStrings)),
];
}
break;
case 1: // Fuzz cwd
options.cwd = randomElement(edgeCaseStrings);
break;
case 2: // Fuzz env
options.env = {};
for (let j = 0; j < randomInt(20); j++) {
options.env[randomElement(edgeCaseStrings)] = randomElement(edgeCaseStrings);
}
break;
case 3: // Fuzz stdin
if (randomBool()) {
options.stdin = randomElement(stdioOptions);
} else {
options.stdin = randomElement(edgeCaseBuffers);
}
break;
case 4: // Fuzz stdout
options.stdout = randomElement(stdioOptions);
break;
case 5: // Fuzz stderr
options.stderr = randomElement(stdioOptions);
break;
case 6: // Fuzz stdio array
options.stdio = [randomElement(stdioOptions), randomElement(stdioOptions), randomElement(stdioOptions)];
break;
case 7: // Fuzz multiple options at once
options.cwd = randomElement(edgeCaseStrings);
options.stdin = randomElement(stdioOptions);
options.stdout = randomElement(stdioOptions);
options.stderr = randomElement(stdioOptions);
break;
case 8: // Fuzz with completely invalid options
options = {
cwd: randomElement([null, undefined, 123, true, {}, []]),
stdin: randomElement([true, false, {}, [], "invalid"]),
stdout: randomElement([true, false, {}, [], "invalid"]),
env: randomElement([null, undefined, 123, true, "invalid", []]),
};
break;
case 9: // Fuzz cmd with invalid types
cmdArray = randomElement([null, undefined, 123, true, {}, "", "string not array"]);
break;
}
// Try spawn - we expect it might throw, but should never crash/panic
try {
if (randomBool()) {
// Test Bun.spawn
const proc = spawn({
cmd: cmdArray,
...options,
});
// Sometimes try to interact with the subprocess
if (randomBool() && proc.stdin) {
try {
proc.stdin.write(randomElement(edgeCaseBuffers));
} catch (e) {
// Expected - ignore errors, we're looking for crashes
}
}
if (randomBool() && proc.stdout) {
try {
proc.stdout.cancel();
} catch (e) {
// Expected - ignore errors
}
}
if (randomBool()) {
try {
proc.kill(randomElement([0, 1, 9, 15, -1, 999, undefined]));
} catch (e) {
// Expected - ignore errors
}
}
if (randomBool()) {
try {
proc.ref();
proc.unref();
} catch (e) {
// Expected - ignore errors
}
}
// Clean up - try to kill process if it's still running
try {
if (!proc.killed) {
proc.kill();
}
} catch (e) {
// Ignore
}
} else {
// Test Bun.spawnSync
const result = spawnSync({
cmd: cmdArray,
...options,
});
// Try to access properties
if (randomBool()) {
try {
result.stdout?.toString();
} catch (e) {
// Expected - ignore errors
}
}
if (randomBool()) {
try {
result.stderr?.toString();
} catch (e) {
// Expected - ignore errors
}
}
}
} catch (e) {
// We expect many errors - that's fine
// We're looking for crashes, not errors
// Just make sure the error is an actual Error object
if (!(e instanceof Error) && typeof e !== "string") {
console.error("Unexpected error type:", typeof e, e);
crashCount++;
}
}
// Occasionally trigger GC
if (i % 50 === 0) {
gcTick();
}
} catch (e) {
// Outer catch for anything really unexpected
console.error("Outer catch - unexpected error in iteration", i, e);
crashCount++;
}
}
// If we get here without crashing, the test passed
expect(crashCount).toBe(0);
}, 120000); // 2 minute timeout
test("fuzz spawn with rapid succession", async () => {
// Spawn many processes rapidly to test race conditions
const promises = [];
for (let i = 0; i < 100; i++) {
try {
const proc = spawn({
cmd: [bunExe(), "-e", "console.log('test')"],
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
});
promises.push(
proc.exited.then(() => {
// Clean up
}),
);
// Sometimes kill immediately
if (i % 3 === 0) {
try {
proc.kill();
} catch (e) {
// Ignore
}
}
} catch (e) {
// Expected - some spawns might fail
}
}
// Wait for all to complete
await Promise.allSettled(promises);
gcTick();
}, 30000);
test("fuzz spawn with large stdin/stdout", async () => {
const sizes = [0, 1, 100, 1000, 10000, 100000, 1000000];
for (const size of sizes) {
try {
const data = new Uint8Array(size).fill(65); // Fill with 'A'
const proc = spawn({
cmd: [bunExe(), "-e", "await Bun.stdin.stream().pipeTo(Bun.stdout.stream())"],
stdin: "pipe",
stdout: "pipe",
stderr: "ignore",
});
// Write data
try {
if (proc.stdin) {
proc.stdin.write(data);
proc.stdin.end();
}
} catch (e) {
// Expected - might fail for large sizes
}
// Try to read - might timeout or fail
try {
const reader = proc.stdout.getReader();
const chunks: Uint8Array[] = [];
let totalSize = 0;
const timeout = setTimeout(() => {
try {
proc.kill();
} catch (e) {
// Ignore
}
}, 5000);
try {
while (totalSize < size) {
const { done, value } = await reader.read();
if (done) break;
if (value) {
chunks.push(value);
totalSize += value.length;
}
}
} finally {
clearTimeout(timeout);
reader.releaseLock();
}
} catch (e) {
// Expected - might fail
}
// Clean up
try {
if (!proc.killed) {
proc.kill();
}
} catch (e) {
// Ignore
}
await proc.exited.catch(() => {});
} catch (e) {
// Expected - some tests might fail
}
gcTick();
}
}, 60000);
test("fuzz spawn with invalid file descriptors", async () => {
const invalidFds = [-1, -2, 999, 1000, 65535, 2147483647, -2147483648];
for (const fd of invalidFds) {
try {
const proc = spawn({
cmd: [bunExe(), "--version"],
stdin: fd,
stdout: "pipe",
stderr: "pipe",
});
await proc.exited.catch(() => {});
try {
if (!proc.killed) {
proc.kill();
}
} catch (e) {
// Ignore
}
} catch (e) {
// Expected - these should throw errors, not crash
}
}
gcTick();
});
test("fuzz spawn with unicode and null bytes", async () => {
const weirdStrings = [
"\u0000",
"test\u0000test",
"\uFFFD",
String.fromCharCode(0xd800),
String.fromCharCode(0xdfff),
"🚀🔥💀",
"\x00\x01\x02\x03",
"a".repeat(1000) + "\u0000" + "b".repeat(1000),
];
for (const str of weirdStrings) {
try {
const proc = spawn({
cmd: [bunExe(), "-e", `console.log(${JSON.stringify(str)})`],
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
});
await proc.exited.catch(() => {});
try {
if (!proc.killed) {
proc.kill();
}
} catch (e) {
// Ignore
}
} catch (e) {
// Expected - might fail
}
}
gcTick();
});
test("fuzz spawnSync with various edge cases", () => {
const testCases = [
// Empty cmd
{ cmd: [] },
// Empty strings
{ cmd: ["", "", ""] },
// Very long args
{ cmd: [bunExe(), "-e", "console.log(1)", ..."x".repeat(1000).split("")] },
// Invalid cwd
{ cmd: [bunExe(), "--version"], cwd: "/this/path/definitely/does/not/exist" },
// Null bytes in env
{ cmd: [bunExe(), "--version"], env: { TEST: "value\u0000test" } },
// Large number of env vars
{
cmd: [bunExe(), "--version"],
env: Object.fromEntries(
Array(1000)
.fill(0)
.map((_, i) => [`VAR${i}`, `value${i}`]),
),
},
];
for (const testCase of testCases) {
try {
spawnSync(testCase as any);
} catch (e) {
// Expected - these should throw errors, not crash
}
}
gcTick();
});
test("fuzz spawn with stream operations", async () => {
// Test various stream edge cases
for (let i = 0; i < 50; i++) {
try {
const proc = spawn({
cmd: [bunExe(), "-e", "console.log('test'); console.error('error')"],
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
});
const operations = [
() => proc.stdout.cancel(),
() => proc.stderr.cancel(),
() => proc.kill(),
() => proc.stdout.getReader().cancel(),
() => proc.stderr.getReader().cancel(),
() => {
const reader = proc.stdout.getReader();
reader.releaseLock();
},
];
// Randomly execute operations
const op = operations[Math.floor(Math.random() * operations.length)];
try {
op();
} catch (e) {
// Expected - operations might fail
}
// Clean up
try {
if (!proc.killed) {
proc.kill();
}
} catch (e) {
// Ignore
}
await proc.exited.catch(() => {});
} catch (e) {
// Expected - some tests might fail
}
}
gcTick();
});
test("fuzz spawn kill with various signals", async () => {
const signals: any[] = [
0,
1,
2,
9,
15,
-1,
999,
"SIGTERM",
"SIGKILL",
"SIGINT",
"SIGHUP",
"invalid",
null,
undefined,
NaN,
Infinity,
];
for (const signal of signals) {
try {
const proc = spawn({
cmd: [bunExe(), "-e", "await Bun.sleep(10000)"],
stdout: "ignore",
stderr: "ignore",
stdin: "ignore",
});
await Bun.sleep(10); // Let it start
try {
proc.kill(signal);
} catch (e) {
// Expected - invalid signals should throw
}
await proc.exited.catch(() => {});
} catch (e) {
// Expected - some might fail
}
}
gcTick();
});
});

View File

@@ -1,75 +0,0 @@
import { spawn } from "bun";
import { test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// Test edge cases with environment variables
test("spawn with empty key in env should not hang", async () => {
try {
const proc = spawn({
cmd: [bunExe(), "-e", "console.log('ok')"],
env: { ...bunEnv, "": "empty key" },
stdout: "pipe",
});
await proc.exited;
} catch (e) {
// Expected
}
}, 3000);
test("spawn with null byte in env value should not hang", async () => {
try {
const proc = spawn({
cmd: [bunExe(), "-e", "console.log('ok')"],
env: { ...bunEnv, KEY: "\u0000" },
stdout: "pipe",
});
await proc.exited;
} catch (e) {
// Expected
}
}, 3000);
test("spawn with null byte in env key should not hang", async () => {
try {
const proc = spawn({
cmd: [bunExe(), "-e", "console.log('ok')"],
env: { ...bunEnv, "KEY\u0000": "value" },
stdout: "pipe",
});
await proc.exited;
} catch (e) {
// Expected
}
}, 3000);
test("spawn with unicode in env should not hang", async () => {
try {
const proc = spawn({
cmd: [bunExe(), "-e", "console.log('ok')"],
env: { ...bunEnv, "🚀": "rocket" },
stdout: "pipe",
});
await proc.exited;
} catch (e) {
// Expected
}
}, 3000);
test("spawn with many env vars should not hang", async () => {
try {
const manyEnvVars = Object.fromEntries(
Array(100)
.fill(0)
.map((_, i) => [`K${i}`, `V${i}`]),
);
const proc = spawn({
cmd: [bunExe(), "-e", "console.log('ok')"],
env: { ...bunEnv, ...manyEnvVars },
stdout: "pipe",
});
await proc.exited;
} catch (e) {
// Expected
}
}, 5000);

View File

@@ -1,35 +0,0 @@
import { spawnSync } from "bun";
import { expect, test } from "bun:test";
import { bunExe } from "harness";
// This test reproduces an integer overflow panic in subprocess.zig:949
// When getArgv tries to compute: cmds_array.len + 2
// If cmds_array.len is close to max integer, this overflows
test("spawnSync should not panic on extremely large cmd array", () => {
// The limit is 1024*1024 = 1048576 arguments
// This should throw an error "cmd array is too large", NOT panic
expect(() => {
spawnSync({
cmd: [bunExe(), ...Array(1048577).fill("-e")],
});
}).toThrow(/too large/);
});
test("spawnSync should handle empty cmd array gracefully", () => {
// Empty arrays should also not panic
expect(() => {
spawnSync({
cmd: [],
});
}).toThrow();
});
test("spawnSync should handle array with empty strings", () => {
// Arrays of empty strings should not panic
expect(() => {
spawnSync({
cmd: ["", "", ""],
});
}).toThrow();
});

View File

@@ -1,55 +0,0 @@
import { spawn } from "bun";
import { test } from "bun:test";
import { bunExe } from "harness";
// This test checks for hangs when writing to stdin
test("double stdin.end() should not hang", async () => {
const proc = spawn({
cmd: [bunExe(), "-e", "await Bun.sleep(10)"],
stdin: "pipe",
stdout: "ignore",
});
proc.stdin.end();
proc.stdin.end(); // Second end() - should not hang
await proc.exited;
}, 3000);
test("write after end should not hang", async () => {
const proc = spawn({
cmd: [bunExe(), "-e", "await Bun.sleep(10)"],
stdin: "pipe",
stdout: "ignore",
});
proc.stdin.end();
try {
proc.stdin.write(new Uint8Array(10));
} catch (e) {
// Expected to throw, but should not hang
}
await proc.exited;
}, 3000);
test("write to stdin of short-lived process should not hang", async () => {
const data = new Uint8Array(1000).fill(65);
const proc = spawn({
cmd: [bunExe(), "-e", "console.log('done')"],
stdin: "pipe",
stdout: "ignore",
});
try {
proc.stdin.write(data);
proc.stdin.end();
} catch (e) {
// Might throw if process exits quickly
}
await proc.exited;
}, 3000);

View File

@@ -1,41 +0,0 @@
import { spawnSync } from "bun";
import { test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// Test for hangs in spawnSync
test("spawnSync with null byte in stdin should not hang", () => {
const inputs = [Buffer.from("\u0000"), Buffer.from("test\u0000test"), new Uint8Array([0])];
for (const input of inputs) {
try {
const result = spawnSync({
cmd: [bunExe(), "-e", "console.log('ok')"],
stdin: input,
env: bunEnv,
});
} catch (e) {
// Expected
}
}
}, 5000);
test("spawnSync with empty stdin should not hang", () => {
const result = spawnSync({
cmd: [bunExe(), "-e", "console.log('ok')"],
stdin: new Uint8Array(0),
env: bunEnv,
});
}, 5000);
test("spawnSync with large stdin should not hang", () => {
try {
const result = spawnSync({
cmd: [bunExe(), "-e", "console.log('ok')"],
stdin: new Uint8Array(10000).fill(65),
env: bunEnv,
});
} catch (e) {
// Expected
}
}, 5000);