Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 13 additions & 2 deletions .buildkite/ci.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ const buildPlatforms = [
{ os: "linux", arch: "aarch64", distro: "amazonlinux", release: "2023", features: ["docker"] },
{ os: "linux", arch: "x64", distro: "amazonlinux", release: "2023", features: ["docker"] },
{ os: "linux", arch: "x64", baseline: true, distro: "amazonlinux", release: "2023", features: ["docker"] },
// ASAN lanes are PR-only (filtered out on main — see getPipelineOptions and
// includeASAN in getPipeline) and never ship in releases.
{ os: "linux", arch: "aarch64", profile: "asan", distro: "amazonlinux", release: "2023", features: ["docker"] },
{ os: "linux", arch: "x64", profile: "asan", distro: "amazonlinux", release: "2023", features: ["docker"] },
{ os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.23" },
{ os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.23" },
Expand Down Expand Up @@ -194,6 +197,7 @@ const testPlatforms = [
{ os: "linux", arch: "aarch64", distro: "debian", release: "13", tier: "latest" },
{ os: "linux", arch: "x64", distro: "debian", release: "13", tier: "latest" },
{ os: "linux", arch: "x64", baseline: true, distro: "debian", release: "13", tier: "latest" },
{ os: "linux", arch: "aarch64", profile: "asan", distro: "debian", release: "13", tier: "latest" },
{ os: "linux", arch: "x64", profile: "asan", distro: "debian", release: "13", tier: "latest" },
{ os: "linux", arch: "aarch64", distro: "ubuntu", release: "25.04", tier: "latest" },
{ os: "linux", arch: "x64", distro: "ubuntu", release: "25.04", tier: "latest" },
Expand Down Expand Up @@ -688,7 +692,12 @@ function getTargetTriplet(platform) {
* @returns {boolean}
*/
function needsBaselineVerification(platform) {
const { os, arch, baseline } = platform;
const { os, arch, baseline, profile } = platform;
// ASAN builds are PR-only test artifacts that never ship, and they don't
// produce the ${triplet}-profile.zip this step downloads (the asan link
// uploads ${triplet}-asan.zip instead — see scripts/build/ci.ts). The
// release lanes already cover instruction-policy verification.
if (profile === "asan") return false;
if (os === "linux") return (arch === "x64" && baseline) || arch === "aarch64";
if (os === "windows") return arch === "x64" && baseline;
return false;
Expand Down Expand Up @@ -717,7 +726,9 @@ const SDE_URL = `https://downloadmirror.intel.com/859732/sde-external-${SDE_VERS
*/
function hasWebKitChanges(options) {
const { changedFiles = [] } = options;
return changedFiles.some(file => file.includes("SetupWebKit.cmake"));
// The WebKit pin (WEBKIT_VERSION) lives in scripts/build/deps/webkit.ts;
// it was previously in cmake/tools/SetupWebKit.cmake, which no longer exists.
return changedFiles.some(file => file.includes("scripts/build/deps/webkit.ts"));
}

/**
Expand Down
11 changes: 10 additions & 1 deletion scripts/verify-baseline-static/allowlist-x64-windows.txt
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,16 @@ sinf [AVX, FMA]
sinh_fma [AVX, FMA]
sinhf_fma [AVX, FMA]
strnlen [AVX, AVX2]
strpbrk [AVX, AVX2] # UCRT vectorized str routine, runtime-gated on __isa_available
# strpbrk: UCRT vectorized str routine, runtime-gated on __isa_available.
# RTM is a disassembly artifact, not real code: MSVC puts the routine's
# switch jump table in .text right after the body's ret. The 4-byte table
# entries (verified by disassembling bun-profile.exe at the flagged VA:
# `c6 f8 1a 03` = target offset 0x031af8c6, one of a +7-stride run matching
# the pslldq/jmp ladder dispatched via `jmp *table(%rsi,%r9,4)`) happen to
# decode as `xabort imm8` (C6 F8 ib). Layout shifts (e.g. a WebKit bump)
# change the offsets stored in the table, so which junk instruction the
# linear sweep sees here varies build to build.
strpbrk [AVX, AVX2, RTM]
tan [AVX, FMA]
tanf [AVX, FMA]
tanh_fma [AVX, FMA]
Expand Down
20 changes: 12 additions & 8 deletions src/http/AsyncHTTP.rs
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,12 @@ fn send_sync_callback(
if let Some(mut real) = async_http.real {
// SAFETY: `real` outlives the HTTP-thread copy by construction.
let real = unsafe { real.as_mut() };
real.response = async_http.response;
// Don't copy `response` back: it's a bitwise alias of
// `metadata.response`, whose buffers are freed when the caller drops
// the `HTTPResponseMetadata` returned by `send_sync`, so a stored copy
// would dangle. (Zig copied it, but Zig also leaked the metadata.)
// No sync caller reads `real.response`; they use the returned value.
real.response = None;
real.request = async_http.request.take();
real.response_headers = core::mem::take(&mut async_http.response_headers);
real.response_encoding = async_http.response_encoding;
Expand All @@ -679,7 +684,7 @@ fn send_sync_callback(
}

impl<'a> AsyncHTTP<'a> {
pub fn send_sync(&mut self) -> Result<picohttp::Response<'static>, bun_core::Error> {
pub fn send_sync(&mut self) -> Result<crate::HTTPResponseMetadata, bun_core::Error> {
crate::http_thread::init(&Default::default());

// PORT NOTE: Zig leaked `ctx` (never destroyed). `Box::leak` is forbidden
Expand All @@ -705,12 +710,11 @@ impl<'a> AsyncHTTP<'a> {
return Err(err);
}
debug_assert!(result.metadata.is_some());
// The returned `Response` borrows `metadata.owned_buf` (status text +
// header slices). Zig's `sendSync` returns `result.metadata.?.response`
// and never `deinit`s the metadata; mirror that by suppressing Drop so
// the borrowed buffer outlives the call. `send_sync` is one-shot CLI.
let metadata = core::mem::ManuallyDrop::new(result.metadata.unwrap());
Ok(metadata.response)
// `metadata.response` borrows `metadata.owned_buf` (status text +
// header slices), so hand the whole metadata to the caller — its
// `Deref` exposes the `Response`, and `Drop` reclaims the buffers
// (Zig's `sendSync` returned the bare `Response` and leaked them).
Ok(result.metadata.unwrap())
}

// ──────────────────────────────────────────────────────────────────────
Expand Down
17 changes: 17 additions & 0 deletions src/http/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,23 @@ pub struct HTTPResponseMetadata {
pub response: bun_picohttp::Response<'static>,
}

// `send_sync` returns the whole metadata so the borrowed `Response` cannot
// outlive its backing buffers; callers only read `Response` fields, so a
// `Deref` keeps the call sites unchanged.
//
// CAUTION: the `Response` is typed `'static`, but its status text and header
// slices actually borrow the sibling `owned_buf` (and the heap headers slice)
// that `Drop` frees. The borrow checker cannot catch a slice from
// `res.headers.get(..)` escaping the metadata binding's scope — keep all such
// slices strictly inside it.
impl core::ops::Deref for HTTPResponseMetadata {
type Target = bun_picohttp::Response<'static>;

fn deref(&self) -> &Self::Target {
&self.response
}
}
Comment thread
robobun marked this conversation as resolved.

impl Default for HTTPResponseMetadata {
fn default() -> Self {
Self {
Expand Down
21 changes: 21 additions & 0 deletions src/jsc/ResolvedSource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ pub struct ResolvedSource {
// -- Bytecode cache fields --
pub bytecode_cache: *mut u8,
pub bytecode_cache_size: usize,
/// Whether `bytecode_cache` is a heap allocation (`heap::into_raw`'d
/// `Box<[u8]>`) that the C++ `JSC::CachedBytecode` destructor must free.
/// `false` when the bytes point into the standalone executable's embedded
/// section (`bun build --compile`). Deliberately separate from
/// `source_code_needs_deref`, which C++ mutates while consuming the
/// source string.
pub bytecode_cache_needs_deref: bool,
pub module_info: *mut c_void,
/// The file path used as the source origin for bytecode cache validation.
/// JSC validates bytecode by checking if the origin URL matches exactly what
Expand All @@ -68,6 +75,7 @@ impl Default for ResolvedSource {
already_bundled: false,
bytecode_cache: core::ptr::null_mut(),
bytecode_cache_size: 0,
bytecode_cache_needs_deref: false,
module_info: core::ptr::null_mut(),
bytecode_origin_path: BunString::empty(),
}
Expand Down Expand Up @@ -139,6 +147,19 @@ impl Drop for OwnedResolvedSource {
if self.0.source_code_needs_deref {
self.0.source_code.deref();
}
if self.0.bytecode_cache_needs_deref && !self.0.bytecode_cache.is_null() {
// SAFETY: `bytecode_cache_needs_deref` marks the pointer as the
// `heap::into_raw`'d `Box<[u8]>` built on the runtime
// `// @bun @bytecode` path. `Drop` only runs when `into_ffi()` was
// never called, so Rust is still the sole owner of the buffer
// (the C++ `CachedBytecode` destructor frees it otherwise).
unsafe {
drop(bun_core::heap::take(core::ptr::slice_from_raw_parts_mut(
self.0.bytecode_cache,
self.0.bytecode_cache_size,
)));
}
}
self.0.specifier.deref();
self.0.source_url.deref();
self.0.bytecode_origin_path.deref();
Expand Down
32 changes: 24 additions & 8 deletions src/jsc/RuntimeTranspilerStore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1032,17 +1032,33 @@ impl TranspilerJob {
}

if !matches!(parse_result.already_bundled, AlreadyBundled::None) {
let bytecode_slice = parse_result.already_bundled.bytecode_slice();
// Move the bytecode out of `parse_result` and transfer ownership
// to C++ (matches `transpile_source_code_inner` in jsc_hooks.rs and
// the Zig default_allocator semantics): `parse_result` is dropped
// when this function returns, so a borrowed `bytecode_slice()`
// pointer would dangle by the time the JS thread builds the
// `JSC::CachedBytecode` over it. `bytecode_cache_needs_deref`
// tells the C++ CachedBytecode destructor to free the buffer.
let already_bundled = core::mem::take(&mut parse_result.already_bundled);
let is_commonjs_module = already_bundled.is_common_js();
let (bytecode_cache, bytecode_cache_size) = match already_bundled {
AlreadyBundled::Bytecode(bytes) | AlreadyBundled::BytecodeCjs(bytes) => {
let len = bytes.len();
if len == 0 {
(ptr::null_mut(), 0)
} else {
(bun_core::heap::into_raw(bytes).cast::<u8>(), len)
}
}
_ => (ptr::null_mut(), 0),
};
self.resolved_source = OwnedResolvedSource::from(ResolvedSource {
source_code: String::clone_latin1(&parse_result.source.contents),
already_bundled: true,
bytecode_cache: if !bytecode_slice.is_empty() {
bytecode_slice.as_ptr().cast_mut()
} else {
ptr::null_mut()
},
bytecode_cache_size: bytecode_slice.len(),
is_commonjs_module: parse_result.already_bundled.is_common_js(),
bytecode_cache,
bytecode_cache_size,
bytecode_cache_needs_deref: !bytecode_cache.is_null(),
is_commonjs_module,
tag: this_tag,
..Default::default()
});
Expand Down
11 changes: 10 additions & 1 deletion src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4495,10 +4495,19 @@ impl VirtualMachine {
// each stored map and `deinit()`s the sibling `saved_source_map_table`.
drop(core::mem::take(&mut self.source_mappings));

if let Some(rare) = self.rare_data.take() {
if self.rare_data.is_some() {
// Must run BEFORE `rare_data.take()`: `CronJob::clear_all_for_vm`
// reads `vm.rare_data` to drain the job list and early-returns on
// `None`. Calling it after the take() made it a no-op, leaking
// every still-registered CronJob (and tripping the
// `cron_jobs.is_empty()` assert in `RareData::drop` when a job
// survived finalization, e.g. `process.exit()` from inside a cron
// callback).
if let Some(hooks) = runtime_hooks() {
(hooks.cron_clear_all_teardown)(self);
}
}
if let Some(rare) = self.rare_data.take() {
// Paired with `rare_data()`'s register_root_region. Without this,
// every terminated Worker leaves a stale LSAN root entry pointing
// into a freed arena.
Expand Down
11 changes: 10 additions & 1 deletion src/jsc/bindings/ZigSourceProvider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,20 @@ Ref<SourceProvider> SourceProvider::create(
const auto destructorNoOp = [](const void* ptr) {
// no-op, for bun build --compile.
};
const auto destructor = resolvedSource.needsDeref ? destructorPtr : destructorNoOp;
// NOTE: do not gate this on `needsDeref` — that flag tracks the
// `source_code` string +1 and is cleared above (and mutated by
// `JSCommonJSModule::evaluate`'s wrapper-override path) before we
// get here, which both leaked runtime `// @bun @bytecode` buffers
// and could free embedded standalone-binary bytecode.
const auto destructor = resolvedSource.bytecode_cache_needs_deref ? destructorPtr : destructorNoOp;

auto origin = getSourceOrigin();

Ref<JSC::CachedBytecode> bytecode = JSC::CachedBytecode::create(std::span<uint8_t>(resolvedSource.bytecode_cache, resolvedSource.bytecode_cache_size), destructor, {});
// The CachedBytecode now owns the buffer; clear the ownership
// marker (mirroring the `needsDeref` clearing above) so the copy
// stored in m_resolvedSource cannot be misread as still owning it.
resolvedSource.bytecode_cache_needs_deref = false;
auto provider = adoptRef(*new SourceProvider(
globalObject->bunVM(),
resolvedSource,
Expand Down
4 changes: 4 additions & 0 deletions src/jsc/bindings/headers-handwritten.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ typedef struct ResolvedSource {
// -- Bytecode cache fields --
uint8_t* bytecode_cache;
size_t bytecode_cache_size;
// Whether `bytecode_cache` is heap-owned (Rust `Box<[u8]>` via
// `heap::into_raw`) and must be freed by the CachedBytecode destructor.
// False for `bun build --compile`, where it points into the executable.
bool bytecode_cache_needs_deref;
void* module_info;
// File path used as source origin for bytecode cache validation.
// Converted to file:// URL. If empty, origin is derived from source_url.
Expand Down
5 changes: 4 additions & 1 deletion src/jsc/bindings/node/JSNodeHTTPServerSocket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,10 @@ void JSNodeHTTPServerSocket::onData(const char* data, int length, bool last)

WebCore::ScriptExecutionContext* scriptExecutionContext = globalObject->scriptExecutionContext();

if (scriptExecutionContext) {
// Mirror onClose: never allocate JS cells or post tasks while the VM is
// shutting down (this can be reached from uws close callbacks during the
// final GC, when allocation is forbidden and posted tasks never run).
if (scriptExecutionContext && !globalObject->isShuttingDown()) {
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(globalObject->vm());
JSC::JSUint8Array* buffer = WebCore::createBuffer(globalObject, std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(data), length));
auto chunk = JSC::JSValue(buffer);
Expand Down
25 changes: 16 additions & 9 deletions src/runtime/bake/DevServer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,12 @@ impl Drop for DevServer {
// SAFETY: stored ref from `init_from_any_blob`; no live borrow.
unsafe { StaticRoute::deref_(cached.as_ptr()) };
}
// Zig `RouteBundle.deinit`: `html.html_bundle.deref()` — release
// the intrusive ref taken by `get_or_put_route_bundle` when the
// bundle was stored. `html_bundle` is a raw `*mut`, so dropping
// the Vec would otherwise leak the route (and its HTMLBundle).
// SAFETY: the slot holds a counted ref taken at store time.
unsafe { bun_ptr::RefCount::<HTMLBundleRoute>::deref(html.html_bundle) };
}
}

Expand Down Expand Up @@ -3919,8 +3925,11 @@ pub(super) fn finalize_bundle(

dev.start_next_bundle_if_present();

// Unref the ref added in `start_async_bundle`
if let Some(server) = dev.server.as_mut() {
// Unref the ref added in `start_async_bundle`. Copy the `AnyServer`
// handle out (no `as_mut()`): the call can drop this very
// `Box<DevServer>` via `deinit_if_we_can`, so no reference into it
// may be live across the call.
if let Some(server) = dev.server {
server.on_static_request_complete();
}
};
Expand Down Expand Up @@ -6677,14 +6686,12 @@ impl UnrefSourceMapRequest {
// SAFETY: caller contract — ctx is the original Box allocation; no
// live borrow of *ctx exists.
let ctx = unsafe { bun_core::heap::take(ctx) };
// Copy the `AnyServer` handle out of the DevServer (no `as_mut()`):
// the call can drop the `Box<DevServer>` via `deinit_if_we_can`, so
// no reference into it may be live across the call.
// SAFETY: dev outlives the request
unsafe {
(*ctx.dev)
.server
.as_mut()
.unwrap()
.on_static_request_complete()
};
let server = unsafe { (*ctx.dev).server.unwrap() };
server.on_static_request_complete();
drop(ctx);
}

Expand Down
12 changes: 5 additions & 7 deletions src/runtime/bake/DevServer/ErrorReportRequest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,16 @@ impl ErrorReportRequest {
/// `ctx` must be the pointer returned by `heap::alloc` in `run`; called
/// exactly once (success path here, or via `on_error` on abort/error).
pub(crate) fn finalize(ctx: *mut ErrorReportRequest) {
// Copy the `AnyServer` handle out of the DevServer (no `as_mut()`):
// the call can drop the `Box<DevServer>` via `deinit_if_we_can`, so
// no reference into it may be live across the call.
// SAFETY: `ctx` is the original Box allocation produced by `run`; no
// live borrow of `*ctx` exists (BodyReaderHandler hands us the raw
// pointer, never `&mut self`). Only reachable via `on_body`/`on_error`,
// both of which uphold this contract.
unsafe {
(*ctx)
.dev
.get_mut()
.server
.as_mut()
.unwrap()
.on_static_request_complete();
let server = (*ctx).dev.get_mut().server.unwrap();
server.on_static_request_complete();
drop(bun_core::heap::take(ctx));
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2732,6 +2732,9 @@ fn transpile_source_code_inner(
already_bundled: true,
bytecode_cache,
bytecode_cache_size,
// C++ owns the buffer (heap::into_raw above); the
// CachedBytecode destructor frees it.
bytecode_cache_needs_deref: !bytecode_cache.is_null(),
is_commonjs_module,
..Default::default()
}));
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/server/FileRoute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ impl FileRoute {
resp.clear_timeout();
// SAFETY: `this` is live (ref held by caller); `deref()` may free it.
unsafe {
if let Some(mut server) = (*this).server.get() {
if let Some(server) = (*this).server.get() {
server.on_static_request_complete();
}
Self::deref(this);
Expand Down
Loading
Loading