Compare commits

..

3 Commits

Author SHA1 Message Date
Jarred Sumner
10afb1bf15 Deflake fetch.tls.test.ts in asan 2026-02-19 16:38:48 -08:00
Claude Bot
47eee09c4b fix(windows): defer WindowsNamedPipe cleanup to prevent use-after-free in SSL callback chain
WindowsNamedPipe.onClose() was calling this.deinit() synchronously,
which freed the SSL wrapper while still on its call stack. When the
SSL wrapper's triggerCloseCallback() fired from within handleTraffic(),
the close callback chain would deinit the wrapper, then control would
return through the now-freed handleTraffic stack frame.

Fix by deferring all resource cleanup to WindowsNamedPipeContext's
deferred deinit (next tick), which already calls named_pipe.deinit().
In onClose, we only stop reading and clear the timer to prevent
further callbacks while waiting for deferred cleanup.

Also fix a memory leak where the incoming ByteList buffer was never
freed in WindowsNamedPipe.deinit().

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 17:58:57 +00:00
Claude Bot
d4b94ba2c1 Use ReleaseFast instead of ReleaseSafe for Windows builds
Remove the Windows-specific override that forced ReleaseSafe since Bun 1.1.
Windows release builds will now use ReleaseFast like other platforms.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 08:51:21 +00:00
5 changed files with 16 additions and 57 deletions

View File

@@ -39,12 +39,6 @@ else()
unsupported(CMAKE_BUILD_TYPE)
endif()
# Since Bun 1.1, Windows has been built using ReleaseSafe.
# This is because it caught more crashes, but we can reconsider this in the future
if(WIN32 AND DEFAULT_ZIG_OPTIMIZE STREQUAL "ReleaseFast")
set(DEFAULT_ZIG_OPTIMIZE "ReleaseSafe")
endif()
optionx(ZIG_OPTIMIZE "ReleaseFast|ReleaseSafe|ReleaseSmall|Debug" "The Zig optimize level to use" DEFAULT ${DEFAULT_ZIG_OPTIMIZE})
# To use LLVM bitcode from Zig, more work needs to be done. Currently, an install of

View File

@@ -167,8 +167,18 @@ fn onClose(this: *WindowsNamedPipe) void {
log("onClose", .{});
if (!this.flags.is_closed) {
this.flags.is_closed = true; // only call onClose once
// Stop reading and clear the timer to prevent further callbacks,
// but don't call deinit() here. The context (owner) will call
// named_pipe.deinit() when it runs its own deferred deinit.
// Calling deinit() synchronously here causes use-after-free when
// this callback is triggered from within the SSL wrapper's
// handleTraffic() call chain (the wrapper.deinit() frees the SSL
// state while we're still on the wrapper's call stack).
this.setTimeout(0);
if (this.writer.getStream()) |stream| {
_ = stream.readStop();
}
this.handlers.onClose(this.handlers.ctx);
this.deinit();
}
}
@@ -585,6 +595,7 @@ pub fn deinit(this: *WindowsNamedPipe) void {
wrapper.deinit();
this.wrapper = null;
}
this.incoming.clearAndFree(bun.default_allocator);
var ssl_error = this.ssl_error;
ssl_error.deinit();
this.ssl_error = .{};

View File

@@ -289,7 +289,7 @@ pub noinline fn next(this: *Rm) Yield {
}
switch (this.state) {
.done => return this.bltn().done(this.state.done.exit_code),
.done => return this.bltn().done(0),
.err => return this.bltn().done(this.state.err),
else => unreachable,
}
@@ -430,7 +430,7 @@ pub fn onShellRmTaskDone(this: *Rm, task: *ShellRmTask) void {
if (tasks_done >= this.state.exec.total_tasks and
exec.getOutputCount(.output_done) >= exec.getOutputCount(.output_count))
{
this.state = .{ .done = .{ .exit_code = if (exec.err != null) 1 else 0 } };
this.state = .{ .done = .{ .exit_code = if (exec.err) |theerr| theerr.errno else 0 } };
this.next().run();
}
}

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe, tmpdirSync } from "harness";
import { bunEnv, bunExe, isASAN, tmpdirSync } from "harness";
import { join } from "node:path";
import tls from "node:tls";
@@ -263,7 +263,7 @@ describe.concurrent("fetch-tls", () => {
});
const start = performance.now();
const TIMEOUT = 200;
const THRESHOLD = 150;
const THRESHOLD = 150 * (isASAN ? 2 : 1); // ASAN can be very slow, so we need to increase the threshold for it
try {
await fetch(server.url, {

View File

@@ -1,46 +0,0 @@
import { $ } from "bun";
import { describe, expect, test } from "bun:test";
import { tempDir } from "harness";
describe("shell .quiet() should preserve exit codes", () => {
test("builtin rm with .quiet() throws on failure", async () => {
using dir = tempDir("issue-18161", {});
try {
await $`rm ${dir}/nonexistent-file.txt`.quiet();
expect.unreachable();
} catch (e: any) {
expect(e.exitCode).not.toBe(0);
}
});
test("builtin rm with .nothrow().quiet() returns non-zero exit code", async () => {
using dir = tempDir("issue-18161", {});
const result = await $`rm ${dir}/nonexistent-file.txt`.nothrow().quiet();
expect(result.exitCode).not.toBe(0);
});
test("builtin rm with .text() throws on failure", async () => {
using dir = tempDir("issue-18161", {});
try {
await $`rm ${dir}/nonexistent-file.txt`.text();
expect.unreachable();
} catch (e: any) {
expect(e.exitCode).not.toBe(0);
}
});
test("builtin rm with .quiet() returns 0 on success", async () => {
using dir = tempDir("issue-18161", {
"existing-file.txt": "hello",
});
const result = await $`rm ${dir}/existing-file.txt`.nothrow().quiet();
expect(result.exitCode).toBe(0);
});
test("builtin rm exit code matches between quiet and non-quiet", async () => {
using dir = tempDir("issue-18161", {});
const nonQuiet = await $`rm ${dir}/nonexistent-file.txt`.nothrow();
const quiet = await $`rm ${dir}/nonexistent-file.txt`.nothrow().quiet();
expect(quiet.exitCode).toBe(nonQuiet.exitCode);
});
});