From d24fa203f5532f96d798f4b54622e136d93192ad Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:24:49 +0000 Subject: [PATCH 1/3] test: run 5 rounds of the reload()/Bun.build() race test on a debug build A round is 4 builds of 80 entry points. A debug build takes about 0.4 s for one such build, a release build about 0.02 s. The 40 rounds took 58 to 85 s on a debug build, and the test has a timeout of 60 s. A debug build now runs 5 rounds (11 to 17 s). A release build still runs 40. With the entries_mutex snapshot of the route loader removed, the 5-round fixture ends in an ASAN heap-use-after-free in 26 of 26 runs. --- test/js/bun/util/filesystem_router.test.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/test/js/bun/util/filesystem_router.test.ts b/test/js/bun/util/filesystem_router.test.ts index 8886fb09657b..70e47198dbe7 100644 --- a/test/js/bun/util/filesystem_router.test.ts +++ b/test/js/bun/util/filesystem_router.test.ts @@ -2,7 +2,17 @@ import { FileSystemRouter } from "bun"; import { heapStats } from "bun:jsc"; import { expect, it } from "bun:test"; import fs, { mkdirSync, rmSync } from "fs"; -import { bunEnv, bunExe, isASAN, isMacOS, isWindows, normalizeBunSnapshot, tempDir, tmpdirSync } from "harness"; +import { + bunEnv, + bunExe, + isASAN, + isDebug, + isMacOS, + isWindows, + normalizeBunSnapshot, + tempDir, + tmpdirSync, +} from "harness"; import path, { dirname } from "path"; function createTree(basedir: string, paths: string[]) { @@ -1023,6 +1033,11 @@ it("reload() while Bun.build() resolves the same directory", async () => { // runs on the bundler thread) share the process-global directory-entry cache. // Run in a subprocess so a crash is observable as a signal instead of taking // down the test runner. + // + // A round is 4 builds, and a debug build takes about 0.4 s for each (a release + // build 0.02 s). ASAN, the default there, fails on the first read of a freed + // listing, so a debug build needs few rounds. + const rounds = isDebug ? 5 : 40; const files: Record = { "fixture.ts": /* ts */ ` import path from "path"; @@ -1044,7 +1059,7 @@ it("reload() while Bun.build() resolves the same directory", async () => { await Bun.build({ entrypoints, target: "bun", throw: false }); let matches = 0; let buildsOk = true; - for (let round = 0; round < 40; round++) { + for (let round = 0; round < ${rounds}; round++) { const builds = Array.from({ length: 4 }, () => Bun.build({ entrypoints, target: "bun", throw: false }), ); @@ -1078,7 +1093,7 @@ it("reload() while Bun.build() resolves the same directory", async () => { stderr: normalizeBunSnapshot(stderr, String(dir)), exitCode, signalCode: proc.signalCode, - }).toEqual({ stdout: "matches 2000 builds-ok true", stderr: "", exitCode: 0, signalCode: null }); + }).toEqual({ stdout: `matches ${rounds * 50} builds-ok true`, stderr: "", exitCode: 0, signalCode: null }); }, 60_000); it("loads routes from a directory already cached by Bun.build()", async () => { From 72567f0b07b047aaa67dc9b8ca106f0b3b263d98 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:11:21 +0000 Subject: [PATCH 2/3] test: keep 40 rounds on a debug build without ASAN The 5 rounds rest on ASAN, which fails on the first read of a freed listing. A debug build without ASAN has no such check, so it keeps the 40 rounds that every other build runs. --- test/js/bun/util/filesystem_router.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/js/bun/util/filesystem_router.test.ts b/test/js/bun/util/filesystem_router.test.ts index 70e47198dbe7..5712d49071d2 100644 --- a/test/js/bun/util/filesystem_router.test.ts +++ b/test/js/bun/util/filesystem_router.test.ts @@ -1034,10 +1034,10 @@ it("reload() while Bun.build() resolves the same directory", async () => { // Run in a subprocess so a crash is observable as a signal instead of taking // down the test runner. // - // A round is 4 builds, and a debug build takes about 0.4 s for each (a release - // build 0.02 s). ASAN, the default there, fails on the first read of a freed - // listing, so a debug build needs few rounds. - const rounds = isDebug ? 5 : 40; + // A round is 4 builds, and a debug build with ASAN takes about 0.4 s for each + // (a release build 0.02 s). ASAN fails on the first read of a freed listing, + // so that build needs few rounds. + const rounds = isDebug && isASAN ? 5 : 40; const files: Record = { "fixture.ts": /* ts */ ` import path from "path"; From 94d8d1d4c4eaacbea7416660d3ce36ac4735da43 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 29 Oct 2026 02:44:17 +0000 Subject: [PATCH 3/3] ci: retrigger