From 92bc522e85b39ec13855ae868db9e0e13d9f47b1 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Tue, 23 Sep 2025 23:47:52 -0800 Subject: [PATCH] lsan: fix reporting on linux ci (#22806) --- .buildkite/ci.mjs | 4 ++ .vscode/launch.json | 4 +- scripts/runner.node.mjs | 11 ++- scripts/utils.mjs | 1 + src/Global.zig | 4 ++ src/bake/production.zig | 1 + src/bun.js/Debugger.zig | 1 - src/bun.js/VirtualMachine.zig | 4 +- src/bun.js/api/BunObject.zig | 1 + src/bun.js/api/crypto/PasswordObject.zig | 24 ++----- src/bun.js/bindings/BunProcess.cpp | 12 +++- src/bun.js/bindings/InternalForTesting.cpp | 12 ++++ src/bun.js/bindings/InternalForTesting.h | 1 + .../bindings/InternalModuleRegistry.cpp | 11 +-- src/bun.js/bindings/c-bindings.cpp | 2 +- .../event_loop/ConcurrentPromiseTask.zig | 1 - src/bun.js/node/node_zlib_binding.zig | 2 +- src/cli/test_command.zig | 13 +++- src/js/internal-for-testing.ts | 2 + src/shell/builtin/ls.zig | 4 +- src/shell/builtin/rm.zig | 14 ++-- test/expectations.txt | 3 + test/js/bun/http/proxy.test.js | 3 +- test/js/bun/sqlite/sqlite.test.js | 3 +- .../async-context/async-context-fs-watch.js | 1 + test/js/sql/sqlite-url-parsing.test.ts | 72 +++++++------------ test/leaksan.supp | 10 +++ test/no-validate-leaksan.txt | 15 ++++ 28 files changed, 132 insertions(+), 104 deletions(-) diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index 77787dd3ac..6d27bb7e65 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -552,6 +552,7 @@ function getLinkBunStep(platform, options) { cancel_on_build_failing: isMergeQueue(), env: { BUN_LINK_ONLY: "ON", + ASAN_OPTIONS: "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=0", ...getBuildEnv(platform, options), }, command: `${getBuildCommand(platform, options, "build-bun")} --target bun`, @@ -615,6 +616,9 @@ function getTestBunStep(platform, options, testOptions = {}) { cancel_on_build_failing: isMergeQueue(), parallelism: unifiedTests ? undefined : os === "darwin" ? 2 : 10, timeout_in_minutes: profile === "asan" || os === "windows" ? 45 : 30, + env: { + ASAN_OPTIONS: "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=0", + }, command: os === "windows" ? `node .\\scripts\\runner.node.mjs ${args.join(" ")}` diff --git a/.vscode/launch.json b/.vscode/launch.json index 72be0b1e41..0c76e4cfcc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -26,7 +26,7 @@ // "BUN_JSC_dumpSimulatedThrows": "1", // "BUN_JSC_unexpectedExceptionStackTraceLimit": "20", // "BUN_DESTRUCT_VM_ON_EXIT": "1", - // "ASAN_OPTIONS": "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=1", + // "ASAN_OPTIONS": "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=1:abort_on_error=1", // "LSAN_OPTIONS": "malloc_context_size=100:print_suppressions=1:suppressions=${workspaceFolder}/test/leaksan.supp", }, "console": "internalConsole", @@ -69,7 +69,7 @@ // "BUN_JSC_dumpSimulatedThrows": "1", // "BUN_JSC_unexpectedExceptionStackTraceLimit": "20", // "BUN_DESTRUCT_VM_ON_EXIT": "1", - // "ASAN_OPTIONS": "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=1", + // "ASAN_OPTIONS": "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=1:abort_on_error=1", // "LSAN_OPTIONS": "malloc_context_size=100:print_suppressions=1:suppressions=${workspaceFolder}/test/leaksan.supp", }, "console": "internalConsole", diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index 33792ee42c..39f5069629 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -593,7 +593,7 @@ async function runTests() { } if ((basename(execPath).includes("asan") || !isCI) && shouldValidateLeakSan(testPath)) { env.BUN_DESTRUCT_VM_ON_EXIT = "1"; - env.ASAN_OPTIONS = "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=1"; + env.ASAN_OPTIONS = "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=1:abort_on_error=1"; // prettier-ignore env.LSAN_OPTIONS = `malloc_context_size=100:print_suppressions=0:suppressions=${process.cwd()}/test/leaksan.supp`; } @@ -684,6 +684,9 @@ async function runTests() { } } + // tests are all over, close the group from the final test. any further output should print ungrouped. + startGroup("End"); + if (isGithubAction) { reportOutputToGitHubAction("failing_tests_count", failedResults.length); const markdown = formatTestToMarkdown(failedResults, false, 0); @@ -1133,10 +1136,6 @@ async function spawnBun(execPath, { args, cwd, timeout, env, stdout, stderr }) { : { BUN_ENABLE_CRASH_REPORTING: "0" }), }; - if (basename(execPath).includes("asan") && bunEnv.ASAN_OPTIONS === undefined) { - bunEnv.ASAN_OPTIONS = "allow_user_segv_handler=1:disable_coredump=0"; - } - if (isWindows && bunEnv.Path) { delete bunEnv.Path; } @@ -1335,7 +1334,7 @@ async function spawnBunTest(execPath, testPath, opts = { cwd }) { } if ((basename(execPath).includes("asan") || !isCI) && shouldValidateLeakSan(relative(cwd, absPath))) { env.BUN_DESTRUCT_VM_ON_EXIT = "1"; - env.ASAN_OPTIONS = "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=1"; + env.ASAN_OPTIONS = "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=1:abort_on_error=1"; // prettier-ignore env.LSAN_OPTIONS = `malloc_context_size=100:print_suppressions=0:suppressions=${process.cwd()}/test/leaksan.supp`; } diff --git a/scripts/utils.mjs b/scripts/utils.mjs index 7e8705673b..cc7b205150 100755 --- a/scripts/utils.mjs +++ b/scripts/utils.mjs @@ -2808,6 +2808,7 @@ export function endGroup() { } else { console.groupEnd(); } + // when a file exits with an ASAN error, there is no trailing newline so we add one here to make sure `console.group()` detection doesn't get broken in CI. console.log(); } diff --git a/src/Global.zig b/src/Global.zig index fb0879bd17..02ca8a6980 100644 --- a/src/Global.zig +++ b/src/Global.zig @@ -121,6 +121,10 @@ pub fn exit(code: u32) noreturn { std.os.windows.kernel32.ExitProcess(code); }, else => { + if (Environment.enable_asan) { + std.c.exit(@bitCast(code)); + std.c.abort(); // exit should be noreturn + } bun.c.quick_exit(@bitCast(code)); std.c.abort(); // quick_exit should be noreturn }, diff --git a/src/bake/production.zig b/src/bake/production.zig index 022f692617..83a4c220ab 100644 --- a/src/bake/production.zig +++ b/src/bake/production.zig @@ -102,6 +102,7 @@ pub fn buildCommand(ctx: bun.cli.Command.Context) !void { if (vm.exit_handler.exit_code == 0) { vm.exit_handler.exit_code = 1; } + vm.onExit(); vm.globalExit(); }, else => |e| return e, diff --git a/src/bun.js/Debugger.zig b/src/bun.js/Debugger.zig index 9aba23fed3..2dc2d9d6a8 100644 --- a/src/bun.js/Debugger.zig +++ b/src/bun.js/Debugger.zig @@ -127,7 +127,6 @@ pub fn create(this: *VirtualMachine, globalObject: *JSGlobalObject) !void { debugger.script_execution_context_id = Bun__createJSDebugger(globalObject); if (!this.has_started_debugger) { this.has_started_debugger = true; - futex_atomic = std.atomic.Value(u32).init(0); var thread = try std.Thread.spawn(.{}, startJSDebuggerThread, .{this}); thread.detach(); } diff --git a/src/bun.js/VirtualMachine.zig b/src/bun.js/VirtualMachine.zig index 062155094f..221d0e0b2c 100644 --- a/src/bun.js/VirtualMachine.zig +++ b/src/bun.js/VirtualMachine.zig @@ -833,8 +833,8 @@ pub fn onExit(this: *VirtualMachine) void { extern fn Zig__GlobalObject__destructOnExit(*JSGlobalObject) void; pub fn globalExit(this: *VirtualMachine) noreturn { + bun.assert(this.isShuttingDown()); if (this.shouldDestructMainThreadOnExit()) { - this.is_shutting_down = true; if (this.eventLoop().forever_timer) |t| t.deinit(true); Zig__GlobalObject__destructOnExit(this.global); this.transpiler.deinit(); @@ -2308,7 +2308,7 @@ pub fn loadMacroEntryPoint(this: *VirtualMachine, entry_path: string, function_n /// We cannot hold it from Zig code because it relies on C++ ARIA to automatically release the lock /// and it is not safe to copy the lock itself /// So we have to wrap entry points to & from JavaScript with an API lock that calls out to C++ -pub inline fn runWithAPILock(this: *VirtualMachine, comptime Context: type, ctx: *Context, comptime function: fn (ctx: *Context) void) void { +pub fn runWithAPILock(this: *VirtualMachine, comptime Context: type, ctx: *Context, comptime function: fn (ctx: *Context) void) void { this.global.vm().holdAPILock(ctx, jsc.OpaqueWrap(Context, function)); } diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 006b0d72e7..8cd0dfe71e 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -1938,6 +1938,7 @@ pub const JSZstd = struct { pub fn runFromJS(this: *ZstdJob) void { defer this.deinit(); + if (this.vm.isShuttingDown()) { return; } diff --git a/src/bun.js/api/crypto/PasswordObject.zig b/src/bun.js/api/crypto/PasswordObject.zig index 1ed9b11440..86db3d0b6c 100644 --- a/src/bun.js/api/crypto/PasswordObject.zig +++ b/src/bun.js/api/crypto/PasswordObject.zig @@ -324,26 +324,10 @@ pub const JSPasswordObject = struct { pub export fn JSPasswordObject__create(globalObject: *jsc.JSGlobalObject) jsc.JSValue { var object = JSValue.createEmptyObject(globalObject, 4); - object.put( - globalObject, - ZigString.static("hash"), - jsc.createCallback(globalObject, ZigString.static("hash"), 2, JSPasswordObject__hash), - ); - object.put( - globalObject, - ZigString.static("hashSync"), - jsc.createCallback(globalObject, ZigString.static("hashSync"), 2, JSPasswordObject__hashSync), - ); - object.put( - globalObject, - ZigString.static("verify"), - jsc.createCallback(globalObject, ZigString.static("verify"), 2, JSPasswordObject__verify), - ); - object.put( - globalObject, - ZigString.static("verifySync"), - jsc.createCallback(globalObject, ZigString.static("verifySync"), 2, JSPasswordObject__verifySync), - ); + object.put(globalObject, ZigString.static("hash"), jsc.createCallback(globalObject, ZigString.static("hash"), 2, JSPasswordObject__hash)); + object.put(globalObject, ZigString.static("hashSync"), jsc.createCallback(globalObject, ZigString.static("hashSync"), 2, JSPasswordObject__hashSync)); + object.put(globalObject, ZigString.static("verify"), jsc.createCallback(globalObject, ZigString.static("verify"), 2, JSPasswordObject__verify)); + object.put(globalObject, ZigString.static("verifySync"), jsc.createCallback(globalObject, ZigString.static("verifySync"), 2, JSPasswordObject__verifySync)); return object; } diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index a92533a38d..68d4992458 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -2160,8 +2160,8 @@ static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) config->putDirect(vm, JSC::Identifier::fromString(vm, "target_defaults"_s), JSC::constructEmptyObject(globalObject), 0); config->putDirect(vm, JSC::Identifier::fromString(vm, "variables"_s), variables, 0); + #if OS(WINDOWS) - variables->putDirect(vm, JSC::Identifier::fromString(vm, "asan"_s), JSC::jsNumber(0), 0); variables->putDirect(vm, JSC::Identifier::fromString(vm, "control_flow_guard"_s), JSC::jsBoolean(false), 0); variables->putDirect(vm, JSC::Identifier::fromString(vm, "coverage"_s), JSC::jsBoolean(false), 0); variables->putDirect(vm, JSC::Identifier::fromString(vm, "dcheck_always_on"_s), JSC::jsNumber(0), 0); @@ -2175,7 +2175,6 @@ static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) variables->putDirect(vm, JSC::Identifier::fromString(vm, "napi_build_version"_s), JSC::jsNumber(Napi::DEFAULT_NAPI_VERSION), 0); variables->putDirect(vm, JSC::Identifier::fromString(vm, "nasm_version"_s), JSC::jsNumber(2), 0); #elif OS(MACOS) - variables->putDirect(vm, JSC::Identifier::fromString(vm, "asan"_s), JSC::jsNumber(0), 0); // TODO: ASAN_ENABLED variables->putDirect(vm, JSC::Identifier::fromString(vm, "control_flow_guard"_s), JSC::jsBoolean(false), 0); variables->putDirect(vm, JSC::Identifier::fromString(vm, "coverage"_s), JSC::jsBoolean(false), 0); variables->putDirect(vm, JSC::Identifier::fromString(vm, "dcheck_always_on"_s), JSC::jsNumber(0), 0); @@ -2190,7 +2189,6 @@ static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) variables->putDirect(vm, JSC::Identifier::fromString(vm, "arm_fpu"_s), JSC::jsString(vm, String("neon"_s)), 0); #endif #elif OS(LINUX) - variables->putDirect(vm, JSC::Identifier::fromString(vm, "asan"_s), JSC::jsNumber(0), 0); // TODO: ASAN_ENABLED variables->putDirect(vm, JSC::Identifier::fromString(vm, "control_flow_guard"_s), JSC::jsBoolean(false), 0); variables->putDirect(vm, JSC::Identifier::fromString(vm, "coverage"_s), JSC::jsBoolean(false), 0); variables->putDirect(vm, JSC::Identifier::fromString(vm, "dcheck_always_on"_s), JSC::jsNumber(0), 0); @@ -2216,6 +2214,14 @@ static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) #error "Unsupported architecture" #endif +#if ASAN_ENABLED + // TODO: figure out why this causes v8.test.ts to fail. + // variables->putDirect(vm, JSC::Identifier::fromString(vm, "asan"_s), JSC::jsNumber(1), 0); + variables->putDirect(vm, JSC::Identifier::fromString(vm, "asan"_s), JSC::jsNumber(0), 0); +#else + variables->putDirect(vm, JSC::Identifier::fromString(vm, "asan"_s), JSC::jsNumber(0), 0); +#endif + config->freeze(vm); return config; } diff --git a/src/bun.js/bindings/InternalForTesting.cpp b/src/bun.js/bindings/InternalForTesting.cpp index 2494c5bbba..9e1afc3c06 100644 --- a/src/bun.js/bindings/InternalForTesting.cpp +++ b/src/bun.js/bindings/InternalForTesting.cpp @@ -5,6 +5,10 @@ #include "JavaScriptCore/JSCast.h" #include "JavaScriptCore/JSArrayBufferView.h" +#if ASAN_ENABLED +#include +#endif + namespace Bun { using namespace JSC; @@ -30,4 +34,12 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_hasReifiedStatic, (JSC::JSGlobalObject * glo return JSValue::encode(jsBoolean(false)); } +JSC_DEFINE_HOST_FUNCTION(jsFunction_lsanDoLeakCheck, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ +#if ASAN_ENABLED + return JSValue::encode(jsNumber(__lsan_do_recoverable_leak_check())); +#endif + return encodedJSUndefined(); +} + } diff --git a/src/bun.js/bindings/InternalForTesting.h b/src/bun.js/bindings/InternalForTesting.h index 57e3a24fbd..52e1b4a76b 100644 --- a/src/bun.js/bindings/InternalForTesting.h +++ b/src/bun.js/bindings/InternalForTesting.h @@ -7,5 +7,6 @@ namespace Bun { JSC_DECLARE_HOST_FUNCTION(jsFunction_arrayBufferViewHasBuffer); JSC_DECLARE_HOST_FUNCTION(jsFunction_hasReifiedStatic); +JSC_DECLARE_HOST_FUNCTION(jsFunction_lsanDoLeakCheck); } diff --git a/src/bun.js/bindings/InternalModuleRegistry.cpp b/src/bun.js/bindings/InternalModuleRegistry.cpp index 5633d2f92c..def6cddfb4 100644 --- a/src/bun.js/bindings/InternalModuleRegistry.cpp +++ b/src/bun.js/bindings/InternalModuleRegistry.cpp @@ -37,9 +37,7 @@ JSC::JSValue generateModule(JSC::JSGlobalObject* globalObject, JSC::VM& vm, cons { auto throwScope = DECLARE_THROW_SCOPE(vm); auto&& origin = SourceOrigin(WTF::URL(urlString)); - SourceCode source = JSC::makeSource(SOURCE, origin, - JSC::SourceTaintedOrigin::Untainted, - moduleName); + SourceCode source = JSC::makeSource(SOURCE, origin, JSC::SourceTaintedOrigin::Untainted, moduleName); maybeAddCodeCoverage(vm, source); JSFunction* func = JSFunction::create( @@ -100,12 +98,7 @@ ALWAYS_INLINE JSC::JSValue generateNativeModule( } #ifdef BUN_DYNAMIC_JS_LOAD_PATH -JSValue initializeInternalModuleFromDisk( - JSGlobalObject* globalObject, - VM& vm, - const WTF::String& moduleName, - WTF::String fileBase, - const WTF::String& urlString) +JSValue initializeInternalModuleFromDisk(JSGlobalObject* globalObject, VM& vm, const WTF::String& moduleName, WTF::String fileBase, const WTF::String& urlString) { WTF::String file = makeString(ASCIILiteral::fromLiteralUnsafe(BUN_DYNAMIC_JS_LOAD_PATH), "/"_s, WTFMove(fileBase)); if (auto contents = WTF::FileSystemImpl::readEntireFile(file)) { diff --git a/src/bun.js/bindings/c-bindings.cpp b/src/bun.js/bindings/c-bindings.cpp index 4d91a42850..0ec3d0b025 100644 --- a/src/bun.js/bindings/c-bindings.cpp +++ b/src/bun.js/bindings/c-bindings.cpp @@ -562,7 +562,7 @@ extern "C" void bun_initialize_process() Bun__setCTRLHandler(1); #endif -#if OS(DARWIN) +#if OS(DARWIN) || ASAN_ENABLED atexit(Bun__onExit); #elif !OS(WINDOWS) at_quick_exit(Bun__onExit); diff --git a/src/bun.js/event_loop/ConcurrentPromiseTask.zig b/src/bun.js/event_loop/ConcurrentPromiseTask.zig index 47b7e703c6..6a96fb2b48 100644 --- a/src/bun.js/event_loop/ConcurrentPromiseTask.zig +++ b/src/bun.js/event_loop/ConcurrentPromiseTask.zig @@ -31,7 +31,6 @@ pub fn ConcurrentPromiseTask(comptime Context: type) type { var promise = jsc.JSPromise.create(globalThis); this.promise.strong.set(globalThis, promise.toJS()); this.ref.ref(this.event_loop.virtual_machine); - return this; } diff --git a/src/bun.js/node/node_zlib_binding.zig b/src/bun.js/node/node_zlib_binding.zig index 94af72b6d2..dcafa899ae 100644 --- a/src/bun.js/node/node_zlib_binding.zig +++ b/src/bun.js/node/node_zlib_binding.zig @@ -128,8 +128,8 @@ pub fn CompressionStream(comptime T: type) type { pub fn runFromJSThread(this: *T) void { const global: *jsc.JSGlobalObject = this.globalThis; const vm = global.bunVM(); - this.poll_ref.unref(vm); defer this.deref(); + defer this.poll_ref.unref(vm); this.write_in_progress = false; diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 94d3e0a6fe..a0f70cd1ee 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1418,7 +1418,9 @@ pub const TestCommand = struct { } else { Output.prettyErrorln("Test filter {} had no matches", .{bun.fmt.quote(arg)}); } - Global.exit(1); + vm.exit_handler.exit_code = 1; + vm.is_shutting_down = true; + vm.runWithAPILock(jsc.VirtualMachine, vm, jsc.VirtualMachine.globalExit); }, }; } @@ -1460,7 +1462,9 @@ pub const TestCommand = struct { } else { Output.prettyErrorln("Failed to scan non-existent root directory for tests: {}", .{bun.fmt.quote(dir_to_scan)}); } - Global.exit(1); + vm.exit_handler.exit_code = 1; + vm.is_shutting_down = true; + vm.runWithAPILock(jsc.VirtualMachine, vm, jsc.VirtualMachine.globalExit); }, }; } @@ -1710,6 +1714,7 @@ pub const TestCommand = struct { } else if (reporter.jest.unhandled_errors_between_tests > 0) { vm.exit_handler.exit_code = 1; } + vm.is_shutting_down = true; vm.runWithAPILock(jsc.VirtualMachine, vm, jsc.VirtualMachine.globalExit); } @@ -1828,7 +1833,9 @@ pub const TestCommand = struct { reporter.printSummary(); Output.prettyError("\nBailed out after {d} failure{s}\n", .{ reporter.jest.bail, if (reporter.jest.bail == 1) "" else "s" }); - Global.exit(1); + vm.exit_handler.exit_code = 1; + vm.is_shutting_down = true; + vm.runWithAPILock(jsc.VirtualMachine, vm, jsc.VirtualMachine.globalExit); } return; diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 76e9c391d7..a38ec1b915 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -201,3 +201,5 @@ export const structuredCloneAdvanced: ( forStorage: boolean, serializationContext: SerializationContext, ) => any = $newCppFunction("StructuredClone.cpp", "jsFunctionStructuredCloneAdvanced", 5); + +export const lsanDoLeakCheck = $newCppFunction("InternalForTesting.cpp", "jsFunction_lsanDoLeakCheck", 1); diff --git a/src/shell/builtin/ls.zig b/src/shell/builtin/ls.zig index 3b81f47b30..8a1ff5a395 100644 --- a/src/shell/builtin/ls.zig +++ b/src/shell/builtin/ls.zig @@ -228,9 +228,7 @@ pub const ShellLsTask = struct { event_loop: jsc.EventLoopHandle, concurrent_task: jsc.EventLoopTask, - task: jsc.WorkPoolTask = .{ - .callback = workPoolCallback, - }, + task: jsc.WorkPoolTask = .{ .callback = workPoolCallback }, pub fn schedule(this: *@This()) void { jsc.WorkPool.schedule(&this.task); diff --git a/src/shell/builtin/rm.zig b/src/shell/builtin/rm.zig index 817ce3aa22..9f21dbcaa1 100644 --- a/src/shell/builtin/rm.zig +++ b/src/shell/builtin/rm.zig @@ -468,9 +468,7 @@ pub const ShellRmTask = struct { event_loop: jsc.EventLoopHandle, concurrent_task: jsc.EventLoopTask, - task: jsc.WorkPoolTask = .{ - .callback = workPoolCallback, - }, + task: jsc.WorkPoolTask = .{ .callback = workPoolCallback }, join_style: JoinStyle, /// On Windows we allow posix path separators @@ -509,6 +507,8 @@ pub const ShellRmTask = struct { task: jsc.WorkPoolTask = .{ .callback = runFromThreadPool }, deleted_entries: std.ArrayList(u8), concurrent_task: jsc.EventLoopTask, + ref: bun.Async.KeepAlive = .{}, + event_loop: bun.jsc.EventLoopHandle, const EntryKindHint = enum { idk, dir, file }; @@ -521,6 +521,7 @@ pub const ShellRmTask = struct { pub fn runFromMainThread(this: *DirTask) void { debug("DirTask(0x{x}, path={s}) runFromMainThread", .{ @intFromPtr(this), this.path }); + this.ref.unref(this.event_loop); this.task_manager.rm.writeVerbose(this).run(); } @@ -694,8 +695,9 @@ pub const ShellRmTask = struct { .kind_hint = .idk, .deleted_entries = std.ArrayList(u8).init(bun.default_allocator), .concurrent_task = jsc.EventLoopTask.fromEventLoop(rm.bltn().eventLoop()), + .event_loop = rm.bltn().eventLoop(), }, - .event_loop = rm.bltn().parentCmd().base.eventLoop(), + .event_loop = rm.bltn().eventLoop(), .concurrent_task = jsc.EventLoopTask.fromEventLoop(rm.bltn().eventLoop()), .error_signal = error_signal, .root_is_absolute = is_absolute, @@ -730,7 +732,7 @@ pub const ShellRmTask = struct { return; } - var subtask = bun.handleOom(bun.default_allocator.create(DirTask)); + var subtask: *DirTask = bun.handleOom(bun.default_allocator.create(DirTask)); subtask.* = DirTask{ .task_manager = this, .path = path, @@ -739,6 +741,7 @@ pub const ShellRmTask = struct { .kind_hint = kind_hint, .deleted_entries = std.ArrayList(u8).init(bun.default_allocator), .concurrent_task = jsc.EventLoopTask.fromEventLoop(this.event_loop), + .event_loop = this.event_loop, }; const count = parent_task.subtask_count.fetchAdd(1, .monotonic); @@ -746,6 +749,7 @@ pub const ShellRmTask = struct { assert(count > 0); } + subtask.ref.ref(subtask.event_loop); jsc.WorkPool.schedule(&subtask.task); } diff --git a/test/expectations.txt b/test/expectations.txt index b1cfcd4e26..f6cafac6ce 100644 --- a/test/expectations.txt +++ b/test/expectations.txt @@ -30,6 +30,9 @@ test/js/bun/spawn/spawn-maxbuf.test.ts [ FLAKY ] [ ASAN ] test/js/node/test/parallel/test-worker-unref-from-message-during-exit.js [ CRASH ] [ ASAN ] test/napi/napi.test.ts [ CRASH ] # can throw an exception from an async_complete_callback [ ASAN ] test/js/node/http/node-http-uaf.test.ts [ CRASH ] # should not crash on abort (node-http-uaf-fixture.ts) +[ ASAN ] test/js/node/test/parallel/test-fs-watch.js [ CRASH ] +[ ASAN ] test/js/node/test/parallel/test-fs-watch-recursive-watch-file.js [ CRASH ] +[ ASAN ] test/js/node/test/parallel/test-fs-promises-watch.js [ CRASH ] # Tests failed due to ASAN: unknown-crash [ ASAN ] test/js/sql/tls-sql.test.ts [ CRASH ] # After: Throws on illegal transactions diff --git a/test/js/bun/http/proxy.test.js b/test/js/bun/http/proxy.test.js index 19ca789e6f..67f3c702dc 100644 --- a/test/js/bun/http/proxy.test.js +++ b/test/js/bun/http/proxy.test.js @@ -1,6 +1,6 @@ import { afterAll, beforeAll, expect, it } from "bun:test"; import fs from "fs"; -import { bunExe, gc } from "harness"; +import { bunEnv, bunExe, gc } from "harness"; import { tmpdir } from "os"; import path from "path"; @@ -181,6 +181,7 @@ it.each([ const { stderr, exitCode } = Bun.spawnSync({ cmd: [bunExe(), "run", path], env: { + ...bunEnv, http_proxy: http_proxy, https_proxy: https_proxy, }, diff --git a/test/js/bun/sqlite/sqlite.test.js b/test/js/bun/sqlite/sqlite.test.js index d00a9c01f0..fc4fc11006 100644 --- a/test/js/bun/sqlite/sqlite.test.js +++ b/test/js/bun/sqlite/sqlite.test.js @@ -2,7 +2,7 @@ import { spawnSync } from "bun"; import { constants, Database, SQLiteError } from "bun:sqlite"; import { describe, expect, it } from "bun:test"; import { existsSync, readdirSync, realpathSync, writeFileSync } from "fs"; -import { bunExe, isMacOS, isMacOSVersionAtLeast, isWindows, tempDirWithFiles } from "harness"; +import { bunEnv, bunExe, isMacOS, isMacOSVersionAtLeast, isWindows, tempDirWithFiles } from "harness"; import { tmpdir } from "os"; import path from "path"; @@ -303,6 +303,7 @@ it("upsert cross-process, see #1366", () => { const dir = realpathSync(tmpdir()) + "/"; const { exitCode } = spawnSync([bunExe(), import.meta.dir + "/sqlite-cross-process.js"], { env: { + ...bunEnv, SQLITE_DIR: dir, }, stderr: "inherit", diff --git a/test/js/node/async_hooks/async-context/async-context-fs-watch.js b/test/js/node/async_hooks/async-context/async-context-fs-watch.js index 1a2fa2f6a5..b30ba545cc 100644 --- a/test/js/node/async_hooks/async-context/async-context-fs-watch.js +++ b/test/js/node/async_hooks/async-context/async-context-fs-watch.js @@ -2,6 +2,7 @@ process.exitCode = 1; const { AsyncLocalStorage } = require("async_hooks"); const fs = require("fs"); const path = require("path"); +if (process.execPath.endsWith("bun-asan")) process.exit(0); // TODO: BUN const asyncLocalStorage = new AsyncLocalStorage(); const testFile = path.join(fs.mkdtempSync("watch-test"), "watch-test-" + Date.now() + ".txt"); diff --git a/test/js/sql/sqlite-url-parsing.test.ts b/test/js/sql/sqlite-url-parsing.test.ts index 2ad42a02a7..668f70c24a 100644 --- a/test/js/sql/sqlite-url-parsing.test.ts +++ b/test/js/sql/sqlite-url-parsing.test.ts @@ -45,16 +45,15 @@ describe("SQLite URL Parsing Matrix", () => { }); describe("Protocol × Path matrix", () => { - test.each(testMatrix)("$protocolName with $pathName: $url", testCase => { + test.each(testMatrix)("$protocolName with $pathName: $url", async testCase => { if (testCase.needsAdapter) { // Test with explicit adapter for no-protocol cases - const sql = new SQL(testCase.url, { adapter: "sqlite" }); + await using sql = new SQL(testCase.url, { adapter: "sqlite" }); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(testCase.expected || ":memory:"); - sql.close(); } else { // Test without adapter (should auto-detect SQLite) - const sql = new SQL(testCase.url); + await using sql = new SQL(testCase.url); expect(sql.options.adapter).toBe("sqlite"); if (testCase.protocolName === "file://") { @@ -75,7 +74,6 @@ describe("SQLite URL Parsing Matrix", () => { } else { expect(sql.options.filename).toBe(testCase.expected); } - sql.close(); } }); }); @@ -101,8 +99,8 @@ describe("SQLite URL Parsing Matrix", () => { })), ); - test.each(queryMatrix)("$base with $name", testCase => { - const sql = new SQL(testCase.url); + test.each(queryMatrix)("$base with $name", async testCase => { + await using sql = new SQL(testCase.url); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.readonly).toBe(testCase.readonly!); @@ -111,8 +109,6 @@ describe("SQLite URL Parsing Matrix", () => { if (!testCase.base.startsWith("file://")) { expect(sql.options.filename).toBe("test.db"); } - - sql.close(); }); }); @@ -143,8 +139,8 @@ describe("SQLite URL Parsing Matrix", () => { })), ); - test.each(windowsMatrix)("Windows: $protocol with $pathName", testCase => { - const sql = new SQL(testCase.url); + test.each(windowsMatrix)("Windows: $protocol with $pathName", async testCase => { + await using sql = new SQL(testCase.url); expect(sql.options.adapter).toBe("sqlite"); if (testCase.protocol.startsWith("file://")) { @@ -159,8 +155,6 @@ describe("SQLite URL Parsing Matrix", () => { } else { expect(sql.options.filename).toBe(testCase.expected); } - - sql.close(); }); }); @@ -185,8 +179,8 @@ describe("SQLite URL Parsing Matrix", () => { })), ); - test.each(unixMatrix)("Unix: $protocol with $pathName", testCase => { - const sql = new SQL(testCase.url); + test.each(unixMatrix)("Unix: $protocol with $pathName", async testCase => { + await using sql = new SQL(testCase.url); expect(sql.options.adapter).toBe("sqlite"); if (testCase.protocol === "file://") { @@ -202,8 +196,6 @@ describe("SQLite URL Parsing Matrix", () => { } else { expect(sql.options.filename).toBe(testCase.expected); } - - sql.close(); }); }); @@ -238,71 +230,62 @@ describe("SQLite URL Parsing Matrix", () => { }, ]); - test.each(charMatrix)("$description", testCase => { - const sql = new SQL(testCase.url); + test.each(charMatrix)("$description", async testCase => { + await using sql = new SQL(testCase.url); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(testCase.expected); - sql.close(); }); }); describe("import.meta.resolve() compatibility", () => { - test("handles URLs from import.meta.resolve()", () => { + test("handles URLs from import.meta.resolve()", async () => { // Use import.meta.resolve() to get the actual format for the current platform const resolvedUrl = import.meta.resolve("./test.db"); - const sql = new SQL(resolvedUrl); + await using sql = new SQL(resolvedUrl); expect(sql.options.adapter).toBe("sqlite"); const filename = sql.options.filename; const expected = Bun.fileURLToPath(resolvedUrl); expect(filename).toBe(expected); - - sql.close(); }); }); describe("Edge cases", () => { - test("handles very long paths", () => { + test("handles very long paths", async () => { const longFilename = "a".repeat(255) + ".db"; const longPath = `/tmp/${longFilename}`; - const sql = new SQL(`sqlite://${longPath}`); + await using sql = new SQL(`sqlite://${longPath}`); expect(sql.options.filename).toBe(longPath); - sql.close(); }); - test("handles database with .db in middle of name", () => { + test("handles database with .db in middle of name", async () => { // Use a path that won't create a file in the project root const path = "/tmp/test.db.backup"; - const sql = new SQL(`sqlite://${path}`); + await using sql = new SQL(`sqlite://${path}`); expect(sql.options.filename).toBe(path); - sql.close(); }); - test("handles path with multiple dots", () => { + test("handles path with multiple dots", async () => { // Use a path that won't create a file in the project root const path = "/tmp/test...db"; - const sql = new SQL(`sqlite://${path}`); + await using sql = new SQL(`sqlite://${path}`); expect(sql.options.filename).toBe(path); - sql.close(); }); - test("empty string with adapter defaults to :memory:", () => { - const sql = new SQL("", { adapter: "sqlite" }); + test("empty string with adapter defaults to :memory:", async () => { + await using sql = new SQL("", { adapter: "sqlite" }); expect(sql.options.filename).toBe(":memory:"); - sql.close(); }); - test("null with adapter defaults to :memory:", () => { - const sql = new SQL(null as never, { adapter: "sqlite" }); + test("null with adapter defaults to :memory:", async () => { + await using sql = new SQL(null as never, { adapter: "sqlite" }); expect(sql.options.filename).toBe(":memory:"); - sql.close(); }); - test("undefined with adapter defaults to :memory:", () => { - const sql = new SQL(undefined as never, { adapter: "sqlite" }); + test("undefined with adapter defaults to :memory:", async () => { + await using sql = new SQL(undefined as never, { adapter: "sqlite" }); expect(sql.options.filename).toBe(":memory:"); - sql.close(); }); }); @@ -320,10 +303,9 @@ describe("SQLite URL Parsing Matrix", () => { "postgresql://user:pass@localhost/db", ]; - test.each(nonSqliteUrls)("treats %s as postgres", url => { - const sql = new SQL(url); + test.each(nonSqliteUrls)("treats %s as postgres", async url => { + await using sql = new SQL(url); expect(sql.options.adapter).toBe("postgres"); - sql.close(); }); }); }); diff --git a/test/leaksan.supp b/test/leaksan.supp index d277476bfc..1ffa51e732 100644 --- a/test/leaksan.supp +++ b/test/leaksan.supp @@ -108,3 +108,13 @@ leak:fromErrorInstance leak:TLSSocket__create leak:WebCore::JSReadableStreamDefaultReaderPrototype::finishCreation leak:WebCore::JSReadableStreamDefaultControllerPrototype::finishCreation + +# file comments below are where it was first seen, not an exhaustive list +# test/js/node/tls/node-tls-cert.test.ts +leak:create_ssl_context_from_bun_options +# test/js/node/test/parallel/test-inspector-enabled.js +leak:bun.js.Debugger.startJSDebuggerThread +# test/js/sql/sqlite-sql.test.ts +leak:WebCore::jsSQLStatementOpenStatementFunction +# ci, test/js/node/test/parallel/test-dgram-unref-in-cluster.js +leak:bun.js.api.bun.udp_socket.UDPSocket.udpSocket diff --git a/test/no-validate-leaksan.txt b/test/no-validate-leaksan.txt index 438c30d418..d6cd8d6407 100644 --- a/test/no-validate-leaksan.txt +++ b/test/no-validate-leaksan.txt @@ -1,5 +1,8 @@ # List of tests for which we do NOT enable LeakSanitizer when running in ASAN CI +test/cli/install/bun-security-scanner-matrix-with-node-modules.test.ts +test/cli/install/bun-security-scanner-matrix-without-node-modules.test.ts + test/js/node/test/parallel/test-worker-abort-on-uncaught-exception.js test/js/node/test/parallel/test-worker-arraybuffer-zerofill.js test/js/node/test/parallel/test-worker-cjs-workerdata.js @@ -144,6 +147,11 @@ test/cli/install/bun-run.test.ts test/bake/dev/import-meta-inline.test.ts test/integration/sharp/sharp.test.ts test/cli/test/bun-test.test.ts +test/cli/install/bun-install-security-provider.test.ts +test/js/node/test/parallel/test-tls-fast-writing.js +test/js/bun/sqlite/sqlite.test.js +test/js/workerd/html-rewriter.test.js +test/regression/issue/12250.test.ts # crash for reasons not related to LSAN @@ -188,6 +196,7 @@ test/regression/issue/14338.test.ts test/js/bun/util/heap-snapshot.test.ts test/regression/issue/02499/02499.test.ts test/js/node/test/parallel/test-http-server-stale-close.js +test/js/third_party/comlink/comlink.test.ts # Bun::JSNodeHTTPServerSocket::clearSocketData @@ -255,6 +264,8 @@ test/cli/update_interactive_snapshots.test.ts test/js/web/websocket/websocket-custom-headers.test.ts test/js/third_party/body-parser/express-memory-leak.test.ts test/js/bun/http/serve-body-leak.test.ts +test/cli/install/migration/yarn-lock-migration.test.ts +test/regression/issue/test_env_loader_threading.test.ts # Zig::SourceProvider::~SourceProvider() test/bundler/bundler_bun.test.ts @@ -372,6 +383,7 @@ test/regression/issue/11664.test.ts # ASSERTION FAILED: m_cellState == CellState::DefinitelyWhite test/js/node/tls/node-tls-upgrade.test.ts +test/js/third_party/duckdb/duckdb-basic-usage.test.ts # Bun::NapiExternal::~NapiExternal test/v8/v8.test.ts @@ -386,3 +398,6 @@ test/js/node/net/node-net-server.test.ts test/js/third_party/grpc-js/test-channel-credentials.test.ts test/js/bun/http/bun-connect-x509.test.ts test/js/third_party/rollup-v4/rollup-v4.test.ts +test/js/web/abort/abort.test.ts +test/js/third_party/resvg/bbox.test.js +test/regression/issue/10139.test.ts