Skip to content

Fix panic when require.resolve options.paths contains non-absolute paths#32680

Closed
robobun wants to merge 1 commit into
mainfrom
farm/777f74d0/fix-require-paths-non-absolute
Closed

Fix panic when require.resolve options.paths contains non-absolute paths#32680
robobun wants to merge 1 commit into
mainfrom
farm/777f74d0/fix-require-paths-non-absolute

Conversation

@robobun

@robobun robobun commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes a panic in the resolver when require.resolve(), require(), or Module._resolveFilename() is called with options.paths containing a non-absolute path.

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

The resolver passed these user-supplied paths directly to dir_info_cached, which asserts the input is absolute. Since options.paths comes from userland, any relative string (or any non-string value that coerces to a non-absolute string) crashed the process.

Relative entries are now resolved against the top-level dir before lookup, which matches Node.js behavior (Node routes each entry through path.resolve() via Module._nodeModulePaths).

How did you verify your code works?

Added regression tests in test/js/node/module/module-resolve-filename-paths.test.js covering require.resolve and Module._resolveFilename with relative string entries, empty strings, and non-string values. Tests pass in both Bun and Node.js, and crash the unpatched Bun.

The resolver asserted that custom_dir_paths entries passed to
check_package_path were absolute. require.resolve and
Module._resolveFilename pass user-supplied paths directly, so a
relative string (or any value coerced to a non-absolute string)
triggered a panic.

Resolve relative entries against top_level_dir before looking up
dir info, matching how Node.js treats relative entries in
options.paths via path.resolve().
@robobun

robobun commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:38 PM PT - Jun 24th, 2026

@robobun, your commit fa253d5 has 1 failures in Build #64546 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32680

That installs a local version of the PR into your bun-32680 executable, so you can run:

bun-32680 --bun

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f9f60079-12a6-4a6e-a628-1386c6297281

📥 Commits

Reviewing files that changed from the base of the PR and between 942c222 and fa253d5.

📒 Files selected for processing (2)
  • src/resolver/resolver.rs
  • test/js/node/module/module-resolve-filename-paths.test.js

Walkthrough

Resolver::resolve_without_symlinks now normalizes custom package search paths against the current working directory when needed. Tests now cover relative options.paths, MODULE_NOT_FOUND failures, and non-string options.paths entries for require.resolve and Module._resolveFilename.

Changes

Relative options.paths resolution

Layer / File(s) Summary
Normalize custom_dir_paths
src/resolver/resolver.rs
Resolver::resolve_without_symlinks keeps absolute candidates, resolves relative candidates against cwd, skips failed resolutions, and passes the absolute slice to check_package_path.
Cover options.paths cases
test/js/node/module/module-resolve-filename-paths.test.js
Adds require.resolve and Module._resolveFilename cases for relative options.paths, MODULE_NOT_FOUND errors, and non-string options.paths entries.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: preventing a panic from non-absolute options.paths values.
Description check ✅ Passed The description follows the required template and includes both the change summary and verification details.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. require.resolve with paths no longer working correctly (bun 1.2.9 onwards) #19419 - Reports that require.resolve with options.paths stopped working correctly in Bun 1.2.9+; this PR fixes the handling of non-absolute entries in options.paths by resolving them against cwd first, matching Node.js behavior.

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #19419

🤖 Generated with Claude Code

@github-actions

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 - Also fixes the panic in require.resolve/Module._resolveFilename when options.paths contains non-absolute paths, using the same approach of absolutizing relative entries against cwd

🤖 Generated with Claude Code

@robobun

robobun commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as a duplicate of #31566, which fixes the same cannot resolve DirInfo for non-absolute path panic in require.resolve/Module._resolveFilename with a more complete change:

  • handles both custom_dir_paths consumers (package and relative loops), not just the package one
  • uses platform::Auto joining so a Windows drive prefix on POSIX is anchored at cwd rather than passed through (platform::Loose treats C:/foo as absolute on every host, which would still hit the assert on POSIX)
  • includes the IDLSequence<IDLDOMString> refactor requested in review

#31566 has already been through several review rounds and is waiting on a maintainer rerun of two flaky macOS jobs.

@robobun robobun closed this Jun 24, 2026
Comment thread src/resolver/resolver.rs
Comment on lines +2095 to +2107
let custom_slice = if bun_paths::is_absolute(custom_utf8.slice()) {
custom_utf8.slice()
} else {
// Node.js resolves relative entries in `options.paths` against cwd.
abs_buf = bun_paths::path_buffer_pool::get();
match self
.fs_ref()
.abs_buf_checked(&[custom_utf8.slice()], &mut *abs_buf)
{
Some(p) => p,
None => continue,
}
};

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.

🔴 The guard uses native bun_paths::is_absolute (POSIX: leading / only), but the else branch joins via abs_buf_checked, which uses platform::Loose semantics — Loose treats Windows drive paths like C:/foo as absolute on any host and replaces the cwd base, so the joined result is still C:/foo, which then fails the native assert!(bun_paths::is_absolute(...)) in dir_info_cached_maybe_log (resolver.rs:4203). On Linux/macOS, require.resolve("pkg", { paths: ["C:/foo"] }) (or "C:\\foo") still panics after this PR. Since options.paths is arbitrary userland input — the very motivation for this fix — either gate on the Loose predicate so the guard matches the join, or re-check the joined result with the native predicate and continue if it isn't native-absolute.

Extended reasoning...

What the bug is

The new guard at resolver.rs:2095 and the join it falls through to at resolver.rs:2102 use different absolute-path predicates, so there is a class of inputs that fails the guard, gets "resolved" by the join, and yet still fails the downstream native-absolute assertion at resolver.rs:4203. Specifically, on POSIX hosts, a Windows-style drive path in options.paths (e.g. "C:/foo" or "C:\\foo") still panics the process after this PR — the exact failure mode the PR is meant to eliminate.

The code path

  • Guard predicatebun_paths::is_absolute (paths/lib.rs:202) → bun_core::path_sep::is_absolute_native. On #[cfg(not(windows))] this is just p[0] == b'/'. For b"C:/foo", 'C' != '/'false → falls into the else branch.
  • Joinself.fs_ref().abs_buf_checked(&[b"C:/foo"], buf) (resolver/lib.rs:353-355) → join_abs_string_buf_checked::<platform::Loose>(top_level_dir, buf, parts).
  • Inside _join_abs_string_buf (resolve_path.rs:1730) on POSIX:
    • Line 1735: P::P == Loose (not Windows) and cfg!(windows) == false → does not take the Windows-specific impl.
    • Line 1783: P::P.is_absolute(parts[0]) with P::P == Looseis_absolute_windows_t (paths/lib.rs:77-93). For b"C:/foo": len >= 3 && p[1] == ':' && p[2] == '/'true. So cwd is replaced with b"C:/foo" and parts becomes empty (line 1784-1785).
    • Line 1821: leading_separator_index for Loose delegates to Windows first (resolve_path.rs:1322-1324) → matches 'C' in 'A'..='Z' && p[1]==':' && p[2]=='/'Some(2) (resolve_path.rs:1299-1308). leading_len = 3, prefix b"C:/" is preserved.
    • Final result written to buf: b"C:/foo".
  • Downstreamcheck_package_path(b"C:/foo", ...) (resolver.rs:2264) immediately calls self.dir_info_cached(source_dir)dir_info_cached_maybe_log. On POSIX: not ./ / .; len < MAX_PATH_BYTES; the #[cfg(windows)] normalization block (4171-4201) is skipped. Reaches:
    assert!(bun_paths::is_absolute(input_path), ...)  // resolver.rs:4203
    is_absolute_native(b"C:/foo") on POSIX → 'C' != '/'panic. This is assert!, not debug_assert!, so it fires in release builds.

The same trace holds for "C:\\foo" (is_absolute_windows_t accepts p[2] == '\\'; slashes_to_posix_in_place at line 1825 normalizes the prefix to C:/).

Why the existing fix doesn't catch it

The fix assumes abs_buf_checked always returns something native-absolute when given a non-native-absolute input. But abs_buf_checked uses platform::Loose, which intentionally honors Windows drive prefixes on any host (resolve_path.rs:1219, 1322-1324). When a part is Loose-absolute, it replaces the cwd base rather than being appended to it, so the function can return a value that is Loose-absolute but not native-absolute on POSIX. The guard predicate (native) and the join semantics (Loose) simply don't agree.

Step-by-step proof (Linux/macOS)

require.resolve("pkg", { paths: ["C:/foo"] });
  1. custom_utf8.slice() = b"C:/foo".
  2. bun_paths::is_absolute(b"C:/foo")b'C' != b'/'false → else branch.
  3. abs_buf_checked([b"C:/foo"]) → Loose join: Loose.is_absolute(b"C:/foo") == true → cwd ← b"C:/foo", parts ← [] → result b"C:/foo".
  4. check_package_path(b"C:/foo", ...)dir_info_cached(b"C:/foo").
  5. assert!(bun_paths::is_absolute(b"C:/foo"))falsepanic: "cannot resolve DirInfo for non-absolute path: C:/foo".

Impact

Same severity as the bug being fixed: a user-reachable process crash from arbitrary userland input to require.resolve / Module._resolveFilename on Linux and macOS. The PR description states "any relative string ... crashed the process" as the problem to solve; Windows-style drive paths are still in that crash class on POSIX after this change.

How to fix

Two straightforward options:

  1. Match the guard to the join: gate on the same predicate the join uses, so anything Loose considers absolute skips the join — but that just moves the problem (it would pass b"C:/foo" directly). Better:
  2. Verify the output, not the input: after abs_buf_checked returns Some(p), check bun_paths::is_absolute(p) and continue if false (the path can't name a real directory on this host anyway). Equivalently, drop the input guard entirely and always join, then check the result:
    let custom_slice = match self.fs_ref().abs_buf_checked(&[custom_utf8.slice()], &mut *abs_buf) {
        Some(p) if bun_paths::is_absolute(p) => p,
        _ => continue,
    };

It would also be worth adding { paths: ["C:/foo"] } to the "does not crash" regression test (POSIX-only) so this input class is covered.

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