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
50 changes: 50 additions & 0 deletions patches/boringssl/fork-detect-startup-snapshot.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
--- a/crypto/rand/fork_detect.cc
+++ b/crypto/rand/fork_detect.cc
@@ -171,6 +171,17 @@
return current_generation;
}

+// Bun: a process resumed from a startup snapshot inherits these statics from the process that built it, including a
+// page address that is not mapped here. Restore is single-threaded; installing this process's own page is enough,
+// since the once flag above already reads as done.
+extern "C" void CRYPTO_fork_detect_reinit_for_startup_snapshot(void) {
+ if (g_fork_detect_addr == nullptr) return; // never initialized (this process's once will), or WIPEONFORK was unavailable there (stays in the always-reseed fallback)
+ uint64_t generation_in_builder = g_fork_generation;
+ g_fork_detect_addr = nullptr;
+ init_fork_detect();
+ g_fork_generation = generation_in_builder + 1; // a restore duplicates the address space like a fork: anything cached against the builder's value must reseed
+}
Comment thread
claude[bot] marked this conversation as resolved.
+
Comment thread
claude[bot] marked this conversation as resolved.
void bssl::CRYPTO_fork_detect_force_madv_wipeonfork_for_testing(int on) {
g_force_madv_wipeonfork = 1;
g_force_madv_wipeonfork_enabled = on;
@@ -197,6 +208,14 @@
g_atfork_fork_generation = 1;
}

+// Bun: see the WIPEONFORK variant; here the build process's atfork registration does not exist in this process.
+extern "C" void CRYPTO_fork_detect_reinit_for_startup_snapshot(void) {
+ if (g_atfork_fork_generation == 0) return; // as above: nothing to redo if the build process never initialized it
+ uint64_t generation_in_builder = g_atfork_fork_generation;
+ init_pthread_fork_detection();
+ g_atfork_fork_generation = generation_in_builder + 1; // as above
+}
+
uint64_t bssl::CRYPTO_get_fork_generation() {
CRYPTO_once(&g_pthread_fork_detection_once, init_pthread_fork_detection);

@@ -210,6 +229,7 @@
// assume address space duplication is not a concern and adding entropy to
// every RAND_bytes call is not needed.
uint64_t bssl::CRYPTO_get_fork_generation() { return 0xc0ffee; }
+extern "C" void CRYPTO_fork_detect_reinit_for_startup_snapshot(void) {}

#else

@@ -218,5 +238,6 @@
// space duplication could have occurred on any call entropy must be added to
// every RAND_bytes call.
uint64_t bssl::CRYPTO_get_fork_generation() { return 0; }
+extern "C" void CRYPTO_fork_detect_reinit_for_startup_snapshot(void) {}

#endif
4 changes: 3 additions & 1 deletion scripts/build/deps/boringssl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export const boringssl: Dependency = {
// Upstream mem.cc gates OPENSSL_memory_* weak-symbol overrides on __ELF__;
// on Mach-O/COFF the hooks compile to static nullptr and OPENSSL_malloc goes
// straight to libc. Declare them as plain externs so lib.rs binds everywhere.
patches: ["patches/boringssl/require-memory-hooks.patch"],
// fork-detect: lets a process resumed from a startup snapshot re-run fork detection's per-process setup
// (its statics arrive holding the build process's WIPEONFORK page / atfork registration).
patches: ["patches/boringssl/require-memory-hooks.patch", "patches/boringssl/fork-detect-startup-snapshot.patch"],

build: cfg => {
// win-x64 uses NASM-syntax .asm; everything else (including win-aarch64)
Expand Down
20 changes: 16 additions & 4 deletions scripts/build/deps/mimalloc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import type { Dependency, DirectBuild } from "../source.ts";

const MIMALLOC_COMMIT = "1803341d6241d8fa4b3f65fa68cb13a32ad92f04";
const MIMALLOC_COMMIT = "7aca49e5b5b49ce2e44490a604d93a4be7a39759"; // oven-sh/mimalloc#13 (snapshot support); swap for the merge sha before landing

export const mimalloc: Dependency = {
name: "mimalloc",
Expand All @@ -27,14 +27,16 @@ export const mimalloc: Dependency = {
build: cfg => {
// ─── Override behavior (global malloc replacement) ───
// ASAN: OFF — ASAN interceptors must see the real malloc.
// macOS: OFF — overriding via zone/interpose breaks NAPI addons and
// system frameworks (SecureTransport etc.).
// macOS: OFF by default — overriding via zone/interpose breaks NAPI addons
// and system frameworks (SecureTransport etc.); BUN_MIMALLOC_OVERRIDE_DARWIN=1
// opts in (what startup snapshots need there).
// Linux: ON — the main win. All malloc/free routes through mimalloc,
// including WebKit's bmalloc when it falls back to system malloc.
// Windows: OFF — Bun links the static CRT and calls mi_* directly;
// alloc-override.c emits _expand/_msize/free which duplicate
// against libucrt(d) at link time.
const override = cfg.linux && !cfg.asan;
const override = !cfg.asan && (cfg.linux || (cfg.darwin && process.env.BUN_MIMALLOC_OVERRIDE_DARWIN === "1"));
const osxZone = cfg.darwin && !cfg.asan && process.env.BUN_MIMALLOC_OVERRIDE_DARWIN === "1";

const defines: Record<string, string | number | true> = {
// The .a path; gates symbol visibility in mimalloc/internal.h.
Expand Down Expand Up @@ -67,6 +69,13 @@ export const mimalloc: Dependency = {

if (cfg.abi === "musl") defines.MI_LIBC_MUSL = 1;
if (override) defines.MI_MALLOC_OVERRIDE = true;
if (osxZone) defines.MI_OSX_ZONE = 1;

// Snapshots (src/jsc/bindings/StartupSnapshot.cpp): executables carrying a snapshot get deterministic address hints from
// their first allocation; a process building one (BUN_STARTUP_SNAPSHOT_OUT) keeps its heap at the base that becomes the snapshot.
// Only where snapshots exist (Snapshot.h): the hook runs inside mimalloc's own initialization, before anything else.
const snapshots = cfg.darwin || cfg.linux;
if (snapshots) defines.MI_STARTUP_SNAPSHOT_BUILD_ENV = "BUN_STARTUP_SNAPSHOT_OUT"; // quoted into a C string literal by the builder

if (cfg.debug) {
// Heavy debug checks: guard bytes, freed-memory poisoning, double-free
Expand All @@ -93,6 +102,9 @@ export const mimalloc: Dependency = {
// Bare token (mi_stringify() pastes it into the banner string), so
// it can't go through DirectBuild.defines which would quote it.
`-DMI_CMAKE_BUILD_TYPE=${cfg.buildType.toLowerCase()}`,
// Bare token as well: the name of the function (defined in c-bindings.cpp) mimalloc calls to learn whether this
// executable carries a snapshot; see the MI_STARTUP_SNAPSHOT_* note above.
...(snapshots ? ["-DMI_STARTUP_SNAPSHOT_HOST_FN=bun_startup_snapshot_placement_wanted"] : []),
];

// TLS model: initial-exec for the static link into bun's executable
Expand Down
3 changes: 2 additions & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "447082ab6897278727b44e1ba3c326ae6e1504c3";
export const WEBKIT_VERSION = "autobuild-preview-pr-397-4c0ca85e"; // oven-sh/WebKit#397 (snapshot support, on current main) — swap for the merge sha before landing

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down Expand Up @@ -331,6 +331,7 @@ export const webkit: Dependency = {
CMAKE_EXPORT_COMPILE_COMMANDS: "ON",
USE_BUN_JSC_ADDITIONS: "ON",
USE_BUN_EVENT_LOOP: "ON",
...(cfg.windows || cfg.asan ? {} : { USE_MIMALLOC: "ON", USE_EXTERNAL_MIMALLOC: "ON" }), // as every other mimalloc routing: not under ASAN
ENABLE_BUN_SKIP_FAILING_ASSERTIONS: "ON",
ALLOW_LINE_AND_COLUMN_NUMBER_IN_BUILTINS: "ON",
ENABLE_REMOTE_INSPECTOR: "ON",
Expand Down
5 changes: 5 additions & 0 deletions scripts/build/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,11 @@ export const defines: Flag[] = [
flag: "USE_BUN_MIMALLOC=1",
desc: "Use mimalloc as default allocator",
},
{
flag: "BUN_MIMALLOC_ZONE_OVERRIDE=1",
when: c => c.darwin && !c.asan && process.env.BUN_MIMALLOC_OVERRIDE_DARWIN === "1", // keep in step with `osxZone` in deps/mimalloc.ts
desc: "mimalloc is registered as the process's malloc zone (what startup snapshots need on macOS)",
},

// ─── Config-dependent ───
{
Expand Down
2 changes: 2 additions & 0 deletions src/boringssl_sys/boringssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,8 @@ unsafe extern "C" {
/// In the event that sufficient random data can not be obtained, `abort`
/// is called. See `rand_bytes` for the safe wrapper.
pub(crate) fn RAND_bytes(buf: *mut u8, len: usize) -> c_int;
/// Bun addition (patches/boringssl/fork-detect-startup-snapshot.patch): redo fork detection's per-process setup.
pub(crate) fn CRYPTO_fork_detect_reinit_for_startup_snapshot();

// ── ERR ──────────────────────────────────────────────────────────────
// Thread-local error queue — no pointer args, no preconditions.
Expand Down
9 changes: 9 additions & 0 deletions src/boringssl_sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
pub mod boringssl;
pub use boringssl::*;

/// After a startup-snapshot restore: fork detection's statics describe the build process (patches/boringssl/fork-detect-startup-snapshot.patch).
/// # Safety
/// No other thread may be using BoringSSL: it rewrites the library's fork-detection statics. A snapshot restore, before
/// any other thread of the new process exists, is the intended caller.
pub unsafe fn reinit_fork_detection_after_snapshot_restore() {
// SAFETY: the caller upholds the single-threaded requirement above; the hook touches only BoringSSL's own statics.
unsafe { boringssl::CRYPTO_fork_detect_reinit_for_startup_snapshot() }
}

/// Fill `buf` with cryptographically-secure random bytes via BoringSSL `RAND_bytes`.
///
/// BoringSSL's `RAND_bytes` is a thread-local AES-CTR DRBG seeded once from the
Expand Down
74 changes: 56 additions & 18 deletions src/bun_alloc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1432,7 +1432,7 @@ macro_rules! bss_singleton {
fn slow() -> *mut $ty {
let p = $crate::bss_heap_init::<$ty>(<$ty>::init_at).as_ptr();
// Race: two threads may both reach here. The mmap'd region is
// process-lifetime and never freed, so the loser is leaked
// process-lifetime and never freed, so the loser is leaked (its arena bytes too: first touch is single-threaded, so unlike the arena mapping this need not be claim-first)
// (≤ one per declare site, which in practice is single-threaded
// — `FileSystem::init` runs once on the main thread). The CAS
// is the publication barrier.
Expand Down Expand Up @@ -1533,26 +1533,32 @@ fn bss_arena_bump(size: usize, align: usize) -> *mut u8 {
static CURSOR: AtomicUsize = AtomicUsize::new(0);

// Resolve the arena base. Fast path is one Acquire load; the cold path
// maps the 4 MiB region once and publishes via CAS. A losing racer's
// mapping is leaked (≤ one per process; `MAP_NORESERVE` so it costs no
// committed memory) — same race policy as `bss_singleton!`.
// maps the 4 MiB region exactly once: a racer claims the right to map before mapping, and the others wait for the
// result. (Map-then-race would let a loser consume a placement hint too, and the arena's address has to be the same in
// every process that may build or restore a snapshot.)
Comment on lines 1535 to +1538

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bss_singleton!'s slow path (lib.rs:1432-1447) still uses init-then-CAS-and-leak-loser, while this PR hardened bss_arena_bump to claim-then-map and deleted the pre-existing "same race policy as bss_singleton!" cross-reference — so two racers can each bump CURSOR and land the singleton at a scheduling-dependent offset inside the arena. Nit only: the race can't fire (both sites document single-threaded first-touch from FileSystem::init/Transpiler::init, lines 1436-1437 / 1566), and unlike #17 the loser doesn't consume a SNAPSHOT_HINT slot — but a debug_assert! that the STORAGE CAS at 1439 never observes a winner when mi_startup_snapshot_hints_enabled() would keep the sibling site the deleted comment named consistent with map_arena_once.

Extended reasoning...

What the finding is

The 06:49 fix (responding to earlier comment #17) rewrote bss_arena_bump's cold path from map-then-CAS-and-leak to claim-then-map, with the new comment at lib.rs:1536-1538: "Map-then-race would let a loser consume a placement hint too, and the arena's address has to be the same in every process that may build or restore a snapshot." That rewrite deleted the pre-existing cross-reference "same race policy as bss_singleton!" — but bss_singleton! itself (lib.rs:1432-1447) was left on the old policy: bss_heap_init first, then CAS on STORAGE, and "the loser is leaked" (comment at 1434-1436).

If two threads raced a singleton's first touch, both would call bss_heap_initbss_lazy_bytesbss_arena_bump, both would succeed the CURSOR CAS loop at line 1578 (each reserving a distinct offset), and which one wins the STORAGE CAS at line 1439 is scheduling-dependent and independent of which got the lower offset. So the singleton's address (base + offset) — and, because CURSOR is left one size_of::<T>() ahead, every subsequent singleton's offset — would differ between runs.

Step-by-step

  1. mi_startup_snapshot_hints_enabled() is true; threads A and B both first-touch the same bss_singleton! site.
  2. Both load STORAGE (line 1423) → null → both enter slow().
  3. Both call bss_heap_init::<T>bss_arena_bump(size_of::<T>(), align_of::<T>()).
  4. BASE is now serialized (map_arena_once, the 06:49 fix), so both see the same deterministic base.
  5. Both enter the CURSOR CAS loop (line 1568). One succeeds 0 → size, the other retries and succeeds size → 2*size. Both return distinct base + offset_{A,B}.
  6. Both run init_at on their respective slot; both attempt STORAGE.compare_exchange(null, p, ...) at line 1439.
  7. Whichever wins is scheduling-dependent. The loser's arena slot is leaked. CURSOR is now 2*size instead of size.

Addressing the refutation

One reviewer's counter-argument is worth engaging directly:

  • "The loser doesn't consume a placement hint." Correct — this is not the same hazard as Fix JSX parser bug: // comment after tagName and before closing tag is broken #17. bss_singleton!'s loser bumps CURSOR (in-arena offset), not SNAPSHOT_HINT (arena base). The 06:49 fix's specific rationale (hint over-consumption making BASE non-deterministic) doesn't apply here.
  • "On restore, the restored STORAGE already points at the restored arena slot — offsets don't need to be re-derived." This depends on what StartupSnapshot.cpp (landing in Startup snapshots (2/4): take/restore runtime and Bun.startupSnapshot #37260) captures. If the executable's .data segment (where STORAGE, BASE, CURSOR live as statics) is captured verbatim alongside the 0x1f0_0000_0000 arena window, everything is self-consistent regardless of which offset the build process picked, and this is at most a build-reproducibility concern. If .data is not captured and the restore process re-derives STORAGE by re-running slow(), then it must arrive at the same offset the build process got — which requires the build process to have been race-free. This PR alone can't settle that; Startup snapshots (2/4): take/restore runtime and Bun.startupSnapshot #37260 does.
  • "'Fix the whole class' applies to sibling sites sharing the same bug, not the same pattern." Fair distinction. What tips it toward mentioning: the deleted comment explicitly named bss_singleton! as the intentionally-coupled sibling, and the author's own rationale ("has to be the same in every process") generalizes to offsets if .data isn't snapshotted. The two sites now silently diverge where they were documented as the same.

Why nit, not normal

The accommodated race cannot fire in practice: line 1436-1437 says "in practice is single-threaded — FileSystem::init runs once on the main thread", and line 1566-1567 says "called a handful of times from Transpiler::init on the main thread" — the same reachability caveat that made #17 a nit which the author fixed anyway. Whether offset non-determinism would even matter for correctness depends on #37260's capture semantics, which this PR doesn't establish. So this is a consistency/hardening observation, not a demonstrated failure.

Suggested fix

Either of:

  • Debug-assert (cheapest, mirrors map_arena_once): at line 1446, in the Err(winner) arm, add debug_assert!(!$crate::mimalloc::mi_startup_snapshot_hints_enabled(), "bss_singleton! raced under snapshot hints — offset non-determinism"); before returning winner. This costs nothing in release and catches the case if snapshot builds ever reach first-touch multi-threaded.
  • Claim-before-init: apply the same CLAIMED AtomicBool + spin pattern from map_arena_once to slow(), so exactly one racer calls bss_heap_init and the rest wait for STORAGE.

The first is proportionate to a race that can't fire; the second fully re-couples the two sites the deleted comment said were the same.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this one as it is: the singleton's own comment already records that first touch is single-threaded, and unlike the arena its address is not something a snapshot depends on directly (the arena's is), which is why only that site was made claim-first.

let mut base = BASE.load(Ordering::Acquire);
if base.is_null() {
static CLAIMED: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
#[cold]
#[inline(never)]
fn map_arena() -> *mut u8 {
bss_mmap_noreserve(BSS_ARENA_SIZE)
fn map_arena_once() -> *mut u8 {
if CLAIMED
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
let fresh = bss_mmap_noreserve(BSS_ARENA_SIZE);
BASE.store(fresh, Ordering::Release);
return fresh;
}
loop {
let b = BASE.load(Ordering::Acquire);
if !b.is_null() {
return b;
}
core::hint::spin_loop();
}
}
let fresh = map_arena();
base = match BASE.compare_exchange(
core::ptr::null_mut(),
fresh,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => fresh,
Err(winner) => winner, // leak `fresh` (untouched MAP_NORESERVE)
};
base = map_arena_once();
}

// Bump the cursor: round up to `align`, reserve `size`. CAS loop because
Expand All @@ -1579,6 +1585,37 @@ fn bss_arena_bump(size: usize, align: usize) -> *mut u8 {
}
}

/// Where a snapshot may be built or mapped (the targets deps/mimalloc.ts builds the hint machinery for), this reservation
/// has to land at the same address in every process; the allocator decides that once and hands out the same kind of bump
/// hint it uses for its own reservations. Null = no preference.
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "android"))]
fn snapshot_reserve_hint(len: usize) -> *mut libc::c_void {
// Bottom of the address window StartupSnapshot.cpp captures as ours (0x1f0'0000'0000..); mimalloc's own hinted arenas start above it.
const SNAPSHOT_RESERVE_BASE: usize = 0x1f0_0000_0000;
const SNAPSHOT_RESERVE_ALIGN: usize = 4 << 20;
static SNAPSHOT_HINT: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
let mut hint: *mut libc::c_void = core::ptr::null_mut();
if mimalloc::mi_startup_snapshot_hints_enabled() {
let _ = SNAPSHOT_HINT.compare_exchange(
0,
SNAPSHOT_RESERVE_BASE,
core::sync::atomic::Ordering::AcqRel,
core::sync::atomic::Ordering::Acquire,
);
let aligned = (len + SNAPSHOT_RESERVE_ALIGN - 1) & !(SNAPSHOT_RESERVE_ALIGN - 1);
hint = SNAPSHOT_HINT.fetch_add(aligned, core::sync::atomic::Ordering::AcqRel)
as *mut libc::c_void;
Comment thread
claude[bot] marked this conversation as resolved.
}
hint
}
#[cfg(all(
unix,
not(any(target_os = "macos", target_os = "linux", target_os = "android"))
))]
fn snapshot_reserve_hint(_len: usize) -> *mut libc::c_void {
core::ptr::null_mut()
}

/// One `mmap(MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE)` of `len` RW bytes.
/// Aborts on `MAP_FAILED`. Returned pointer is page-aligned and the region
/// reads as all-zeros until written.
Expand All @@ -1595,11 +1632,12 @@ fn bss_mmap_noreserve(len: usize) -> *mut u8 {
const MAP_FLAGS: libc::c_int = libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_NORESERVE;
#[cfg(not(any(target_os = "linux", target_os = "android")))]
const MAP_FLAGS: libc::c_int = libc::MAP_PRIVATE | libc::MAP_ANONYMOUS;
let hint = snapshot_reserve_hint(len);
// SAFETY: anonymous private mapping — fd/offset ignored, `len` is non-zero
// (callers pass `size_of` of a non-ZST); failure handled below.
// (callers pass `size_of` of a non-ZST); the hint is advisory; failure handled below.
let p = unsafe {
libc::mmap(
core::ptr::null_mut(),
hint,
len,
libc::PROT_READ | libc::PROT_WRITE,
MAP_FLAGS,
Expand Down
53 changes: 53 additions & 0 deletions src/jsc/bindings/c-bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,52 @@ extern "C" uint64_t* Bun__getStandaloneModuleGraphELFVaddr()

#endif // OS(DARWIN) / __linux__

// Whether this executable carries a payload at all; StartupSnapshot.cpp gates on it. (The allocator asks the narrower question below.)
extern "C" __attribute__((visibility("default"), used)) int bun_is_compiled_executable(void)
{
return BUN_COMPILED.size != 0;
}

#if OS(DARWIN) || defined(__linux__) // the only builds whose allocator is given this hook (deps/mimalloc.ts)
// Layout of the trailer the standalone graph writes at the end of its payload (StandaloneModuleGraph.rs `Offsets`; the runtime
// that adds the snapshot fields to it also const-asserts these three numbers, so the two cannot drift apart): ... | Offsets (kOffsetsSize bytes) | 16-byte trailer magic. Only the two fields that
// say "marked to take a snapshot" and "carries one" are read here, because this runs before main, from the allocator.
static constexpr size_t kOffsetsSize = 40;
static constexpr size_t kOffsetsFlagsOffset = 28;
static constexpr size_t kOffsetsSnapshotLengthOffset = 36;
static constexpr uint32_t kTakeStartupSnapshotFlag = 1u << 4;
static constexpr char kPayloadTrailer[16] = { '\n', '-', '-', '-', '-', ' ', 'B', 'u', 'n', '!', ' ', '-', '-', '-', '-', '\n' };

// Asked by the pinned mimalloc during its own initialization (MI_STARTUP_SNAPSHOT_HOST_FN): deterministic placement is only
// wanted by an executable that is marked to take a snapshot or carries one, so an ordinary compiled executable pays nothing.
extern "C" __attribute__((visibility("default"), used)) int bun_startup_snapshot_placement_wanted(void)
{
const uint8_t* base;
uint64_t len;
#if OS(DARWIN)
base = BUN_COMPILED.data;
len = BUN_COMPILED.size;
#else
if (!BUN_COMPILED.size)
return 0;
// BUN_COMPILED.size holds the injected payload's address: a BlobHeader-shaped [u64 length][bytes...], but only page-aligned
// (4K on x86-64), so it must not be read through the 16K-aligned type.
const uint8_t* header = reinterpret_cast<const uint8_t*>(static_cast<uintptr_t>(BUN_COMPILED.size));
memcpy(&len, header, sizeof len);
base = header + sizeof(uint64_t);
#endif
if (len < kOffsetsSize + sizeof kPayloadTrailer)
return 0;
if (memcmp(base + len - sizeof kPayloadTrailer, kPayloadTrailer, sizeof kPayloadTrailer) != 0)
return 0;
const uint8_t* offsets = base + len - sizeof kPayloadTrailer - kOffsetsSize;
uint32_t flags, snapshotLength;
memcpy(&flags, offsets + kOffsetsFlagsOffset, sizeof flags);
memcpy(&snapshotLength, offsets + kOffsetsSnapshotLengthOffset, sizeof snapshotLength);
return (flags & kTakeStartupSnapshotFlag) != 0 || snapshotLength != 0;
}
Comment thread
claude[bot] marked this conversation as resolved.
#endif

#elif defined(_WIN32)
// Windows PE section handling
#include <windows.h>
Expand Down Expand Up @@ -1161,4 +1207,11 @@ extern "C" uint8_t* Bun__getStandaloneModuleGraphPEData()
return pe_section_data;
}

// Called by StartupSnapshot.cpp's unsupported-platform stubs (Bun__isCompiledExecutable); the PE payload is loaded later by the
// Rust side, and nothing here needs to know about it before then.
extern "C" int bun_is_compiled_executable(void)
{
return 0;
}

#endif
3 changes: 3 additions & 0 deletions src/mimalloc_sys/mimalloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ unsafe extern "C" {
/// free blocks inside its still-used pages, and hands the arena purge to the scavenger.
/// Safe on any thread; a no-op on a thread that never allocated. No preconditions.
pub safe fn mi_on_thread_idle();
/// Whether this process places its OS reservations deterministically (an executable that can carry a heap
/// snapshot, or `MIMALLOC_DETERMINISTIC_HINT=1`); decided once. No preconditions.
pub safe fn mi_startup_snapshot_hints_enabled() -> bool;
pub fn mi_stats_print_out(out: core::option::Option<mi_output_fun>, arg: *mut c_void);
pub fn mi_process_info(
elapsed_msecs: *mut usize,
Expand Down
2 changes: 1 addition & 1 deletion test/js/node/process/process.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ it("process.versions", () => {
const expectedVersions = {
boringssl: "1a41b9025c2c0a37edd07ff10f6944f03e028522",
libarchive: "ded82291ab41d5e355831b96b0e1ff49e24d8939",
mimalloc: "1803341d6241d8fa4b3f65fa68cb13a32ad92f04",
mimalloc: "7aca49e5b5b49ce2e44490a604d93a4be7a39759",
picohttpparser: "066d2b1e9ab820703db0837a7255d92d30f0c9f5",
zlib: "12731092979c6d07f42da27da673a9f6c7b13586",
tinycc: "05f0fafaa3be31e31d7b4b5c17dc60f62c991171",
Expand Down