Compare commits

...

3 Commits

Author SHA1 Message Date
Dylan Conway
c8b15f6d60 fix(shell): prevent PATH_MAX panic in rm and add test
Add length check before path.join in rm's root-check loop to prevent
index out of bounds panic when path exceeds the fixed-size buffer.
A path exceeding PATH_MAX can't be root, so skip the check and let
the actual operation return ENAMETOOLONG.

Also add rm test and make all PATH_MAX tests concurrent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 03:07:32 +00:00
Dylan Conway
68a542b4b3 fix(shell): move PATH_MAX check before joinZ to prevent panic
The previous fix checked the path length after joinZ(), but joinZ()
itself panics with "index out of bounds" when the combined path exceeds
its fixed-size buffer. Move the check before the joinZ() call.

Add regression tests for touch and mkdir with paths exceeding PATH_MAX.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 02:24:14 +00:00
Dylan Conway
ad429b1a39 fix(shell): return ENAMETOOLONG instead of panicking on paths > PATH_MAX
When `mkdir` or `touch` builtins receive a path longer than 4096 bytes
(MAX_PATH_BYTES), `PathLike.sliceZ` panics with "index out of bounds"
because it uses a fixed-size buffer. Add a length check before calling
into node_fs, returning a proper ENAMETOOLONG error instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:18:19 +00:00
4 changed files with 109 additions and 0 deletions

View File

@@ -235,6 +235,16 @@ pub const ShellMkdirTask = struct {
// implementation for it to work with cwd
const filepath: [:0]const u8 = brk: {
if (ResolvePath.Platform.auto.isAbsolute(this.filepath)) break :brk this.filepath;
// Check combined length before joining to avoid panic in joinZ's fixed-size buffer
if (this.cwd_path.len + this.filepath.len + 1 >= bun.MAX_PATH_BYTES) {
this.err = bun.sys.Error.fromCode(.NAMETOOLONG, .mkdir).withPath(bun.handleOom(bun.default_allocator.dupe(u8, this.filepath))).toShellSystemError();
if (this.event_loop == .js) {
this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
} else {
this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
}
return;
}
const parts: []const []const u8 = &.{
this.cwd_path[0..],
this.filepath[0..],

View File

@@ -185,6 +185,11 @@ pub noinline fn next(this: *Rm) Yield {
for (filepath_args) |filepath| {
const path = filepath[0..bun.len(filepath)];
// Skip root check if combined path exceeds buffer size —
// a path that long can't be root, and the join would panic.
// The actual rm operation will return ENAMETOOLONG.
if (!ResolvePath.Platform.auto.isAbsolute(path) and cwd.len + path.len + 1 >= bun.MAX_PATH_BYTES)
continue;
const resolved_path = if (ResolvePath.Platform.auto.isAbsolute(path)) path else bun.path.join(&[_][]const u8{ cwd, path }, .auto);
const is_root = brk: {
const normalized = bun.path.normalizeString(resolved_path, false, .auto);

View File

@@ -221,6 +221,16 @@ pub const ShellTouchTask = struct {
// We have to give an absolute path
const filepath: [:0]const u8 = brk: {
if (ResolvePath.Platform.auto.isAbsolute(this.filepath)) break :brk this.filepath;
// Check combined length before joining to avoid panic in joinZ's fixed-size buffer
if (this.cwd_path.len + this.filepath.len + 1 >= bun.MAX_PATH_BYTES) {
this.err = bun.sys.Error.fromCode(.NAMETOOLONG, .open).withPath(bun.handleOom(bun.default_allocator.dupe(u8, this.filepath))).toShellSystemError();
if (this.event_loop == .js) {
this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
} else {
this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
}
return;
}
const parts: []const []const u8 = &.{
this.cwd_path[0..],
this.filepath[0..],

View File

@@ -0,0 +1,84 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isPosix } from "harness";
// Regression test: touch, mkdir, and rm with paths exceeding PATH_MAX (4096)
// used to panic with "index out of bounds" in resolve_path.zig.
// After the fix, they return ENAMETOOLONG error instead.
describe.if(isPosix)("builtins with paths exceeding PATH_MAX should not crash", () => {
const longPath = Buffer.alloc(5000, "A").toString();
test.concurrent("touch with path > PATH_MAX returns error", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
import { $ } from "bun";
$.throws(false);
const r = await $\`touch ${longPath}\`;
console.log("exitCode:" + r.exitCode);
`,
],
env: { ...bunEnv, longPath },
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Should print exit code and not crash (exit 132=illegal instruction, 134=abort, 139=segfault)
expect(stdout).toContain("exitCode:1");
expect(stderr).toContain("File name too long");
expect(exitCode).toBe(0);
});
test.concurrent("mkdir with path > PATH_MAX returns error", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
import { $ } from "bun";
$.throws(false);
const r = await $\`mkdir ${longPath}\`;
console.log("exitCode:" + r.exitCode);
`,
],
env: { ...bunEnv, longPath },
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("exitCode:1");
expect(stderr).toContain("File name too long");
expect(exitCode).toBe(0);
});
test.concurrent("rm with path > PATH_MAX does not crash", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
import { $ } from "bun";
$.throws(false);
const r = await $\`rm ${longPath}\`;
console.log("exitCode:" + r.exitCode);
`,
],
env: { ...bunEnv, longPath },
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// rm should not crash (exit 132=illegal instruction). It should exit with an error.
expect(stdout).toContain("exitCode:");
expect(stdout).not.toContain("exitCode:0");
expect(exitCode).toBe(0);
});
});