Skip to content
Closed
Show file tree
Hide file tree
Changes from 15 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
42 changes: 42 additions & 0 deletions .claude/skills/verify/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
description: Verify a change to the Bun runtime by driving the debug build end-to-end.
---

# Verifying a Bun runtime change

**Drive the debug build with `bun bd <args>`** — with exec args present,
build output is suppressed and a no-op rebuild is a quick dep-check, so
you get only the binary's output.

```bash
bun bd --revision # build; prints version+hash on success
bun bd -e '<snippet>' # drive it
BUN_DEBUG_QUIET_LOGS=1 ... # suppress the very chatty debug tracing
```

## Surfaces by area

- **JS-visible API** (`Bun.*`, Web APIs, `node:*` modules): a `-e`
one-liner is the surface. `bun bd -e 'console.log(new Request("https://x").url)'`.
- **CLI** (`bun install`, `bun build`, `bun test`): run the subcommand
in a `mktemp -d` scratch dir. Use `bunEnv` from `test/harness.ts` if
you need the CI-equivalent env.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
- **Server/socket** (`Bun.serve`, `net`/`tls`/`http`): start a server on
`port: 0` in the `-e` script and hit it from the same process.
- **Memory/lifetime fixes** (leaks, UAF, teardown): set
`BUN_DESTRUCT_VM_ON_EXIT=1` so the VM actually tears down instead of
`_exit`ing; ASAN in the debug build then reports on stderr. A clean
`exitCode 0` + `signalCode null` is the pass signal — don't grep
stderr for "AddressSanitizer".

## Gotchas

- `test/` deps need `cd test && bun install` first; some experimental
React deps (`react-server-dom-bun`) only resolve against the public
registry, not internal mirrors — set
`NPM_CONFIG_REGISTRY=https://registry.npmjs.org` if `bun install` 404s.
- Debug builds are 10-100× slower than release; a 5s test-file timeout
that CI hits comfortably will time out locally. Widen with
`--timeout 30000` before assuming a hang.
- `require("harness")` only resolves inside `test/` (path-mapped);
from a bare `-e` script, `cd test` first and `require("./harness.ts")`.
16 changes: 16 additions & 0 deletions scripts/runner.node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,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 @@ -768,6 +769,15 @@ async function runTests() {
// (test-child-process-*-detached.js), which this flag defeats.
env.BUN_FEATURE_FLAG_NO_ORPHANS = "1";
}
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 @@ -1364,6 +1374,12 @@ 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).
// POSIX-only: there is no Windows ASAN lane, and relocating testRoot to
// realpath(%TEMP%) breaks path-shape assumptions in a few Windows tests.
NODE_TEST_DIR: isWindows ? undefined : tmpdirPath,
TEST_TMPDIR: tmpdirPath, // Used in Node.js tests.
...(typeof remapPort == "number"
? { BUN_CRASH_REPORT_URL: `http://localhost:${remapPort}` }
Expand Down
10 changes: 6 additions & 4 deletions src/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,9 @@ let url = URL::from_utf8(href)?; // Option<NonNull<URL>>
// caller owns the C++ object — destroy it when done:
// unsafe { URL::destroy(url.as_ptr()) }

url.protocol() // bun_core::String
url.pathname() // bun_core::String
url.search() // bun_core::String
url.protocol() // bun_core::OwnedString (+1; Drop derefs)
url.pathname() // bun_core::OwnedString
url.search() // bun_core::OwnedString
url.port() // u32 (u32::MAX = unset; otherwise u16 range)

// NOTE: host()/hostname() are SWAPPED relative to JS:
Expand All @@ -170,7 +170,9 @@ url.hostname() // hostname WITH port (opposite of JS!)
```

`URL::href_from_string`, `URL::file_url_from_string`, `URL::path_from_file_url`
do whole-string conversions.
do whole-string conversions. Every string getter returns `OwnedString` — use
`.into_inner()` only when you must transfer the +1 out (e.g. into a struct
field that will deref later).

## MIME Types (`bun_http_types::MimeType`)

Expand Down
10 changes: 4 additions & 6 deletions src/http/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,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, strings};
use bun_core::{String as BunString, Tag as BunStringTag, strings};
use bun_http_types::ETag::StringPointer;
use bun_uws as uws;
// the std Wyhash algorithm, not Wyhash11.
Expand Down Expand Up @@ -5273,8 +5273,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 @@ -5334,8 +5333,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 @@ -5359,7 +5357,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
18 changes: 6 additions & 12 deletions src/js/node/child_process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1380,8 +1380,6 @@ class ChildProcess extends EventEmitter {
const detachedOption = options.detached;
this.#encoding = options.encoding || undefined;
this.#stdioOptions = bunStdio;
const stdioCount = stdio.length;
const hasSocketsToEagerlyLoad = stdioCount >= 3;

validateString(options.file, "options.file");
var file;
Expand Down Expand Up @@ -1413,12 +1411,10 @@ class ChildProcess extends EventEmitter {
this.pid = this.#handle.pid;
$debug("ChildProcess: onExit", exitCode, signalCode, err, this.pid);

if (hasSocketsToEagerlyLoad) {
process.nextTick(() => {
void this.stdio;
$debug("ChildProcess: onExit", exitCode, signalCode, err, this.pid);
});
}
process.nextTick(() => {
void this.stdio;
$debug("ChildProcess: onExit", exitCode, signalCode, err, this.pid);
});

process.nextTick(
(exitCode, signalCode, err) => this.#handleOnExit(exitCode, signalCode, err),
Expand Down Expand Up @@ -1458,10 +1454,8 @@ class ChildProcess extends EventEmitter {
if (options[kFromNode]) this.#closesNeeded += 1;
}

if (hasSocketsToEagerlyLoad) {
for (let item of this.stdio) {
item?.ref?.();
}
for (let item of this.stdio) {
item?.ref?.();
}
} catch (ex) {
const exCode = ex != null && typeof ex === "object" && Object.hasOwn(ex, "code") ? ex.code : undefined;
Expand Down
Loading
Loading