Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
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
22 changes: 21 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 @@ -677,7 +678,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 @@ -693,7 +694,22 @@ async function runTests() {
FORCE_COLOR: "0",
NO_COLOR: "1",
BUN_DEBUG_QUIET_LOGS: "1",
// common/tmpdir.js keys its directory on this; without it every
// test shares `.tmp.0` and --parallel runs race each other's
// tmpdir.refresh() (rm -rf) against open() calls. spawnBun also
// sets NODE_TEST_DIR to the per-test tmpdir so aborted tests
// don't leave `.tmp.N` behind in the repo checkout.
TEST_THREAD_ID: String(testIndex),
};
if (isMacOS && basename(execPath).includes("asan")) {
// 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; give dyld a last-resort search path (prepending
// rather than clobbering any inherited value).
const dir = dirname(realpathSync(execPath));
env.DYLD_FALLBACK_LIBRARY_PATH = [dir, process.env.DYLD_FALLBACK_LIBRARY_PATH].filter(Boolean).join(":");
}
if ((basename(execPath).includes("asan") || !isCI) && shouldValidateExceptions(testPath)) {
env.BUN_JSC_validateExceptionChecks = "1";
env.BUN_JSC_dumpSimulatedThrows = "1";
Expand Down Expand Up @@ -1275,6 +1291,10 @@ async function spawnBun(execPath, { args, cwd, timeout, env, stdout, stderr }) {
BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0",
BUN_INSTALL_CACHE_DIR: tmpdirPath,
SHELLOPTS: isWindows ? "igncr" : undefined, // ignore "\r" on Windows
// common/tmpdir.js reads NODE_TEST_DIR — point it at the per-test tmpdir
// so its `.tmp.<id>` subdir is swept by the finally-rmSync below even
// when the test aborts (ASAN abort_on_error skips its exit handler).
NODE_TEST_DIR: tmpdirPath,
Comment thread
alii marked this conversation as resolved.
Outdated
TEST_TMPDIR: tmpdirPath, // Used in Node.js tests.
...(typeof remapPort == "number"
? { BUN_CRASH_REPORT_URL: `http://localhost:${remapPort}` }
Expand Down
4 changes: 1 addition & 3 deletions src/boringssl/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,7 @@ pub fn check_x509_server_identity(x509: &mut boring::X509, hostname: &[u8]) -> b
let names_ = boring::X509V3_EXT_d2i(ext);
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)
});
let _guard = scopeguard::guard(names, |n| boring::GENERAL_NAMES_free(n));
for i in 0..boring::sk_GENERAL_NAME_num(names) {
let r#gen = boring::sk_GENERAL_NAME_value(names, i);
if let Some(name) = r#gen.as_ref() {
Expand Down
63 changes: 3 additions & 60 deletions src/boringssl_sys/boringssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,6 @@ pub struct GENERAL_NAME {
// OPENSSL_STACK low-level ABI (used by the typed `sk_*` inline wrappers)
// ═══════════════════════════════════════════════════════════════════════════

pub(crate) type OPENSSL_sk_free_func = Option<unsafe extern "C" fn(*mut c_void)>;
pub(crate) type OPENSSL_sk_call_free_func =
Option<unsafe extern "C" fn(OPENSSL_sk_free_func, *mut c_void)>;
pub(crate) type OPENSSL_sk_cmp_func =
Option<unsafe extern "C" fn(*const *const c_void, *const *const c_void) -> c_int>;

Expand All @@ -283,12 +280,6 @@ pub(crate) struct OPENSSL_STACK {
unsafe extern "C" {
fn sk_num(sk: *const OPENSSL_STACK) -> usize;
fn sk_value(sk: *const OPENSSL_STACK, i: usize) -> *mut c_void;
fn sk_free(sk: *mut OPENSSL_STACK);
fn sk_pop_free_ex(
sk: *mut OPENSSL_STACK,
call_free_func: OPENSSL_sk_call_free_func,
free_func: OPENSSL_sk_free_func,
);
}

// ═══════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -428,6 +419,9 @@ 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;
/// Deep-frees a `STACK_OF(GENERAL_NAME)` and every element's nested ASN1
/// values. Use this to release `X509V3_EXT_d2i` results for SAN extensions.
pub fn GENERAL_NAMES_free(gens: *mut struct_stack_st_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 All @@ -439,9 +433,6 @@ unsafe extern "C" {
// symbol — they bottom out on the untyped `sk_*` ABI above.
// ═══════════════════════════════════════════════════════════════════════════

/// Per-stack free callback type used by `sk_GENERAL_NAME_pop_free`.
pub(crate) type sk_GENERAL_NAME_free_func = unsafe extern "C" fn(*mut struct_stack_st_GENERAL_NAME);

#[inline]
pub unsafe fn sk_X509_value(sk: *const struct_stack_st_X509, i: usize) -> *mut X509 {
// SAFETY: Two independent type casts, not a const→mut provenance laundering:
Expand Down Expand Up @@ -471,54 +462,6 @@ pub unsafe fn sk_GENERAL_NAME_value(
unsafe { sk_value(sk.cast::<OPENSSL_STACK>(), i).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.
// Caller's `unsafe` contract guarantees `sk` is NULL or an owned
// BoringSSL stack; `sk_free` is documented to accept both.
unsafe { sk_free(sk.cast::<OPENSSL_STACK>()) }
}

unsafe extern "C" fn sk_GENERAL_NAME_call_free_func(
free_func: OPENSSL_sk_free_func,
ptr: *mut c_void,
) {
// SAFETY: `free_func` was originally an `sk_GENERAL_NAME_free_func` erased
// through `OPENSSL_sk_free_func` by `sk_GENERAL_NAME_pop_free` below; both
// are `extern "C" fn(*mut _)` so the pointer round-trip is ABI-sound.
let f: sk_GENERAL_NAME_free_func = unsafe {
core::mem::transmute::<unsafe extern "C" fn(*mut c_void), sk_GENERAL_NAME_free_func>(
free_func.expect("non-null free_func"),
)
};
// SAFETY: `ptr` is an element handed to this trampoline by `sk_pop_free_ex`
// while draining the `STACK_OF(GENERAL_NAME)` passed in below; the cast
// restores the typed pointer `f` was declared to accept before erasure.
unsafe { f(ptr.cast::<struct_stack_st_GENERAL_NAME>()) }
}

#[inline]
pub unsafe fn sk_GENERAL_NAME_pop_free(
sk: *mut struct_stack_st_GENERAL_NAME,
free_func: sk_GENERAL_NAME_free_func,
) {
// SAFETY: `sk` cast is mut→mut between opaque aliases; caller guarantees it
// is NULL or an owned `STACK_OF(GENERAL_NAME)`. The transmute erases
// `free_func`'s typed arg to `*mut c_void` — both sides are
// `extern "C" fn(*mut _)` so the fn-pointer reinterpret is ABI-sound, and
// `sk_GENERAL_NAME_call_free_func` restores the type before invoking it.
unsafe {
sk_pop_free_ex(
sk.cast::<OPENSSL_STACK>(),
Some(sk_GENERAL_NAME_call_free_func),
Some(core::mem::transmute::<
sk_GENERAL_NAME_free_func,
unsafe extern "C" fn(*mut c_void),
>(free_func)),
)
}
}

// ═══════════════════════════════════════════════════════════════════════════
// SSL / TLS — error codes, verify modes, shutdown flags, renegotiate modes
// (`vendor/boringssl/include/openssl/ssl.h`)
Expand Down
10 changes: 4 additions & 6 deletions src/http/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ use bun_boringssl as boringssl;
use bun_collections::{ArrayHashMap, VecExt};
use bun_core::StringBuilder;
use bun_core::{FeatureFlags, Global, Output, err};
use bun_core::{OwnedString, String as BunString, Tag as BunStringTag, immutable as strings};
use bun_core::{String as BunString, Tag as BunStringTag, immutable as strings};
use bun_http_types::ETag::StringPointer;
use bun_uws as uws;
// the std Wyhash algorithm, not Wyhash11.
Expand Down Expand Up @@ -4664,8 +4664,7 @@ impl<'a> HTTPClient<'a> {

let input =
BunString::borrow_utf8(string_builder.allocated_slice());
let normalized_url =
OwnedString::new(bun_url::href_from_string(&input));
let normalized_url = bun_url::href_from_string(&input);
if normalized_url.tag() == BunStringTag::Dead {
// URL__getHref failed, dont pass dead tagged string to toOwnedSlice.
return Err(err!(RedirectURLInvalid));
Expand Down Expand Up @@ -4725,8 +4724,7 @@ impl<'a> HTTPClient<'a> {

let input =
BunString::borrow_utf8(string_builder.allocated_slice());
let normalized_url =
OwnedString::new(bun_url::href_from_string(&input));
let normalized_url = bun_url::href_from_string(&input);
if normalized_url.tag() == BunStringTag::Dead {
return Err(err!(RedirectURLInvalid));
}
Expand All @@ -4750,7 +4748,7 @@ impl<'a> HTTPClient<'a> {

let base = BunString::borrow_utf8(original_url.href);
let rel = BunString::borrow_utf8(location);
let new_url_ = OwnedString::new(bun_url::join(&base, &rel));
let new_url_ = bun_url::join(&base, &rel);

if new_url_.is_empty() {
return Err(err!(InvalidRedirectURL));
Expand Down
4 changes: 2 additions & 2 deletions src/install/NetworkTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,10 @@ impl NetworkTask {
// `OwnedString` derefs the WTF-backed result on scope exit —
// covers both the
// success path and the InvalidURL early returns below.
let tmp = bun_core::OwnedString::new(bun_url::join(
let tmp = bun_url::join(
&bun_core::String::borrow_utf8(scope.url.href()),
&bun_core::String::borrow_utf8(encoded_name),
));
);

if tmp.tag() == bun_core::Tag::Dead {
if !is_optional {
Expand Down
16 changes: 8 additions & 8 deletions src/install/hosted_git_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ use std::io::Write as _;
use bstr::BStr;
use bun_alloc::AllocError;
use bun_core::StringBuilder;
use bun_core::{OwnedString, strings};
use bun_core::strings;
use bun_url::PercentEncoding;
use bun_url::whatwg::URL as JscUrl;
use enum_map::{Enum, EnumMap};
Expand Down Expand Up @@ -990,7 +990,7 @@ impl HostProvider {

/// Parse a URL and return the appropriate host provider, if any.
fn from_url(url: &JscUrl) -> Option<HostProvider> {
let proto_str = OwnedString::new(url.protocol());
let proto_str = url.protocol();

// Try shortcut first (github:, gitlab:, etc.)
if let Some(provider) = HostProvider::from_shortcut(proto_str.byte_slice(), false) {
Expand All @@ -1004,7 +1004,7 @@ impl HostProvider {
fn from_url_domain(url: &JscUrl) -> Option<HostProvider> {
const _MAX_HOSTNAME_LEN: usize = 253;

let hostname_str = OwnedString::new(url.hostname());
let hostname_str = url.hostname();

let hostname_utf8 = hostname_str.to_utf8();
let hostname = strings::without_prefix(hostname_utf8.slice(), b"www.");
Expand Down Expand Up @@ -1379,7 +1379,7 @@ pub mod formatters {
// valid until it's copied into the StringBuilder.
let fragment_utf8;
let committish: Option<&[u8]> = if type_part.is_none() {
let fragment_str = OwnedString::new(url.fragment_identifier());
let fragment_str = url.fragment_identifier();
fragment_utf8 = fragment_str.to_utf8();
let fragment = fragment_utf8.slice();
if !fragment.is_empty() {
Expand Down Expand Up @@ -1440,7 +1440,7 @@ pub mod formatters {
return Ok(None);
}

let fragment_str = OwnedString::new(url.fragment_identifier());
let fragment_str = url.fragment_identifier();
let fragment_utf8 = fragment_str.to_utf8();
let fragment = fragment_utf8.slice();
let committish: Option<&[u8]> = if !fragment.is_empty() {
Expand Down Expand Up @@ -1495,7 +1495,7 @@ pub mod formatters {
return Ok(None);
}

let fragment_str = OwnedString::new(url.fragment_identifier());
let fragment_str = url.fragment_identifier();
let fragment_utf8 = fragment_str.to_utf8();
let committish = fragment_utf8.slice();

Expand Down Expand Up @@ -1560,7 +1560,7 @@ pub mod formatters {
return Ok(None);
}

let fragment_str = OwnedString::new(url.fragment_identifier());
let fragment_str = url.fragment_identifier();
let fragment_utf8 = fragment_str.to_utf8();
let fragment = fragment_utf8.slice();
let committish: Option<&[u8]> = if !fragment.is_empty() {
Expand Down Expand Up @@ -1637,7 +1637,7 @@ pub mod formatters {
return Ok(None);
}

let fragment_str = OwnedString::new(url.fragment_identifier());
let fragment_str = url.fragment_identifier();
let fragment_utf8 = fragment_str.to_utf8();
let fragment = fragment_utf8.slice();
let committish: Option<&[u8]> = if !fragment.is_empty() {
Expand Down
11 changes: 9 additions & 2 deletions src/js/node/child_process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1244,7 +1244,10 @@
switch (io) {
case "pipe":
if (!NetModule) NetModule = require("node:net");
const fd = handle && handle.stdio[i];
// takeStdio() transferred ownership of Bun-created pipe fds; the
// Socket becomes the sole closer. Reading `handle.stdio` here
// instead would leave the native side owning the same fd.
const fd = this.#nativeStdio && this.#nativeStdio[i];
if (!fd) return null;
Comment thread
alii marked this conversation as resolved.
Outdated
return NetModule.connect({ fd });
}
Expand All @@ -1256,6 +1259,7 @@
#stdout;
#stderr;
#stdioObject;
#nativeStdio;
#encoding;
#stdioOptions;

Expand Down Expand Up @@ -1340,7 +1344,9 @@
const detachedOption = options.detached;
this.#encoding = options.encoding || undefined;
this.#stdioOptions = bunStdio;
const stdioCount = stdio.length;
// `bunStdio` is already padded to length >= 3 by normalizeStdio; using
// the raw `stdio.length` here left short arrays un-eagerly-loaded.
const stdioCount = bunStdio.length;

Check warning on line 1349 in src/js/node/child_process.ts

View check run for this annotation

Claude / Claude Code Review

hasSocketsToEagerlyLoad guard is now a tautology

Since `bunStdio` is always padded to length ≥ 3 by `normalizeStdio`, `const hasSocketsToEagerlyLoad = stdioCount >= 3` is now always true, making both `if (hasSocketsToEagerlyLoad)` guards dead code. Unconditional eager-loading is the intended fix, so the boolean and its two `if` wrappers can be removed (note `stdioCount` itself is still live for the `> 3` check).
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
const hasSocketsToEagerlyLoad = stdioCount >= 3;

validateString(options.file, "options.file");
Expand Down Expand Up @@ -1417,6 +1423,7 @@
}

if (hasSocketsToEagerlyLoad) {
if (stdioCount > 3) this.#nativeStdio = this.#handle.takeStdio();
for (let item of this.stdio) {
item?.ref?.();
}
Expand Down
Loading