Compare commits

..

2 Commits

Author SHA1 Message Date
Dylan Conway
32c6c47a40 ci: add QEMU baseline CPU and JIT stress verification for baseline/aarch64 builds
Add unified baseline verification script that runs both basic CPU
instruction checks and JIT stress test fixtures under QEMU (Linux)
or Intel SDE (Windows). Replaces the separate verify-baseline-cpu.sh
and verify-jit-stress-qemu.sh steps with a single TypeScript script.

- Linux x64 baseline: QEMU with Nehalem CPU (no AVX)
- Linux aarch64: QEMU with Cortex-A53 (no LSE/SVE)
- Windows x64 baseline: Intel SDE v9.58 with -nhm (no AVX)
- SDE violations detected by checking output for chip-check errors
  rather than exit codes, avoiding false positives from app errors
- JIT stress fixtures now run on every build (previously gated on
  WebKit changes)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-18 21:51:22 -08:00
SUZUKI Sosuke
fb2f304100 fix(node:fs): remove unnecessary path buffer pool alloc on Windows (#27115)
## Summary

- Removes an unnecessary 64KB `path_buffer_pool` allocation in
`PathLike.sliceZWithForceCopy` on Windows for paths that already have a
drive letter
- For drive-letter paths (e.g. `C:\foo\bar`),
`resolveCWDWithExternalBufZ` just does a memcpy, so the intermediate
buffer is unnecessary — we can pass the input slice directly to
`normalizeBuf`
- Eliminates an OOM crash path where `ObjectPool.get()` would panic via
`catch unreachable` when the allocator fails

## Test plan

- [ ] Verify Windows CI passes (this code path is Windows-only)
- [ ] Verify node:fs operations with absolute Windows paths still work
correctly
- [ ] Monitor BUN-Z4V crash reports after deployment to confirm fix

## Context

Speculative fix for BUN-Z4V (124 occurrences on Windows) showing `Panic:
attempt to unwrap error: OutOfMemory` in `sliceZWithForceCopy` →
`path_buffer_pool.get()` → `allocBytesWithAlignment`. We have not been
able to reproduce the crash locally, but the code analysis shows the
allocation is unnecessary for the drive-letter path case.

## Changelog
<!-- CHANGELOG:START -->
Fixed a crash on Windows (`OutOfMemory` panic) in `node:fs` path
handling when the system is under memory pressure.
<!-- CHANGELOG:END -->

🤖 Generated with [Claude Code](https://claude.com/claude-code) (0%
8-shotted by claude-opus-4-6)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:16:16 -08:00
6 changed files with 210 additions and 83 deletions

View File

@@ -593,19 +593,54 @@ function getTargetTriplet(platform) {
*/
function needsBaselineVerification(platform) {
const { os, arch, baseline } = platform;
if (os !== "linux") return false;
return (arch === "x64" && baseline) || arch === "aarch64";
if (os === "linux") return (arch === "x64" && baseline) || arch === "aarch64";
if (os === "windows") return arch === "x64" && baseline;
return false;
}
/**
* Returns the emulator binary name for the given platform.
* Linux uses QEMU user-mode; Windows uses Intel SDE.
* @param {Platform} platform
* @returns {string}
*/
function getEmulatorBinary(platform) {
const { os, arch } = platform;
if (os === "windows") return "sde-external/sde.exe";
if (arch === "aarch64") return "qemu-aarch64-static";
return "qemu-x86_64-static";
}
const SDE_VERSION = "9.58.0-2025-06-16";
const SDE_URL = `https://downloadmirror.intel.com/859732/sde-external-${SDE_VERSION}-win.tar.xz`;
/**
* @param {Platform} platform
* @param {PipelineOptions} options
* @returns {Step}
*/
function getVerifyBaselineStep(platform, options) {
const { arch } = platform;
const { os } = platform;
const targetKey = getTargetKey(platform);
const archArg = arch === "x64" ? "x64" : "aarch64";
const triplet = getTargetTriplet(platform);
const emulator = getEmulatorBinary(platform);
const setupCommands = [
`buildkite-agent artifact download '*.zip' . --step ${targetKey}-build-bun`,
`unzip -o '${triplet}.zip'`,
];
if (os !== "windows") {
setupCommands.push(`chmod +x ${triplet}/bun`);
}
if (os === "windows") {
setupCommands.push(
`curl.exe -fsSL -o sde.tar.xz "${SDE_URL}"`,
`tar -xf sde.tar.xz`,
`ren sde-external-${SDE_VERSION}-win sde-external`,
);
}
return {
key: `${targetKey}-verify-baseline`,
@@ -614,57 +649,10 @@ function getVerifyBaselineStep(platform, options) {
agents: getLinkBunAgent(platform, options),
retry: getRetry(),
cancel_on_build_failing: isMergeQueue(),
timeout_in_minutes: 5,
command: [
`buildkite-agent artifact download '*.zip' . --step ${targetKey}-build-bun`,
`unzip -o '${getTargetTriplet(platform)}.zip'`,
`unzip -o '${getTargetTriplet(platform)}-profile.zip'`,
`chmod +x ${getTargetTriplet(platform)}/bun ${getTargetTriplet(platform)}-profile/bun-profile`,
`./scripts/verify-baseline-cpu.sh --arch ${archArg} --binary ${getTargetTriplet(platform)}/bun`,
`./scripts/verify-baseline-cpu.sh --arch ${archArg} --binary ${getTargetTriplet(platform)}-profile/bun-profile`,
],
};
}
/**
* Returns true if the PR modifies SetupWebKit.cmake (WebKit version changes).
* JIT stress tests under QEMU should run when WebKit is updated to catch
* JIT-generated code that uses unsupported CPU instructions.
* @param {PipelineOptions} options
* @returns {boolean}
*/
function hasWebKitChanges(options) {
const { changedFiles = [] } = options;
return changedFiles.some(file => file.includes("SetupWebKit.cmake"));
}
/**
* Returns a step that runs JSC JIT stress tests under QEMU.
* This verifies that JIT-compiled code doesn't use CPU instructions
* beyond the baseline target (no AVX on x64, no LSE on aarch64).
* @param {Platform} platform
* @param {PipelineOptions} options
* @returns {Step}
*/
function getJitStressTestStep(platform, options) {
const { arch } = platform;
const targetKey = getTargetKey(platform);
const archArg = arch === "x64" ? "x64" : "aarch64";
return {
key: `${targetKey}-jit-stress-qemu`,
label: `${getTargetLabel(platform)} - jit-stress-qemu`,
depends_on: [`${targetKey}-build-bun`],
agents: getLinkBunAgent(platform, options),
retry: getRetry(),
cancel_on_build_failing: isMergeQueue(),
// JIT stress tests are slow under QEMU emulation
timeout_in_minutes: 30,
command: [
`buildkite-agent artifact download '*.zip' . --step ${targetKey}-build-bun`,
`unzip -o '${getTargetTriplet(platform)}.zip'`,
`chmod +x ${getTargetTriplet(platform)}/bun`,
`./scripts/verify-jit-stress-qemu.sh --arch ${archArg} --binary ${getTargetTriplet(platform)}/bun`,
...setupCommands,
`bun scripts/verify-baseline.ts --binary ${triplet}/${os === "windows" ? "bun.exe" : "bun"} --emulator ${emulator}`,
],
};
}
@@ -1264,10 +1252,6 @@ async function getPipeline(options = {}) {
if (needsBaselineVerification(target)) {
steps.push(getVerifyBaselineStep(target, options));
// Run JIT stress tests under QEMU when WebKit is updated
if (hasWebKitChanges(options)) {
steps.push(getJitStressTestStep(target, options));
}
}
return getStepWithDependsOn(

163
scripts/verify-baseline.ts Normal file
View File

@@ -0,0 +1,163 @@
// Verify that a Bun binary doesn't use CPU instructions beyond its baseline target.
//
// Detects the platform and chooses the appropriate emulator:
// Linux x64: QEMU with Nehalem CPU (no AVX)
// Linux arm64: QEMU with Cortex-A53 (no LSE/SVE)
// Windows x64: Intel SDE with -nhm (no AVX)
//
// Usage:
// bun scripts/verify-baseline.ts --binary ./bun --emulator /usr/bin/qemu-x86_64
// bun scripts/verify-baseline.ts --binary ./bun.exe --emulator ./sde.exe
import { readdirSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
const { parseArgs } = require("node:util");
const { values } = parseArgs({
args: process.argv.slice(2),
options: {
binary: { type: "string" },
emulator: { type: "string" },
},
strict: true,
});
const binary = resolve(values.binary!);
const emulatorPath = resolve(values.emulator!);
const scriptDir = dirname(import.meta.path);
const repoRoot = resolve(scriptDir, "..");
const fixturesDir = join(repoRoot, "test", "js", "bun", "jsc-stress", "fixtures");
const wasmFixturesDir = join(fixturesDir, "wasm");
const preloadPath = join(repoRoot, "test", "js", "bun", "jsc-stress", "preload.js");
// Platform detection
const isWindows = process.platform === "win32";
const isAarch64 = process.arch === "arm64";
// SDE outputs this when a chip-check violation occurs
const SDE_VIOLATION_PATTERN = /SDE-ERROR:.*not valid for specified chip/i;
// Configure emulator based on platform
const config = isWindows
? {
runnerCmd: [emulatorPath, "-nhm", "--"],
cpuDesc: "Nehalem (SSE4.2, no AVX/AVX2/AVX512)",
// SDE must run from its own directory for Pin DLL resolution
cwd: dirname(emulatorPath),
}
: isAarch64
? {
runnerCmd: [emulatorPath, "-cpu", "cortex-a53"],
cpuDesc: "Cortex-A53 (ARMv8.0-A+CRC, no LSE/SVE)",
cwd: undefined,
}
: {
runnerCmd: [emulatorPath, "-cpu", "Nehalem"],
cpuDesc: "Nehalem (SSE4.2, no AVX/AVX2/AVX512)",
cwd: undefined,
};
function isInstructionViolation(exitCode: number, output: string): boolean {
if (isWindows) return SDE_VIOLATION_PATTERN.test(output);
return exitCode === 132; // SIGILL = 128 + signal 4
}
console.log(`--- Verifying ${basename(binary)} on ${config.cpuDesc}`);
console.log(` Binary: ${binary}`);
console.log(` Emulator: ${config.runnerCmd.join(" ")}`);
console.log();
let instructionFailures = 0;
let otherFailures = 0;
let passed = 0;
async function runTest(label: string, binaryArgs: string[], cwd?: string): Promise<boolean> {
console.log(`+++ ${label}`);
const proc = Bun.spawn([...config.runnerCmd, binary, ...binaryArgs], {
cwd: cwd ?? config.cwd,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
const output = stdout + "\n" + stderr;
if (exitCode === 0) {
if (stdout.trim()) console.log(stdout.trim());
console.log(" PASS");
passed++;
return true;
}
if (isInstructionViolation(exitCode, output)) {
if (output.trim()) console.log(output.trim());
console.log();
console.log(" FAIL: CPU instruction violation detected");
if (isAarch64) {
console.log(" The aarch64 build targets Cortex-A53 (ARMv8.0-A+CRC).");
console.log(" LSE atomics, SVE, and dotprod instructions are not allowed.");
} else {
console.log(" The baseline x64 build targets Nehalem (SSE4.2).");
console.log(" AVX, AVX2, and AVX512 instructions are not allowed.");
}
instructionFailures++;
} else {
if (output.trim()) console.log(output.trim());
console.log(` WARN: exit code ${exitCode} (not a CPU instruction issue)`);
otherFailures++;
}
return false;
}
// Phase 1: Basic binary verification
console.log("--- Basic binary verification");
const versionOk = await runTest("bun --version", ["--version"]);
if (!versionOk && instructionFailures > 0) {
console.error("Binary immediately fails on baseline CPU. Aborting.");
process.exit(1);
}
await runTest("bun -e eval", ["-e", "console.log(JSON.stringify({ok:1+1}))"]);
// Phase 2: JIT stress fixtures
console.log();
console.log("--- JS fixtures (DFG/FTL)");
for (const fixture of readdirSync(fixturesDir)
.filter(f => f.endsWith(".js"))
.sort()) {
await runTest(fixture, ["--preload", preloadPath, join(fixturesDir, fixture)]);
}
console.log();
console.log("--- Wasm fixtures (BBQ/OMG)");
for (const fixture of readdirSync(wasmFixturesDir)
.filter(f => f.endsWith(".js"))
.sort()) {
await runTest(fixture, ["--preload", preloadPath, join(wasmFixturesDir, fixture)], wasmFixturesDir);
}
// Summary
console.log();
console.log("--- Summary");
console.log(` Passed: ${passed}`);
console.log(` Instruction failures: ${instructionFailures}`);
console.log(` Other failures: ${otherFailures} (warnings, not CPU instruction issues)`);
console.log();
if (instructionFailures > 0) {
console.error(" FAILED: Code uses unsupported CPU instructions.");
process.exit(1);
}
if (otherFailures > 0) {
console.log(" Some tests failed for reasons unrelated to CPU instructions.");
}
console.log(` All baseline verification passed on ${config.cpuDesc}.`);

View File

@@ -91,8 +91,6 @@ pub const Features = struct {
pub var yaml_parse: usize = 0;
pub var cpu_profile: usize = 0;
pub var heap_snapshot: usize = 0;
pub var fs_watch: usize = 0;
pub var fs_watchfile: usize = 0;
comptime {
@export(&napi_module_register, .{ .name = "Bun__napi_module_register_count" });

View File

@@ -486,11 +486,9 @@ pub const StatWatcher = struct {
/// After a restat found the file changed, this calls the listener function.
pub fn swapAndCallListenerOnMainThread(this: *StatWatcher) void {
bun.analytics.Features.fs_watchfile += 1;
defer this.deref(); // Balance the ref from restat().
const prev_jsvalue = this.last_jsvalue.swap();
const globalThis = this.globalThis;
const current_jsvalue = statToJSStats(globalThis, &this.getLastStat(), this.bigint) catch return; // TODO: properly propagate exception upwards
this.last_jsvalue.set(globalThis, current_jsvalue);

View File

@@ -261,7 +261,6 @@ pub const FSWatcher = struct {
pub fn onPathUpdatePosix(ctx: ?*anyopaque, event: Event, is_file: bool) void {
const this = bun.cast(*FSWatcher, ctx.?);
bun.analytics.Features.fs_watch += 1;
if (this.verbose) {
switch (event) {
@@ -282,7 +281,6 @@ pub const FSWatcher = struct {
pub fn onPathUpdateWindows(ctx: ?*anyopaque, event: Event, is_file: bool) void {
const this = bun.cast(*FSWatcher, ctx.?);
bun.analytics.Features.fs_watch += 1;
if (this.verbose) {
switch (event) {
@@ -301,23 +299,9 @@ pub const FSWatcher = struct {
return;
}
// The event's path comes from PathWatcherManager.onFileUpdate as a
// []const u8 sub-slice of the watchlist's file_path storage, auto-coerced
// to StringOrBytesToDecode{.bytes_to_free}. We must create a properly
// owned copy: either a bun.String for utf8 encoding or a duped []const u8
// for other encodings.
const owned_event: Event = switch (event) {
inline .rename, .change => |path, t| @unionInit(Event, @tagName(t), if (this.encoding == .utf8)
FSWatchTaskWindows.StringOrBytesToDecode{ .string = bun.String.cloneUTF8(path.bytes_to_free) }
else
FSWatchTaskWindows.StringOrBytesToDecode{ .bytes_to_free = bun.default_allocator.dupe(u8, path.bytes_to_free) catch return }),
.@"error" => |err| .{ .@"error" = err.clone(bun.default_allocator) },
inline else => |value, t| @unionInit(Event, @tagName(t), value),
};
const task = bun.new(FSWatchTaskWindows, .{
.ctx = this,
.event = owned_event,
.event = event,
});
this.eventLoop().enqueueTask(jsc.Task.init(task));
}

View File

@@ -587,12 +587,12 @@ pub const PathLike = union(enum) {
if (std.fs.path.isAbsolute(sliced)) {
if (sliced.len > 2 and bun.path.isDriveLetter(sliced[0]) and sliced[1] == ':' and bun.path.isSepAny(sliced[2])) {
// Add the long path syntax. This affects most of node:fs
const drive_resolve_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(drive_resolve_buf);
const rest = path_handler.PosixToWinNormalizer.resolveCWDWithExternalBufZ(drive_resolve_buf, sliced) catch @panic("Error while resolving path.");
// Normalize the path directly into buf without an intermediate
// buffer. The input (sliced) already has a drive letter, so
// resolveCWDWithExternalBufZ would just memcpy it, making the
// temporary allocation unnecessary.
buf[0..4].* = bun.windows.long_path_prefix_u8;
// When long path syntax is used, the entire string should be normalized
const n = bun.path.normalizeBuf(rest, buf[4..], .windows).len;
const n = bun.path.normalizeBuf(sliced, buf[4..], .windows).len;
buf[4 + n] = 0;
return buf[0 .. 4 + n :0];
}