Skip to content

Fix panic when require "paths" entries are not absolute paths - #33173

Closed
robobun wants to merge 1 commit into
mainfrom
farm/56299602/resolver-relative-paths
Closed

robobun wants to merge 1 commit into
mainfrom
farm/56299602/resolver-relative-paths

Conversation

@robobun

@robobun robobun commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes a panic in the module resolver when the paths option of require(), require.resolve(), or Module._resolveFilename() contains an entry that is not an absolute path:

require.resolve("anything", { paths: ["relative"] });
panic: cannot resolve DirInfo for non-absolute path: relative

Any non-absolute entry hits this: a relative directory name, an empty string, or a non-string value (the array elements are stringified, so paths: [2.2250738585072014e-308] becomes the "path" "2.2250738585072014e-308", which is how the fuzzer found it). The assert is a plain assert!, so release builds abort too.

How did you verify your code works?

The custom_dir_paths loop in resolve_without_symlinks handed the raw user string to check_package_path, which requires an absolute directory (dir_info_cached asserts it). That assert is a real invariant of the directory cache, so it stays. Instead, non-absolute entries are now joined against the top-level directory (cwd) before use. That is what Node does: Module._nodeModulePaths runs each options.paths entry through path.resolve(), so relative entries mean "relative to the current working directory". The sibling loop for relative specifiers already behaved this way via abs_buf_checked.

With this change, require.resolve("pkg", { paths: ["vendor"] }) resolves through $CWD/vendor/node_modules like Node instead of panicking, and garbage entries produce a normal MODULE_NOT_FOUND error.

Added two tests to test/js/bun/resolve/require.test.ts. Both fail with the panic above on an unfixed build (checked against the release build via USE_SYSTEM_BUN=1) and pass with this change:

  • non-absolute paths entries (numbers, "relative", "./relative", "") throw MODULE_NOT_FOUND across require, require.resolve, and Module._resolveFilename
  • a relative paths entry resolves the same module as its absolute spelling

require(), require.resolve(), and Module._resolveFilename() accept an
options.paths array of directories to search. Entries that were not
absolute paths were passed as-is into the directory cache, which asserts
that its input is absolute:

  panic: cannot resolve DirInfo for non-absolute path: relative

Join non-absolute entries against the top-level directory before using
them, matching Node, which resolves options.paths entries with
path.resolve() and therefore treats relative entries as relative to the
current working directory.
@github-actions github-actions Bot added the claude label Jul 1, 2026
@robobun

robobun commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:08 AM PT - Jul 1st, 2026

@robobun, your commit 389f16b is building: #67519

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix crash in require.resolve when options.paths contains invalid entries #31566 - Both fix the same panic when dir_info_cached receives non-absolute paths from options.paths by absolutizing relative entries against cwd in resolver.rs

🤖 Generated with Claude Code

@robobun

robobun commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Confirmed, this is the same panic and the same fix idea as #31566: absolutize non-absolute options.paths entries against cwd before they reach dir_info_cached.

#31566 is further along (it covers both custom_dir_paths loops, converts the paths array through the WebIDL sequence converter on the C++ side, was recently rebased onto main, and is waiting on a maintainer), so I'm closing this one in favor of it. The tests here are a subset of what that PR already covers.

@robobun robobun closed this Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Modifies the resolver's package path resolution to normalize non-absolute entries from options.paths into absolute paths relative to the current working directory before lookup, adding a new scratch buffer field. Adds a test suite validating this behavior, including error handling for invalid paths.

Changes

Relative paths resolution

Layer / File(s) Summary
Resolver path normalization
src/resolver/resolver.rs
Adds custom_dir_path_abs field to Bufs and normalizes non-absolute options.paths entries by joining them with the current working directory into an absolute custom_dir before passing it to check_package_path, replacing the previous raw string passthrough.
Require paths option tests
test/js/bun/resolve/require.test.ts
Adds a describe("require(specifier, { paths })") suite with a temp fixture directory verifying that require.resolve, require, and Module._resolveFilename throw MODULE_NOT_FOUND for non-absolute/invalid paths values, and that relative paths entries resolve correctly against the cwd for vendored packages.

Related PRs: None identified.

Suggested labels: bug, resolver, needs-tests-pass

Suggested reviewers: None identified.

🥕 A path once bare, now anchored true,
From cwd it finds its clue,
No more crashes, errors thrown polite,
MODULE_NOT_FOUND takes flight,
Vendor packages found anew.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: preventing crashes when require paths are not absolute.
Description check ✅ Passed It includes both required sections and explains the fix plus verification, with enough implementation detail.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/resolver/resolver.rs (1)

2097-2109: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use abs_buf_checked for options.paths entries. Long JS-controlled entries can still take the unchecked join path and panic on overflow; the checked helper already returns None instead of crashing resolution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/resolver/resolver.rs` around lines 2097 - 2109, The custom paths handling
in resolver resolution should use the checked absolute-path helper instead of
the unchecked join path. Update the logic in Resolver::check_package_path
processing self.custom_dir_paths so JS-controlled options.paths entries go
through abs_buf_checked, preserving the existing cwd-relative behavior but
returning None on overflow rather than panicking. Ensure the change is applied
in the custom_path/custom_dir branch before calling check_package_path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/resolver/resolver.rs`:
- Around line 2097-2109: The custom paths handling in resolver resolution should
use the checked absolute-path helper instead of the unchecked join path. Update
the logic in Resolver::check_package_path processing self.custom_dir_paths so
JS-controlled options.paths entries go through abs_buf_checked, preserving the
existing cwd-relative behavior but returning None on overflow rather than
panicking. Ensure the change is applied in the custom_path/custom_dir branch
before calling check_package_path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d3d760f8-3042-4196-b3c3-328d18121ba9

📥 Commits

Reviewing files that changed from the base of the PR and between d816daf and 389f16b.

📒 Files selected for processing (2)
  • src/resolver/resolver.rs
  • test/js/bun/resolve/require.test.ts

Comment thread src/resolver/resolver.rs
Comment on lines +2103 to +2108
let custom_dir = if bun_paths::is_absolute(custom_utf8.slice()) {
custom_utf8.slice()
} else {
self.fs_ref()
.abs_buf(&[custom_utf8.slice()], bufs!(custom_dir_path_abs))
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 This trades one user-reachable panic for two others on the same input surface. (1) abs_buf is the unchecked variant — it normalizes into the fixed-size custom_dir_path_abs PathBuffer (~4096 bytes), so require.resolve('x', { paths: ['a'.repeat(1<<20)] }) overflows and panics; the sibling check_relative_path loop the PR description cites as precedent uses abs_buf_checked for exactly this reason (see the doc comment at resolve_path.rs:1697). (2) The gate uses bun_paths::is_absolute (host-native) but abs_buf joins under Platform::Loose, which treats [A-Z]:/... as absolute on every host — so on Linux/macOS, require.resolve('x', { paths: ['C:/foo'] }) has abs_buf return "C:/foo" unchanged and dir_info_cached's assert (resolver.rs:4204) fires with the exact panic this PR set out to fix. Suggested fix: use abs_buf_checked (continue on None) and additionally verify the result is native-absolute before handing it to check_package_path.

Extended reasoning...

What the bug is

The PR replaces the dir_info_cached "cannot resolve DirInfo for non-absolute path" panic with a call to self.fs_ref().abs_buf(...) for non-absolute options.paths entries. Two distinct panics remain reachable from the same user-controlled input surface:

  1. Long input → abs_buf buffer overflow. abs_buf (src/resolver/lib.rs:347-350) wraps join_abs_string_buf::<platform::Loose>, the unchecked variant. _join_abs_string_buf heap-allocates a JoinScratch for the intermediate concatenation, but the final normalize_string_buf at resolve_path.rs:1850-1852 writes into the caller-supplied buf — here bufs!(custom_dir_path_abs), a fixed-size PathBuffer of PATH_MAX_BYTES (~4096 on POSIX). A relative entry longer than that (no .. segments to shrink it) overflows the output slice and panics on the Rust bounds check, in both debug and release.

  2. Windows-style path on POSIX → original dir_info_cached assert. The gate at resolver.rs:2103 uses bun_paths::is_absolute, which on cfg(not(windows)) is exactly p[0] == b'/' (bun_core/lib.rs:317-321). But abs_buf joins under Platform::Loose, whose is_absolute delegates to is_absolute_windows_t and treats X:/... / X:\\... as absolute on every host. When those two predicates disagree, abs_buf discards top_level_dir and returns the input unchanged — which then fails the native-absolute assert downstream.

Step-by-step proof

Case 1 — require.resolve('pkg', { paths: ['a'.repeat(1<<20)] }):

  • custom_dir_paths is populated directly from JS at BunObject.rs:1398-1434 with no per-entry length clamping.
  • resolver.rs:2103: bun_paths::is_absolute(b"aaa...") → 'a' != '/' → false → else branch.
  • resolver.rs:2106-2107: abs_buf(&[b"aaa..."], bufs!(custom_dir_path_abs)) → _join_abs_string_buf::<Loose>. JoinScratch::init (resolve_path.rs:1669-1677) heap-allocates temp_buf large enough to hold cwd + the 1 MiB segment, so the concat step succeeds.
  • resolve_path.rs:1848-1853: buf[..leading_len].copy_from_slice(...) then normalize_string_buf(&temp_buf[..], &mut buf[leading_len..]). buf is ~4096 bytes; the normalized output is ~1 MiB → slice bounds panic.
  • The doc comment on join_abs_string_buf_checked (resolve_path.rs:1697-1701) explicitly says: "Use this when parts may contain user-controlled input of arbitrary length." The sibling check_relative_path loop over the same custom_dir_paths input (resolver.rs:2136-2141) already uses abs_buf_checked and continues on None — the PR description cites that loop as its model but then uses the unchecked variant.

Case 2 — require.resolve('pkg', { paths: ['C:/foo'] }) on Linux/macOS:

  • resolver.rs:2103: bun_paths::is_absolute(b"C:/foo") → is_absolute_native on POSIX → 'C' != '/' → false → else branch.
  • abs_buf → _join_abs_string_buf::<Loose>(top_level_dir, buf, &[b"C:/foo"]). At resolve_path.rs:1748, P::P == Windows || (cfg!(windows) && Loose) is false on POSIX+Loose, so it does not dispatch to the Windows joiner.
  • resolve_path.rs:1796: Platform::Loose.is_absolute(b"C:/foo") → is_absolute_windows_t (paths/lib.rs:77-93): len >= 3 && p[1] == ':' && p[2] == '/' → true. So cwd is replaced with b"C:/foo", top_level_dir is discarded, and parts becomes empty.
  • resolve_path.rs:1834: Platform::Loose.leading_separator_index tries the Windows arm first (line 1322). Lines 1299-1304: 'C' is in 'A'..='Z', p[1] == ':', p[2] == '/' → Some(2). leading_len = 3, leading_buf = b"C:/"; normalized tail is "foo". Result: b"C:/foo" — first byte 'C', not '/'.
  • check_package_path(b"C:/foo", ...) (resolver.rs:2265) → dir_info_cached(b"C:/foo"). Not "."/"./"; len < MAX_PATH_BYTES; the #[cfg(windows)] normalization block is skipped on POSIX; then resolver.rs:4204: assert!(bun_paths::is_absolute(b"C:/foo")) → 'C' != '/' → panic with the exact message this PR set out to fix. This is a plain assert!, so release builds abort.
  • (Lowercase drive letters happen to escape: is_absolute_windows_t has no alphabetic gate so cwd is still replaced, but leading_separator_index's Windows arm gates on 'A'..='Z', so lowercase falls through to the leading_buf[0] = b'/' fallback at resolve_path.rs:1843 and produces a /-prefixed result. Only [A-Z]:/... and [A-Z]:\\... hit the panic — but that is the common form.)

Why existing code doesn't prevent it

Nothing upstream validates options.paths entries for length or shape — BunObject.rs stores the raw BunString slice directly. The new gate checks native absoluteness, but the join it guards uses a different absoluteness predicate (Loose), and the buffer it writes into does not grow. The downstream dir_info_cached assert uses the native predicate again, so anything abs_buf returns that isn't native-absolute still trips it.

Impact

Both are user-reachable process aborts from plain JavaScript on the exact input class the PR title claims to fix ("paths entries are not absolute paths" — neither 'a'.repeat(1<<20) nor 'C:/foo' is an absolute path on POSIX). Per the repo's guidance, "a panic on user input is a DoS" and "fix the whole class in the same PR".

How to fix

Match the sibling loop: use abs_buf_checked and continue on None (handles case 1). Then, because abs_buf's Loose semantics can still hand back a non-native-absolute result, either gate on Platform::Loose.is_absolute() so the predicate matches what abs_buf will do, or — simpler and more robust — check bun_paths::is_absolute(custom_dir) on the result and continue if it's still not native-absolute before calling check_package_path (handles case 2). Both are one-line changes; worth adding ['a'.repeat(1<<20)] and ['C:/foo'] to the pathsValues array in the new test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant