install: report ENAMETOOLONG for a --cwd value that does not fit the path buffer
Closed
RO
2mo ago+82+82+01234567890123456789-31-31-01234567890123456789
Problem
- Every package manager subcommand (
bun install,add,remove,update,pm, ...) aborts when its--cwdvalue is at leastPATH_MAXbytes long (4096 on Linux, 1024 on macOS), instead of printing an error:
(exit 134). A value one byte shorter already printed$ bun install --cwd "$(head -c 5000 /dev/zero | tr '\0' a)" panic: range end index 5000 out of range for slice of length 4096 $ bun install --cwd "./$(head -c 5000 /dev/zero | tr '\0' a)" panic: range end index 5014 out of range for slice of length 4095 $ bun install --cwd "$(head -c 4096 /dev/zero | tr '\0' a)" panic: index out of bounds: the len is 4096 but the index is 4096error: failed to change directory to "...": ENAMETOOLONGand exited 1. - Cause: the
--cwdblock insrc/install/PackageManager/CommandLineArguments.rs(lines 1298-1329 on main) stages the value in aPathBufferof exactlyPATH_MAXbytes with no length check. A value not starting with.is copied in withcopy_from_sliceand then NUL-terminated one byte further; a value starting with.is resolved with the uncheckedjoin_abs_string_buf, which panics inside the normalizer when the result does not fit the buffer. - The runtime's global
bun --cwdflag has the same bug in separate code (src/runtime/cli/Arguments.rs); that one is fixed in #38368. This PR only covers the package manager's own flag.
Fix
- The block moves into
change_directory. The.-prefixed branch resolves withjoin_abs_string_buf_checkedintoPATH_MAX - 1bytes of the buffer; the other branch copies the value only when it leaves that last byte free. Both keep the last byte for the NUL terminatorchdirreads. When the value does not fit, the existingfailed to change directory to "<value>": <errno>message is printed withENAMETOOLONGand the process exits 1, the same as whenchdiritself fails. - Why
ENAMETOOLONGis the right answer rather than some new error:PATH_MAXcounts the terminator, so a path ofPATH_MAXor more bytes is onechdirwould reject withENAMETOOLONGif it could be passed at all. The check only turns away values that could never have been entered, and the user sees the same line either side of the buffer boundary (thePATH_MAX - 1control in the test is the kernel's own rejection, printed through the unchangedchdirerror path). - Why the checked join rather than a length check on the argument in the
.branch: that branch already normalizes (--cwd ./moo/..enters the parent), andjoin_abs_string_buf_checkedfails only when the normalized result does not fit, so that behaviour is kept. Mapping itsNonetoENAMETOOLONGis how the lockfile code already treats it (src/install/lockfile/Package/WorkspaceMap.rs). - Messages for failures that are not about length are unchanged: a missing directory still prints the value as given (
"/nonexistent") or, for a.value, the resolved path ("/tmp/proj/nonexistent"). - Verified with the new
--cwd that does not fit the path bufferblock intest/cli/install/bun-install.test.ts:PATH_MAXbytes,PATH_MAX + 1000bytes, the./form, andbun add, plusPATH_MAX - 1as the kernel-rejected control. On the current release build the four new lengths abort with the panics above and the control passes; with this change all five pass, as does the existingshould handle --cwdtest. - Checked by hand with the debug build that
--cwd moo,./moo,moo/,.,./moo/.., an absolute path,"", a missing directory and./<5000 bytes>/..(normalizes to the cwd and succeeds) behave as before. - The test block is skipped on Windows:
PathBufferthere is about 96 KiB, longer than any command line, so the overflow is unreachable.
Background
PathBufferis a[u8; MAX_PATH_BYTES]scratch buffer, whereMAX_PATH_BYTESis the platformPATH_MAX(4096 on Linux, 1024 on macOS and the BSDs,32767 * 3 + 1on Windows).PATH_MAXincludes the NUL terminator, so the longest path a syscall accepts isPATH_MAX - 1bytes.join_abs_string_buf(cwd, buf, parts)ispath.resolveinto a caller buffer; it assumes the result fits.join_abs_string_buf_checkedis the variant for user-controlled input: it normalizes into heap scratch when needed and returnsNoneonly if the normalized result does not fitbuf.Output::err_generic+Global::crash()is how this argument parser reports every invalid flag value: it printserror: ...and exits 1.