Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/install/PackageManager/PackageManagerEnqueue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,13 +596,18 @@ pub fn enqueue_dependency_to_root(
// raw `*mut` — `sleep_until`
// also receives this pointer, so `&mut` here would alias.
manager: *mut PackageManager,
// `sleep_until` ticks the JS event loop, and JS run there can
// swap `manager.log` and leave it pointing at a dead stack
// `Log`. `is_done` re-asserts this snapshot before each poll.
log: *mut bun_ast::Log,
}
impl Closure {
fn is_done(&mut self) -> bool {
// SAFETY: `self.manager` is the raw provenance root set
// below; `sleep_until`/`tick_raw` hold no `&mut` across
// this callback, so this is the unique live borrow.
let manager = unsafe { &mut *self.manager };
manager.log = self.log;
if manager.pending_task_count() > 0 {
// All callbacks void: `VoidRunTasksCallbacks` (below)
// has `Ctx = ()` and every `HAS_* = false`.
Expand Down Expand Up @@ -639,6 +644,7 @@ pub fn enqueue_dependency_to_root(
let mut closure = Closure {
err: None,
manager: mgr,
log: this.log,
};
// SAFETY: `mgr` derived from the live exclusive `this` borrow;
// `sleep_until` + `tick_raw` hold no `&mut PackageManager` across
Expand Down
5 changes: 5 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4734,8 +4734,10 @@ impl VirtualMachine {
// so the `Option` is purely a
// zeroed-init nicety; the `expect` is infallible.
let old_log: NonNull<bun_ast::Log> = jsc_vm.log.expect("vm.log set in init");
let old_transpiler_log: *mut bun_ast::Log = jsc_vm.transpiler.log;
let mut log = bun_ast::Log::default();
jsc_vm.log = NonNull::new(&raw mut log);
jsc_vm.transpiler.log = &raw mut log;
jsc_vm.transpiler.resolver.log = NonNull::from(&mut log);
jsc_vm.transpiler.linker.log = &raw mut log;
if let Some(pm) = jsc_vm.transpiler.resolver.package_manager {
Expand All @@ -4752,13 +4754,15 @@ impl VirtualMachine {
struct RestoreLog {
vm: bun_ptr::BackRef<VirtualMachine>,
old_log: NonNull<bun_ast::Log>,
old_transpiler_log: *mut bun_ast::Log,
}
impl Drop for RestoreLog {
fn drop(&mut self) {
// `vm` is the live per-thread VM (caller is on the JS
// thread); `old_log` outlives the VM (Box::leak in `init`).
let jsc_vm = self.vm.get().as_mut();
jsc_vm.log = Some(self.old_log);
jsc_vm.transpiler.log = self.old_transpiler_log;
jsc_vm.transpiler.resolver.log = self.old_log;
jsc_vm.transpiler.linker.log = self.old_log.as_ptr();
// `_resolve` may have lazily created the PM with
Expand All @@ -4776,6 +4780,7 @@ impl VirtualMachine {
let _restore = RestoreLog {
vm: bun_ptr::BackRef::from(NonNull::new(jsc_vm_ptr).expect("vm non-null")),
old_log,
old_transpiler_log,
};
// Note: reshaped for borrowck — re-derive from raw so the unique
// borrow doesn't span the guard's drop.
Expand Down
65 changes: 65 additions & 0 deletions test/js/bun/resolve/resolve-autoinstall-log-dangling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,68 @@ test("repeated failing auto-install resolves at varying stack depth don't read a
expect(stdout.trim()).toBe("ok");
expect(exitCode).toBe(0);
});

// Auto-install's `sleep_until` ticks the JS event loop while waiting for the
// manifest response. JS that runs during that tick (module transpile) swaps
// `PackageManager.log` (and the related resolver/linker/transpiler log
// pointers) and restores them from `transpiler.log` — which the outer resolve
// hadn't swapped — instead of the resolve's scoped log. The next `run_tasks`
// poll then wrote its 404 diagnostic into the VM's persistent log, which is
// dumped to stderr at process exit. With enough interleaving across REPRL
// iterations this escalated to `pm.log` pointing at dead stack memory (ASAN
// stack-buffer-overflow).
test("module transpile during auto-install's event-loop tick doesn't desync pm.log", async () => {
let hits = 0;
await using server = Bun.serve({
port: 0,
fetch() {
hits++;
return new Response("Not Found", { status: 404 });
},
});

using dir = tempDir("resolve-autoinstall-log-tick", {
"index.js": `
const Module = require("module");
const fs = require("fs");
const path = require("path");

let n = 0;
function load() {
const f = path.join(import.meta.dir, "dyn" + n++ + ".cjs");
fs.writeFileSync(f, "module.exports = 0;");
try { require(f); } catch {}
}

for (let i = 0; i < 20; i++) {
setImmediate(load);
setImmediate(load);
try {
Module._resolveFilename("autoinstall-missing-pkg-" + i, { filename: import.meta.path });
} catch {}
}

console.log("ok " + n);
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "--install=force", "index.js"],
cwd: String(dir),
env: {
...bunEnv,
BUN_CONFIG_REGISTRY: server.url.href,
NPM_CONFIG_REGISTRY: server.url.href,
},
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).toBe("");
expect(stdout).toMatch(/^ok \d+$/m);
expect(Number(stdout.trim().slice(3))).toBeGreaterThan(0);
expect(hits).toBeGreaterThan(0);
expect(exitCode).toBe(0);
});
Loading