Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8c373a0
Make the full Node parallel/sequential suite pass leak-clean under th…
cirospaciari Jun 4, 2026
2aec5c2
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 4, 2026
ab7a099
Merge branch 'main' into claude/node-suite-asan-leak-clean
robobun Jun 10, 2026
9b7a19b
boringssl: free SAN stacks with GENERAL_NAMES_free
alii Jul 8, 2026
87ac1ab
url: return OwnedString from WTF::URL getters
alii Jul 8, 2026
671eff8
child_process: read normalized stdio length; add explicit takeStdio
alii Jul 8, 2026
8756b01
vm: consolidate pre-teardown Strong-handle release; call from Worker …
alii Jul 8, 2026
b340e97
test: narrow leaksan suppressions; scope FLAKY entry to ASAN; runner …
alii Jul 8, 2026
1b7f0fb
Merge branch 'main' into claude/node-suite-asan-leak-clean
alii Jul 8, 2026
89c5a16
Merge branch 'main' into claude/node-suite-asan-leak-clean
alii Jul 8, 2026
c9aaff8
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 8, 2026
3df81f7
Merge branch 'main' into claude/node-suite-asan-leak-clean
alii Jul 9, 2026
d538d79
runner: keep NODE_TEST_DIR unset on Windows
alii Jul 9, 2026
10af51d
Merge remote-tracking branch 'origin/main' into HEAD
alii Jul 10, 2026
f04711d
verify skill: use bun bd for probes to match CLAUDE.md build-then-exe…
alii Jul 10, 2026
151cf3c
Merge remote-tracking branch 'origin/main' into claude/node-suite-asa…
cirospaciari Jul 14, 2026
a0d36f9
test: unquarantine test-worker-terminate-http2-respond-with-file
cirospaciari Jul 14, 2026
e06fa0a
vm: release Strong handles in destroy() too; strengthen child_process…
cirospaciari Jul 14, 2026
f7bd302
test: drop the Bun.main teardown smoke test
cirospaciari Jul 14, 2026
b16da77
Merge origin/main into claude/node-suite-asan-leak-clean
cirospaciari Jul 14, 2026
a9612fb
Merge remote-tracking branch 'origin/main' into claude/node-suite-asa…
robobun Aug 3, 2026
6686a7f
trim comments to <=3 lines, cite spec/node source
robobun Aug 3, 2026
703ab06
test: await stream finished() instead of asserting readableEnded at exit
robobun Aug 4, 2026
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
14 changes: 13 additions & 1 deletion scripts/runner.node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ function getNodeParallelTestTimeout(testPath) {
if (testPath.includes("test-cluster-")) return 60_000; // cluster IPC + socket-handle passing is process-heavy under runner concurrency
if (testPath.includes("-docker-")) return 60_000;
if (testPath.includes("test-stdin-pipe-large")) return 60_000; // pipes 1MB stdin->stdout through an extra child process; slow under runner concurrency
if (testPath.includes("test-require-builtins")) return 120_000; // requires every builtin module; ~60s alone under local ASAN debug builds
if (!isCI) return 60_000; // everything slower in debug mode
if (options["step"]?.includes("-asan-")) return 60_000;
return 20_000;
Expand Down Expand Up @@ -624,7 +625,7 @@ async function runTests() {
}

await Promise.all(
tests.map(testPath =>
tests.map((testPath, testIndex) =>
limit(() => {
const absoluteTestPath = join(testsPath, testPath);
const title = relative(cwd, absoluteTestPath).replaceAll(sep, "/");
Expand All @@ -640,7 +641,18 @@ async function runTests() {
FORCE_COLOR: "0",
NO_COLOR: "1",
BUN_DEBUG_QUIET_LOGS: "1",
// common/tmpdir.js derives its directory from this; without it
// every test shares `.tmp.0` and --parallel runs race each
// other's tmpdir.refresh() (rm -rf) against open() calls.
TEST_THREAD_ID: String(testIndex),
};
if (isMacOS) {
// ASAN debug builds resolve asan-dyld-shim.dylib via @rpath
// relative to the binary. Tests that copy process.execPath
// elsewhere (fork-exec-path, stdin-from-file-spawn, ...) lose
// that anchor; DYLD_LIBRARY_PATH is dyld's documented fallback.
env.DYLD_LIBRARY_PATH = dirname(realpathSync(execPath));
}
if ((basename(execPath).includes("asan") || !isCI) && shouldValidateExceptions(testPath)) {
env.BUN_JSC_validateExceptionChecks = "1";
env.BUN_JSC_dumpSimulatedThrows = "1";
Expand Down
4 changes: 2 additions & 2 deletions src/boringssl/boringssl.zig
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ pub fn checkX509ServerIdentity(

if (boring.X509V3_EXT_d2i(ext)) |names_| {
const names: *boring.struct_stack_st_GENERAL_NAME = bun.cast(*boring.struct_stack_st_GENERAL_NAME, names_);
defer boring.sk_GENERAL_NAME_pop_free(names, boring.sk_GENERAL_NAME_free);
defer boring.sk_GENERAL_NAME_pop_free(names, boring.sk_GENERAL_NAME_element_free);
for (0..boring.sk_GENERAL_NAME_num(names)) |i| {
const gen = boring.sk_GENERAL_NAME_value(names, i);
if (gen) |name| {
Expand All @@ -199,7 +199,7 @@ pub fn checkX509ServerIdentity(
} else {
if (boring.X509V3_EXT_d2i(ext)) |names_| {
const names: *boring.struct_stack_st_GENERAL_NAME = bun.cast(*boring.struct_stack_st_GENERAL_NAME, names_);
defer boring.sk_GENERAL_NAME_pop_free(names, boring.sk_GENERAL_NAME_free);
defer boring.sk_GENERAL_NAME_pop_free(names, boring.sk_GENERAL_NAME_element_free);
for (0..boring.sk_GENERAL_NAME_num(names)) |i| {
const gen = boring.sk_GENERAL_NAME_value(names, i);
if (gen) |name| {
Expand Down
5 changes: 4 additions & 1 deletion src/boringssl/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,10 @@ pub fn check_x509_server_identity(x509: &mut boring::X509, hostname: &[u8]) -> b
if !names_.is_null() {
let names = names_.cast::<boring::struct_stack_st_GENERAL_NAME>();
let _guard = scopeguard::guard(names, |n| {
boring::sk_GENERAL_NAME_pop_free(n, boring::sk_GENERAL_NAME_free)
// GENERAL_NAME_free per element — the container free
// (`sk_GENERAL_NAME_free`) here leaks every nested
// ASN1_STRING in the SAN list.
boring::sk_GENERAL_NAME_pop_free(n, boring::sk_GENERAL_NAME_element_free)
});
for i in 0..boring::sk_GENERAL_NAME_num(names) {
let r#gen = boring::sk_GENERAL_NAME_value(names, i);
Expand Down
13 changes: 13 additions & 0 deletions src/boringssl_sys/boringssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ unsafe extern "C" {
pub fn X509_NAME_get_entry(name: *const X509_NAME, loc: c_int) -> *mut X509_NAME_ENTRY;
pub fn X509_NAME_ENTRY_get_data(entry: *const X509_NAME_ENTRY) -> *mut ASN1_STRING;
pub fn X509V3_EXT_d2i(ext: *mut X509_EXTENSION) -> *mut c_void;
pub fn GENERAL_NAME_free(name: *mut GENERAL_NAME);
pub fn X509V3_EXT_get(ext: *mut X509_EXTENSION) -> *const X509V3_EXT_METHOD;
pub safe fn X509V3_EXT_get_nid(nid: c_int) -> *const X509V3_EXT_METHOD;
}
Expand Down Expand Up @@ -476,6 +477,18 @@ pub unsafe fn sk_GENERAL_NAME_value(
unsafe { sk_value(sk.cast::<OPENSSL_STACK>(), i).cast::<GENERAL_NAME>() }
}

/// Element destructor for `sk_GENERAL_NAME_pop_free`: frees one
/// `GENERAL_NAME` and its nested ASN1 values. The parameter is spelled as the
/// erased stack alias so it matches `sk_GENERAL_NAME_free_func`, but the
/// pointer is a stack *element* (`GENERAL_NAME*`). Passing the container free
/// (`sk_GENERAL_NAME_free`) here instead leaks every nested ASN1_STRING.
pub unsafe extern "C" fn sk_GENERAL_NAME_element_free(name: *mut struct_stack_st_GENERAL_NAME) {
// SAFETY: per fn doc — `name` is a `GENERAL_NAME*` erased through the
// stack-typed callback signature; restore the element type for the
// exported BoringSSL destructor.
unsafe { GENERAL_NAME_free(name.cast::<GENERAL_NAME>()) }
}

#[inline]
pub unsafe extern "C" fn sk_GENERAL_NAME_free(sk: *mut struct_stack_st_GENERAL_NAME) {
// SAFETY: mut→mut cast between opaque aliases of the same allocation.
Expand Down
9 changes: 9 additions & 0 deletions src/boringssl_sys/boringssl.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2967,6 +2967,15 @@
const sk = arg_sk;
sk_free(@as([*c]_STACK, @ptrCast(@alignCast(sk))));
}
pub extern fn GENERAL_NAME_free(name: ?*GENERAL_NAME) void;
/// Element destructor for `sk_GENERAL_NAME_pop_free`: frees one GENERAL_NAME
/// and its nested ASN1 values. The parameter is spelled as the erased stack
/// alias to match `stack_GENERAL_NAME_free_func`, but the pointer is a stack
/// *element* (`GENERAL_NAME*`). Passing the container free
/// (`sk_GENERAL_NAME_free`) there instead leaks every nested ASN1_STRING.
pub fn sk_GENERAL_NAME_element_free(name: ?*struct_stack_st_GENERAL_NAME) callconv(.c) void {
GENERAL_NAME_free(@as(?*GENERAL_NAME, @ptrCast(name)));
}

Check failure on line 2978 in src/boringssl_sys/boringssl.zig

View check run for this annotation

Claude / Claude Code Review

Zig sk_GENERAL_NAME_element_free missing @alignCast

The cast `@as(?*GENERAL_NAME, @ptrCast(name))` goes from `?*struct_stack_st_GENERAL_NAME` (an `opaque {}` — pointer alignment 1) to `?*GENERAL_NAME` (an `extern struct` containing a `c_int` + pointer union — alignment 8), which Zig rejects as "@ptrCast increases pointer alignment" once this function is reached by semantic analysis. Every comparable cast in this file (e.g. `sk_GENERAL_NAME_value` at line 2994 doing `?*anyopaque → ?*GENERAL_NAME`) uses `@ptrCast(@aligncast(...))`; the fix is the o
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
pub const stack_GENERAL_NAME_free_func = ?*const fn (?*struct_stack_st_GENERAL_NAME) callconv(.c) void;

pub fn sk_GENERAL_NAME_call_free_func(arg_free_func: stack_free_func, arg_ptr: ?*anyopaque) callconv(.c) void {
Expand Down
8 changes: 7 additions & 1 deletion src/js/node/child_process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1221,7 +1221,13 @@ class ChildProcess extends EventEmitter {
return stream;
}

const pipe = require("internal/streams/native-readable").constructNativeReadable(value, { encoding });
// Use the guarded adapter: after the child exits (lazy spawn),
// the native stream may no longer carry $bunNativePtr, and
// constructNativeReadable asserts on it. The adapter transfers
// natively when possible and falls back to ReadableFromWeb.
const pipe = require("internal/webstreams_adapters").newStreamReadableFromReadableStream(value, {
encoding,
});
this.#closesNeeded++;
pipe.once("close", () => this.#maybeClose());
if (autoResume) pipe.resume();
Expand Down
18 changes: 18 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,19 @@ impl VirtualMachine {
// JSC `Strong`/`Weak` handles against a live HandleSet.
self.event_loop_mut().release_queued_tasks_for_shutdown();

// RareData's and RuntimeState's JSC `Strong` handles must release
// while the HandleSet is still alive. `destroy()` below runs after
// `destructOnExit`, where dropping them reads freed handle storage
// (ASAN UAF in `Bun__StrongRef__delete`).
if let Some(rare) = self.rare_data.as_deref_mut() {
rare.s3_default_client.deinit();
}
if let Some(hooks) = runtime_hooks() {
// SAFETY: JS thread, live VM; the hook only touches the
// per-thread RuntimeState it owns.
unsafe { (hooks.release_runtime_state_js_handles)(core::ptr::from_mut(self)) };
}

Zig__GlobalObject__destructOnExit(self.global());

// lastChanceToFinalize() above runs Listener/Server finalize →
Expand Down Expand Up @@ -1668,6 +1681,11 @@ pub struct RuntimeHooks {
/// VirtualMachine.zig: `timer`/`entry_point` are value fields freed in
/// worker `destroy()`; without this slot every worker leaked one box.
pub deinit_runtime_state: unsafe fn(vm: *mut VirtualMachine, state: RuntimeState),
/// Release every JSC `Strong` handle owned by `RuntimeState` (the SQL
/// contexts' on_query callbacks). Must run before the JSC VM teardown
/// (`destructOnExit`); dropping them in `deinit_runtime_state` afterwards
/// reads freed HandleSet storage (ASAN UAF in `Bun__StrongRef__delete`).
pub release_runtime_state_js_handles: unsafe fn(vm: *mut VirtualMachine),
/// `ServerEntryPoint.generate(watch, entry_path)` — produces the synthetic
/// `bun:main` module body for `entry_path`. Returns `false` on error
/// (error already logged into `vm.log`).
Expand Down
5 changes: 5 additions & 0 deletions src/jsc/bindings/ConsoleObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ using namespace JSC;

class ConsoleObject final : public JSC::ConsoleClient {
WTF_DEPRECATED_MAKE_FAST_ALLOCATED(ConsoleObject);
// ConsoleClient is CanMakeThreadSafeCheckedPtr; deletion must go through
// the checked-ptr destroying-delete or ~CanMakeCheckedPtrBase asserts
// (m_didBeginDeletion). FAST_ALLOCATED above shadows the base override,
// so redeclare it here (JSGlobalObjectConsoleClient does the same).
WTF_OVERRIDE_DELETE_FOR_CHECKED_PTR(ConsoleObject);

public:
~ConsoleObject() final {}
Expand Down
7 changes: 6 additions & 1 deletion src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,12 @@ void GlobalObject::promiseRejectionTracker(JSGlobalObject* obj, JSC::JSPromise*

void GlobalObject::setConsole(void* console)
{
this->setConsoleClient(new Bun::ConsoleObject(console));
// JSGlobalObject::setConsoleClient only stores a WeakPtr — keep the
// owning pointer on this global so the ConsoleObject (and its buffered
// messages) is freed when the global is destroyed (ShadowRealm globals
// are created and destroyed many times per process).
m_ownedConsoleClient = std::unique_ptr<JSC::ConsoleClient>(new Bun::ConsoleObject(console));
Comment thread
alii marked this conversation as resolved.
Outdated
this->setConsoleClient(m_ownedConsoleClient.get());
}

JSC_DEFINE_CUSTOM_GETTER(errorConstructorPrepareStackTraceGetter,
Expand Down
4 changes: 4 additions & 0 deletions src/jsc/bindings/ZigGlobalObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,10 @@ class GlobalObject : public Bun::GlobalScope {
Lock m_gcLock;
Ref<WebCore::DOMWrapperWorld> m_world;
RefPtr<WebCore::Performance> m_performance { nullptr };
// Owns the ConsoleClient installed by setConsole(). JSGlobalObject only
// keeps a WeakPtr, so without an owner every global (notably each
// ShadowRealm-derived one) leaks its ConsoleObject and buffered messages.
std::unique_ptr<JSC::ConsoleClient> m_ownedConsoleClient;

public:
// De-optimization once `require("module")._resolveFilename` is written to
Expand Down
14 changes: 14 additions & 0 deletions src/runtime/api/bun/subprocess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,20 @@
}
}
}

// Exposing a raw fd hands it to JS, which wraps it in
// `net.Socket({ fd })` (child_process.ts `"pipe"` case) — the socket
// takes ownership and closes the fd on destroy. Downgrade our entries
// so `finalize_streams` doesn't close them again (fd-UAF assert).
#[cfg(not(windows))]
this.stdio_pipes.with_mut(|v| {
for item in v.iter_mut() {
if let ExtraPipe::OwnedFd(fd) = item {
*item = ExtraPipe::UnownedFd(*fd);
}
}
});

Check failure on line 880 in src/runtime/api/bun/subprocess.rs

View check run for this annotation

Claude / Claude Code Review

Reading subprocess.stdio leaks extra-pipe fds for direct Bun.spawn callers

Reading `subprocess.stdio` now unconditionally transfers ownership of every extra-pipe fd to JS, but `.stdio` is a documented public `Bun.spawn` getter (not just a child_process.ts implementation detail) — a direct user who merely inspects `proc.stdio` without wrapping each fd in a `net.Socket` will leak those fds, since `finalize_streams()` no longer closes them. This trades the child_process double-close for an fd leak in the bare `Bun.spawn` path; consider an explicit ownership-transfer entry
Comment thread
claude[bot] marked this conversation as resolved.
Outdated

Ok(array)
}

Expand Down
18 changes: 18 additions & 0 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1455,11 +1455,29 @@ mod vm_loader_ctx {
}
}

/// Hook: drop the JSC `Strong` handles held inside `RuntimeState` while the
/// JSC VM (and its HandleSet) is still alive. Idempotent — `deinit()` leaves
/// the `StrongOptional`s empty so the later `RuntimeState` drop is a no-op.
unsafe fn release_runtime_state_js_handles(_vm: *mut VirtualMachine) {
let state = runtime_state();
if state.is_null() {
return;
}
// SAFETY: `state` is the live per-thread `RuntimeState`; this runs on the
// JS thread during `global_exit`, before any teardown frees it.
let state = unsafe { &mut *state };
state.sql_rare.mysql_context.on_query_resolve_fn.deinit();
state.sql_rare.mysql_context.on_query_reject_fn.deinit();
state.sql_rare.postgresql_context.on_query_resolve_fn.deinit();
state.sql_rare.postgresql_context.on_query_reject_fn.deinit();
}

/// The static `RuntimeHooks` instance handed to `bun_jsc`.
#[unsafe(no_mangle)]
pub(crate) static __BUN_RUNTIME_HOOKS: RuntimeHooks = RuntimeHooks {
init_runtime_state,
deinit_runtime_state,
release_runtime_state_js_handles,
generate_entry_point,
load_preloads,
ensure_debugger,
Expand Down
5 changes: 4 additions & 1 deletion src/runtime/test_runner/ScopeFunctions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,10 @@ pub(crate) fn bind(value: JSValue, global: &JSGlobalObject, name: BunString) ->
// `#[bun_jsc::host_fn]` on `call_as_function` emits the C-ABI thunk
// `__jsc_host_call_as_function`; `JSFunction::create` wants the raw
// `JSHostFn` shape, not the safe Rust signature.
let call_fn = bun_jsc::JSFunction::create(global, name.clone(), __jsc_host_call_as_function, 1, Default::default());
// `name` is passed by value (bit-copy) with borrow semantics —
// `JSFunction__createFromZig` only reads it (`toWTFString()`), so an
// owned `clone()` here would never be deref'd and leaks the StringImpl.
let call_fn = bun_jsc::JSFunction::create(global, name, __jsc_host_call_as_function, 1, Default::default());
let bound = JSValueTestExt::bind(call_fn, global, value, &name, 1.0, &[])?;
set_prototype_direct(bound, value.get_prototype(global), global)?;
Ok(bound)
Expand Down
12 changes: 7 additions & 5 deletions src/runtime/valkey_jsc/js_valkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,9 @@ impl JSValkeyClient {
let parsed_url = bun_ptr::BackRef::from(parsed_url);

// Extract protocol string
let protocol_str = parsed_url.protocol();
// URL component getters return +1 `BunString`s (no `Drop`); wrap in
// `OwnedString` for scope-exit deref or each one leaks its StringImpl.
let protocol_str = bun_core::OwnedString::new(parsed_url.protocol());
let protocol_utf8 = protocol_str.to_utf8();
// Remove the trailing ':' from protocol (e.g., "redis:" -> "redis")
let p = protocol_utf8.slice();
Expand All @@ -584,16 +586,16 @@ impl JSValkeyClient {
};

// Extract all URL components
let username_str = parsed_url.username();
let username_str = bun_core::OwnedString::new(parsed_url.username());
let username_utf8 = username_str.to_utf8();

let password_str = parsed_url.password();
let password_str = bun_core::OwnedString::new(parsed_url.password());
let password_utf8 = password_str.to_utf8();

let hostname_str = parsed_url.host();
let hostname_str = bun_core::OwnedString::new(parsed_url.host());
let hostname_utf8 = hostname_str.to_utf8();

let pathname_str = parsed_url.pathname();
let pathname_str = bun_core::OwnedString::new(parsed_url.pathname());
let pathname_utf8 = pathname_str.to_utf8();

// Determine hostname based on protocol type
Expand Down
6 changes: 6 additions & 0 deletions test/expectations.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ test/cli/run/run-crash-handler.test.ts [ FAIL ] # automatic crash reporter > seg

# Tests that are flaky
test/js/bun/spawn/spawn-maxbuf.test.ts [ FLAKY ]
# Worker entry-point load races terminate(): debug invariant panic "JavaScript
# functions were called outside of the microtask queue without draining
# microtasks" in tick_queue_with_count (load_entry_point_for_web_worker →
# wait_for_promise_with_termination). Reproduces on unmodified origin/main;
# timing-dependent (fails reliably on a quiet machine, passes under load).
test/js/node/test/parallel/test-worker-terminate-http2-respond-with-file.js [ FLAKY ]

# Tests skipped due to different log/line outputs
[ ASAN ] test/js/bun/util/reportError.test.ts [ SKIP ] # log line mismatch
Expand Down
50 changes: 50 additions & 0 deletions test/leaksan.supp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ leak:JSC::ScriptExecutable::newCodeBlockFor
leak:JSC::Parser<JSC::Lexer<unsigned char>>::parseFunctionExpression
leak:JSC::Parser<JSC::Lexer<unsigned char>>::parsePrimaryExpression
leak:JSC::Parser<JSC::Lexer<unsigned char>>::parseStatement
leak:JSC::Parser<JSC::Lexer<unsigned char>>::parseImportDeclaration
leak:JSCInitialize
leak:getaddrinfo_send_reply
leak:start_wqthread
Expand Down Expand Up @@ -121,3 +122,52 @@ leak:WebCore::jsSQLStatementOpenStatementFunction
# is called before firing (WaiterListManager::clearTimer on notify/unregister),
# the DispatchTimer and its Bun-side WTFTimer Box leak. JSC-owned ref-cycle.
leak:WTF::RunLoop::dispatchAfter
leak:WTF::ParkingLot::parkConditionallyImpl
# Parser-arena identifiers pinned in the atom table at VM-destroy exit —
# covers all JSC::Parser parse productions (same family as the parse* entries above).
leak:JSC::IdentifierArena::makeIdentifier
# Live-thread TLS at exit: RunLoop holder of the vm watchdog/aux threads (same class as ParkingLot above).
leak:WTF::RunLoop::currentSingleton
# ASCIILiteral StringImpl wrapper for internal module names; pinned for process lifetime.
leak:Bun::InternalModuleRegistry::createInternalModuleById
# macOS libdispatch/XPC continuation cached inside dns_configuration_free while
# c-ares reads the system resolver config — OS-internal, not reachable by us.
leak:ares_init_sysconfig_macos
# FSEvents watcher thread (std::thread spawn block) still running at exit.
leak:FSEventsLoop
# Apple CoreAnalytics XPC telemetry triggered inside SecTrustCopyAnchorCertificates /
# system CA reads — OS-internal dispatch continuation.
leak:CoreAnalytics
# ASCIILiteral StringImpl wrapper created while formatting a stack frame's source
# URL on the exit path; same class as the InternalModuleRegistry entry above.
leak:Zig::sourceURL
# Apple Security.framework keychain internals reached from our run_once system
# root-CA load — cached for process lifetime by design.
leak:us_get_root_system_cert_instances
# JSC structure-heap bookkeeping (BitVector in StructureMemoryManager); grows
# once per structure block and lives for the VM's lifetime.
leak:JSC::StructureMemoryManager::tryMallocStructureBlock
# libsystem_info per-thread user-info cache (getpwuid via CFPreferences inside
# Security.framework) — OS-internal thread-local storage.
leak:LI_get_thread_info
# backtrace_symbols() buffer malloc'd inside debug-only stack-trace dumps
# (fd-UAF warning path); diagnostics memory, never freed by design.
leak:backtrace_symbols
# Per-VM JSON atom cache entry pinned in the atom table at VM-destroy exit
# (same family as IdentifierArena::makeIdentifier above).
leak:JSC::JSONAtomStringCache
# Inspector/debugger server thread (bun_jsc::debugger::Debugger::create) still
# parked at exit — live-thread allocation. Matches the Rust v0-mangled symbol.
leak:bun_jsc8debugger
# Per-worker WebCore::EventNames not reclaimed when a Worker thread exits —
# bounded by live worker count at exit; needs a ThreadGlobalData teardown
# follow-up rather than blocking every worker test locally.
leak:WebCore::EventNames::operator new
# `selectors` crate global caches (hashbrown tables) — process-lifetime statics.
leak:9selectors
# crypto.subtle lazy property: SubtleCrypto impl pinned by its JS wrapper at
# VM-destroy exit. Same JSC-owned ref-cycle class as RunLoop::dispatchAfter.
leak:WebCore::SubtleCrypto::create
# Rust std lazily-allocated pthread mutex storage (sys::sync::once_box) —
# intentionally never freed; one block per static mutex.
leak:8once_box
Comment on lines +123 to +191

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding leaksan supressions is the opposite of making code leak-clean

Loading