shell(mkdir, touch): report operands longer than the path buffers instead of aborting
Merged
RO
3mo ago+164+164+012345678901234567890123456789-8-8-0123456789
Problem
Bun.$mkdir <operand>with a relative operand longer than the join buffer, andtouch <operand>with any operand longer than a path buffer, abort the process:panic: range end index 5004 out of range for slice of length 4094(cwd/tmp, 5000-byte operand; same formkdir -pand for an absolutetouchoperand).mkdirwith an absolute operand that does not fit a path buffer reports the wrong error:mkdir: /aaa...: No such file or directoryinstead ofFile name too long.- Cause, crash:
ShellMkdirTask::run_from_thread_pool(src/runtime/shell/builtin/mkdir.rs) joins a relative operand onto the cwd withresolve_path::join_z, which writes into a fixed 4096-byte thread-local buffer;ShellTouchTask::run_from_thread_pool(touch.rs) joins both kinds of operand withjoin_z_bufinto a stackPathBuffer. Neither join bounds-checks its output (normalize_string_generic_tzinsrc/paths/resolve_path.rs), and the operand comes straight from the user. - Cause, wrong error: mkdir hands the joined path to the node:fs mkdir implementation as a
PathLike. For JS callers,Valid::path_string_length(src/runtime/node/types.rs) rejects paths ofMAX_PATH_BYTESor more up front; for anything longer,PathLike::slice_zreturns""andmkdir("")fails with ENOENT. The shell builds thePathLikeitself and skipped that check.
Fix
- Both builtins join through
join_z_spill, the existing variant that falls back to a caller-ownedVecwhen the parts would not fit (the same helperfs.readdir,Bun.Globand the openrmfix #37521 use). The result is the same normalized path as before for every operand that used to work. mkdirthen reportsENAMETOOLONGitself, naming the path, when the path it is about to create isMAX_PATH_BYTESor longer, i.e. the bound node:fs assumes. For a relative operand that is the joined, normalized path, so a long././.../xspelling is still created; an absolute operand is passed on as written (as before: normalizing it would change what..means across a symlink), so it is bounded as written, which is also what the kernel and coreutils do with such an operand. Both cases are in the test.touchneeds no check of its own: it callsbun_sys::utimens/openwith the string as is, and the OS reportsENAMETOOLONGfor it like for any other operand. It also no longer puts aPathBufferon the worker's stack (about 96 KB on Windows).- Verified with
test/js/bun/shell/commands/mkdir.test.tsandtouch.test.ts(thecommands/directory has one file per builtin; these two had none). Each runs the builtin in a child bun and covers a 5000-byte relative and absolute operand,mkdir -p, a 100000-byte operand (longer than the buffer on Windows too), a long operand next to a normal one that must still be created, and a 6000-byte./spelling that must still succeed (for mkdir also the absolute form of it, which must be refused as written; POSIX only, since on Windows it fits the buffer).USE_SYSTEM_BUN=1 bun test test/js/bun/shell/commands/mkdir.test.ts test/js/bun/shell/commands/touch.test.ts: both fail, the child aborts with the panic above. Without the new mkdir check, the mkdir cases fail on the ENOENT message instead.bun bd test test/js/bun/shell/commands/mkdir.test.ts test/js/bun/shell/commands/touch.test.ts: pass.bunshell.test.ts,file-io.test.ts,commands/rm.test.tspass as well;cargo checkfor the Windows and macOS targets is clean.- Windows: the released build aborts on the 5000-byte relative
mkdiroperand there too (the join buffer is 4096 bytes on every platform), and creates the directory for the absolute././.../xspelling (the branch this change does not touch below 98302 bytes), which is what the Windows side of the test asserts; both checked on a Windows x64 machine. The Windows lanes of this PR's CI runs pass both test files.
- Related open PRs:
rm(#37521),mv(#37527) andcp(#38162) fix the same pattern in their builtins; #38162 addsshell_join_pathhelpers ininterpreter.rsthat touch could switch to in one line once either PR lands. #38002 (empty operands) also createscommands/mkdir.test.tsandtouch.test.ts; whichever lands second appends its test to the other's file.ls -Rjoins real directory entries through the same thread-local buffer, but that needs an on-disk tree within PATH_MAX whose entries join past 4096 bytes rather than a long operand, and is left alone here.
Background
- Shell builtins run in-process;
mkdirandtouchschedule one task per operand on the worker pool. A task either succeeds or stores abun_sys::Error, which the builtin prints as<cmd>: <path>: <coreutils message>and turns into exit code 1 once every task has finished, so one failing operand does not stop the others. - The shell has its own cwd (
$.cwd(),cd), which can differ from the process cwd, so these builtins make relative operands absolute by joining them onto the shell cwd as strings before calling an implementation that takes a path. resolve_path::join_zandjoin_z_bufconcatenate and normalize their parts (.,.., repeated separators) into a fixed buffer without checking that the result fits; the*_spillvariants take aVecto grow into when the unnormalized length would not fit, and otherwise behave identically.PathBufferis[u8; MAX_PATH_BYTES]: 4096 bytes on Linux, 1024 on macOS, 98302 on Windows. The node:fs layer copies every path it receives into one, which is why its JS entry points reject longer paths before calling it.
no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/shell/commands/mkdir.test.ts test/js/bun/shell/commands/touch.test.ts