From 5607d4f51102e98941cf3318948de34d55ca7879 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 5 May 2026 18:45:15 +0000 Subject: [PATCH] resolver: free formatted error message after ResolveMessage.create clones it resolveMaybeNeedsTrailingSlash allocPrints the "Cannot find package '...' from '...'" text into the VM arena, then passes it to ResolveMessage.create which immediately msg.clone()s it into a second arena allocation owned by the ResolveMessage. The original was never freed, so every failed require()/import of a bare specifier leaked one string into the same mimalloc heap that ResolveMessage structs are destroyed back into on GC. Same pattern for the NameTooLong fast path (bun.default_allocator) and the empty-log branch of processFetchLog. In a long-running Fuzzilli REPRL process this accumulated across thousands of iterations and surfaced as an intermittent use-after-poison (12-byte read in a poisoned ~280-byte arena block) with the same signature as the issue addressed in 9a2997bf67. Also switch the create() call to use the already-bound jsc_vm.allocator instead of re-fetching VirtualMachine.get().allocator. --- src/jsc/VirtualMachine.zig | 28 ++++++++------------ test/js/bun/resolve/resolve-error.test.ts | 32 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/jsc/VirtualMachine.zig b/src/jsc/VirtualMachine.zig index 461906c6b3a..b4d683920a8 100644 --- a/src/jsc/VirtualMachine.zig +++ b/src/jsc/VirtualMachine.zig @@ -1892,6 +1892,7 @@ pub fn resolveMaybeNeedsTrailingSlash( error.NameTooLong, if (is_esm) .stmt else if (is_user_require_resolve) .require_resolve else .require, ) catch |err| bun.handleOom(err); + defer bun.default_allocator.free(printed); const msg = logger.Msg{ .data = logger.rangeData( null, @@ -1955,6 +1956,8 @@ pub fn resolveMaybeNeedsTrailingSlash( } jsc_vm._resolve(&result, specifier_utf8.slice(), normalizeSource(source_utf8.slice()), is_esm, is_a_file_path) catch |err_| { var err = err_; + var printed: []const u8 = ""; + defer if (printed.len > 0) jsc_vm.allocator.free(printed); const msg: logger.Msg = brk: { const msgs: []logger.Msg = log.msgs.items; @@ -1972,7 +1975,7 @@ pub fn resolveMaybeNeedsTrailingSlash( else .require; - const printed = try bun.api.ResolveMessage.fmt( + printed = try bun.api.ResolveMessage.fmt( jsc_vm.allocator, specifier_utf8.slice(), source_utf8.slice(), @@ -1995,7 +1998,7 @@ pub fn resolveMaybeNeedsTrailingSlash( }; { - res.* = ErrorableString.err(err, (try bun.api.ResolveMessage.create(global, VirtualMachine.get().allocator, msg, source_utf8.slice()))); + res.* = ErrorableString.err(err, (try bun.api.ResolveMessage.create(global, jsc_vm.allocator, msg, source_utf8.slice()))); } return; @@ -2032,21 +2035,12 @@ pub fn drainMicrotasks(this: *VirtualMachine) void { pub fn processFetchLog(globalThis: *JSGlobalObject, specifier: bun.String, referrer: bun.String, log: *logger.Log, ret: *ErrorableResolvedSource, err: anyerror) void { switch (log.msgs.items.len) { 0 => { - const msg: logger.Msg = brk: { - if (err == error.UnexpectedPendingResolution) { - break :brk logger.Msg{ - .data = logger.rangeData( - null, - logger.Range.None, - std.fmt.allocPrint(globalThis.allocator(), "Unexpected pending import in \"{f}\". To automatically install npm packages with Bun, please use an import statement instead of require() or dynamic import().\nThis error can also happen if dependencies import packages which are not referenced anywhere. Worst case, run `bun install` and opt-out of the node_modules folder until we come up with a better way to handle this error.", .{specifier}) catch unreachable, - ), - }; - } - - break :brk logger.Msg{ - .data = logger.rangeData(null, logger.Range.None, std.fmt.allocPrint(globalThis.allocator(), "{s} while building {f}", .{ @errorName(err), specifier }) catch unreachable), - }; - }; + const text = if (err == error.UnexpectedPendingResolution) + std.fmt.allocPrint(globalThis.allocator(), "Unexpected pending import in \"{f}\". To automatically install npm packages with Bun, please use an import statement instead of require() or dynamic import().\nThis error can also happen if dependencies import packages which are not referenced anywhere. Worst case, run `bun install` and opt-out of the node_modules folder until we come up with a better way to handle this error.", .{specifier}) catch unreachable + else + std.fmt.allocPrint(globalThis.allocator(), "{s} while building {f}", .{ @errorName(err), specifier }) catch unreachable; + defer globalThis.allocator().free(text); + const msg: logger.Msg = .{ .data = logger.rangeData(null, logger.Range.None, text) }; { ret.* = ErrorableResolvedSource.err(err, (bun.api.BuildMessage.create(globalThis, globalThis.allocator(), msg) catch |e| globalThis.takeException(e))); } diff --git a/test/js/bun/resolve/resolve-error.test.ts b/test/js/bun/resolve/resolve-error.test.ts index 56a509866f1..1fe4f71e804 100644 --- a/test/js/bun/resolve/resolve-error.test.ts +++ b/test/js/bun/resolve/resolve-error.test.ts @@ -109,6 +109,38 @@ describe("ResolveMessage", () => { } expect().pass(); }); + + it("does not leak the formatted message on the bare-package error path", () => { + // resolveMaybeNeedsTrailingSlash() allocPrints a "Cannot find package + // '...' from '...'" message into the VM arena before handing it to + // ResolveMessage.create(), which immediately clones it. The original + // was never freed, so every failed bare-specifier resolve leaked one + // string into the same mimalloc heap that ResolveMessage structs are + // freed back into. In a long-running Fuzzilli REPRL process this + // surfaced as a flaky use-after-poison in the resolver. + for (let i = 0; i < 50; i++) { + let errs: any[] = []; + for (let j = 0; j < 5; j++) { + try { + require("804"); + } catch (e) { + errs.push(e); + } + } + for (const e of errs) { + void e.message; + void e.code; + void e.specifier; + void e.referrer; + void e.level; + void e.importKind; + void String(e); + } + errs = []; + Bun.gc(true); + } + expect().pass(); + }); }); // These tests reproduce panics where the module resolver wrote past fixed-size