Skip to content

Fix dangling log pointer in PackageManager after resolve#28500

Closed
robobun wants to merge 2 commits into
mainfrom
farm/8b3726dc/fix-dangling-pm-log
Closed

Fix dangling log pointer in PackageManager after resolve#28500
robobun wants to merge 2 commits into
mainfrom
farm/8b3726dc/fix-dangling-pm-log

Conversation

@robobun

@robobun robobun commented Mar 24, 2026

Copy link
Copy Markdown
Collaborator

Root Cause

resolveMaybeNeedsTrailingSlash temporarily swaps resolver.log to a stack-local Log for the duration of module resolution. If getPackageManager() is called during that resolve — initializing the PackageManager singleton for the first time via bun.once — the PM permanently captures the stack-local log pointer in its .log field.

After resolveMaybeNeedsTrailingSlash returns, the stack-local Log is destroyed but PackageManager.log still points to it. Later calls to PM.runTasks that use manager.log.addErrorFmt(...) then read from freed stack memory, causing a stack-buffer-overflow.

The crash was triggered by mock.module(Intl.Segmenter, ...) which stringifies the constructor to "function Segmenter() { [native code] }" and passes it through the resolver, triggering auto-install (and thus PM initialization) inside the poisoned scope.

Fix

Include the package manager's .log in the defer block that restores log pointers after the resolve completes, matching the existing pattern for jsc_vm.log, transpiler.resolver.log, and transpiler.linker.log.

Fingerprint: Address:stack-buffer-overflow:bun-debug+0x900741e


Verification: Fix traced through resolver.zig (getPackageManager at line 532 captures this.log via initWithRuntime) confirming the dangling pointer. Only one log-swap site exists in VirtualMachine.zig. Test exercises the exact PM-init-during-resolve path via mock.module(Intl.Segmenter). CI: Lint JS pass, Buildkite pipeline pass, build in progress with no failures. No TODO/FIXME/HACK in diff. No unrelated changes.

resolveMaybeNeedsTrailingSlash temporarily swaps resolver.log to a
stack-local Log for the duration of the resolve. If getPackageManager()
is called during that resolve (initializing the PM singleton for the
first time), the PM permanently captures the stack-local log pointer.
After the function returns, PM.log is dangling.

Later calls to PM.runTasks that use manager.log then read from freed
stack memory, causing a stack-buffer-overflow detected by ASAN.

Fix: include the package manager in the defer that restores log pointers.
@robobun

robobun commented Mar 24, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:39 AM PT - Mar 24th, 2026

@autofix-ci[bot], your commit 40d548c has 4 failures in Build #41581 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 28500

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

bun-28500 --bun

@coderabbitai

coderabbitai Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

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: 2981abaf-9fa4-4440-8607-46fc898857b5

📥 Commits

Reviewing files that changed from the base of the PR and between 639bc43 and 40d548c.

📒 Files selected for processing (2)
  • src/bun.js/VirtualMachine.zig
  • test/js/bun/test/mock/mock-module-resolve-crash.test.ts

Walkthrough

Fixed a memory safety issue in VirtualMachine.zig where the package manager's log pointer retained a stack-local reference after frame exit. Added corresponding regression test for mock.module with non-string module specifiers.

Changes

Cohort / File(s) Summary
Package Manager Log Restoration
src/bun.js/VirtualMachine.zig
Modified deferred cleanup in resolveMaybeNeedsTrailingSlash to restore jsc_vm.transpiler.resolver.package_manager.log to old_log, preventing dangling pointer references alongside existing log restorations.
Regression Test
test/js/bun/test/mock/mock-module-resolve-crash.test.ts
Added test that spawns Bun to exercise mock.module with non-string module specifiers (e.g., Intl.Segmenter), ensuring process stability without crash.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main fix: addressing a dangling log pointer issue in PackageManager that occurs after module resolution.
Description check ✅ Passed The PR description covers the required sections with comprehensive detail about root cause, fix, and verification, though it goes beyond the basic template structure.

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


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

@claude claude 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.

LGTM — minimal, targeted fix that correctly extends the existing log-pointer restoration pattern to cover the PackageManager.

Extended reasoning...

Overview

This PR modifies resolveMaybeNeedsTrailingSlash in src/bun.js/VirtualMachine.zig to add one additional pointer restoration in the existing defer block, and adds a regression test in test/js/bun/test/mock/mock-module-resolve-crash.test.ts.

Security Risks

None. The change only restores a pointer to a pre-existing valid log object (old_log), and only does so conditionally if the PackageManager was initialized. No new attack surface is introduced.

Level of Scrutiny

Low. The fix is a 4-line addition following an already-established pattern (lines 1840–1842 restore jsc_vm.log, linker.log, and resolver.log; the new code does the same for pm.log). The logic is trivially correct: if the PM was initialized during the resolve, its log pointer must be restored to old_log before the stack-local log is destroyed. The conditional null-check on package_manager is safe and avoids touching the PM when it was not initialized during this call.

Other Factors

The regression test spawns a child process that exercises the exact crash path (mock.module with a non-string specifier) and verifies it exits cleanly with exit code 0. The test structure matches existing patterns in the test suite. No CODEOWNER concerns; this is a narrow VM-level bug fix.

@claude claude 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.

The fix looks correct and follows the established pattern for log pointer restoration, but the CI report shows widespread build-zig failures across multiple platforms that should be investigated before merging.

Extended reasoning...

Overview

This PR adds pm.log = old_log to the existing defer block in resolveMaybeNeedsTrailingSlash (VirtualMachine.zig) that already restores jsc_vm.log, transpiler.linker.log, and transpiler.resolver.log. It also adds a regression test that spawns a subprocess exercising mock.module with a non-string specifier.

Security risks

No security risks introduced. This is a memory safety fix — it prevents use-after-free of a stack-local Log struct that could be captured by the PackageManager singleton if PM is first initialized during a resolve call.

Level of scrutiny

The fix is logically sound and follows an established pattern exactly. The root cause (PM initialized inside the poisoned log-swap scope capturing the stack-local pointer) is well-documented. old_log is the pre-existing VM log pointer, so restoring pm.log = old_log is correct regardless of whether PM was initialized before or during this resolve. However, the robobun CI comment reports build-zig failures across all major platforms (x64, aarch64, musl variants, ASAN, baseline) for the PR's commit. The PR description claims CI is passing, which is contradictory and warrants human verification.

Other factors

No bugs were found by the automated bug hunting system. coderabbitai also raised no actionable concerns. The change is minimal (5 lines of Zig) with a clear, well-reasoned test. The only blocker is resolving whether the CI failures are pre-existing infrastructure noise or were introduced by this change.

Jarred-Sumner pushed a commit that referenced this pull request Apr 17, 2026
## Problem

Fuzzilli hit a flaky SIGSEGV (fingerprint `2519cad1804eace1`) from:

```js
const v13 = Bun.jest().vi;
try { v13.mock("function f2() {\n    const v6 = new ArrayBuffer();\n    ...\n}"); } catch (e) {}
Bun.gc(true);
```

`JSMock__jsModuleMock` calls `Bun__resolveSyncWithSource` on the
specifier before validating the callback, which sends the garbage string
through the resolver. The resolver's auto-install gate at
`loadNodeModules` only checks `esm_ != null`; `ESModule.Package.parse`
accepts anything that doesn't start with `.` or contain `\` / `%`, so
the whole function source is treated as a package name.
`enqueueDependencyToRoot` then calls `PackageManager.sleepUntil`, which
re-enters `EventLoop.tick()` from inside a call that is itself running
inside an event-loop tick:

```
#0 ConcurrentTask.PackedNextPtr.atomicLoadPtr
#1 UnboundedQueue(ConcurrentTask).popBatch
#3 event_loop.tickConcurrentWithCount
#7 AnyEventLoop.tick
#8 PackageManager.sleepUntil
#9 PackageManager.enqueueDependencyToRoot
#10 Resolver.resolveAndAutoInstall
#16 Bun__resolveSyncWithSource
#17 JSMock__jsModuleMock
```

The same path is reachable from `Bun.resolveSync`, `import()`, and
`require.resolve` with any user-provided string.

## Fix

Gate the auto-install branch on `strings.isNPMPackageName(esm_.?.name)`.
That validator already exists and is used by `bun link`, `bun pm view`,
and the bundler; it rejects newlines, spaces, braces, and anything else
that could never be a registry package. Specifiers failing the check
fall straight through to `.not_found` — the same result the registry
fetch would eventually produce — without initializing the package
manager or ticking the event loop.

This is a resolver-level fix, so it covers every entry point (not just
`mock.module`). It also avoids spurious network requests for garbage
specifiers; on this container a single resolve of a multi-line specifier
dropped from ~275ms to ~16ms.

## Tests

- `test/js/bun/resolve/resolve-autoinstall-invalid-name.test.ts` stands
up a local registry and verifies zero manifest requests for a set of
invalid names with `--install=force`, plus a positive control that a
valid name still hits the registry.
- `test/js/bun/test/mock/mock-module-non-string.test.ts` gains a case
for `mock.module` with newline / whitespace / bracket specifiers (with
and without a callback).
- Existing `test/cli/run/run-autoinstall.test.ts` (11 tests) and
`test/js/bun/test/mock/mock-module.test.ts` all pass.

Related: #28945, #28956, #28500, #28511.
Fingerprint: `2519cad1804eace1`
@robobun

robobun commented Apr 21, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #28511 which landed the same fix.

@robobun robobun closed this Apr 21, 2026
structwafel pushed a commit to structwafel/bun that referenced this pull request Apr 25, 2026
)

## Problem

Fuzzilli hit a flaky SIGSEGV (fingerprint `2519cad1804eace1`) from:

```js
const v13 = Bun.jest().vi;
try { v13.mock("function f2() {\n    const v6 = new ArrayBuffer();\n    ...\n}"); } catch (e) {}
Bun.gc(true);
```

`JSMock__jsModuleMock` calls `Bun__resolveSyncWithSource` on the
specifier before validating the callback, which sends the garbage string
through the resolver. The resolver's auto-install gate at
`loadNodeModules` only checks `esm_ != null`; `ESModule.Package.parse`
accepts anything that doesn't start with `.` or contain `\` / `%`, so
the whole function source is treated as a package name.
`enqueueDependencyToRoot` then calls `PackageManager.sleepUntil`, which
re-enters `EventLoop.tick()` from inside a call that is itself running
inside an event-loop tick:

```
#0 ConcurrentTask.PackedNextPtr.atomicLoadPtr
#1 UnboundedQueue(ConcurrentTask).popBatch
#3 event_loop.tickConcurrentWithCount
oven-sh#7 AnyEventLoop.tick
oven-sh#8 PackageManager.sleepUntil
oven-sh#9 PackageManager.enqueueDependencyToRoot
oven-sh#10 Resolver.resolveAndAutoInstall
oven-sh#16 Bun__resolveSyncWithSource
oven-sh#17 JSMock__jsModuleMock
```

The same path is reachable from `Bun.resolveSync`, `import()`, and
`require.resolve` with any user-provided string.

## Fix

Gate the auto-install branch on `strings.isNPMPackageName(esm_.?.name)`.
That validator already exists and is used by `bun link`, `bun pm view`,
and the bundler; it rejects newlines, spaces, braces, and anything else
that could never be a registry package. Specifiers failing the check
fall straight through to `.not_found` — the same result the registry
fetch would eventually produce — without initializing the package
manager or ticking the event loop.

This is a resolver-level fix, so it covers every entry point (not just
`mock.module`). It also avoids spurious network requests for garbage
specifiers; on this container a single resolve of a multi-line specifier
dropped from ~275ms to ~16ms.

## Tests

- `test/js/bun/resolve/resolve-autoinstall-invalid-name.test.ts` stands
up a local registry and verifies zero manifest requests for a set of
invalid names with `--install=force`, plus a positive control that a
valid name still hits the registry.
- `test/js/bun/test/mock/mock-module-non-string.test.ts` gains a case
for `mock.module` with newline / whitespace / bracket specifiers (with
and without a callback).
- Existing `test/cli/run/run-autoinstall.test.ts` (11 tests) and
`test/js/bun/test/mock/mock-module.test.ts` all pass.

Related: oven-sh#28945, oven-sh#28956, oven-sh#28500, oven-sh#28511.
Fingerprint: `2519cad1804eace1`
xhjkl pushed a commit to xhjkl/bun that referenced this pull request May 14, 2026
)

## Problem

Fuzzilli hit a flaky SIGSEGV (fingerprint `2519cad1804eace1`) from:

```js
const v13 = Bun.jest().vi;
try { v13.mock("function f2() {\n    const v6 = new ArrayBuffer();\n    ...\n}"); } catch (e) {}
Bun.gc(true);
```

`JSMock__jsModuleMock` calls `Bun__resolveSyncWithSource` on the
specifier before validating the callback, which sends the garbage string
through the resolver. The resolver's auto-install gate at
`loadNodeModules` only checks `esm_ != null`; `ESModule.Package.parse`
accepts anything that doesn't start with `.` or contain `\` / `%`, so
the whole function source is treated as a package name.
`enqueueDependencyToRoot` then calls `PackageManager.sleepUntil`, which
re-enters `EventLoop.tick()` from inside a call that is itself running
inside an event-loop tick:

```
#0 ConcurrentTask.PackedNextPtr.atomicLoadPtr
oven-sh#1 UnboundedQueue(ConcurrentTask).popBatch
oven-sh#3 event_loop.tickConcurrentWithCount
oven-sh#7 AnyEventLoop.tick
oven-sh#8 PackageManager.sleepUntil
oven-sh#9 PackageManager.enqueueDependencyToRoot
oven-sh#10 Resolver.resolveAndAutoInstall
oven-sh#16 Bun__resolveSyncWithSource
oven-sh#17 JSMock__jsModuleMock
```

The same path is reachable from `Bun.resolveSync`, `import()`, and
`require.resolve` with any user-provided string.

## Fix

Gate the auto-install branch on `strings.isNPMPackageName(esm_.?.name)`.
That validator already exists and is used by `bun link`, `bun pm view`,
and the bundler; it rejects newlines, spaces, braces, and anything else
that could never be a registry package. Specifiers failing the check
fall straight through to `.not_found` — the same result the registry
fetch would eventually produce — without initializing the package
manager or ticking the event loop.

This is a resolver-level fix, so it covers every entry point (not just
`mock.module`). It also avoids spurious network requests for garbage
specifiers; on this container a single resolve of a multi-line specifier
dropped from ~275ms to ~16ms.

## Tests

- `test/js/bun/resolve/resolve-autoinstall-invalid-name.test.ts` stands
up a local registry and verifies zero manifest requests for a set of
invalid names with `--install=force`, plus a positive control that a
valid name still hits the registry.
- `test/js/bun/test/mock/mock-module-non-string.test.ts` gains a case
for `mock.module` with newline / whitespace / bracket specifiers (with
and without a callback).
- Existing `test/cli/run/run-autoinstall.test.ts` (11 tests) and
`test/js/bun/test/mock/mock-module.test.ts` all pass.

Related: oven-sh#28945, oven-sh#28956, oven-sh#28500, oven-sh#28511.
Fingerprint: `2519cad1804eace1`
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