mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 14:22:01 +00:00
Compare commits
3 Commits
claude/fix
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f39a1859d | ||
|
|
70b354aa04 | ||
|
|
9d5a800c3d |
@@ -4,7 +4,7 @@ const TimerObjectInternals = @This();
|
||||
/// Identifier for this timer that is exposed to JavaScript (by `+timer`)
|
||||
id: i32 = -1,
|
||||
interval: u31 = 0,
|
||||
strong_this: jsc.Strong.Optional = .empty,
|
||||
this_value: jsc.JSRef = .empty(),
|
||||
flags: Flags = .{},
|
||||
|
||||
/// Used by:
|
||||
@@ -76,31 +76,41 @@ pub fn runImmediateTask(this: *TimerObjectInternals, vm: *VirtualMachine) bool {
|
||||
// loop alive other than setImmediates
|
||||
(!this.flags.is_keeping_event_loop_alive and !vm.isEventLoopAliveExcludingImmediates()))
|
||||
{
|
||||
this.setEnableKeepingEventLoopAlive(vm, false);
|
||||
this.this_value.downgrade();
|
||||
this.deref();
|
||||
return false;
|
||||
}
|
||||
|
||||
const timer = this.strong_this.get() orelse {
|
||||
const timer = this.this_value.tryGet() orelse {
|
||||
if (Environment.isDebug) {
|
||||
@panic("TimerObjectInternals.runImmediateTask: this_object is null");
|
||||
}
|
||||
this.setEnableKeepingEventLoopAlive(vm, false);
|
||||
this.deref();
|
||||
return false;
|
||||
};
|
||||
const globalThis = vm.global;
|
||||
this.strong_this.deinit();
|
||||
this.this_value.downgrade();
|
||||
this.eventLoopTimer().state = .FIRED;
|
||||
this.setEnableKeepingEventLoopAlive(vm, false);
|
||||
timer.ensureStillAlive();
|
||||
|
||||
vm.eventLoop().enter();
|
||||
const callback = ImmediateObject.js.callbackGetCached(timer).?;
|
||||
const arguments = ImmediateObject.js.argumentsGetCached(timer).?;
|
||||
this.ref();
|
||||
const exception_thrown = this.run(globalThis, timer, callback, arguments, this.asyncID(), vm);
|
||||
this.deref();
|
||||
|
||||
if (this.eventLoopTimer().state == .FIRED) {
|
||||
this.deref();
|
||||
}
|
||||
const exception_thrown = brk: {
|
||||
this.ref();
|
||||
defer {
|
||||
if (this.eventLoopTimer().state == .FIRED) {
|
||||
this.deref();
|
||||
}
|
||||
this.deref();
|
||||
}
|
||||
break :brk this.run(globalThis, timer, callback, arguments, this.asyncID(), vm);
|
||||
};
|
||||
// --- after this point, the timer is no longer guaranteed to be alive ---
|
||||
|
||||
vm.eventLoop().exitMaybeDrainMicrotasks(!exception_thrown) catch return true;
|
||||
|
||||
@@ -120,7 +130,13 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
|
||||
this.eventLoopTimer().state = .FIRED;
|
||||
|
||||
const globalThis = vm.global;
|
||||
const this_object = this.strong_this.get().?;
|
||||
const this_object = this.this_value.tryGet() orelse {
|
||||
this.setEnableKeepingEventLoopAlive(vm, false);
|
||||
this.flags.has_cleared_timer = true;
|
||||
this.this_value.downgrade();
|
||||
this.deref();
|
||||
return;
|
||||
};
|
||||
|
||||
const callback: JSValue, const arguments: JSValue, var idle_timeout: JSValue, var repeat: JSValue = switch (kind) {
|
||||
.setImmediate => .{
|
||||
@@ -143,7 +159,7 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
|
||||
}
|
||||
this.setEnableKeepingEventLoopAlive(vm, false);
|
||||
this.flags.has_cleared_timer = true;
|
||||
this.strong_this.deinit();
|
||||
this.this_value.downgrade();
|
||||
this.deref();
|
||||
|
||||
return;
|
||||
@@ -152,7 +168,7 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
|
||||
var time_before_call: timespec = undefined;
|
||||
|
||||
if (kind != .setInterval) {
|
||||
this.strong_this.clearWithoutDeallocation();
|
||||
this.this_value.downgrade();
|
||||
} else {
|
||||
time_before_call = timespec.msFromNow(.allow_mocked_time, this.interval);
|
||||
}
|
||||
@@ -239,7 +255,7 @@ fn convertToInterval(this: *TimerObjectInternals, global: *JSGlobalObject, timer
|
||||
|
||||
// https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L613
|
||||
TimeoutObject.js.idleTimeoutSetCached(timer, global, repeat);
|
||||
this.strong_this.set(global, timer);
|
||||
this.this_value.setStrong(timer, global);
|
||||
this.flags.kind = .setInterval;
|
||||
this.interval = new_interval;
|
||||
this.reschedule(timer, vm, global);
|
||||
@@ -297,7 +313,7 @@ pub fn init(
|
||||
this.reschedule(timer, vm, global);
|
||||
}
|
||||
|
||||
this.strong_this.set(global, timer);
|
||||
this.this_value.setStrong(timer, global);
|
||||
}
|
||||
|
||||
pub fn doRef(this: *TimerObjectInternals, _: *jsc.JSGlobalObject, this_value: JSValue) JSValue {
|
||||
@@ -327,7 +343,7 @@ pub fn doRefresh(this: *TimerObjectInternals, globalObject: *jsc.JSGlobalObject,
|
||||
return this_value;
|
||||
}
|
||||
|
||||
this.strong_this.set(globalObject, this_value);
|
||||
this.this_value.setStrong(this_value, globalObject);
|
||||
this.reschedule(this_value, VirtualMachine.get(), globalObject);
|
||||
|
||||
return this_value;
|
||||
@@ -350,12 +366,18 @@ pub fn cancel(this: *TimerObjectInternals, vm: *VirtualMachine) void {
|
||||
this.setEnableKeepingEventLoopAlive(vm, false);
|
||||
this.flags.has_cleared_timer = true;
|
||||
|
||||
if (this.flags.kind == .setImmediate) return;
|
||||
if (this.flags.kind == .setImmediate) {
|
||||
// Release the strong reference so the GC can collect the JS object.
|
||||
// The immediate task is still in the event loop queue and will be skipped
|
||||
// by runImmediateTask when it sees has_cleared_timer == true.
|
||||
this.this_value.downgrade();
|
||||
return;
|
||||
}
|
||||
|
||||
const was_active = this.eventLoopTimer().state == .ACTIVE;
|
||||
|
||||
this.eventLoopTimer().state = .CANCELLED;
|
||||
this.strong_this.deinit();
|
||||
this.this_value.downgrade();
|
||||
|
||||
if (was_active) {
|
||||
vm.timer.remove(this.eventLoopTimer());
|
||||
@@ -442,12 +464,12 @@ pub fn getDestroyed(this: *TimerObjectInternals) bool {
|
||||
}
|
||||
|
||||
pub fn finalize(this: *TimerObjectInternals) void {
|
||||
this.strong_this.deinit();
|
||||
this.this_value.finalize();
|
||||
this.deref();
|
||||
}
|
||||
|
||||
pub fn deinit(this: *TimerObjectInternals) void {
|
||||
this.strong_this.deinit();
|
||||
this.this_value.deinit();
|
||||
const vm = VirtualMachine.get();
|
||||
const kind = this.flags.kind;
|
||||
|
||||
|
||||
@@ -1087,10 +1087,32 @@ pub const WindowsSpawnOptions = struct {
|
||||
dup2: struct { out: bun.jsc.Subprocess.StdioKind, to: bun.jsc.Subprocess.StdioKind },
|
||||
|
||||
pub fn deinit(this: *const Stdio) void {
|
||||
if (this.* == .buffer) {
|
||||
bun.default_allocator.destroy(this.buffer);
|
||||
switch (this.*) {
|
||||
.buffer => |pipe| closePipeAndDestroy(pipe),
|
||||
.ipc => |pipe| closePipeAndDestroy(pipe),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Close a pipe that may have been initialized with uv_pipe_init.
|
||||
/// After uv_pipe_init, the pipe is registered in the event loop's
|
||||
/// handle_queue. Freeing it without uv_close corrupts the queue's
|
||||
/// linked list, causing segfaults on subsequent handle insertions.
|
||||
pub fn closePipeAndDestroy(pipe: *bun.windows.libuv.Pipe) void {
|
||||
if (pipe.loop == null or pipe.isClosed()) {
|
||||
// Never initialized or already fully closed — safe to free directly.
|
||||
bun.default_allocator.destroy(pipe);
|
||||
} else if (!pipe.isClosing()) {
|
||||
// Initialized and not yet closing — must uv_close to remove from handle queue.
|
||||
pipe.close(&onPipeCloseForDeinit);
|
||||
}
|
||||
// else: isClosing — uv_close was already called, the pending close
|
||||
// callback owns the lifetime.
|
||||
}
|
||||
|
||||
fn onPipeCloseForDeinit(pipe: *bun.windows.libuv.Pipe) callconv(.c) void {
|
||||
bun.default_allocator.destroy(pipe);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn deinit(this: *const WindowsSpawnOptions) void {
|
||||
@@ -1630,8 +1652,9 @@ pub fn spawnProcessWindows(
|
||||
stdio.data.fd = fd_i;
|
||||
},
|
||||
.ipc => |my_pipe| {
|
||||
// ipc option inside stdin, stderr or stdout are not supported
|
||||
bun.default_allocator.destroy(my_pipe);
|
||||
// ipc option inside stdin, stderr or stdout are not supported.
|
||||
// Must close properly since the pipe may have been initialized.
|
||||
WindowsSpawnOptions.Stdio.closePipeAndDestroy(my_pipe);
|
||||
stdio.flags = uv.UV_IGNORE;
|
||||
},
|
||||
.ignore => {
|
||||
|
||||
@@ -235,10 +235,10 @@ pub const Stdio = union(enum) {
|
||||
return .{ .err = .blob_used_as_out };
|
||||
}
|
||||
|
||||
break :brk .{ .buffer = bun.handleOom(bun.default_allocator.create(uv.Pipe)) };
|
||||
break :brk .{ .buffer = createZeroedPipe() };
|
||||
},
|
||||
.ipc => .{ .ipc = bun.handleOom(bun.default_allocator.create(uv.Pipe)) },
|
||||
.capture, .pipe, .array_buffer, .readable_stream => .{ .buffer = bun.handleOom(bun.default_allocator.create(uv.Pipe)) },
|
||||
.ipc => .{ .ipc = createZeroedPipe() },
|
||||
.capture, .pipe, .array_buffer, .readable_stream => .{ .buffer = createZeroedPipe() },
|
||||
.fd => |fd| .{ .pipe = fd },
|
||||
.dup2 => .{ .dup2 = .{ .out = stdio.dup2.out, .to = stdio.dup2.to } },
|
||||
.path => |pathlike| .{ .path = pathlike.slice() },
|
||||
@@ -487,6 +487,15 @@ pub const Stdio = union(enum) {
|
||||
}
|
||||
};
|
||||
|
||||
/// Allocate a zero-initialized uv.Pipe. Zero-init ensures `pipe.loop` is null
|
||||
/// for pipes that were never passed to `uv_pipe_init`, which
|
||||
/// `closePipeAndDestroy` relies on to decide whether `uv_close` is needed.
|
||||
fn createZeroedPipe() *uv.Pipe {
|
||||
const pipe = bun.default_allocator.create(uv.Pipe) catch |err| bun.handleOom(err);
|
||||
pipe.* = std.mem.zeroes(uv.Pipe);
|
||||
return pipe;
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
|
||||
@@ -68,6 +68,16 @@ pub const Chunk = struct {
|
||||
}
|
||||
|
||||
pub fn getCSSChunkForHTML(this: *const Chunk, chunks: []Chunk) ?*Chunk {
|
||||
// Look up the CSS chunk via the JS chunk's css_chunks indices.
|
||||
// This correctly handles deduplicated CSS chunks that are shared
|
||||
// across multiple HTML entry points (see issue #23668).
|
||||
if (this.getJSChunkForHTML(chunks)) |js_chunk| {
|
||||
const css_chunk_indices = js_chunk.content.javascript.css_chunks;
|
||||
if (css_chunk_indices.len > 0) {
|
||||
return &chunks[css_chunk_indices[0]];
|
||||
}
|
||||
}
|
||||
// Fallback: match by entry_point_id for cases without a JS chunk.
|
||||
const entry_point_id = this.entry_point.entry_point_id;
|
||||
for (chunks) |*other| {
|
||||
if (other.content == .css) {
|
||||
|
||||
@@ -22,7 +22,6 @@ pub noinline fn computeChunks(
|
||||
|
||||
const entry_source_indices = this.graph.entry_points.items(.source_index);
|
||||
const css_asts = this.graph.ast.items(.css);
|
||||
const css_chunking = this.options.css_chunking;
|
||||
var html_chunks = bun.StringArrayHashMap(Chunk).init(temp_allocator);
|
||||
const loaders = this.parse_graph.input_files.items(.loader);
|
||||
const ast_targets = this.graph.ast.items(.target);
|
||||
@@ -148,10 +147,11 @@ pub noinline fn computeChunks(
|
||||
if (css_source_indices.len > 0) {
|
||||
const order = this.findImportedFilesInCSSOrder(temp_allocator, css_source_indices.slice());
|
||||
|
||||
const use_content_based_key = css_chunking or has_server_html_imports;
|
||||
const hash_to_use = if (!use_content_based_key)
|
||||
bun.hash(try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len)))
|
||||
else brk: {
|
||||
// Always use content-based hashing for CSS chunk deduplication.
|
||||
// This ensures that when multiple JS entry points import the
|
||||
// same CSS files, they share a single CSS output chunk rather
|
||||
// than producing duplicates that collide on hash-based naming.
|
||||
const hash_to_use = brk: {
|
||||
var hasher = std.hash.Wyhash.init(5);
|
||||
bun.writeAnyToHasher(&hasher, order.len);
|
||||
for (order.slice()) |x| x.hash(&hasher);
|
||||
@@ -322,7 +322,10 @@ pub noinline fn computeChunks(
|
||||
const remapped_css_indexes = try temp_allocator.alloc(u32, css_chunks.count());
|
||||
|
||||
const css_chunk_values = css_chunks.values();
|
||||
for (sorted_css_keys, js_chunks.count()..) |key, sorted_index| {
|
||||
// Use sorted_chunks.len as the starting index because HTML chunks
|
||||
// may be interleaved with JS chunks, so js_chunks.count() would be
|
||||
// incorrect when HTML entry points are present.
|
||||
for (sorted_css_keys, sorted_chunks.len..) |key, sorted_index| {
|
||||
const index = css_chunks.getIndex(key) orelse unreachable;
|
||||
sorted_chunks.appendAssumeCapacity(css_chunk_values[index]);
|
||||
remapped_css_indexes[index] = @intCast(sorted_index);
|
||||
|
||||
@@ -222,7 +222,11 @@ pub const Source = union(enum) {
|
||||
|
||||
switch (pipe.open(fd)) {
|
||||
.err => |err| {
|
||||
bun.default_allocator.destroy(pipe);
|
||||
// The pipe was already registered in the event loop's handle_queue
|
||||
// by uv_pipe_init above. We must call uv_close to properly remove
|
||||
// it from the queue before freeing the memory, otherwise the
|
||||
// handle_queue linked list becomes corrupted (dangling pointers).
|
||||
pipe.close(&onPipeOpenFailClose);
|
||||
return .{
|
||||
.err = err,
|
||||
};
|
||||
@@ -233,6 +237,10 @@ pub const Source = union(enum) {
|
||||
return .{ .result = pipe };
|
||||
}
|
||||
|
||||
fn onPipeOpenFailClose(pipe: *Pipe) callconv(.c) void {
|
||||
bun.default_allocator.destroy(pipe);
|
||||
}
|
||||
|
||||
pub const StdinTTY = struct {
|
||||
var data: uv.uv_tty_t = undefined;
|
||||
var lock: bun.Mutex = .{};
|
||||
|
||||
@@ -766,19 +766,13 @@ pub extern fn napi_type_tag_object(env: napi_env, _: napi_value, _: [*c]const na
|
||||
pub extern fn napi_check_object_type_tag(env: napi_env, _: napi_value, _: [*c]const napi_type_tag, _: *bool) napi_status;
|
||||
|
||||
// do nothing for both of these
|
||||
pub export fn napi_open_callback_scope(env_: napi_env, _: napi_value, _: *anyopaque, _: *anyopaque) napi_status {
|
||||
pub export fn napi_open_callback_scope(_: napi_env, _: napi_value, _: *anyopaque, _: *anyopaque) napi_status {
|
||||
log("napi_open_callback_scope", .{});
|
||||
const env = env_ orelse {
|
||||
return envIsNull();
|
||||
};
|
||||
return env.ok();
|
||||
return @intFromEnum(NapiStatus.ok);
|
||||
}
|
||||
pub export fn napi_close_callback_scope(env_: napi_env, _: *anyopaque) napi_status {
|
||||
pub export fn napi_close_callback_scope(_: napi_env, _: *anyopaque) napi_status {
|
||||
log("napi_close_callback_scope", .{});
|
||||
const env = env_ orelse {
|
||||
return envIsNull();
|
||||
};
|
||||
return env.ok();
|
||||
return @intFromEnum(NapiStatus.ok);
|
||||
}
|
||||
pub extern fn napi_throw(env: napi_env, @"error": napi_value) napi_status;
|
||||
pub extern fn napi_throw_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status;
|
||||
|
||||
@@ -843,4 +843,60 @@ body {
|
||||
api.expectFile("out/" + jsFile).toContain("sourceMappingURL");
|
||||
},
|
||||
});
|
||||
|
||||
// Test that multiple HTML entrypoints sharing the same CSS file both get
|
||||
// the CSS link tag in production mode (css_chunking deduplication).
|
||||
// Regression test for https://github.com/oven-sh/bun/issues/23668
|
||||
itBundled("html/SharedCSSProductionMultipleEntries", {
|
||||
outdir: "out/",
|
||||
production: true,
|
||||
files: {
|
||||
"/entry1.html": `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./global.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./main1.tsx"></script>
|
||||
</body>
|
||||
</html>`,
|
||||
"/entry2.html": `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./global.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./main2.tsx"></script>
|
||||
</body>
|
||||
</html>`,
|
||||
"/global.css": `h1 { font-size: 24px; }`,
|
||||
"/main1.tsx": `console.log("entry1");`,
|
||||
"/main2.tsx": `console.log("entry2");`,
|
||||
},
|
||||
entryPoints: ["/entry1.html", "/entry2.html"],
|
||||
onAfterBundle(api) {
|
||||
const entry1Html = api.readFile("out/entry1.html");
|
||||
const entry2Html = api.readFile("out/entry2.html");
|
||||
|
||||
// Both HTML files must contain a CSS link tag
|
||||
const cssMatch1 = entry1Html.match(/href="(.*\.css)"/);
|
||||
const cssMatch2 = entry2Html.match(/href="(.*\.css)"/);
|
||||
|
||||
expect(cssMatch1).not.toBeNull();
|
||||
expect(cssMatch2).not.toBeNull();
|
||||
|
||||
// Both should reference the same deduplicated CSS chunk
|
||||
expect(cssMatch1![1]).toBe(cssMatch2![1]);
|
||||
|
||||
// The CSS file should contain the shared styles
|
||||
const cssContent = api.readFile("out/" + cssMatch1![1]);
|
||||
expect(cssContent).toContain("font-size");
|
||||
|
||||
// Both HTML files should also have their respective JS bundles
|
||||
expect(entry1Html).toMatch(/src=".*\.js"/);
|
||||
expect(entry2Html).toMatch(/src=".*\.js"/);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2150,10 +2150,7 @@ c {
|
||||
toplevel-tilde.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
|
||||
`, */
|
||||
});
|
||||
// TODO: Bun's bundler doesn't support multiple entry points generating CSS outputs
|
||||
// with identical content hashes to the same output path. This test exposes that
|
||||
// limitation. Skip until the bundler can deduplicate or handle this case.
|
||||
itBundled.skip("css/MetafileCSSBundleTwoToOne", {
|
||||
itBundled("css/MetafileCSSBundleTwoToOne", {
|
||||
files: {
|
||||
"/foo/entry.js": /* js */ `
|
||||
import '../common.css'
|
||||
|
||||
38
test/regression/issue/27063.test.ts
Normal file
38
test/regression/issue/27063.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe } from "harness";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/27063
|
||||
// On Windows, when Bun.spawn fails (e.g., ENOENT for a nonexistent executable),
|
||||
// pipes initialized with uv_pipe_init were freed without calling uv_close first.
|
||||
// This corrupted libuv's internal handle_queue linked list, causing segfaults
|
||||
// on subsequent spawn calls.
|
||||
|
||||
test("spawning nonexistent executables repeatedly does not crash", async () => {
|
||||
// Spawn a nonexistent executable multiple times. Before the fix, on Windows
|
||||
// this would corrupt the libuv handle queue and crash on a subsequent spawn.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["this-executable-does-not-exist-27063"],
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
await proc.exited;
|
||||
} catch {
|
||||
// Expected to fail - we're testing that it doesn't crash
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here without crashing, the handle queue is intact.
|
||||
// Verify a valid spawn still works after the failed ones.
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", "console.log('ok')"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
|
||||
expect(stdout.trim()).toBe("ok");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user