mirror of
https://github.com/oven-sh/bun
synced 2026-02-05 16:38:55 +00:00
Compare commits
3 Commits
dylan/pyth
...
claude/spa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cc2af38c3 | ||
|
|
f88483d6fb | ||
|
|
36eb01cb25 |
@@ -944,9 +944,6 @@ 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", .{});
|
||||
@@ -956,6 +953,18 @@ 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;
|
||||
|
||||
337
test/js/bun/spawn/spawn-fuzz-continued.test.ts
Normal file
337
test/js/bun/spawn/spawn-fuzz-continued.test.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
568
test/js/bun/spawn/spawn-fuzz.test.ts
Normal file
568
test/js/bun/spawn/spawn-fuzz.test.ts
Normal file
@@ -0,0 +1,568 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
75
test/regression/issue/spawn-env-edge-cases.test.ts
Normal file
75
test/regression/issue/spawn-env-edge-cases.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
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);
|
||||
35
test/regression/issue/spawn-integer-overflow.test.ts
Normal file
35
test/regression/issue/spawn-integer-overflow.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
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();
|
||||
});
|
||||
55
test/regression/issue/spawn-stdin-hang.test.ts
Normal file
55
test/regression/issue/spawn-stdin-hang.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
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);
|
||||
41
test/regression/issue/spawn-sync-hang.test.ts
Normal file
41
test/regression/issue/spawn-sync-hang.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user