diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx
index 77cb6a6ba3c4..a99232595f95 100644
--- a/docs/bundler/executables.mdx
+++ b/docs/bundler/executables.mdx
@@ -345,6 +345,14 @@ Bytecode compilation moves parsing overhead for large input files from runtime t
Bytecode compilation supports both `cjs` and `esm` formats when used with `--compile`.
+### Startup snapshots (experimental)
+
+`--snapshot` goes one step further than bytecode: after producing the executable, `bun build` runs it once and embeds a snapshot of its started-up state — modules evaluated, startup objects built, JIT code included — so later launches resume from that point instead of booting. See [Startup Snapshots](/bundler/startup-snapshots).
+
+```bash icon="terminal" terminal
+bun build --compile --bytecode --snapshot ./app.ts --outfile myapp
+```
+
### What do these flags do?
The `--minify` argument reduces the size of the transpiled output code. For a large application, this can save megabytes of space. For smaller applications, it might still improve start time a little.
diff --git a/docs/bundler/startup-snapshots.mdx b/docs/bundler/startup-snapshots.mdx
new file mode 100644
index 000000000000..f78b2ccd0d3d
--- /dev/null
+++ b/docs/bundler/startup-snapshots.mdx
@@ -0,0 +1,159 @@
+---
+title: Startup Snapshots
+description: Embed a snapshot of your program's started-up state in a compiled executable, so every launch resumes instead of booting
+---
+
+A startup snapshot is a build-time optimization for [single-file executables](/bundler/executables). `bun build` runs your executable once, and when it has finished starting up, the runtime writes out the state of the whole process — every module loaded and evaluated, every object your startup code built, JIT-compiled code included — and embeds it in the executable. Every later launch maps that snapshot back into memory and continues from there. The work your program does while starting up happens once, on the build machine, instead of on every launch.
+
+```bash icon="terminal" terminal
+bun build --compile --snapshot ./app.ts --outfile myapp
+```
+
+This is experimental. It is available on macOS and Linux (glibc).
+
+## What it saves
+
+[Bytecode caching](/bundler/bytecode) removes parsing and compiling from startup; the modules still have to be evaluated on every launch, and everything they build — parsers, tables, plugin registries, the object graph a framework assembles before it does anything — is built again each time. A snapshot removes that too: a launch costs what it takes to map a file, and the snapshotted memory is shared between concurrent processes and stays untouched on disk until the program writes to it.
+
+Whether it is worth it depends on how much your program does before it becomes useful. A tool that imports a large dependency graph, or a long-running program that spends its first second building state, benefits. A small script does not.
+
+## Command-line tools
+
+For a program that starts, does one job and exits, the requirement is that the job runs _after_ the snapshot is restored, not while the modules are being loaded. Import everything at the top level and put the program itself in `Bun.startupSnapshot.main()`:
+
+```ts cli.ts icon="/icons/typescript.svg"
+import { format } from "./formatter"; // the expensive part: this, and everything it imports, ends up in the snapshot
+
+Bun.startupSnapshot.main(async () => {
+ // Runs in every launch — after the snapshot has been restored, when there is one — with the
+ // launch's own argv, cwd, environment and stdio.
+ const [file] = process.argv.slice(2);
+ process.stdout.write(await format(file));
+});
+```
+
+`main()` calls the function immediately in a launch that has no snapshot, stores it in the launch that takes the snapshot, and calls it after the restore in every launch that resumes from one. Because nothing but imports runs before the snapshot is taken, there is nothing machine-specific in it, and the default build settings need no adjustment:
+
+```bash icon="terminal" terminal
+bun build --compile --bytecode --snapshot ./cli.ts --outfile fmt
+```
+
+### WebAssembly
+
+A WebAssembly module compiled and instantiated before the snapshot is taken is in it, compiled code and linear memory included, so a tool built around a large wasm module (a compiler or formatter compiled to wasm) starts without compiling it. Code that JavaScriptCore compiles in the background while the program runs is included too, so exercising the module's main entry point once before the snapshot is taken (with representative input) puts the tiered-up code in the snapshot rather than leaving each launch to produce it again.
+
+## Long-running programs
+
+A server or an interactive program keeps running after it starts, so its snapshot is taken at the point where startup is over. By default (`--snapshot`, or `--snapshot=auto`) the runtime decides: it evaluates the entry point, lets whatever that started run to completion, and takes the snapshot once nothing is pending. Timers that are still armed at that point survive, with their remaining time preserved across the restore. Nothing in the program has to change.
+
+A program that wants to choose the moment itself — for example because it wants to release things right before, or because its startup involves timers — builds with `--snapshot=manual` and calls `take()` when it is ready:
+
+```ts app.ts icon="/icons/typescript.svg"
+import { boot } from "./app";
+
+const app = await boot();
+
+process.on("restore", () => {
+ // Runs first thing in every launch that resumes from the snapshot, before the first event-loop
+ // turn: re-read whatever depends on the machine, re-open what has to be open.
+ app.reload();
+});
+
+// Returns immediately in every process except the one the build runs to take the snapshot.
+Bun.startupSnapshot.take({
+ timers: "keep", // or "cancel"; by default a manual snapshot refuses while timers are armed
+ envGate: ["APP_MODE"], // launches whose values for these differ from the build's boot normally
+});
+```
+
+`process.on("restore")` is available in both modes. Its listeners run before anything else in a restored launch, so state that must be recomputed per machine can be recomputed there; `main()` functions run after them.
+
+## What a restored launch gets from its environment
+
+The runtime refreshes what it owns before your code runs again: `process.argv`, `process.env`, the working directory, `process.pid`, the clocks (`performance.now()`, `process.uptime()`), the time zone, every source of randomness, timers, the DNS cache, its own threads, standard input and output, which are the launcher's, the IPC channel to a parent that spawned the launch with one (`process.send`), and the defaults it derives from the environment, such as the default S3 client and TLS verification. Files and sockets that were open when the snapshot was taken are gone; the program receives hangups for them after `"restore"`.
+
+What it cannot refresh is anything your code derived from those before the snapshot was taken:
+
+- A value read from `process.env`, `os.homedir()`, `process.cwd()` and the like and kept in a variable is the build machine's value in every launch. Holding a reference to `process.env` itself is fine — it is updated in place — but a copy of it (`{ ...process.env }`, `dotenv`-style merging into another object) is not.
+- Random values and timestamps produced before the snapshot (an id generated at startup, `Date.now()` stored in a variable) are identical in every launch.
+- Anything computed from files or the network before the snapshot is whatever the build machine had.
+- `.env` files are read when the snapshot is built, not at launch: a launch gets the build's `.env` values (the launch's own environment still takes precedence), and any `${VAR}` references in them were expanded against the build's environment.
+- The JavaScript engine's own options (`BUN_JSC_*` variables) are read once when the engine starts, so a snapshot carries the build's; setting them when launching a snapshot has no effect.
+
+When the snapshot is written, the build prints which environment variables were read by name before it, and every place `process.env` was copied, with the code that did it. Each entry is either something to move into `main()` or a `"restore"` listener, or a variable that genuinely changes what the program is — those go in `envGate`, and a launch whose value differs boots normally.
+
+## What the build may do
+
+While the snapshot is being taken, operations whose result would be frozen into every launch are refused: `fetch()` and other network use, `node:fs`, subprocesses, DNS and sockets throw an error saying so. If the program fails because of that, or never becomes quiet, no snapshot is taken and the build fails, saying what happened; the executable it built is left in place, so it can still be used or the step re-run with different settings.
+
+Programs that legitimately read their own files or run helpers while starting up can be allowed to:
+
+| Setting | Allowed while the snapshot is taken |
+| -------------------------------- | ---------------------------------------------------------------- |
+| `--snapshot-io=strict` (default) | Nothing that touches the machine. |
+| `--snapshot-io=local` | Files, subprocesses, local sockets, name resolution. |
+| `--snapshot-io=network` | The above, and the network. What it answered is in the snapshot. |
+
+With either of the last two, the build prints every operation it allowed, attributed to the code that performed it, so what went into the snapshot can be reviewed.
+
+## Building in CI
+
+The snapshot has to be taken on the platform the executable is for, because taking it means running the executable. The snapshot step therefore also works by itself, on an executable built earlier, and modifies it in place; running it again replaces the previous snapshot.
+
+```bash icon="terminal" terminal
+# On any machine: build for each target
+bun build --compile --target=bun-linux-x64 ./app.ts --outfile dist/app-linux-x64
+bun build --compile --target=bun-darwin-arm64 ./app.ts --outfile dist/app-darwin-arm64
+
+# On a Linux x64 machine, and on an Apple silicon machine respectively:
+bun build --snapshot --outfile dist/app-linux-x64
+bun build --snapshot=manual --snapshot-io=local --outfile dist/app-darwin-arm64
+```
+
+Take snapshots in an environment that holds nothing you would not ship: the snapshot is a copy of the process's memory, and whatever the build environment exposed to the program — variables, files, credentials — is in it.
+
+## `Bun.build()`
+
+The programmatic API takes the same thing as a top-level option. It requires `compile`, and the executable is built for the machine running the build.
+
+```ts build.ts icon="/icons/typescript.svg"
+await Bun.build({
+ entrypoints: ["./app.ts"],
+ compile: { outfile: "./myapp" },
+ snapshot: true, // or { mode: "auto" | "manual", io: "strict" | "local" | "network" }
+});
+```
+
+## When a launch does not use its snapshot
+
+A launch uses its snapshot only when it can do so exactly: the same executable, the same operating system build and system libraries, a CPU with the features the snapshot was taken on, and agreement on every `envGate` variable. Otherwise it boots normally — the program behaves the same either way, it just starts up the slow way. `Bun.startupSnapshot.epoch()` returns `0` in a launch that booted normally and a positive number in one that resumed.
+
+The snapshot is embedded in the executable as it is, and a launch maps the executable's own pages: nothing is unpacked, and nothing is written anywhere. It makes the executable larger by the size of the snapshotted state — typically tens of megabytes for a tool that loads a large dependency graph — and the build prints the size. For debugging, a launch prefers a `.snapshot` file placed next to the executable over the embedded one, so an alternative snapshot can be tried without re-embedding it.
+
+## Limitations
+
+- A launch that resumes from a snapshot runs with address-space layout randomization disabled, because the snapshot has to be mapped at the addresses it was taken at.
+- Native addons and `bun:ffi` libraries that keep state of their own, and worker threads that are alive when the snapshot is taken, are not supported.
+- macOS and glibc Linux only; on other platforms and builds `--snapshot` reports that it is unavailable.
+- Programs that were not written with the snapshot in mind may hold state of the kinds described above; the reports the build prints are the way to find it.
+
+## Reference
+
+**`bun build`**
+
+| Flag | Meaning |
+| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `--snapshot`, `--snapshot=auto` | With `--compile`: run the executable once and embed its snapshot, taken when startup has drained. Without entrypoints: do this to the existing `--outfile`. |
+| `--snapshot=manual` | The same, but the program calls `Bun.startupSnapshot.take()` to say when. |
+| `--snapshot-io=strict\|local\|network` | What the program may do while the snapshot is taken (default `strict`). |
+
+**`Bun.startupSnapshot`**
+
+| | |
+| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `main(fn)` | The program. Called now in an ordinary launch, after the restore in a launch that resumes from a snapshot, and not at all in the run that takes the snapshot. |
+| `take(options?)` | Manual mode: take the snapshot now. `options.timers` is `"keep"` or `"cancel"`; `options.envGate` is a list of variable names. Returns immediately in every other process. |
+| `isBuildingSnapshot()` | `true` only in the run that takes the snapshot. |
+| `epoch()` | `0` in a launch that booted normally; otherwise the number of times this process has been resumed. |
+| `reclean()` | In a resumed launch, hand pages the program wrote and then restored to their original contents back to the shared snapshot. Optional; useful for programs that idle for a long time. |
+| `process.on("restore", fn)` | Runs first in every launch that resumes from a snapshot. |
diff --git a/docs/docs.json b/docs/docs.json
index 54f3545a1c1c..f20ec1cb34ff 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -251,7 +251,7 @@
{
"group": "Single File Executable",
"icon": "binary",
- "pages": ["/bundler/executables"]
+ "pages": ["/bundler/executables", "/bundler/startup-snapshots"]
},
{
"group": "Extensions",
diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts
index 6a47f7c54350..347130d668b2 100644
--- a/packages/bun-types/bun.d.ts
+++ b/packages/bun-types/bun.d.ts
@@ -3222,6 +3222,15 @@ declare module "bun" {
* ```
*/
compile?: boolean | Bun.Build.CompileTarget | CompileBuildOptions;
+ /**
+ * Snapshots (experimental; requires `compile`): after producing the executable, run it once and
+ * embed a snapshot of its started-up state, so later launches resume instead of booting. `true`
+ * takes the snapshot once startup work drains (`bun build --snapshot`); use `mode: "manual"` when
+ * the app calls `Bun.startupSnapshot.take()` itself, and `io` to let the build touch this machine
+ * (`"local"`: files, subprocesses, local sockets; `"network"`: the network too) — every use is
+ * reported when the snapshot is written. Default `io` is `"strict"`.
+ */
+ snapshot?: boolean | { mode?: "auto" | "manual"; io?: "strict" | "local" | "network" };
}
interface CompileBuildOptions {
@@ -4943,6 +4952,56 @@ declare module "bun" {
function memoryFootprint(): number | undefined;
}
+ /**
+ * Startup snapshots (experimental) — see `bun build --snapshot`. A snapshot of the started-up
+ * process is embedded in a compiled executable, and later launches resume from it instead of
+ * booting; `process.on("restore")` runs first thing in such a launch.
+ */
+ namespace startupSnapshot {
+ /**
+ * The program itself, for tools that start, do a job and exit. Called immediately in a launch
+ * that has no snapshot; stored — not called — in the run that takes the snapshot, so the snapshot
+ * holds the loaded program; called after `"restore"` in a launch that resumes from the snapshot,
+ * with that launch's argv, cwd, environment and stdio. A snapshot taken with a `main()` registered
+ * is used for every invocation, whatever the arguments.
+ */
+ function main(program: () => unknown): void;
+
+ /**
+ * With `--snapshot=manual`, the point in startup at which the snapshot is taken. In the run
+ * `bun build` makes for that purpose this never returns: the process exits once the snapshot is
+ * written (or with a message naming what kept it busy). In every other process it returns at
+ * once, so it can be called unconditionally. With `--snapshot` (auto) the runtime picks the
+ * moment itself and a call only contributes the options.
+ */
+ function take(options?: {
+ /**
+ * Timers still armed when the process goes quiet: `"keep"` lets them survive with their
+ * remaining time preserved across the restore; `"cancel"` drops them. By default armed
+ * timers keep a manual snapshot from being taken; auto mode keeps them.
+ */
+ timers?: "keep" | "cancel";
+ /**
+ * Environment variables the snapshotted startup depended on. A launch whose values for these
+ * differ from the build's boots normally instead of resuming from the snapshot.
+ */
+ envGate?: string[];
+ }): void;
+
+ /** True only in the run `bun build --snapshot` makes to take the snapshot. */
+ function isBuildingSnapshot(): boolean;
+
+ /** 0 in a process that booted normally; otherwise how many times this process has been resumed from a snapshot. */
+ function epoch(): number;
+
+ /**
+ * In a process resumed from a snapshot: hand back to the shared snapshot any page this process
+ * wrote and then restored to its original contents. Cheap; call it once startup work has
+ * settled. A no-op elsewhere.
+ */
+ function reclean(): void;
+ }
+
type DigestEncoding = "utf8" | "ucs2" | "utf16le" | "latin1" | "ascii" | "base64" | "base64url" | "hex";
/**
diff --git a/packages/bun-usockets/src/eventing/epoll_kqueue.c b/packages/bun-usockets/src/eventing/epoll_kqueue.c
index 90c655d88185..7c4839684d1b 100644
--- a/packages/bun-usockets/src/eventing/epoll_kqueue.c
+++ b/packages/bun-usockets/src/eventing/epoll_kqueue.c
@@ -775,6 +775,7 @@ struct us_internal_async *us_internal_create_async(struct us_loop_t *loop, int f
return (struct us_internal_async *) cb;
}
+
// identical code as for timer, make it shared for "callback types"
void us_internal_async_close(struct us_internal_async *a) {
struct us_internal_callback_t *cb = (struct us_internal_callback_t *) a;
@@ -1019,3 +1020,49 @@ int us_socket_get_error(struct us_socket_t *s) {
}
#endif
+
+#ifdef LIBUS_USE_EPOLL
+/* Snapshot restore (Linux): fresh epoll fd; the wakeup async is an eventfd-backed poll — give it a new eventfd and re-add it. */
+void us_loop_reinit_for_snapshot(struct us_loop_t *loop) {
+ loop->fd = epoll_create1(EPOLL_CLOEXEC);
+ loop->num_ready_polls = 0;
+ loop->current_ready_poll = 0;
+ struct us_poll_t *p = (struct us_poll_t *) loop->data.wakeup_async;
+ if (p) {
+ int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
+ if (efd != -1) {
+ int events = us_poll_events(p);
+ us_poll_init(p, efd, us_internal_poll_type(p));
+ us_poll_start(p, loop, events ? events : LIBUS_SOCKET_READABLE);
+ /* Same upgrade as us_internal_async_set(): the callback's leave_poll_ready (no drain) came with the snapshot and is
+ * only correct for an edge-triggered registration; level-triggered, the undrained eventfd would wake the loop forever. */
+ struct epoll_event event;
+ event.events = EPOLLIN | EPOLLET;
+ event.data.ptr = p;
+ epoll_ctl(loop->fd, EPOLL_CTL_MOD, efd, &event);
+ }
+ }
+}
+#endif
+
+#ifdef LIBUS_USE_KQUEUE
+#if defined(__APPLE__)
+/* Experiment (snapshot restore): the loop struct came from another process; give it a fresh kqueue and
+ * re-create/re-register the wakeup mach port so us_wakeup_loop() works again. */
+void us_loop_reinit_for_snapshot(struct us_loop_t *loop) {
+ loop->fd = kqueue();
+ loop->num_ready_polls = 0;
+ loop->current_ready_poll = 0;
+ struct us_internal_callback_t *cb = (struct us_internal_callback_t *) loop->data.wakeup_async;
+ if (cb) {
+ mach_port_t self = mach_task_self();
+ if (mach_port_allocate(self, MACH_PORT_RIGHT_RECEIVE, &cb->port) == KERN_SUCCESS
+ && mach_port_insert_right(self, cb->port, cb->port, MACH_MSG_TYPE_MAKE_SEND) == KERN_SUCCESS) {
+ mach_port_limits_t limits = { .mpl_qlimit = 1 };
+ mach_port_set_attributes(self, cb->port, MACH_PORT_LIMITS_INFO, (mach_port_info_t)&limits, MACH_PORT_LIMITS_INFO_COUNT);
+ us_internal_async_set((struct us_internal_async *) cb, (void (*)(struct us_internal_async *)) cb->cb);
+ }
+ }
+}
+#endif
+#endif
diff --git a/packages/bun-uws/src/Loop.h b/packages/bun-uws/src/Loop.h
index b62bddf47469..e69eb2506051 100644
--- a/packages/bun-uws/src/Loop.h
+++ b/packages/bun-uws/src/Loop.h
@@ -108,6 +108,12 @@ struct Loop {
}
public:
+ /* snapshot restore: this thread's TLS is fresh but the loop object lives on (in the snapshot); make get() return it instead of creating a second loop. */
+ static void adoptForCurrentThread(Loop *loop) {
+ getLazyLoop().loop = loop;
+ getLazyLoop().cleanMe = false;
+ }
+
/* Lazily initializes a per-thread loop and returns it.
* Will automatically free all initialized loops at exit. */
static Loop *get(void *existingNativeLoop = nullptr) {
diff --git a/patches/boringssl/fork-detect-startup-snapshot.patch b/patches/boringssl/fork-detect-startup-snapshot.patch
new file mode 100644
index 000000000000..b63154adb7b6
--- /dev/null
+++ b/patches/boringssl/fork-detect-startup-snapshot.patch
@@ -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
++}
++
+ 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
diff --git a/scripts/build/deps/boringssl.ts b/scripts/build/deps/boringssl.ts
index 395739e710bc..14a90a692db5 100644
--- a/scripts/build/deps/boringssl.ts
+++ b/scripts/build/deps/boringssl.ts
@@ -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)
diff --git a/scripts/build/deps/mimalloc.ts b/scripts/build/deps/mimalloc.ts
index 8e03973d6a0e..da88a6694a50 100644
--- a/scripts/build/deps/mimalloc.ts
+++ b/scripts/build/deps/mimalloc.ts
@@ -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",
@@ -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 = {
// The .a path; gates symbol visibility in mimalloc/internal.h.
@@ -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
@@ -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
diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts
index 37386d50099e..36596b283a0c 100644
--- a/scripts/build/deps/webkit.ts
+++ b/scripts/build/deps/webkit.ts
@@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=` 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.
@@ -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",
diff --git a/scripts/build/flags.ts b/scripts/build/flags.ts
index 59e199b6e579..d87a26d6fe82 100644
--- a/scripts/build/flags.ts
+++ b/scripts/build/flags.ts
@@ -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 ───
{
diff --git a/src/boringssl_sys/boringssl.rs b/src/boringssl_sys/boringssl.rs
index ed3994d474b1..eaac44dfb70a 100644
--- a/src/boringssl_sys/boringssl.rs
+++ b/src/boringssl_sys/boringssl.rs
@@ -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.
diff --git a/src/boringssl_sys/lib.rs b/src/boringssl_sys/lib.rs
index d898c92b7375..3adbb7e916cc 100644
--- a/src/boringssl_sys/lib.rs
+++ b/src/boringssl_sys/lib.rs
@@ -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
diff --git a/src/bun_alloc/lib.rs b/src/bun_alloc/lib.rs
index d99683419c75..9e642ecb2073 100644
--- a/src/bun_alloc/lib.rs
+++ b/src/bun_alloc/lib.rs
@@ -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.
@@ -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.)
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
@@ -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;
+ }
+ 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.
@@ -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,
diff --git a/src/bun_bin/lib.rs b/src/bun_bin/lib.rs
index ba124904593d..d26134723ab6 100644
--- a/src/bun_bin/lib.rs
+++ b/src/bun_bin/lib.rs
@@ -169,6 +169,12 @@ pub(crate) unsafe extern "C" fn main(argc: c_int, argv: *const *const c_char) ->
libc::signal(libc::SIGPIPE, libc::SIG_IGN);
libc::signal(libc::SIGXFSZ, libc::SIG_IGN);
}
+ // Every platform: where snapshots are unsupported this is the stub that refuses the environment knobs out loud.
+ unsafe extern "C" {
+ fn Bun__startupSnapshotInit();
+ }
+ // SAFETY: no arguments; called once, on the main thread, before any other thread exists.
+ unsafe { Bun__startupSnapshotInit() };
// Windows-only startup. Must run BEFORE the first libuv
// call (uv allocator) and before anything reads `Bun.env`/`process.env`
@@ -199,6 +205,15 @@ pub(crate) unsafe extern "C" fn main(argc: c_int, argv: *const *const c_char) ->
// wires stdout/stderr `Source`s.
output::stdio::init();
let _flush = output::flush_guard();
+ // Snapshot: with stdio and Output ready, a process that has a snapshot to map diverges here and never returns.
+ #[cfg(unix)]
+ // SAFETY: single-threaded startup; the callee takes no arguments and either returns or continues the snapshotd process.
+ unsafe {
+ unsafe extern "C" {
+ fn Bun__startupSnapshotMaybeRestore();
+ }
+ Bun__startupSnapshotMaybeRestore();
+ }
// 5. Per-thread stack-limit cache for the JS recursion guard.
StackCheck::configure_thread();
diff --git a/src/bun_core/env_var.rs b/src/bun_core/env_var.rs
index 9ca6d375d233..93c105b504a8 100644
--- a/src/bun_core/env_var.rs
+++ b/src/bun_core/env_var.rs
@@ -34,6 +34,17 @@ use core::sync::atomic::{AtomicPtr, AtomicU8, AtomicU64, AtomicUsize, Ordering};
// MOVE_DOWN: bun_core::ZStr → bun_core (move-in pass).
use crate::ZStr;
+/// Publishes the restore epoch a cached value was (re)loaded in, after the value itself, whatever path the loader took.
+struct PublishEpoch<'a>(&'a core::sync::atomic::AtomicU32);
+impl Drop for PublishEpoch<'_> {
+ fn drop(&mut self) {
+ self.0.store(
+ crate::startup_snapshot::epoch(),
+ core::sync::atomic::Ordering::Release,
+ );
+ }
+}
+
// ──────────────────────────────────────────────────────────────────────────────
// Declarations
// ──────────────────────────────────────────────────────────────────────────────
@@ -87,6 +98,14 @@ new!(pub BUN_FEATURE_FLAG_DUMP_CODE: string, "BUN_FEATURE_FLAG_DUMP_CODE", {});
new!(pub BUN_GC_RUNS_UNTIL_SKIP_RELEASE_ACCESS: unsigned, "BUN_GC_RUNS_UNTIL_SKIP_RELEASE_ACCESS", {});
new!(pub BUN_GC_TIMER_DISABLE: boolean, "BUN_GC_TIMER_DISABLE", {});
new!(pub BUN_GC_TIMER_INTERVAL: unsigned, "BUN_GC_TIMER_INTERVAL", {});
+new!(pub BUN_STARTUP_SNAPSHOT_VERBOSE: boolean, "BUN_STARTUP_SNAPSHOT_VERBOSE", {});
+// Set for the process `bun build --snapshot` runs to take the snapshot: what it may touch on this machine ("strict" unless "local"),
+// and whether the runtime takes the snapshot itself once startup drains (auto) or waits for Bun.startupSnapshot.take() (manual).
+new!(pub BUN_STARTUP_SNAPSHOT_OUT: string, "BUN_STARTUP_SNAPSHOT_OUT", {});
+new!(pub BUN_STARTUP_SNAPSHOT_IO: string, "BUN_STARTUP_SNAPSHOT_IO", {});
+new!(pub BUN_STARTUP_SNAPSHOT_AUTO: boolean, "BUN_STARTUP_SNAPSHOT_AUTO", {});
+new!(pub BUN_STARTUP_SNAPSHOT_KEEP_SIDECAR: boolean, "BUN_STARTUP_SNAPSHOT_KEEP_SIDECAR", {});
+new!(pub BUN_STARTUP_SNAPSHOT_QUIET_TIMEOUT: unsigned, "BUN_STARTUP_SNAPSHOT_QUIET_TIMEOUT", {});
// TODO(markovejnovic): It's unclear why the default here is 100_000, but this was legacy behavior
// so we'll keep it for now.
new!(pub BUN_INOTIFY_COALESCE_INTERVAL: unsigned, "BUN_INOTIFY_COALESCE_INTERVAL", { default: 100_000 });
@@ -332,6 +351,7 @@ pub(crate) mod kind {
pub(crate) struct Cache {
ptr_value: AtomicPtr,
len_value: AtomicUsize,
+ epoch: core::sync::atomic::AtomicU32, // snapshot restore epoch the value was loaded in; stale => reload
}
type PointerType = *mut u8; // AtomicPtr requires *mut
@@ -347,10 +367,14 @@ pub(crate) mod kind {
Self {
ptr_value: AtomicPtr::new(NOT_LOADED_PTR),
len_value: AtomicUsize::new(NOT_LOADED_LEN),
+ epoch: core::sync::atomic::AtomicU32::new(0),
}
}
pub(crate) fn get_cached(&self) -> Output {
+ if self.epoch.load(Ordering::Acquire) != crate::startup_snapshot::epoch() {
+ return CacheOutput::Unknown;
+ }
let len = self.len_value.load(Ordering::Acquire);
if len == NOT_LOADED_LEN {
@@ -373,6 +397,7 @@ pub(crate) mod kind {
&self,
raw_env: Option<&'static [u8]>,
) -> Option {
+ let _publish = PublishEpoch(&self.epoch); // stored last, on every exit path: a reader that sees the epoch sees the value
// The implementation is racy and allows two threads to both set the value at
// the same time, as long as the value they are setting is the same. This is
// difficult to write an assertion for since it requires the DEV path take a
@@ -410,7 +435,8 @@ pub(crate) mod kind {
// Cache type is emitted for every environment variable.
// (In Rust, per-var statics give us per-var caches without distinct types.)
pub(crate) struct Cache {
- value: AtomicU8, // StoredType
+ value: AtomicU8, // StoredType
+ epoch: core::sync::atomic::AtomicU32, // snapshot restore epoch the value was loaded in; stale => reload
}
#[repr(u8)]
@@ -426,11 +452,15 @@ pub(crate) mod kind {
pub(crate) const fn new() -> Self {
Self {
value: AtomicU8::new(StoredType::Unknown as u8),
+ epoch: core::sync::atomic::AtomicU32::new(0),
}
}
#[inline]
pub(crate) fn get_cached(&self) -> Output {
+ if self.epoch.load(Ordering::Acquire) != crate::startup_snapshot::epoch() {
+ return CacheOutput::Unknown;
+ }
// only ever stored from StoredType discriminants
let cached: StoredType = match self.value.load(Ordering::Relaxed) {
1 => StoredType::NotSet,
@@ -451,6 +481,7 @@ pub(crate) mod kind {
#[inline]
pub(crate) fn deser_and_invalidate(&self, raw_env: Option<&[u8]>) -> Option {
+ let _publish = PublishEpoch(&self.epoch); // stored last, on every exit path: a reader that sees the epoch sees the value
let Some(raw_env) = raw_env else {
self.value
.store(StoredType::NotSet as u8, Ordering::Relaxed);
@@ -544,6 +575,7 @@ pub(crate) mod kind {
pub(crate) struct Cache {
value: AtomicU64,
ip: Input,
+ epoch: core::sync::atomic::AtomicU32, // snapshot restore epoch the value was loaded in; stale => reload
}
type StoredType = ValueType;
@@ -558,11 +590,15 @@ pub(crate) mod kind {
Self {
value: AtomicU64::new(UNKNOWN_SENTINEL),
ip,
+ epoch: core::sync::atomic::AtomicU32::new(0),
}
}
#[inline]
pub(crate) fn get_cached(&self) -> Output {
+ if self.epoch.load(Ordering::Acquire) != crate::startup_snapshot::epoch() {
+ return CacheOutput::Unknown;
+ }
match self.value.load(Ordering::Relaxed) {
UNKNOWN_SENTINEL => {
crate::hint::cold();
@@ -575,6 +611,7 @@ pub(crate) mod kind {
#[inline]
pub(crate) fn deser_and_invalidate(&self, raw_env: Option<&[u8]>) -> Option {
+ let _publish = PublishEpoch(&self.epoch); // stored last, on every exit path: a reader that sees the epoch sees the value
let Some(raw_env) = raw_env else {
self.value.store(NOT_SET_SENTINEL, Ordering::Relaxed);
return None;
diff --git a/src/bun_core/lib.rs b/src/bun_core/lib.rs
index 7a6664d9e134..4951db3cf0ce 100644
--- a/src/bun_core/lib.rs
+++ b/src/bun_core/lib.rs
@@ -19,6 +19,7 @@ pub mod comptime_string_map;
pub mod error;
pub mod hint;
pub mod result;
+pub mod startup_snapshot;
pub mod thread_id;
pub mod tty;
pub mod util;
diff --git a/src/bun_core/output.rs b/src/bun_core/output.rs
index 3274e5c074cb..bc2e7d37f2ab 100644
--- a/src/bun_core/output.rs
+++ b/src/bun_core/output.rs
@@ -513,6 +513,35 @@ impl Source {
}
}
}
+
+ /// A process that resumed from a snapshot inherited the build's idea of its stdio (the flags in `bun_stdio_tty` have
+ /// already been recomputed for this process's descriptors by `bun_refresh_stdio_after_snapshot_restore`); rederive
+ /// what was computed from them and from the environment at startup.
+ #[cfg(not(target_arch = "wasm32"))]
+ pub fn refresh_stdio_after_snapshot_restore() {
+ STDOUT_DESCRIPTOR_TYPE.reset_for_snapshot_restore();
+ STDERR_DESCRIPTOR_TYPE.reset_for_snapshot_restore();
+ LAZY_COLOR_DEPTH.reset_for_snapshot_restore();
+ let is_stdout_tty = stdio_tty_flag(1);
+ let is_stderr_tty = stdio_tty_flag(2);
+ if is_stdout_tty {
+ let _ = STDOUT_DESCRIPTOR_TYPE.set(OutputStreamDescriptor::Terminal);
+ }
+ if is_stderr_tty {
+ let _ = STDERR_DESCRIPTOR_TYPE.set(OutputStreamDescriptor::Terminal);
+ }
+ let enable_color = if Self::is_force_color() {
+ Some(true)
+ } else if Self::is_no_color() {
+ Some(false)
+ } else if Self::is_color_terminal() && (is_stdout_tty || is_stderr_tty) {
+ Some(true)
+ } else {
+ None
+ };
+ ENABLE_ANSI_COLORS_STDOUT.store(enable_color.unwrap_or(is_stdout_tty), Ordering::Relaxed);
+ ENABLE_ANSI_COLORS_STDERR.store(enable_color.unwrap_or(is_stderr_tty), Ordering::Relaxed);
+ }
}
// ── Source::WindowsStdio ──────────────────────────────────────────────────
diff --git a/src/bun_core/startup_snapshot.rs b/src/bun_core/startup_snapshot.rs
new file mode 100644
index 000000000000..1ee68a8f0eb1
--- /dev/null
+++ b/src/bun_core/startup_snapshot.rs
@@ -0,0 +1,214 @@
+//! Snapshot process state: are we building a snapshot, and which restore epoch are we in.
+use core::sync::atomic::{AtomicU32, Ordering};
+
+// One epoch for the whole process (Rust, C++ and vendored C): the exported `bun_snapshot_epoch` symbol, defined here.
+#[unsafe(no_mangle)]
+pub static bun_snapshot_epoch: AtomicU32 = AtomicU32::new(0);
+static BUILDING: AtomicU32 = AtomicU32::new(0);
+
+/// 0 in a normally booted process; bumped each time this process resumed from a snapshot.
+#[inline]
+pub fn epoch() -> u32 {
+ bun_snapshot_epoch.load(Ordering::Acquire)
+}
+#[inline]
+pub fn restored() -> bool {
+ epoch() != 0
+}
+/// Called once per restore (the C++ restore sequence has already bumped `bun_snapshot_epoch`).
+pub fn did_restore() {
+ BUILDING.store(0, Ordering::Release);
+}
+/// True while this process is producing a snapshot: OS resources created now will not exist when the snapshot runs.
+#[inline]
+pub fn building() -> bool {
+ BUILDING.load(Ordering::Acquire) != 0
+}
+pub fn set_building(on: bool) {
+ BUILDING.store(on as u32, Ordering::Release);
+}
+
+/// A `Once` whose "done" state belongs to a process epoch: work that created OS state (threads, fds, ports) re-runs after a snapshot restore.
+pub struct SnapshotOnce {
+ done_epoch: AtomicU32, // epoch+1 in which it last ran; 0 = never
+ lock: std::sync::Mutex<()>,
+}
+impl SnapshotOnce {
+ pub const fn new() -> Self {
+ Self {
+ done_epoch: AtomicU32::new(0),
+ lock: std::sync::Mutex::new(()),
+ }
+ }
+ #[inline]
+ pub fn is_done(&self) -> bool {
+ self.done_epoch.load(Ordering::Acquire) == epoch() + 1
+ }
+ pub fn call(&self, f: impl FnOnce()) {
+ if self.is_done() {
+ return;
+ }
+ let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
+ if self.is_done() {
+ return;
+ }
+ f();
+ self.done_epoch.store(epoch() + 1, Ordering::Release);
+ }
+}
+
+/// A cached value derived from the launch context (argv, env, cwd, uid, terminal…), recomputed after a restore; use it instead of `Once`/`OnceLock` for anything read from the OS and cached.
+pub struct ProcessDerived {
+ epoch_plus_one: AtomicU32,
+ lock: std::sync::Mutex<()>,
+ ptr: core::sync::atomic::AtomicPtr,
+}
+impl ProcessDerived {
+ pub const fn new() -> Self {
+ Self {
+ epoch_plus_one: AtomicU32::new(0),
+ lock: std::sync::Mutex::new(()),
+ ptr: core::sync::atomic::AtomicPtr::new(core::ptr::null_mut()),
+ }
+ }
+ /// The current process's value: `init` runs on first use after each restore; earlier values are leaked so references stay valid.
+ pub fn get(&'static self, init: impl FnOnce() -> T) -> &'static T {
+ let want = epoch() + 1;
+ if self.epoch_plus_one.load(Ordering::Acquire) != want {
+ let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
+ if self.epoch_plus_one.load(Ordering::Acquire) != want {
+ self.ptr
+ .store(Box::leak(Box::new(init())), Ordering::Release);
+ self.epoch_plus_one.store(want, Ordering::Release);
+ }
+ }
+ // SAFETY: non-null (stored above for this epoch) and leaked for the process lifetime.
+ unsafe { &*self.ptr.load(Ordering::Acquire) }
+ }
+ /// True if a value has been computed for the current process.
+ pub fn is_current(&self) -> bool {
+ self.epoch_plus_one.load(Ordering::Acquire) == epoch() + 1
+ }
+}
+
+/// What the snapshot run may touch on the build machine (`--snapshot-io`); every allowed use is recorded and reported when the snapshot is written.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum IoPolicy {
+ Strict,
+ Local,
+ /// BUN_STARTUP_SNAPSHOT_IO=network: the network too — its answers are frozen into every launch — still recorded and reported.
+ Network,
+}
+pub fn io_policy() -> IoPolicy {
+ match crate::env_var::BUN_STARTUP_SNAPSHOT_IO.get() {
+ Some(b"local") => IoPolicy::Local,
+ Some(b"network") => IoPolicy::Network,
+ _ => IoPolicy::Strict,
+ }
+}
+/// Whether the policy admits an operation of this class, which the gate then records.
+pub fn io_allowed(kind: &str) -> bool {
+ let local_class = matches!(
+ kind,
+ "node:fs"
+ | "Bun.file"
+ | "Bun.write"
+ | "Bun.spawn"
+ | "Bun.listen"
+ | "Bun.serve"
+ | "Bun.udpSocket"
+ | "dns"
+ );
+ match io_policy() {
+ IoPolicy::Strict => false,
+ IoPolicy::Local => local_class,
+ IoPolicy::Network => true,
+ }
+}
+
+/// Local I/O performed while building, keyed by (kind, JS call site) -> count. Only ever touched on the JS thread of the builder.
+static LOCAL_IO_AUDIT: std::sync::Mutex, u32)>> =
+ std::sync::Mutex::new(Vec::new());
+pub fn note_local_io(kind: &'static str, site: Vec) {
+ let mut audit = LOCAL_IO_AUDIT.lock().unwrap_or_else(|e| e.into_inner());
+ if let Some(entry) = audit.iter_mut().find(|(k, s, _)| *k == kind && *s == site) {
+ entry.2 += 1;
+ } else {
+ audit.push((kind, site, 1));
+ }
+}
+static STDIO_NOTES: std::sync::Mutex)>> = std::sync::Mutex::new(Vec::new());
+/// A `process.std*` stream was created during the snapshot run: whatever the app derived from it (isTTY, colors) describes the build's descriptors.
+pub fn note_stdio_stream(fd: i32, site: Vec) {
+ STDIO_NOTES
+ .lock()
+ .unwrap_or_else(|e| e.into_inner())
+ .push((fd, site));
+}
+pub fn take_stdio_notes() -> Vec<(i32, Vec)> {
+ std::mem::take(&mut *STDIO_NOTES.lock().unwrap_or_else(|e| e.into_inner()))
+}
+/// The audit, most frequent first; empty unless the build did local I/O.
+pub fn take_local_io_audit() -> Vec<(&'static str, Vec, u32)> {
+ let mut audit = std::mem::take(&mut *LOCAL_IO_AUDIT.lock().unwrap_or_else(|e| e.into_inner()));
+ audit.sort_by_key(|entry| core::cmp::Reverse(entry.2));
+ audit
+}
+
+static SNAPSHOT_REQUESTED: AtomicU32 = AtomicU32::new(0);
+static SNAPSHOT_PATH: std::sync::Mutex>> = std::sync::Mutex::new(None);
+/// Ask the main run loop to leave JS entirely and take the snapshot at top level (caller then unwinds via a termination exception).
+pub fn request_snapshot(path: &[u8]) {
+ *SNAPSHOT_PATH.lock().unwrap_or_else(|e| e.into_inner()) = Some(path.to_owned());
+ SNAPSHOT_REQUESTED.store(1, Ordering::Release);
+}
+#[inline]
+pub fn snapshot_requested() -> bool {
+ SNAPSHOT_REQUESTED.load(Ordering::Acquire) != 0
+}
+static SNAPSHOT_IN_PROGRESS: AtomicU32 = AtomicU32::new(0);
+/// Set once the runtime is draining the process for the snapshot; later `take()` calls only contribute their options.
+pub fn set_snapshot_in_progress() {
+ SNAPSHOT_IN_PROGRESS.store(1, Ordering::Release);
+}
+pub fn snapshot_in_progress() -> bool {
+ SNAPSHOT_IN_PROGRESS.load(Ordering::Acquire) != 0
+}
+pub fn take_snapshot_request() -> Option> {
+ if SNAPSHOT_REQUESTED.swap(0, Ordering::AcqRel) == 0 {
+ return None;
+ }
+ SNAPSHOT_PATH
+ .lock()
+ .unwrap_or_else(|e| e.into_inner())
+ .take()
+}
+
+/// What `Bun.startupSnapshot.take()` does about timers that are still armed when the process goes quiet.
+#[derive(Clone, Copy, PartialEq, Eq)]
+#[repr(u8)]
+pub enum StartupSnapshotTimers {
+ /// Armed timers keep the process from being snapshotted (the default: the app is expected to clear them itself).
+ Refuse = 0,
+ /// Timers survive the snapshot; their deadlines are re-based onto the restoring process's clock.
+ Keep = 1,
+ /// The runtime drops every armed timer as part of taking the snapshot (the app re-arms what it needs after restore).
+ Cancel = 2,
+}
+static SNAPSHOT_TIMERS: core::sync::atomic::AtomicU8 = core::sync::atomic::AtomicU8::new(0);
+pub fn set_snapshot_timers(mode: StartupSnapshotTimers) {
+ SNAPSHOT_TIMERS.store(mode as u8, Ordering::Release);
+}
+pub fn snapshot_timers() -> StartupSnapshotTimers {
+ match SNAPSHOT_TIMERS.load(Ordering::Acquire) {
+ 1 => StartupSnapshotTimers::Keep,
+ 2 => StartupSnapshotTimers::Cancel,
+ _ => StartupSnapshotTimers::Refuse,
+ }
+}
+
+/// Monotonic (sec, nsec) at the moment the snapshot was frozen; lives in __DATA so the restored process can compute how far its own clock is from it.
+pub static SNAPSHOT_MONOTONIC: [core::sync::atomic::AtomicI64; 2] = [
+ core::sync::atomic::AtomicI64::new(0),
+ core::sync::atomic::AtomicI64::new(0),
+];
diff --git a/src/bun_core/util.rs b/src/bun_core/util.rs
index 705ae0158e99..b02abe64d68c 100644
--- a/src/bun_core/util.rs
+++ b/src/bun_core/util.rs
@@ -2458,6 +2458,16 @@ impl Once {
{
*self.get_or_init(f)
}
+ /// Back to uninitialized, so the next use recomputes. Only for `Copy` payloads (nothing to drop) and only while no other
+ /// thread can be using the value: a process that just resumed from a snapshot recomputing what it inherited from the build.
+ pub fn reset_for_snapshot_restore(&self)
+ where
+ T: Copy,
+ {
+ self.state
+ .store(ONCE_UNINIT, core::sync::atomic::Ordering::Release);
+ }
+
/// Fast path: already initialised?
#[inline(always)]
pub fn get(&self) -> Option<&T> {
@@ -2761,8 +2771,11 @@ fn os_entropy(bytes: &mut [u8]) {
// Memoized into a process-lifetime
// static buffer; thread-safe via `Once`. Returns a `&'static ZStr`.
pub fn self_exe_path() -> crate::CrateResult<&'static ZStr> {
- static CELL: Once> = Once::new();
- let r = CELL.get_or_init(|| {
+ // Per process, not once: a snapshot built at one path is routinely restored by the same executable at another (a compiled
+ // executable is built in one place and deployed to another), and process.execPath must say where this one is.
+ static CELL: crate::startup_snapshot::ProcessDerived> =
+ crate::startup_snapshot::ProcessDerived::new();
+ let r = CELL.get(|| {
let path = std::env::current_exe().map_err(|_| crate::CrateError::Unexpected)?;
// Symlink resolution: Rust's
// `current_exe()` already resolves on Linux (`readlink /proc/self/exe`),
@@ -3544,7 +3557,12 @@ pub fn fast_random() -> u64 {
use core::cell::Cell;
use core::sync::atomic::{AtomicU64, Ordering as O};
static SEED: AtomicU64 = AtomicU64::new(0);
+ static SEED_EPOCH: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
fn random_seed() -> u64 {
+ let epoch = crate::startup_snapshot::epoch();
+ if SEED_EPOCH.swap(epoch, O::Relaxed) != epoch {
+ SEED.store(0, O::Relaxed); // seed cached by the snapshot builder: draw a fresh one here
+ }
let mut v = SEED.load(O::Relaxed);
while v == 0 {
// Should also apply to canary builds, but bun_core has no `canary`
@@ -3564,14 +3582,16 @@ pub fn fast_random() -> u64 {
v
}
thread_local! {
- static PRNG: Cell> = const { Cell::new(None) };
+ static PRNG: Cell > = const { Cell::new(None) };
}
PRNG.with(|p| {
- let mut prng = p
- .take()
- .unwrap_or_else(|| rand::DefaultPrng::init(random_seed()));
+ let epoch = crate::startup_snapshot::epoch();
+ let mut prng = match p.take() {
+ Some((e, prng)) if e == epoch => prng,
+ _ => rand::DefaultPrng::init(random_seed()), // first use, or first use since a snapshot restore (the snapshotd stream is the builder's)
+ };
let v = prng.next_u64();
- p.set(Some(prng));
+ p.set(Some((epoch, prng)));
v
})
}
@@ -3726,10 +3746,16 @@ pub use bun_alloc::secure_zero;
// `Argv` wrapper so call sites can use it both as a slice (`.get(0)`,
// `.iter()`, `.len()`, `.as_slice()`) and as an `IntoIterator- `
// for `for arg in argv()`.
-static ARGV_STORAGE: Once
> = Once::new();
-static ARGV_VIEW: Once> = Once::new();
-static ARGV: RacyCell<&'static [&'static ZStr]> = RacyCell::new(&[]);
-static ARGV_INIT: std::sync::Once = std::sync::Once::new();
+// Launch-context derived (recomputed after a snapshot restore — see `snapshot::ProcessDerived`).
+static ARGV_STORAGE: crate::startup_snapshot::ProcessDerived> =
+ crate::startup_snapshot::ProcessDerived::new();
+struct ArgvView(RacyCell<&'static [&'static ZStr]>);
+// SAFETY: the view is written during single-threaded startup / restore adoption only (see `set_argv`).
+unsafe impl Sync for ArgvView {}
+// SAFETY: as above; the view holds only `'static` data.
+unsafe impl Send for ArgvView {}
+static ARGV: crate::startup_snapshot::ProcessDerived =
+ crate::startup_snapshot::ProcessDerived::new();
/// Raw `(argc, argv)` as passed to `main` by the C runtime. Captured by
/// [`init_argv`] before any other code runs. On glibc / macOS / Windows,
@@ -3758,6 +3784,28 @@ pub unsafe fn init_argv(argc: core::ffi::c_int, argv: *const *const core::ffi::c
OS_ARGV.store(argv.cast_mut(), core::sync::atomic::Ordering::Relaxed);
}
+/// The raw launch inputs `main` received. A snapshot restore overlays this crate's statics with the builder's,
+/// so the restore sequence reads these before the overlay and hands them back after (`bun_launch_context_*`).
+#[repr(C)]
+pub struct LaunchContext {
+ pub argc: usize,
+ pub argv: *const *const core::ffi::c_char,
+}
+#[unsafe(no_mangle)]
+pub extern "C" fn bun_launch_context_capture(out: &mut LaunchContext) {
+ out.argc = OS_ARGC.load(core::sync::atomic::Ordering::Relaxed);
+ out.argv = OS_ARGV
+ .load(core::sync::atomic::Ordering::Relaxed)
+ .cast_const();
+}
+/// # Safety
+/// `ctx` must come from `bun_launch_context_capture` in this same process.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn bun_launch_context_restore(ctx: &LaunchContext) {
+ OS_ARGC.store(ctx.argc, core::sync::atomic::Ordering::Relaxed);
+ OS_ARGV.store(ctx.argv.cast_mut(), core::sync::atomic::Ordering::Relaxed);
+}
+
/// Kernel-provided argv slice if [`init_argv`] was called, else `None`.
#[inline]
#[cfg(not(windows))]
@@ -3773,7 +3821,7 @@ fn raw_os_argv() -> Option<&'static [*const core::ffi::c_char]> {
}
fn argv_storage() -> &'static [ZBox] {
- ARGV_STORAGE.get_or_init(|| {
+ ARGV_STORAGE.get(|| {
// Windows: the CRT-provided `char** argv` captured by `init_argv` is
// ANSI-encoded (CP_ACP) — `WideCharToMultiByte` lossy-converts the
// UTF-16 command line, replacing unrepresentable code points with `?`.
@@ -3830,29 +3878,78 @@ fn argv_storage() -> &'static [ZBox] {
})
}
+/// Options a `--compile`d executable carries (its `compile_exec_argv`); spliced after argv[0] like BUN_OPTIONS.
+/// Invariant for the executable, so a plain static (set once by `boot_standalone`, before `argv()` is derived).
+static COMPILE_EXEC_ARGV: RacyCell<&'static [u8]> = RacyCell::new(b"");
+static COMPILE_EXEC_ARGC: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
+/// # Safety
+/// Single-threaded startup, before the first `argv()` read that should see the splice.
+pub unsafe fn set_compile_exec_argv(opts: &'static [u8]) {
+ // SAFETY: see fn doc.
+ unsafe { COMPILE_EXEC_ARGV.write(opts) };
+}
+/// Number of argv tokens that came from the executable's `compile_exec_argv` (valid after `argv()` was read).
+pub fn compile_exec_argc() -> usize {
+ let _ = argv_view();
+ COMPILE_EXEC_ARGC.load(core::sync::atomic::Ordering::Relaxed)
+}
+/// Where the user's own arguments begin in `argv()`, kept relative to the spliced-options prefix (argv[0] + compile-time
+/// options + `BUN_OPTIONS` tokens): that prefix is recomputed with `argv()` itself after a snapshot restore, and the launching
+/// process may carry a different `BUN_OPTIONS` than the one that built the snapshot, so an absolute index would slice wrong.
+static PASSTHROUGH_AFTER_PREFIX: core::sync::atomic::AtomicUsize =
+ core::sync::atomic::AtomicUsize::new(PASSTHROUGH_DISABLED);
+const PASSTHROUGH_DISABLED: usize = usize::MAX;
+fn spliced_prefix_len() -> usize {
+ let _ = argv_view();
+ 1 + COMPILE_EXEC_ARGC.load(core::sync::atomic::Ordering::Relaxed)
+ + BUN_OPTIONS_ARGC.load(core::sync::atomic::Ordering::Relaxed)
+}
+/// `offset` is an absolute index into the current `argv()`; 0 disables (the arguments are not a verbatim argv tail).
+pub fn set_standalone_passthrough_offset(offset: usize) {
+ // (name kept: also set for plain `bun entry args…` when the args are a verbatim argv tail)
+ let value = if offset == 0 {
+ PASSTHROUGH_DISABLED
+ } else {
+ offset.saturating_sub(spliced_prefix_len())
+ };
+ PASSTHROUGH_AFTER_PREFIX.store(value, core::sync::atomic::Ordering::Relaxed);
+}
+/// Absolute index into the current process's `argv()` (0 = disabled).
+pub fn standalone_passthrough_offset() -> usize {
+ match PASSTHROUGH_AFTER_PREFIX.load(core::sync::atomic::Ordering::Relaxed) {
+ PASSTHROUGH_DISABLED => 0,
+ after_prefix => (spliced_prefix_len() + after_prefix).min(argv_view().len()),
+ }
+}
+
#[cold]
#[inline(never)]
-fn argv_view_init() {
+fn argv_view_init() -> ArgvView {
let storage: &'static [ZBox] = argv_storage();
- // ARGV_STORAGE is process-static via `Once`; `as_zstr` borrows for `'static`.
let mut view: Vec<&'static ZStr> = storage.iter().map(ZBox::as_zstr).collect();
- // Splice BUN_OPTIONS tokens after argv[0].
+ // Final order is [argv0, compile-time options, BUN_OPTIONS tokens, the user's arguments], so BUN_OPTIONS overrides what
+ // was compiled in. `append_options_env` inserts at position 1, so the group that must end up leftmost is spliced last.
+ // Counts are stored even when a group is absent: this runs again in a process restored from a snapshot, whose
+ // environment may lack a BUN_OPTIONS the building process had.
+ let before = view.len();
if let Some(opts) = crate::env_var::BUN_OPTIONS.get() {
- let original_len = view.len();
append_options_env::<&'static ZStr>(opts, &mut view);
- set_bun_options_argc(view.len() - original_len);
}
- let view: &'static [&'static ZStr] = ARGV_VIEW.get_or_init(move || view);
- // SAFETY: single-threaded lazy init guarded by Once.
- unsafe { ARGV.write(view) };
+ set_bun_options_argc(view.len() - before);
+ // SAFETY: written once during single-threaded startup.
+ let compile_opts = unsafe { COMPILE_EXEC_ARGV.read() };
+ let before = view.len();
+ if !compile_opts.is_empty() {
+ append_options_env::<&'static ZStr>(compile_opts, &mut view);
+ }
+ COMPILE_EXEC_ARGC.store(view.len() - before, core::sync::atomic::Ordering::Relaxed);
+ ArgvView(RacyCell::new(Vec::leak(view)))
}
#[inline]
fn argv_view() -> &'static [&'static ZStr] {
- ARGV_INIT.call_once(argv_view_init);
- // SAFETY: ARGV is a Copy fat-pointer; only mutated via `set_argv` during
- // single-threaded startup or by the Once above.
- unsafe { ARGV.read() }
+ // SAFETY: the RacyCell is only written by `set_argv` during single-threaded startup.
+ unsafe { ARGV.get(argv_view_init).0.read() }
}
#[derive(Clone, Copy)]
@@ -4109,10 +4206,8 @@ pub fn append_options_env(env: &[u8], args: &mut Vec) {
/// Caller must ensure no concurrent reads of `argv()` are in flight.
#[inline]
pub unsafe fn set_argv(v: &'static [&'static ZStr]) {
- // Prevent the lazy OS-argv init from later clobbering a manually-set view.
- ARGV_INIT.call_once(|| {});
// SAFETY: see fn doc — single-threaded startup.
- unsafe { ARGV.write(v) };
+ unsafe { ARGV.get(argv_view_init).0.write(v) };
}
/// Park an owned argv `Vec` in process-static storage and return the
@@ -4509,6 +4604,11 @@ impl SpawnStatus {
pub fn is_ok(self) -> bool {
self.code == 0
}
+ /// Exit status as the spawner reports it; -1 when the child died of a signal (or, on Windows, no code was available).
+ #[inline]
+ pub fn code(self) -> i32 {
+ self.code
+ }
}
// ── posix_spawn_bun FFI (canonical #[repr(C)] mirror) ─────────────────────
diff --git a/src/dotenv/env_loader.rs b/src/dotenv/env_loader.rs
index 0ae6c8e6a18d..9a89cb38fb18 100644
--- a/src/dotenv/env_loader.rs
+++ b/src/dotenv/env_loader.rs
@@ -126,6 +126,8 @@ pub struct Loader {
pub quiet: bool,
pub(crate) did_load_process: bool,
+ /// Keys that came from the OS environment (so a snapshot restore can drop the builder's and load this process's).
+ process_keys: Vec>,
pub(crate) reject_unauthorized: Cell>,
// Local POD mirror of `bun_s3_signing::S3Credentials` — see type doc above.
@@ -575,11 +577,31 @@ impl Loader {
custom_files_loaded: StringArrayHashMap::default(),
quiet: false,
did_load_process: false,
+ process_keys: Vec::new(),
reject_unauthorized: Cell::new(None),
aws_credentials: None,
}
}
+ /// snapshot restore: the map holds the *builder's* environment. Drop those entries and load this process's environ.
+ pub fn reload_process_after_snapshot_restore(&mut self) -> Result<(), AllocError> {
+ for key in core::mem::take(&mut self.process_keys) {
+ self.map.remove(&key);
+ }
+ self.did_load_process = false;
+ // Derived lazily from the environment being replaced: re-derived from the new one on next use. The credentials in
+ // particular must not outlive the environment they came from.
+ self.reject_unauthorized.set(None);
+ self.aws_credentials = None;
+ self.load_process()?;
+ for (key, value) in core::mem::take(&mut self.map.shadowed_by_process) {
+ if self.map.get(&key).is_none() {
+ self.map.put(&key, &value)?;
+ }
+ }
+ Ok(())
+ }
+
pub fn load_process(&mut self) -> Result<(), AllocError> {
if self.did_load_process {
return Ok(());
@@ -595,10 +617,12 @@ impl Loader {
let value = &env[i as usize + 1..];
if !key.is_empty() {
self.map.put(key, value)?;
+ self.process_keys.push(Box::from(key));
}
} else {
if !env.is_empty() {
self.map.put(env, b"")?;
+ self.process_keys.push(Box::from(env));
}
}
}
@@ -1252,6 +1276,7 @@ impl<'a> Parser<'a> {
map: &mut Map,
) -> Result<(), AllocError> {
let mut count = map.map.count();
+ let shadowed_start = map.shadowed_by_process.len();
while self.pos < self.src.len() {
let Some(key) = self.parse_key::() else {
self.skip_line();
@@ -1266,6 +1291,9 @@ impl<'a> Parser<'a> {
// Allow keys defined later in the same file to override keys defined earlier
// https://github.com/oven-sh/bun/issues/1262
if !OVERRIDE {
+ if bun_core::startup_snapshot::building() {
+ map.shadowed_by_process.push((Box::from(key), value_owned));
+ }
continue;
}
}
@@ -1289,6 +1317,29 @@ impl<'a> Parser<'a> {
}
idx += 1;
}
+ // A key repeated in one file: the later line wins at boot (#1262), so keep only the last stash of each key from this file.
+ let mut i = shadowed_start;
+ while i < map.shadowed_by_process.len() {
+ let key = map.shadowed_by_process[i].0.clone();
+ let last = map
+ .shadowed_by_process
+ .iter()
+ .rposition(|(k, _)| *k == key)
+ .unwrap();
+ if last != i {
+ map.shadowed_by_process.remove(i);
+ } else {
+ i += 1;
+ }
+ }
+ let mut idx = shadowed_start;
+ while idx < map.shadowed_by_process.len() {
+ let current: Box<[u8]> = map.shadowed_by_process[idx].1.clone();
+ if let Some(expanded) = self.expand_value(map, ¤t)? {
+ map.shadowed_by_process[idx].1 = Box::from(expanded);
+ }
+ idx += 1;
+ }
count = 0;
}
let _ = count;
@@ -1334,6 +1385,8 @@ pub type HashTable = bun_collections::CaseInsensitiveAsciiStringArrayHashMap, Box<[u8]>)>,
}
impl Default for Map {
@@ -1436,8 +1489,13 @@ impl Map {
#[inline]
pub(crate) fn init() -> Map {
+ Self::with_table(HashTable::default())
+ }
+
+ pub fn with_table(map: HashTable) -> Map {
Map {
- map: HashTable::default(),
+ map,
+ shadowed_by_process: Vec::new(),
}
}
@@ -1522,9 +1580,7 @@ impl Map {
pub fn clone_with_allocator(&self) -> Result {
// allocator param dropped — global mimalloc
- Ok(Map {
- map: self.map.clone()?,
- })
+ Ok(Map::with_table(self.map.clone()?))
}
}
diff --git a/src/exe_format/elf.rs b/src/exe_format/elf.rs
index 9d231ec56e0a..0083e1b0505b 100644
--- a/src/exe_format/elf.rs
+++ b/src/exe_format/elf.rs
@@ -208,15 +208,17 @@ impl ElfFile {
/// middle of a `PT_LOAD` segment — sections like `.dynamic`, `.got`,
/// `.got.plt` come after it, and expanding in-place would invalidate their
/// absolute virtual addresses.
+ ///
+ /// Re-injecting into our own earlier output works too: every block ends with the `BUN_COMPILED` slot's vaddr, so a rewrite finds the previous block and replaces it: in place when the new payload fits, otherwise at the same address with the segment regrown, so a file never carries more than one block.
pub fn write_bun_section(&mut self, payload: &[u8]) -> Result<(), ElfError> {
let ehdr = read_ehdr(&self.data);
let bun_section = self.find_bun_section(ehdr)?;
- let bun_section_offset = bun_section.file_offset;
let bun_section_vaddr = bun_section.vaddr;
let page_size = Self::page_size(ehdr);
let header_size: u64 = size_of::() as u64;
- let new_content_size: u64 = header_size + payload.len() as u64;
+ let trailer_size: u64 = size_of::() as u64;
+ let new_content_size: u64 = header_size + payload.len() as u64 + trailer_size;
let aligned_new_size = align_up(new_content_size, page_size);
// Extend the writable PT_LOAD that contains `.bun` (matched by vaddr,
@@ -253,6 +255,20 @@ impl ElfFile {
return Err(ElfError::NoWritableLoadSegment);
};
+ let previous_block = self.previous_block_slot(&bun_section, &rw_phdr);
+ let compiled_slot_vaddr = previous_block.unwrap_or(bun_section.vaddr);
+ if previous_block.is_some() && new_content_size <= bun_section.size {
+ // Rewriting our own earlier output with something that fits: overwrite the block in place (the slot already
+ // points at it; the trailer stays at the end of the block's capacity).
+ let start = usize::try_from(bun_section.file_offset).expect("int cast");
+ let cap = usize::try_from(bun_section.size).expect("int cast");
+ write_u64_le(&mut self.data[start..][..8], payload.len() as u64);
+ self.data[start + 8..][..payload.len()].copy_from_slice(payload);
+ self.data[start + 8 + payload.len()..start + cap - 8].fill(0);
+ write_u64_le(&mut self.data[start + cap - 8..][..8], compiled_slot_vaddr);
+ return Ok(());
+ }
+
// Place the new data at a page-aligned virtual address past every
// existing mapping. page_size is ≥ 128 so this also guarantees the
// 128-byte alignment that JSC's bytecode cache requires — see
@@ -266,7 +282,13 @@ impl ElfFile {
// `new_file_offset` follows the segment's existing (vaddr - offset)
// delta, so the kernel's mmap at `rw_phdr.p_offset → rw_phdr.p_vaddr`
// covers our new payload continuously once we grow p_filesz.
- let new_vaddr = align_up(max_vaddr_end, page_size);
+ // A superseded block of ours is the last thing in the segment: the new one takes its place instead of
+ // accumulating behind it, so a file rewritten any number of times carries exactly one block.
+ let new_vaddr = if previous_block.is_some() {
+ bun_section.vaddr
+ } else {
+ align_up(max_vaddr_end, page_size)
+ };
let offset_in_segment = new_vaddr - rw_phdr.p_vaddr;
let new_file_offset = rw_phdr.p_offset + offset_in_segment;
@@ -274,7 +296,7 @@ impl ElfFile {
// memsz range (the loop above folds every PT_LOAD), so new_vaddr is
// past it by construction. This guard catches pathological inputs
// (e.g. corrupt ELF with rw_phdr.p_vaddr past max_vaddr_end).
- if new_vaddr < rw_phdr.p_vaddr + rw_phdr.p_memsz {
+ if previous_block.is_none() && new_vaddr < rw_phdr.p_vaddr + rw_phdr.p_memsz {
return Err(ElfError::NewVaddrCollides);
}
@@ -334,9 +356,11 @@ impl ElfFile {
// Zero the bytes between the old RW file-content end and the payload
// start. This entire range is now inside the extended PT_LOAD's
// file-backed region; keeping it zero preserves BSS semantics.
- self.data[usize::try_from(move_src_start).expect("int cast")
- ..usize::try_from(new_file_offset).expect("int cast")]
- .fill(0);
+ if new_file_offset > move_src_start {
+ self.data[usize::try_from(move_src_start).expect("int cast")
+ ..usize::try_from(new_file_offset).expect("int cast")]
+ .fill(0);
+ }
// Write the payload at the new location: [u64 LE size][data][zero padding]
write_u64_le(
@@ -348,19 +372,24 @@ impl ElfFile {
.copy_from_slice(payload);
// Zero the padding between payload end and the relocated tail
- let payload_end = new_file_offset + new_content_size;
+ let payload_end = new_file_offset + header_size + payload.len() as u64; // the trailer is written separately at the end of the block
if move_dst_start > payload_end {
self.data[usize::try_from(payload_end).expect("int cast")
..usize::try_from(move_dst_start).expect("int cast")]
.fill(0);
}
- // Write the vaddr of the appended data at the ORIGINAL .bun section location
- // (where BUN_COMPILED symbol points). At runtime, BUN_COMPILED.size will be
- // this vaddr (always non-zero), which the runtime dereferences as a pointer.
- // Non-standalone binaries have BUN_COMPILED.size = 0, so 0 means "no data".
+ // The block's trailer: where the `BUN_COMPILED` slot is, for the next rewrite of this file.
write_u64_le(
- &mut self.data[usize::try_from(bun_section_offset).expect("int cast")..][..8],
+ &mut self.data[usize::try_from(new_file_offset + aligned_new_size - trailer_size)
+ .expect("int cast")..][..8],
+ compiled_slot_vaddr,
+ );
+
+ // The runtime reads BUN_COMPILED.size as the payload's vaddr (0 = no payload); the slot sits at a fixed offset into the RW segment in every rewrite of this file.
+ let compiled_slot_offset = rw_phdr.p_offset + (compiled_slot_vaddr - rw_phdr.p_vaddr);
+ write_u64_le(
+ &mut self.data[usize::try_from(compiled_slot_offset).expect("int cast")..][..8],
new_vaddr,
);
@@ -388,7 +417,7 @@ impl ElfFile {
if i == bun_section.section_index as usize {
shdr.sh_offset = new_file_offset;
- shdr.sh_size = new_content_size;
+ shdr.sh_size = aligned_new_size;
shdr.sh_addr = new_vaddr;
} else if shdr.sh_type != SHT_NOBITS
&& shdr.sh_offset >= move_src_start
@@ -432,6 +461,28 @@ impl ElfFile {
// --- Internal helpers ---
+ /// If the `.bun` header describes a block this function wrote earlier, the `BUN_COMPILED` slot recorded in its
+ /// trailer (validated: inside the RW segment and currently pointing at this block); `None` for a clean template.
+ fn previous_block_slot(
+ &self,
+ bun_section: &BunSectionInfo,
+ rw_phdr: &Elf64_Phdr,
+ ) -> Option {
+ let word = size_of::() as u64;
+ if bun_section.size < 2 * word {
+ return None;
+ }
+ let trailer_off =
+ usize::try_from(bun_section.file_offset + bun_section.size - word).ok()?;
+ let slot_vaddr = read_u64_le(self.data.get(trailer_off..trailer_off + 8)?);
+ if slot_vaddr < rw_phdr.p_vaddr || slot_vaddr + word > rw_phdr.p_vaddr + rw_phdr.p_filesz {
+ return None;
+ }
+ let slot_off = usize::try_from(rw_phdr.p_offset + (slot_vaddr - rw_phdr.p_vaddr)).ok()?;
+ (read_u64_le(self.data.get(slot_off..slot_off + 8)?) == bun_section.vaddr)
+ .then_some(slot_vaddr)
+ }
+
/// Returns the file offset and section index of the `.bun` section.
fn find_bun_section(&self, ehdr: Elf64_Ehdr) -> Result {
let shdr_size = size_of::();
@@ -467,6 +518,7 @@ impl ElfFile {
return Ok(BunSectionInfo {
file_offset: shdr.sh_offset,
vaddr: shdr.sh_addr,
+ size: shdr.sh_size,
section_index: u16::try_from(i).expect("int cast"),
});
}
@@ -501,6 +553,8 @@ struct BunSectionInfo {
file_offset: u64,
/// Virtual address of the .bun section (sh_addr).
vaddr: u64,
+ /// Size of whatever the header currently describes (sh_size).
+ size: u64,
/// Index of the .bun section in the section header table.
section_index: u16,
}
@@ -735,6 +789,10 @@ pub(crate) struct Elf64_Shdr {
// --- byte helpers ---
+fn read_u64_le(bytes: &[u8]) -> u64 {
+ u64::from_le_bytes(bytes[..8].try_into().expect("8 bytes"))
+}
+
#[inline]
fn write_u64_le(bytes: &mut [u8], value: u64) {
bytes[..8].copy_from_slice(&value.to_le_bytes());
diff --git a/src/http/AsyncHTTP.rs b/src/http/AsyncHTTP.rs
index e46ff76410e9..b81b76576bee 100644
--- a/src/http/AsyncHTTP.rs
+++ b/src/http/AsyncHTTP.rs
@@ -67,6 +67,10 @@ unsafe impl bun_threading::Linked for AsyncHTTP<'static> {
}
pub(crate) static ACTIVE_REQUESTS_COUNT: AtomicUsize = AtomicUsize::new(0);
+/// Requests the HTTP thread is currently working on (for the snapshot quiescence gate).
+pub fn active_requests_count() -> usize {
+ ACTIVE_REQUESTS_COUNT.load(Ordering::Relaxed)
+}
pub static MAX_SIMULTANEOUS_REQUESTS: AtomicUsize = AtomicUsize::new(256);
// ──────────────────────────────────────────────────────────────────────────
diff --git a/src/http/HTTPThread.rs b/src/http/HTTPThread.rs
index eaec4535e8d5..e0aa62bbfa7d 100644
--- a/src/http/HTTPThread.rs
+++ b/src/http/HTTPThread.rs
@@ -1233,9 +1233,8 @@ use core::cell::Cell;
mod _event_loop_draft {
use super::*;
- use std::sync::Once;
-
- static INIT_ONCE: Once = Once::new();
+ static INIT_ONCE: bun_core::startup_snapshot::SnapshotOnce =
+ bun_core::startup_snapshot::SnapshotOnce::new();
// Note: `Builder::spawn` allocates an `Arc` (48 B)
// shared between the `JoinHandle` and the new thread's TLS `current()`.
// Dropping the handle leaves the only strong ref inside the spawned
@@ -1249,7 +1248,7 @@ mod _event_loop_draft {
std::sync::OnceLock::new();
pub(super) fn init(opts: &InitOpts) {
- INIT_ONCE.call_once(|| init_once(opts));
+ INIT_ONCE.call(|| init_once(opts));
}
fn init_once(opts: &InitOpts) {
@@ -1417,6 +1416,13 @@ static SHUTDOWN_DONE: (bun_threading::Guarded, bun_threading::Condvar) = (
bun_threading::Condvar::new(),
);
+/// After the freeze-time shutdown: the snapshot must not carry "shutting down", or the thread `init` starts again in a
+/// restored process (its `SnapshotOnce` re-runs there) would exit at once; and that process's own exit must still drain.
+pub fn reset_shutdown_state_for_snapshot() {
+ SHUTDOWN_REQUESTED.store(false, Ordering::Release);
+ *SHUTDOWN_DONE.0.lock() = false;
+}
+
/// Called from `bun_jsc::VirtualMachine::global_exit()` on the JS thread,
/// before `~VM`. Asks the HTTP daemon thread to reclaim every in-flight
/// `ThreadlocalAsyncHTTP` box and waits (with a short timeout) for it to ack.
diff --git a/src/http/lib.rs b/src/http/lib.rs
index 5e27bd2f4e12..53b01118787c 100644
--- a/src/http/lib.rs
+++ b/src/http/lib.rs
@@ -242,6 +242,7 @@ impl Default for Flags {
// ───────────────────────────── globals ─────────────────────────────
pub(crate) static ASYNC_HTTP_ID_MONOTONIC: AtomicU32 = AtomicU32::new(0);
+pub use crate::async_http::active_requests_count;
/// Set once at startup from `--experimental-http2-fetch` (before the HTTP
/// thread spawns) and then only read on that thread.
diff --git a/src/install_jsc/ini_jsc.rs b/src/install_jsc/ini_jsc.rs
index ce6729ca685b..c05131d65b51 100644
--- a/src/install_jsc/ini_jsc.rs
+++ b/src/install_jsc/ini_jsc.rs
@@ -78,7 +78,9 @@ impl IniTestingAPIs {
)?;
}
- env_storage.insert(dotenv::Loader::init_with_map(dotenv::Map { map: envmap }))
+ env_storage.insert(dotenv::Loader::init_with_map(dotenv::Map::with_table(
+ envmap,
+ )))
};
let mut install = Box::new(BunInstall::default());
diff --git a/src/io/ParentDeathWatchdog.rs b/src/io/ParentDeathWatchdog.rs
index fe05cc8c709a..a32bfafdce86 100644
--- a/src/io/ParentDeathWatchdog.rs
+++ b/src/io/ParentDeathWatchdog.rs
@@ -291,6 +291,21 @@ pub fn enable() {
#[inline]
pub fn ensure_kill_on_close_job() {}
+/// A process restored from a snapshot inherited the builder's parent pid and (on macOS) the builder's registered watch, both
+/// meaningless here, and its main thread is not yet marked as the arming thread: watch this process's own parent instead. The inherited poll is left alone by the restore (see
+/// `FilePoll::rearm_after_snapshot_restore`); it never kept the loop alive and nothing will ever fire it.
+pub fn reinstall_after_snapshot_restore(handle: EventLoopCtx) {
+ if !ENABLED.load(Ordering::Relaxed) {
+ return;
+ }
+ // SAFETY: getppid cannot fail.
+ ORIGINAL_PPID.store(unsafe { libc::getppid() }, Ordering::Relaxed);
+ bun_spawn_sys::pdeathsig::readopt_arming_thread();
+ #[cfg(target_os = "macos")]
+ EVENT_LOOP_INSTALLED.store(false, Ordering::Relaxed);
+ install_on_event_loop(handle);
+}
+
/// Register `EVFILT_PROC`/`NOTE_EXIT` for the original parent on the main
/// event loop's kqueue. Called from `VirtualMachine.init` once the uws loop is
/// up. macOS-only; no-op elsewhere and on subsequent calls.
diff --git a/src/io/lib.rs b/src/io/lib.rs
index e17d52e9384b..b8d251583c98 100644
--- a/src/io/lib.rs
+++ b/src/io/lib.rs
@@ -264,6 +264,8 @@ pub mod parent_death_watchdog {
#[inline]
pub fn install_on_event_loop(_handle: EventLoopCtx) {}
+ #[inline]
+ pub fn reinstall_after_snapshot_restore(_handle: EventLoopCtx) {}
}
pub use parent_death_watchdog as ParentDeathWatchdog;
@@ -444,6 +446,8 @@ impl EventLoopCtx {
}
#[cfg(not(windows))]
pub use posix_event_loop::Store;
+#[cfg(any(target_os = "macos", target_os = "linux", target_os = "android"))]
+pub use posix_event_loop::dispatch_snapshot_hangups;
#[cfg(windows)]
pub use windows_event_loop::Store;
@@ -786,6 +790,16 @@ static LOOP: bun_core::ThreadCell> =
#[cfg(not(windows))]
static ONCE: std::sync::OnceLock<()> = std::sync::OnceLock::new();
+/// Snapshot freeze: the "IO Watcher" thread has no stop protocol, so a snapshot is refused while it exists.
+#[cfg(not(windows))]
+pub fn io_watcher_snapshot_blocker() -> Option<&'static str> {
+ ONCE.get().map(|_| "the file I/O watcher thread is running (Bun.file()/Bun.write() on a pipe or terminal started it) — a snapshot cannot contain a thread")
+}
+#[cfg(windows)]
+pub fn io_watcher_snapshot_blocker() -> Option<&'static str> {
+ None
+}
+
impl IoRequestLoop {
#[cfg(not(windows))]
fn load() {
diff --git a/src/io/posix_event_loop.rs b/src/io/posix_event_loop.rs
index 86f853834a59..ddf56c98feb9 100644
--- a/src/io/posix_event_loop.rs
+++ b/src/io/posix_event_loop.rs
@@ -315,8 +315,64 @@ impl Default for FilePoll {
}
}
+/// Outcome of `FilePoll::rearm_after_snapshot_restore`.
+#[cfg(any(target_os = "macos", target_os = "linux", target_os = "android"))]
+enum StartupSnapshotRearm {
+ Untouched,
+ Rearmed,
+ HungUp,
+}
+
#[cfg(not(windows))]
impl FilePoll {
+ /// snapshot restore (`Store::rearm_after_snapshot_restore`): re-add this poll to the new kqueue if its fd still means the same
+ /// thing in this process, otherwise mark it hung up so the owner hears about it once the app has been told to restore.
+ #[cfg(any(target_os = "macos", target_os = "linux", target_os = "android"))]
+ fn rearm_after_snapshot_restore(&mut self, loop_: &mut Loop) -> StartupSnapshotRearm {
+ if self.fd == INVALID_FD
+ || !self.flags.contains(Flags::WasEverRegistered)
+ || self.flags.contains(Flags::Closed)
+ {
+ return StartupSnapshotRearm::Untouched;
+ }
+ let want = if self.flags.contains(Flags::PollReadable) {
+ Flags::Readable
+ } else if self.flags.contains(Flags::PollWritable) {
+ Flags::Writable
+ } else {
+ // A process poll's "fd" is a pid from the building process; the one such poll a snapshot can hold (the parent-death
+ // watchdog: children block the freeze) is re-created for this process's parent by its owner, not re-armed or hung up.
+ return StartupSnapshotRearm::Untouched;
+ };
+ // Only fds the restore re-seated mean the same thing in this process: stdio (dup'd from the launcher onto the
+ // builder's numbers) and the controlling tty. Every other number is stale or parked on /dev/null.
+ // SAFETY: probing an integer fd.
+ let reseated = self.fd.native() <= 2 || unsafe { libc::isatty(self.fd.native()) } == 1;
+ if reseated {
+ let one_shot =
+ self.flags.contains(Flags::OneShot) || self.flags.contains(Flags::NeedsRearm);
+ self.flags.remove(Flags::NeedsRearm);
+ // Nothing is registered in this process's epoll/kqueue yet: registering must add, not modify.
+ self.flags
+ .remove_all(Flags::PollReadable | Flags::PollWritable | Flags::PollProcess);
+ let one_shot = if one_shot {
+ OneShotFlag::OneShot
+ } else {
+ OneShotFlag::None
+ };
+ if self
+ .register_with_fd(loop_, want, one_shot, self.fd)
+ .is_ok()
+ {
+ return StartupSnapshotRearm::Rearmed;
+ }
+ } else {
+ self.flags
+ .remove_all(Flags::PollReadable | Flags::PollWritable | Flags::PollProcess);
+ }
+ self.flags.insert(Flags::Hup);
+ StartupSnapshotRearm::HungUp
+ }
fn update_flags(&mut self, updated: FlagsSet) {
let mut flags = self.flags;
flags.remove(Flags::Readable);
@@ -1319,15 +1375,63 @@ pub struct Store {
hive: FilePollHive,
pending_free_head: *mut FilePoll,
pending_free_tail: *mut FilePoll,
+ /// Polls whose fd did not survive a snapshot restore; their hangups are delivered from the event loop once the
+ /// process is fully adopted and the app has heard 'restore', not from inside the restore itself.
+ #[cfg(any(target_os = "macos", target_os = "linux", target_os = "android"))]
+ snapshot_hangups: Vec<*mut FilePoll>,
}
#[cfg(not(windows))]
impl Store {
+ /// Live polls in the inline hive. At `HIVE_SIZE` there may also be overflow polls, which nothing can enumerate after a
+ /// restore, so a freeze at that point is refused (`snapshot_blockers`).
+ #[cfg(any(target_os = "macos", target_os = "linux", target_os = "android"))]
+ pub fn inline_hive_is_full(&self) -> bool {
+ let mut it = self.hive.hive.used.iter_set();
+ let mut n = 0usize;
+ while it.next().is_some() {
+ n += 1;
+ }
+ n >= HIVE_SIZE
+ }
+
+ /// snapshot restore: the kqueue is new and every knote from the build process is gone. Re-add polls whose fd still exists; hang up the rest.
+ #[cfg(any(target_os = "macos", target_os = "linux", target_os = "android"))]
+ pub fn rearm_after_snapshot_restore(&mut self, loop_: &mut Loop) -> (usize, usize) {
+ let (mut rearmed, mut hung_up) = (0usize, 0usize);
+ let mut polls: Vec<*mut FilePoll> = Vec::new();
+ let mut it = self.hive.hive.used.iter_set();
+ while let Some(i) = it.next() {
+ polls.push(self.hive.hive.at(i as u16));
+ }
+ for poll_ptr in polls {
+ // SAFETY: the slot is marked used in the hive and nothing runs concurrently during restore; the call is the
+ // only access through this pointer.
+ match unsafe { (*poll_ptr).rearm_after_snapshot_restore(loop_) } {
+ StartupSnapshotRearm::Untouched => {}
+ StartupSnapshotRearm::Rearmed => rearmed += 1,
+ StartupSnapshotRearm::HungUp => {
+ self.snapshot_hangups.push(poll_ptr);
+ hung_up += 1;
+ }
+ }
+ }
+ (rearmed, hung_up)
+ }
+
+ /// The polls whose fds did not survive the restore, handed out so they can be delivered with no borrow of the store live.
+ #[cfg(any(target_os = "macos", target_os = "linux", target_os = "android"))]
+ pub fn take_snapshot_hangups(&mut self) -> Vec<*mut FilePoll> {
+ core::mem::take(&mut self.snapshot_hangups)
+ }
+
pub fn init() -> Store {
Store {
hive: FilePollHive::init(),
pending_free_head: ptr::null_mut(),
pending_free_tail: ptr::null_mut(),
+ #[cfg(any(target_os = "macos", target_os = "linux", target_os = "android"))]
+ snapshot_hangups: Vec::new(),
}
}
@@ -1413,6 +1517,29 @@ impl Store {
}
}
+/// Deliver the hangups collected at restore. Not a `Store` method on purpose (see [`Store::take_snapshot_hangups`]).
+#[cfg(any(target_os = "macos", target_os = "linux", target_os = "android"))]
+pub fn dispatch_snapshot_hangups(pending: Vec<*mut FilePoll>) -> usize {
+ let mut delivered = 0usize;
+ for poll_ptr in pending {
+ // SAFETY: collected from used hive slots during restore; a slot is only recycled through the deferred-free list, which
+ // does not run until the loop ticks. A poll closed by an earlier hangup's owner is still readable and reads as closed.
+ let still_hung_up = unsafe {
+ let poll = &*poll_ptr;
+ poll.fd != INVALID_FD
+ && !poll.flags.contains(Flags::Closed)
+ && poll.flags.contains(Flags::Hup)
+ };
+ if !still_hung_up {
+ continue;
+ }
+ // SAFETY: as above; the owner may re-enter the store freely, since nothing of it is borrowed here.
+ unsafe { (*poll_ptr).on_update(0) };
+ delivered += 1;
+ }
+ delivered
+}
+
// ──────────────────────────────────────────────────────────────────────────
// onTick (exported)
// ──────────────────────────────────────────────────────────────────────────
diff --git a/src/jsc/AsyncModule.rs b/src/jsc/AsyncModule.rs
index 5bfc14267a0c..3c2604475352 100644
--- a/src/jsc/AsyncModule.rs
+++ b/src/jsc/AsyncModule.rs
@@ -1253,9 +1253,7 @@ impl AsyncModule {
// swap the buffer out and write it back via the `_writeback`
// guard — same observable effect (the thread-local's buffer is
// reused). Matches RuntimeTranspilerStore.rs.
- let mut printer_ptr = crate::virtual_machine::SOURCE_CODE_PRINTER
- .get()
- .expect("source_code_printer not initialized");
+ let mut printer_ptr = crate::virtual_machine::source_code_printer();
// SAFETY: thread-local owns the leaked Box; only this thread touches it.
let mut printer = core::mem::replace(
unsafe { printer_ptr.as_mut() },
diff --git a/src/jsc/JSGlobalObject.rs b/src/jsc/JSGlobalObject.rs
index 032eacd0c94e..179ed65328b6 100644
--- a/src/jsc/JSGlobalObject.rs
+++ b/src/jsc/JSGlobalObject.rs
@@ -312,6 +312,56 @@ impl JSGlobalObject {
JSValue::from_encoded(std::ptr::from_ref::(self) as usize)
}
+ /// I/O whose result would be baked into a snapshot is refused while one is being built. Under `IoPolicy::Local`,
+ /// what only touches this machine (files, subprocesses, local listeners, the resolver) is allowed and recorded for the
+ /// report the snapshot writer prints; network use is refused regardless.
+ pub fn throw_disabled_in_snapshot_error_if_needed(
+ &self,
+ what: &'static str,
+ ) -> Result<(), JsError> {
+ if !bun_core::startup_snapshot::building() {
+ return Ok(());
+ }
+ if bun_core::startup_snapshot::io_allowed(what) {
+ bun_core::startup_snapshot::note_local_io(what, self.current_call_site_for_report());
+ return Ok(());
+ }
+ Err(self.throw_invalid_arguments(format_args!(
+ "{what} is not available while building a snapshot: its result would be frozen into every launch. Do it after restore (process.on('restore')), or allow it for the build with --snapshot-io, which reports every use"
+ )))
+ }
+
+ /// The innermost JS frames as an error stack would print them (source maps applied), for the build-time reports.
+ fn current_call_site_for_report(&self) -> Vec {
+ let err = self.create_error_instance(format_args!(""));
+ let Ok(Some(stack)) = err.get(self, "stack") else {
+ self.clear_exception(); // a user Error.prepareStackTrace may have thrown; the report is best-effort
+ return Vec::new();
+ };
+ let Ok(stack) = stack.to_bun_string(self) else {
+ self.clear_exception();
+ return Vec::new();
+ };
+ let utf8 = stack.to_utf8();
+ let mut site = Vec::new();
+ // Skip the message line; keep the four innermost frames.
+ for line in bun_core::strings::split(utf8.slice(), b"\n")
+ .skip(1)
+ .take(4)
+ {
+ let line = line.trim_ascii();
+ if line.is_empty() {
+ continue;
+ }
+ if !site.is_empty() {
+ site.push(b'\n');
+ }
+ site.extend_from_slice(b" ");
+ site.extend_from_slice(line);
+ }
+ site
+ }
+
pub fn throw_invalid_arguments(&self, args: Arguments<'_>) -> JsError {
let err = self.to_invalid_arguments(args);
self.throw_value(err)
diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs
index 0ee8a9c4075e..f3139d690f0a 100644
--- a/src/jsc/VirtualMachine.rs
+++ b/src/jsc/VirtualMachine.rs
@@ -286,7 +286,7 @@ pub struct VirtualMachine {
pub argv: Vec>,
pub origin_timer: std::time::Instant,
- pub(crate) origin_timestamp: u64,
+ pub origin_timestamp: u64,
/// For fake timers: override performance.now() with a specific value (in nanoseconds).
pub overridden_performance_now: Option,
pub(crate) macro_event_loop: EventLoop,
@@ -296,7 +296,7 @@ pub struct VirtualMachine {
pub(crate) ref_strings: crate::ref_string::Map,
pub(crate) ref_strings_mutex: bun_threading::Mutex,
- pub(crate) active_tasks: usize,
+ pub active_tasks: usize,
pub rare_data: Option>,
pub proxy_env_storage: crate::rare_data::ProxyEnvStorage,
@@ -464,6 +464,12 @@ impl VMHolder {
pub(crate) fn set_vm(vm: Option<*mut VirtualMachine>) {
VM.set(vm)
}
+ /// Snapshot: install an existing VM (from the snapshot's static) as this thread's VM.
+ pub(crate) fn adopt(vm: *mut VirtualMachine) {
+ VM.set(Some(vm));
+ // SAFETY: `vm` is the snapshot's live main-thread VM.
+ CACHED_GLOBAL_OBJECT.set(Some(unsafe { (*vm).global }));
+ }
#[inline(always)]
fn set_cached_global_object(g: Option<*mut JSGlobalObject>) {
CACHED_GLOBAL_OBJECT.set(g)
@@ -704,6 +710,14 @@ unsafe impl Sync for VirtualMachine {}
unsafe impl Send for VirtualMachine {}
impl VirtualMachine {
+ /// Snapshot: the main-thread VM recorded in a plain static (survives a snapshot restore, unlike TLS).
+ pub fn main_thread_vm_ptr() -> *mut VirtualMachine {
+ MAIN_THREAD_VM.load(core::sync::atomic::Ordering::Acquire)
+ }
+ pub fn adopt_on_current_thread(vm: *mut VirtualMachine) {
+ VMHolder::adopt(vm);
+ }
+
/// Safe `&'static` accessor for the current thread's VM. The VM is a
/// per-thread singleton allocated once in [`init`] and never freed until
/// thread teardown, so the `'static` lifetime is sound. Mutation goes
@@ -1189,6 +1203,7 @@ impl VirtualMachine {
self.is_event_loop_alive_excluding_immediates()
|| !el.immediate_tasks.is_empty()
|| !el.next_immediate_tasks.is_empty()
+ || (self.is_main_thread() && bun_core::startup_snapshot::snapshot_requested()) // keep turning until the outermost tick takes the snapshot
}
pub fn wakeup(&mut self) {
@@ -2076,6 +2091,8 @@ pub struct RuntimeHooks {
/// (forward-dep cycle), so [`uncaught_exception`] reaches it through this
/// slot instead of the linker.
pub process_exit: unsafe fn(global: *mut JSGlobalObject, code: u8),
+ /// A snapshot was requested and the resulting termination reached the top of the event loop: quiesce and write the snapshot (noreturn).
+ pub take_snapshot: fn(vm: *mut VirtualMachine) -> !,
/// `onBeforePrint()` for the `bun:test` runner, which lives in `bun_runtime`;
/// `console.log` calls this so the test reporter can flush its line state
/// before user output interleaves with it. No-op when `bun test` isn't
@@ -2326,7 +2343,7 @@ unsafe extern "C" {
) -> *mut JSInternalPromise;
}
-fn get_origin_timestamp() -> u64 {
+pub fn get_origin_timestamp() -> u64 {
// Subtract the Y2K epoch so the timestamp fits in a u64 (nanoseconds).
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -3251,6 +3268,14 @@ fn ensure_source_code_printer() {
}
}
+/// Made on first use: thread-locals are not in a snapshot, so a restored process arrives here with the slot empty.
+pub(crate) fn source_code_printer() -> NonNull {
+ ensure_source_code_printer();
+ SOURCE_CODE_PRINTER
+ .get()
+ .expect("ensure_source_code_printer just set it")
+}
+
/// Free this thread's [`SOURCE_CODE_PRINTER`] Box (if any).
fn drop_source_code_printer() {
if let Some(printer) = SOURCE_CODE_PRINTER.take() {
@@ -3479,29 +3504,17 @@ impl VirtualMachine {
self.rare_data().mime_type_from_string(str_)
}
- /// Applies env-derived runtime settings, claims the per-thread source code printer, and adopts `NODE_CHANNEL_FD` for IPC.
- pub fn load_extra_env_and_source_code_printer(&mut self) {
- // `Transpiler::env_mut()` encapsulates the raw-ptr deref; the returned
- // `&'static mut Loader` is independent of `&self`, so `map` may be held
- // across the `&mut self` writes below.
+ /// Snapshot restore: defaults latched from the builder's environment must not survive; TLS verification in particular must not stay off.
+ pub fn forget_env_derived_defaults_for_snapshot_restore(&mut self) {
+ self.default_tls_reject_unauthorized = None;
+ self.default_verbose_fetch.set(None);
+ }
+
+ /// The channel belongs to the launch: run at boot, and again once a snapshot restore has reloaded the environment.
+ pub fn adopt_ipc_channel_from_env(&mut self) {
+ self.pending_ipc = None; // a builder that was itself spawned with a channel left one here
let env = self.transpiler.env_mut();
let map = &mut env.map;
-
- ensure_source_code_printer();
- // The runtime VM owns the printer from here on — even if a macro had
- // allocated it first, `__bun_macro_context_deinit` must not free it.
- SOURCE_CODE_PRINTER_FROM_MACRO.set(false);
-
- if map.get(b"BUN_SHOW_BUN_STACKFRAMES").is_some() {
- self.hide_bun_stackframes = false;
- }
-
- if bun_core::env_var::feature_flag::BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER::get()
- .unwrap_or(false)
- {
- self.transpiler_store.enabled = false;
- }
-
if let Some(idx) = map.map.get_index(b"NODE_CHANNEL_FD") {
let (_, kv) = map.map.swap_remove_at(idx);
let fd_s = kv.value;
@@ -3535,6 +3548,30 @@ impl VirtualMachine {
}
}
}
+ }
+
+ /// Applies env-derived runtime settings, claims the per-thread source code printer, and adopts `NODE_CHANNEL_FD` for IPC.
+ pub fn load_extra_env_and_source_code_printer(&mut self) {
+ // `Transpiler::env_mut()` encapsulates the raw-ptr deref; the returned
+ // `&'static mut Loader` is independent of `&self`, so `map` may be held
+ // across the `&mut self` writes below.
+ let env = self.transpiler.env_mut();
+ let map = &mut env.map;
+
+ ensure_source_code_printer();
+ // The runtime VM owns the printer from here on — even if a macro had
+ // allocated it first, `__bun_macro_context_deinit` must not free it.
+ SOURCE_CODE_PRINTER_FROM_MACRO.set(false);
+
+ if map.get(b"BUN_SHOW_BUN_STACKFRAMES").is_some() {
+ self.hide_bun_stackframes = false;
+ }
+
+ if bun_core::env_var::feature_flag::BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER::get()
+ .unwrap_or(false)
+ {
+ self.transpiler_store.enabled = false;
+ }
// Node.js checks if this is set to "1" and no other value
if let Some(value) = map.get(b"NODE_PRESERVE_SYMLINKS") {
@@ -3575,6 +3612,8 @@ impl VirtualMachine {
}
}
}
+
+ self.adopt_ipc_channel_from_env(); // last: it takes its own borrow of the env map, so nothing above may still hold one
}
/// Routes an unhandled promise rejection to the configured handler, bumping the unhandled-error counter.
@@ -4228,9 +4267,7 @@ impl VirtualMachine {
}
let mut guard = ArenaReset(jsc_vm, flags != FetchFlags::PrintSource);
- let printer = SOURCE_CODE_PRINTER
- .get()
- .expect("source_code_printer not initialized");
+ let printer = source_code_printer();
// Note: the §Dispatch shim takes path/loader/module_type/printer/
// promise_ptr bundled as `TranspileExtra` behind `args.extra` (see
diff --git a/src/jsc/VmHandle.rs b/src/jsc/VmHandle.rs
index 1a536254a518..b2eed0eb10a0 100644
--- a/src/jsc/VmHandle.rs
+++ b/src/jsc/VmHandle.rs
@@ -94,7 +94,7 @@ pub struct Shared {
/// [`VmHandle::embedded_work_scheduled`]); teardown waits for zero.
embedded: AtomicU32,
#[cfg(debug_assertions)]
- js_thread: std::thread::ThreadId,
+ js_thread: std::sync::Mutex,
/// Test suite only — see [`refusal_gate`].
#[cfg(debug_assertions)]
park_posts: core::sync::atomic::AtomicBool,
@@ -149,7 +149,7 @@ impl VmHandle {
drained: (Mutex::new(), Condvar::new()),
embedded: AtomicU32::new(0),
#[cfg(debug_assertions)]
- js_thread: std::thread::current().id(),
+ js_thread: std::sync::Mutex::new(std::thread::current().id()),
#[cfg(debug_assertions)]
park_posts: core::sync::atomic::AtomicBool::new(false),
}))
@@ -329,12 +329,24 @@ impl VmHandle {
#[cfg(debug_assertions)]
pub(crate) fn assert_js_thread(&self) {
- debug_assert_eq!(std::thread::current().id(), self.0.js_thread);
+ debug_assert_eq!(
+ std::thread::current().id(),
+ *self.0.js_thread.lock().unwrap()
+ );
}
#[cfg(not(debug_assertions))]
#[inline(always)]
pub(crate) fn assert_js_thread(&self) {}
+ /// Snapshot restore: the JS thread is now the calling thread, not the builder's.
+ #[cfg(debug_assertions)]
+ pub fn readopt_js_thread(&self) {
+ *self.0.js_thread.lock().unwrap() = std::thread::current().id();
+ }
+ #[cfg(not(debug_assertions))]
+ #[inline(always)]
+ pub fn readopt_js_thread(&self) {}
+
/// The VM is going away: `Open → Stopping` (idempotent; never reopens or
/// un-closes). Any thread — a parent's `terminate()` calls it at request
/// time, as Node's `Environment::ExitEnv` sets `is_stopping` from the
@@ -403,7 +415,7 @@ mod refusal_gate {
pub(super) fn before_post(h: &VmHandle) {
if !h.posts_parked()
- || std::thread::current().id() == h.0.js_thread
+ || std::thread::current().id() == *h.0.js_thread.lock().unwrap()
|| h.0.embedded.load(Ordering::SeqCst) != 0
{
return;
diff --git a/src/jsc/bindings/BunJSCEventLoop.cpp b/src/jsc/bindings/BunJSCEventLoop.cpp
index 736fdd6207fb..f9fa4139f2d4 100644
--- a/src/jsc/bindings/BunJSCEventLoop.cpp
+++ b/src/jsc/bindings/BunJSCEventLoop.cpp
@@ -23,9 +23,13 @@ extern "C" uint64_t us_internal_monotonic_ns(void);
// it as an atomic rather than through a plain `int`.
extern "C" std::atomic Bun__defaultRemainingRunsUntilSkipReleaseAccess;
+#include "StartupSnapshot.h"
extern "C" void Bun__JSC_onBeforeWait(JSC::VM* _Nonnull vm, uint64_t nowNs)
{
ASSERT(vm);
+#if BUN_STARTUP_SNAPSHOT_TOOLING
+ Bun__startupSnapshotToolingTick(vm);
+#endif
const bool previouslyHadAccess = vm->heap.hasHeapAccess();
// sanity check for debug builds to ensure we're not doing a
// use-after-free here
diff --git a/src/jsc/bindings/BunObject+exports.h b/src/jsc/bindings/BunObject+exports.h
index 31319ae85641..51916e49fb7b 100644
--- a/src/jsc/bindings/BunObject+exports.h
+++ b/src/jsc/bindings/BunObject+exports.h
@@ -40,6 +40,7 @@
macro(origin) \
macro(s3) \
macro(semver) \
+ macro(startupSnapshot) \
macro(unsafe) \
macro(valkey) \
diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp
index 8a022228eb4c..bf11237526e4 100644
--- a/src/jsc/bindings/BunObject.cpp
+++ b/src/jsc/bindings/BunObject.cpp
@@ -1006,6 +1006,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
resolveSync BunObject_callback_resolveSync DontDelete|Function 1
revision constructBunRevision ReadOnly|DontDelete|PropertyCallback
semver BunObject_lazyPropCb_wrap_semver ReadOnly|DontDelete|PropertyCallback
+ startupSnapshot BunObject_lazyPropCb_wrap_startupSnapshot ReadOnly|DontDelete|PropertyCallback
sql defaultBunSQLObject DontDelete|PropertyCallback
postgres defaultBunSQLObject DontDelete|PropertyCallback
SQL constructBunSQLObject DontDelete|PropertyCallback
@@ -1201,3 +1202,44 @@ void generateNativeModule_BunObject(JSC::JSGlobalObject* lexicalGlobalObject,
}
} // namespace Zig
+
+// snapshot restore: launch-derived lazy properties that were already reified into own properties on the Bun object
+// get their value recomputed for this process (same callbacks as first access).
+extern "C" void Bun__BunObject__refreshLaunchDerivedProperties(Zig::GlobalObject* globalObject)
+{
+ auto& vm = JSC::getVM(globalObject);
+ auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
+ globalObject->armStdioBlobs(); // whether or not Bun itself was reified: the slots behind Bun.stdin/stdout/stderr hold the builder's blobs
+ if (!globalObject->m_bunObject.isInitialized())
+ return; // never touched during the build: nothing was reified, and making it now would only dirty pages
+ JSObject* bunObject = globalObject->bunObject();
+ struct LaunchDerivedProp {
+ ASCIILiteral name;
+ JSValue (*make)(VM&, JSObject*);
+ };
+ static const LaunchDerivedProp props[] = {
+ { "argv"_s, BunObject_lazyPropCb_wrap_argv },
+ { "cwd"_s, BunObject_lazyPropCb_wrap_cwd },
+ { "enableANSIColors"_s, BunObject_lazyPropCb_wrap_enableANSIColors },
+ // importing anything from "bun" reifies every entry, so these are present in practically every snapshot
+ { "s3"_s, BunObject_lazyPropCb_wrap_s3 },
+ { "stdin"_s, Bun::BunObject_lazyPropCb_wrap_stdin },
+ { "stdout"_s, Bun::BunObject_lazyPropCb_wrap_stdout },
+ { "stderr"_s, Bun::BunObject_lazyPropCb_wrap_stderr },
+ { "redis"_s, BunObject_lazyPropCb_wrap_valkey }, // the default client is built from REDIS_URL, credentials included
+ };
+ for (auto& p : props) {
+ JSC::Identifier id = JSC::Identifier::fromString(vm, p.name);
+ if (bunObject->getDirectOffset(vm, id) == invalidOffset)
+ continue; // never accessed: the static-table callback will run on first access
+ JSValue fresh = p.make(vm, bunObject);
+ if (scope.exception() || !fresh) { // e.g. this launch's REDIS_URL is malformed: say so and leave nothing of the builder's behind; 'restore' must still fire
+ auto* exception = scope.exception();
+ (void)scope.tryClearException(); // before stringifying: toString bails while an exception is pending
+ WTF::String why = exception ? exception->value().toWTFStringForConsole(globalObject) : "no value"_s;
+ fprintf(stderr, "[snapshot] Bun.%s could not be remade for this launch: %s\n", p.name.characters(), why.utf8().data());
+ fresh = JSC::jsUndefined();
+ }
+ bunObject->putDirect(vm, id, fresh, 0);
+ }
+}
diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp
index cac125f29df3..45798fe7c88a 100644
--- a/src/jsc/bindings/BunProcess.cpp
+++ b/src/jsc/bindings/BunProcess.cpp
@@ -1570,6 +1570,18 @@ extern "C" void Bun__installWatchModeSignalHandler(int signalNumber)
}
#endif
+// Signal dispositions are kernel state: a resumed process has the listener table but none of the handlers the build installed.
+extern "C" void Bun__Process__reinstallSignalHandlersAfterSnapshotRestore()
+{
+#if !OS(WINDOWS)
+ if (!signalToContextIdsMap || signalToContextIdsMap->isEmpty())
+ return;
+ Bun__ensureSignalHandler();
+ for (auto& entry : *signalToContextIdsMap)
+ installForwardSignalHandler(entry.key);
+#endif
+}
+
extern "C" void Bun__MemoryPressure__install(JSC::JSGlobalObject* global);
extern "C" void Bun__MemoryPressure__uninstall(JSC::JSGlobalObject* global);
@@ -2953,13 +2965,25 @@ static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, JSC:
return resultObject->getIndex(globalObject, 0);
}
+extern "C" bool Bun__startupSnapshotIsBuilding();
+extern "C" void Bun__startupSnapshotNoteStdioStream(int fd, const uint8_t* site, size_t len);
+static void noteStdioStreamForSnapshotBuild(JSObject* processObject, int fd)
+{
+ if (!Bun__startupSnapshotIsBuilding()) [[likely]]
+ return;
+ auto site = Bun::snapshotReportCallSite(processObject->globalObject()).utf8();
+ Bun__startupSnapshotNoteStdioStream(fd, reinterpret_cast(site.data()), site.length());
+}
+
static JSValue constructStdout(VM& vm, JSObject* processObject)
{
+ noteStdioStreamForSnapshotBuild(processObject, 1);
return constructStdioWriteStream(processObject->globalObject(), processObject, 1);
}
static JSValue constructStderr(VM& vm, JSObject* processObject)
{
+ noteStdioStreamForSnapshotBuild(processObject, 2);
return constructStdioWriteStream(processObject->globalObject(), processObject, 2);
}
@@ -2969,6 +2993,7 @@ static JSValue constructStderr(VM& vm, JSObject* processObject)
static JSValue constructStdin(VM& vm, JSObject* processObject)
{
+ noteStdioStreamForSnapshotBuild(processObject, 0);
auto* globalObject = processObject->globalObject();
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
JSC::JSFunction* getStdinStream = JSC::JSFunction::create(vm, globalObject, processObjectInternalsGetStdinStreamCodeGenerator(vm), globalObject);
@@ -3078,6 +3103,8 @@ static JSValue constructExecPath(VM& vm, JSObject* processObject)
return JSValue::decode(Bun__Process__getExecPath(globalObject));
}
+static void refreshReifiedLaunchProperties(VM&, JSObject*);
+
extern "C" EncodedJSValue Bun__Process__getArgv(JSGlobalObject* lexicalGlobalObject)
{
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
@@ -3215,6 +3242,110 @@ static JSValue constructRevision(VM& vm, JSObject* processObject)
return JSC::jsString(vm, makeAtomString(ASCIILiteral::fromLiteralUnsafe(Bun__version_sha)));
}
+// snapshot restore: process.env holds the builder's environment; rebuild it from the reloaded loader map.
+extern "C" void Bun__BunObject__refreshLaunchDerivedProperties(Zig::GlobalObject*);
+// Streams created before the snapshot describe the build's descriptors: terminal-to-terminal keeps the object (captured references stay valid) and refreshes its size; anything else is rebuilt for this launch.
+extern "C" void Bun__Process__recreateStdioAfterSnapshotRestore(JSC::JSGlobalObject* lexicalGlobalObject)
+{
+ auto* globalObject = uncheckedDowncast(lexicalGlobalObject);
+ auto& vm = JSC::getVM(globalObject);
+ JSC::JSLockHolder lock(vm);
+ auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
+ auto* process = globalObject->processObject();
+ struct {
+ ASCIILiteral name;
+ int fd;
+ JSValue (*construct)(VM&, JSObject*);
+ } streams[] = {
+ { "stdin"_s, 0, constructStdin },
+ { "stdout"_s, 1, constructStdout },
+ { "stderr"_s, 2, constructStderr },
+ };
+ for (auto& stream : streams) {
+ auto ident = Identifier::fromString(vm, stream.name);
+ JSValue existing = process->getDirect(vm, ident);
+ if (existing.isEmpty())
+ continue; // never created: the lazy property will build one for this launch on first use
+ bool wasTerminal = false;
+ if (auto* object = existing.getObject()) {
+ wasTerminal = object->get(globalObject, Identifier::fromString(vm, "isTTY"_s)).isTrue();
+ if (scope.exception()) [[unlikely]] {
+ (void)scope.tryClearException();
+ wasTerminal = false;
+ }
+ }
+ // A stream whose fd is a terminal again is kept: the app's listeners stay attached, its poll was re-armed onto the
+ // re-seated fd; the size is the new terminal's.
+ if (wasTerminal && bun_stdio_tty[stream.fd]) {
+ auto* object = existing.getObject();
+#if !OS(WINDOWS)
+ struct winsize size;
+ if (ioctl(stream.fd, TIOCGWINSZ, &size) == 0) {
+ object->putDirect(vm, Identifier::fromString(vm, "columns"_s), jsNumber(size.ws_col));
+ object->putDirect(vm, Identifier::fromString(vm, "rows"_s), jsNumber(size.ws_row));
+ }
+#endif
+ continue;
+ }
+ process->putDirect(vm, ident, stream.construct(vm, process));
+ }
+}
+
+extern "C" void Bun__Process__reloadEnvAfterSnapshotRestore(JSC::JSGlobalObject* lexicalGlobalObject)
+{
+ auto* globalObject = uncheckedDowncast(lexicalGlobalObject);
+ auto& vm = JSC::getVM(globalObject);
+ JSC::JSLockHolder lock(vm);
+ auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
+ JSValue fresh = Bun::createEnvironmentVariablesMap(globalObject);
+ if (scope.exception() || !fresh || !fresh.isObject()) {
+ (void)scope.tryClearException();
+ return;
+ }
+ // Snapshots built by `bun` use a store-backed process.env whose contents change under every reference; only other builds need the object replaced.
+ JSObject* process = globalObject->processObject();
+ if (!Bun::refillSharedEnvAfterSnapshotRestore(globalObject, fresh.getObject())) {
+ globalObject->m_processEnvObject.set(vm, globalObject, fresh.getObject());
+ process->putDirect(vm, JSC::Identifier::fromString(vm, "env"_s), fresh, 0);
+ }
+ uncheckedDowncast(process)->invalidateLaunchContext();
+ refreshReifiedLaunchProperties(vm, process);
+ (void)scope.tryClearException();
+}
+
+// PropertyCallback entries are reified into own properties on first read; a builder that read them left the building
+// process's values on the object. Same shape as Bun__BunObject__refreshLaunchDerivedProperties.
+static void refreshReifiedLaunchProperties(VM& vm, JSObject* processObject)
+{
+ struct LaunchDerivedProp {
+ ASCIILiteral name;
+ JSValue (*make)(VM&, JSObject*);
+ };
+ static const LaunchDerivedProp props[] = {
+ { "pid"_s, constructPid },
+ { "argv0"_s, constructArgv0 },
+ { "execPath"_s, constructExecPath },
+ // undefined in the builder, which has no channel; `if (process.send)` at module scope reifies that during the build
+ { "send"_s, constructProcessSend },
+ { "disconnect"_s, constructProcessDisconnect },
+ { "channel"_s, constructProcessChannel },
+ };
+ for (auto& p : props) {
+ JSC::Identifier id = JSC::Identifier::fromString(vm, p.name);
+ if (processObject->getDirectOffset(vm, id) == invalidOffset)
+ continue; // never read during the build: the static-table callback runs on first access
+ processObject->putDirect(vm, id, p.make(vm, processObject), 0);
+ }
+}
+
+void Process::invalidateLaunchContext()
+{
+ // Lazily materialized from argv / cwd / exec path / title of the launching process; the getters rebuild them.
+ m_argv.clear();
+ m_execArgv.clear();
+ m_cachedCwd.clear();
+}
+
static JSValue constructEnv(VM& vm, JSObject* processObject)
{
auto* globalObject = uncheckedDowncast(processObject->globalObject());
diff --git a/src/jsc/bindings/BunProcess.h b/src/jsc/bindings/BunProcess.h
index ee606dce1ce3..76db456a91be 100644
--- a/src/jsc/bindings/BunProcess.h
+++ b/src/jsc/bindings/BunProcess.h
@@ -88,6 +88,8 @@ class Process : public WebCore::JSEventEmitter {
JSString* cachedCwd() { return m_cachedCwd.get(); }
void setCachedCwd(JSC::VM& vm, JSString* cwd) { m_cachedCwd.set(vm, this, cwd); }
void clearCachedCwd() { m_cachedCwd.clear(); }
+ // snapshot restore: drop every JS value materialized from the launch context of the process that built the snapshot.
+ void invalidateLaunchContext();
JSValue getArgv(JSGlobalObject* globalObject);
void setArgv(JSGlobalObject* globalObject, JSValue argv);
diff --git a/src/jsc/bindings/JSEnvironmentVariableMap.cpp b/src/jsc/bindings/JSEnvironmentVariableMap.cpp
index 108048b74051..53acff280478 100644
--- a/src/jsc/bindings/JSEnvironmentVariableMap.cpp
+++ b/src/jsc/bindings/JSEnvironmentVariableMap.cpp
@@ -1,8 +1,11 @@
#include "root.h"
+#include
+#include
#include "ZigGlobalObject.h"
#include "helpers.h"
#include "JSEnvironmentVariableMap.h"
+#include "FormatStackTraceForJS.h"
#include
#include
@@ -632,6 +635,8 @@ bool JSSharedEnvMap::getOwnPropertySlot(JSObject* object, JSGlobalObject* global
}
auto* store = sharedEnvStoreFor(object);
+ if (store && store->isRecordingReads()) [[unlikely]]
+ store->noteRead(String(uid));
String value = store ? store->get(String(uid)) : String();
if (value.isNull()) {
return Base::getOwnPropertySlot(object, globalObject, propertyName, slot);
@@ -673,6 +678,16 @@ static void applyTZFromString(JSGlobalObject* globalObject, const String& value)
if (value.length() < 32 && WTF::setTimeZoneOverride(value))
resetDateCachesAfterTimeZoneChange(JSC::getVM(globalObject));
}
+// Snapshot restore: the override static and the VM's date cache both hold the building process's zone; this launch's TZ (or its
+// absence, i.e. the system zone of this machine) applies instead. The caches are reset either way, since the cached zone is stale either way.
+extern "C" void Bun__refreshTimeZoneAfterSnapshotRestore(JSGlobalObject* globalObject, const char* tz, size_t tzLen)
+{
+ WTF::setTimeZoneOverride(String()); // first: a zone ICU rejects must leave the system zone in effect, as at boot, not the builder's override
+ if (tzLen > 0) // no length cap: boot (JSGlobalObject__setTimeZone) has none either
+ WTF::setTimeZoneOverride(String::fromUTF8(std::span { tz, tzLen }));
+ resetDateCachesAfterTimeZoneChange(JSC::getVM(globalObject));
+}
+
static void applyTLSRejectFromString(JSGlobalObject*, const String& value)
{
/* Node only treats the exact string "0" as disabling verification. */
@@ -783,10 +798,35 @@ bool JSSharedEnvMap::deleteProperty(JSCell* cell, JSGlobalObject* globalObject,
return Base::deleteProperty(cell, globalObject, propertyName, slot);
}
+// The innermost few JS frames, formatted like an error stack (source maps applied), as the key a copy of process.env is reported under.
+String snapshotReportCallSite(JSGlobalObject* lexicalGlobalObject)
+{
+ auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
+ VM& vm = JSC::getVM(globalObject);
+ WTF::Vector frames;
+ vm.interpreter.getStackTrace(globalObject, frames, 0, 4);
+ OrdinalNumber line = OrdinalNumber::beforeFirst();
+ OrdinalNumber column = OrdinalNumber::beforeFirst();
+ String sourceURL;
+ String formatted = Bun::formatStackTrace(vm, globalObject, lexicalGlobalObject, "Error"_s, String(), line, column, sourceURL, frames, nullptr);
+ StringBuilder site;
+ bool first = true;
+ for (auto frameLine : StringView(formatted).split('\n')) {
+ if (first) { // the "Error" header line
+ first = false;
+ continue;
+ }
+ site.append(site.isEmpty() ? ""_s : "\n"_s, " "_s, frameLine.trim(isASCIIWhitespace));
+ }
+ return site.toString();
+}
+
void JSSharedEnvMap::getOwnPropertyNames(JSObject* object, JSGlobalObject* globalObject, PropertyNameArrayBuilder& propertyNames, DontEnumPropertiesMode mode)
{
VM& vm = JSC::getVM(globalObject);
if (auto* store = sharedEnvStoreFor(object)) {
+ if (store->isRecordingReads()) [[unlikely]]
+ store->noteEnumeration(snapshotReportCallSite(globalObject));
for (const auto& key : store->keys())
propertyNames.add(JSC::Identifier::fromString(vm, key));
}
@@ -951,6 +991,80 @@ RefPtr ensureSharedEnvStoreForWorker(Zig::GlobalObject* globalOb
return store;
}
+// Snapshot build: from here on `process.env` is a view over a store, so a restored process can swap the contents
+// underneath every reference the app captured, and reads before the freeze can be reported when the snapshot is written.
+extern "C" void Bun__Process__useSharedEnvForSnapshotBuild(JSC::JSGlobalObject* lexicalGlobalObject)
+{
+ auto* globalObject = uncheckedDowncast(lexicalGlobalObject);
+ JSC::JSLockHolder lock(globalObject->vm());
+ if (RefPtr store = ensureSharedEnvStoreForWorker(globalObject))
+ store->startRecordingReads();
+}
+
+// Restore: refill the store from this process's environment (the loader has already been reloaded). Returns false when
+// process.env is not store-backed (a snapshot built without the step above), and the caller replaces the object instead.
+bool refillSharedEnvAfterSnapshotRestore(Zig::GlobalObject* globalObject, JSC::JSObject* freshEnvObject)
+{
+ auto* store = sharedEnvStoreFor(globalObject);
+ if (!store)
+ return false;
+ VM& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+ JSC::PropertyNameArrayBuilder keys(vm, JSC::PropertyNameMode::Strings, JSC::PrivateSymbolMode::Exclude);
+ freshEnvObject->methodTable()->getOwnPropertyNames(freshEnvObject, globalObject, keys, JSC::DontEnumPropertiesMode::Exclude);
+ RETURN_IF_EXCEPTION(scope, false);
+ Vector> entries;
+ entries.reserveInitialCapacity(keys.size());
+ for (const auto& key : keys) {
+ JSValue value = freshEnvObject->get(globalObject, key);
+ RETURN_IF_EXCEPTION(scope, false);
+ if (value.isCallable())
+ continue;
+ String str = value.toWTFString(globalObject);
+ RETURN_IF_EXCEPTION(scope, false);
+ entries.append({ String(key.impl()), WTF::move(str) });
+ }
+ store->replaceAll(WTF::move(entries));
+ return true;
+}
+
+// Printed by the snapshot writer. `excluded` are the envGate names: those are handled by construction.
+void printEnvReadsBeforeSnapshot(Zig::GlobalObject* globalObject, const Vector& excluded)
+{
+ auto* store = sharedEnvStoreFor(globalObject);
+ if (!store)
+ return;
+ Vector names;
+ for (auto& name : store->readKeys()) {
+ if (!excluded.contains(name))
+ names.append(name);
+ }
+ std::sort(names.begin(), names.end(), WTF::codePointCompareLessThan);
+ unsigned enumerations = store->enumerations();
+ auto sites = store->enumerationSites();
+ store->finishRecordingReads();
+ if (names.isEmpty() && !enumerations)
+ return;
+ StringBuilder out;
+ out.append("snapshot: values read from process.env before the freeze are baked into the snapshot; read them in a 'restore' listener or list them in envGate:"_s);
+ if (enumerations) {
+ out.append("\n process.env was enumerated or copied "_s, enumerations, enumerations == 1 ? " time"_s : " times"_s, " (every variable)"_s);
+ std::sort(sites.begin(), sites.end(), [](auto& a, auto& b) { return a.second > b.second; });
+ for (auto& [site, count] : sites) {
+ out.append("\n "_s, count, count == 1 ? " copy from:\n"_s : " copies from:\n"_s, site);
+ }
+ }
+ // A copy reads every variable on the way through; listing them individually would say nothing more.
+ if (!names.isEmpty() && (!enumerations || names.size() < store->keys().size())) {
+ out.append("\n "_s);
+ for (size_t i = 0; i < names.size(); i++)
+ out.append(i ? ", "_s : ""_s, names[i]);
+ }
+ out.append('\n');
+ auto utf8 = out.toString().utf8();
+ fwrite(utf8.data(), 1, utf8.length(), stderr);
+}
+
JSValue createEnvironmentVariablesMap(Zig::GlobalObject* globalObject)
{
VM& vm = globalObject->vm();
diff --git a/src/jsc/bindings/JSEnvironmentVariableMap.h b/src/jsc/bindings/JSEnvironmentVariableMap.h
index 90d0f055e7ee..b6c62b81bc7b 100644
--- a/src/jsc/bindings/JSEnvironmentVariableMap.h
+++ b/src/jsc/bindings/JSEnvironmentVariableMap.h
@@ -1,3 +1,4 @@
+#pragma once
#include "root.h"
#include "SharedEnvStore.h"
@@ -77,5 +78,13 @@ bool isProcessEnvClassInfo(const JSC::ClassInfo*);
// else a fresh one seeded from its `process.env` (then swapped to a write-through view).
// Returns null if seeding threw.
RefPtr ensureSharedEnvStoreForWorker(Zig::GlobalObject* globalObject);
+// Snapshots (see the definitions): refill the store-backed process.env in place after a restore; report build-time reads.
+bool refillSharedEnvAfterSnapshotRestore(Zig::GlobalObject*, JSC::JSObject* freshEnvObject);
+void printEnvReadsBeforeSnapshot(Zig::GlobalObject*, const Vector& excludedNames);
}
+
+namespace Bun {
+// The JS frames (up to 4) currently on the stack, one per line, for the reports the snapshot build prints.
+WTF::String snapshotReportCallSite(JSC::JSGlobalObject*);
+}
diff --git a/src/jsc/bindings/SharedEnvStore.h b/src/jsc/bindings/SharedEnvStore.h
index 752282f953ca..ca80ab45d84d 100644
--- a/src/jsc/bindings/SharedEnvStore.h
+++ b/src/jsc/bindings/SharedEnvStore.h
@@ -2,6 +2,8 @@
#include "root.h"
#include
+#include
+#include
#include
#include
#include
@@ -62,6 +64,58 @@ class SharedEnvStore : public ThreadSafeRefCounted {
return out;
}
+ // Snapshot restore: the environment is the launching process's now; every view of this store sees it at once.
+ void replaceAll(Vector>&& entries)
+ {
+ {
+ Locker locker { m_lock };
+ m_map.clear();
+ }
+ for (auto& [key, value] : entries)
+ set(key, value);
+ }
+
+ // While a snapshot is being built, remember what the app read: values read before the freeze are baked into the snapshot.
+ void startRecordingReads() { m_recordReads = true; }
+ void finishRecordingReads() // called once the report is printed, before the freeze: the snapshot bakes in "not recording"
+ {
+ m_recordReads = false;
+ Locker locker { m_lock };
+ m_readKeys = {};
+ m_enumerationSites = {};
+ }
+ bool isRecordingReads() const { return m_recordReads; }
+ void noteRead(const String& key)
+ {
+ Locker locker { m_lock };
+ m_readKeys.add(key.isolatedCopy());
+ }
+ void noteEnumeration(const String& site)
+ {
+ Locker locker { m_lock };
+ m_enumerations++;
+ m_enumerationSites.add(site.isolatedCopy(), 0).iterator->value++;
+ }
+ Vector readKeys()
+ {
+ Locker locker { m_lock };
+ return copyToVector(m_readKeys);
+ }
+ unsigned enumerations()
+ {
+ Locker locker { m_lock };
+ return m_enumerations;
+ }
+ Vector> enumerationSites()
+ {
+ Locker locker { m_lock };
+ Vector> out;
+ out.reserveInitialCapacity(m_enumerationSites.size());
+ for (auto& entry : m_enumerationSites)
+ out.append({ entry.key.isolatedCopy(), entry.value });
+ return out;
+ }
+
// Windows env keys are case-insensitive. This follows bun's own Windows env
// object, not node: node only folds case for a main-rooted tree (RealEnvStore),
// and is case-sensitive for one rooted at a snapshot worker (MapKVStore).
@@ -90,6 +144,10 @@ class SharedEnvStore : public ThreadSafeRefCounted {
Lock m_lock;
HashMap m_map WTF_GUARDED_BY_LOCK(m_lock);
+ bool m_recordReads { false };
+ unsigned m_enumerations WTF_GUARDED_BY_LOCK(m_lock) { 0 };
+ HashSet m_readKeys WTF_GUARDED_BY_LOCK(m_lock);
+ HashMap m_enumerationSites WTF_GUARDED_BY_LOCK(m_lock);
};
} // namespace Bun
diff --git a/src/jsc/bindings/StartupSnapshot.cpp b/src/jsc/bindings/StartupSnapshot.cpp
new file mode 100644
index 000000000000..738c7b9c2919
--- /dev/null
+++ b/src/jsc/bindings/StartupSnapshot.cpp
@@ -0,0 +1,2003 @@
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE 1 // dl_iterate_phdr / dl_phdr_info (Linux)
+#endif
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wformat" // uint64_t is unsigned long on Linux, unsigned long long on Darwin; this file prints a lot of addresses
+#include "root.h"
+#include "StartupSnapshot.h"
+#include "JSEnvironmentVariableMap.h"
+// Supported platforms build the real thing; elsewhere the same entry points exist (so everything links) and report the feature as absent.
+#if BUN_STARTUP_SNAPSHOT_SUPPORTED
+#include
+#if OS(DARWIN)
+#include
+#endif
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#if OS(DARWIN)
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#endif
+#if OS(LINUX)
+#include
+#include
+#include
+#include
+#include
+#include
+#endif
+#ifndef MAP_JIT
+#define MAP_JIT 0
+#endif
+#include
+#include
+#include
+#include "wtf/SIMDUTF.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+extern "C" int mi_prof_dump_to_file(const char*) noexcept;
+extern "C" void mi_prof_enable(size_t) noexcept;
+typedef void(mi_output_fun)(const char* msg, void* arg);
+extern "C" void mi_stats_print_out(mi_output_fun* out, void* arg) noexcept;
+extern "C" void mi_arenas_print(void) noexcept;
+extern "C" void mi_collect(bool force) noexcept;
+extern "C" size_t mi_usable_size(const void*) noexcept;
+extern "C" int mi_heap_snapshot_to_file(const char* path, unsigned flags) noexcept;
+extern "C" void mi_arenas_freeze_pages() noexcept;
+extern "C" void mi_prof_visit_live(bool (*cb)(uintptr_t addr, size_t size, const uintptr_t* frames, uint8_t nframes, void* arg), void* arg) noexcept;
+#include
+#include "ZigGlobalObject.h"
+namespace Bun::StartupSnapshot {
+std::vector> frozenRanges; // sorted [start,end)
+std::vector snapshotRuns;
+int snapshotFd = -1;
+::mi_heap_s* freshHeap = nullptr;
+off_t snapshotBaseOff = 0;
+ssize_t ipread(int fd, void* buf, size_t n, off_t off) { return ::pread(fd, buf, n, off + snapshotBaseOff); }
+void* immap(void* addr, size_t len, int prot, int flags, int fd, off_t off) { return ::mmap(addr, len, prot, flags, fd, off + snapshotBaseOff); }
+} // namespace Bun::StartupSnapshot
+using namespace Bun::StartupSnapshot;
+#if OS(DARWIN)
+#define OS_DARWIN_ONLY(x) x
+#else
+#define OS_DARWIN_ONLY(x) 0
+#endif
+// Snapshot bytes may live at an offset inside a bigger file (embedded in the executable's __BUN/.bun section): all snapshot-file reads/maps add this.
+
+static void snapshotRestoreAndRun(const char* path);
+extern "C" struct mach_header_64 _mh_execute_header;
+// A snapshot needs the executable at its link address: if dyld slid us, re-exec ourselves unslid (macOS private posix_spawn flag), carrying the allocator/JIT settings in the env.
+extern "C" void bun_refresh_stdio_after_snapshot_restore();
+extern "C" volatile sig_atomic_t bun_stdio_modified[3];
+extern "C" int bun_is_compiled_executable(void);
+extern "C" bool Bun__isCompiledExecutable() { return bun_is_compiled_executable(); }
+extern "C" bool Bun__startupSnapshotMode() { return bun_is_compiled_executable() || getenv("BUN_STARTUP_SNAPSHOT_IN") || getenv("BUN_STARTUP_SNAPSHOT_OUT"); }
+// Restore epoch (0 = booted normally or building; N after the Nth restore): statics caching process/OS/CPU state key their once-token on `epoch + 1` instead of a bool.
+extern "C" uint32_t bun_snapshot_epoch; // defined (exported, unmangled) in bun_core::startup_snapshot; std::atomic layout == uint32_t
+
+namespace bssl {
+void OPENSSL_cpuid_setup();
+}
+// CPU-dispatch latches in vendored code chose paths on the build machine (valid here per the header's feature-superset check); re-probing lets a better CPU do better.
+static void snapshotReprobeCPUDispatch()
+{
+ hwy::GetChosenTarget().DeInit(); // next HWY_DYNAMIC_DISPATCH re-detects
+ simdutf::get_active_implementation() = simdutf::get_available_implementations().detect_best_supported();
+ bssl::OPENSSL_cpuid_setup(); // refills OPENSSL_ia32cap_P / OPENSSL_armcap_P
+}
+
+static bool s_snapshotActive = false; // set once this process is building a snapshot or has restored one (decided in Bun__startupSnapshotMaybeRestore, before VM init)
+// A launch that resumes from its snapshot, or declines it and boots normally, says nothing unless asked (BUN_STARTUP_SNAPSHOT_VERBOSE=1).
+static bool snapshotVerbose()
+{
+ return !!getenv("BUN_STARTUP_SNAPSHOT_VERBOSE"); // not cached: a value cached while the snapshot was written would be restored along with it
+}
+extern "C" bool Bun__startupSnapshotActive() { return s_snapshotActive; }
+#if OS(DARWIN) && defined(BUN_MIMALLOC_ZONE_OVERRIDE)
+extern "C" size_t mi_malloc_zone_process_owned_ranges(uintptr_t (*out)[2], size_t cap);
+#endif
+// On macOS snapshots need mimalloc registered as the process malloc zone (BUN_MIMALLOC_OVERRIDE_DARWIN at build time), which official builds do not enable yet.
+extern "C" bool Bun__startupSnapshotSupported()
+{
+#if OS(DARWIN) && !defined(BUN_MIMALLOC_ZONE_OVERRIDE)
+ return false;
+#else
+ return true;
+#endif
+}
+
+// BUN_STARTUP_SNAPSHOT_VERBOSE timing: milliseconds since the process was exec'd (Darwin: the kernel's start time, which the re-exec keeps).
+static double snapshotMsSinceExec()
+{
+ struct timeval now;
+ gettimeofday(&now, nullptr);
+#if OS(DARWIN)
+ struct proc_bsdinfo info;
+ if (proc_pidinfo(getpid(), PROC_PIDTBSDINFO, 0, &info, sizeof info) == sizeof info)
+ return (now.tv_sec - (double)info.pbi_start_tvsec) * 1000.0 + (now.tv_usec - (double)info.pbi_start_tvusec) / 1000.0;
+#endif
+ static struct timeval first = now;
+ return (now.tv_sec - first.tv_sec) * 1000.0 + (now.tv_usec - first.tv_usec) / 1000.0;
+}
+static void snapshotTimingMark(const char* what)
+{
+ if (snapshotVerbose())
+ fprintf(stderr, "[snapshot] t=%.2fms since exec: %s\n", snapshotMsSinceExec(), what);
+}
+
+extern "C" bool Bun__isCompiledExecutable();
+static void setSnapshotEnvDefaults()
+{
+ bool building = getenv("BUN_STARTUP_SNAPSHOT_OUT");
+ if (Bun__isCompiledExecutable() && !building)
+ return; // compiled executables configure the allocator/JIT from BUN_COMPILED before main: nothing to pass through the environment
+ setenv("MIMALLOC_DETERMINISTIC_HINT", "1", 0);
+ // The builder's heap (= the snapshot) starts at mimalloc's 2TiB base; a restoring process puts its own early heap 64GiB higher so it never occupies snapshot addresses.
+ if (getenv("BUN_STARTUP_SNAPSHOT_OUT"))
+ unsetenv("MIMALLOC_HINT_FLOOR");
+ else
+ setenv("MIMALLOC_HINT_FLOOR", "0x21000000000", 0);
+ setenv("BUN_STARTUP_SNAPSHOT_JIT_ADDR", "0x3c0000000", 0);
+}
+static bool snapshotEnvIsSet()
+{
+ bool building = getenv("BUN_STARTUP_SNAPSHOT_OUT");
+ if (Bun__isCompiledExecutable() && !building) return true; // compiled executables configure allocator/JIT from BUN_COMPILED before main: nothing to inject
+ return getenv("MIMALLOC_DETERMINISTIC_HINT") && getenv("BUN_STARTUP_SNAPSHOT_JIT_ADDR") && (building || getenv("MIMALLOC_HINT_FLOOR"));
+}
+
+static void reexecWithoutASLRIfSlid()
+{
+ // The re-exec'd generation is tagged in argv[0] so it never re-execs again even if disabling ASLR silently failed; the tag is
+ // cut off again right here, before anything (Bun's argv capture, ps) can see it.
+ static constexpr const char* kReexecTag = " [snapshot-reexec]";
+#if OS(DARWIN)
+ bool alreadyReexeced = false;
+ if (char* argv0 = (*_NSGetArgv())[0]) {
+ if (char* tag = strstr(argv0, kReexecTag)) {
+ *tag = '\0';
+ alreadyReexeced = true;
+ }
+ }
+#else
+ const bool alreadyReexeced = false;
+#endif
+ if (getenv("BUN_STARTUP_SNAPSHOT_REEXECED") || alreadyReexeced)
+ return;
+ bool needEnv = !snapshotEnvIsSet();
+#if OS(DARWIN)
+ constexpr uintptr_t linkBase = 0x100000000ull;
+ if ((uintptr_t)&_mh_execute_header == linkBase && !needEnv)
+ return;
+ setenv("BUN_STARTUP_SNAPSHOT_REEXECED", "1", 1);
+ setSnapshotEnvDefaults();
+ char exe[4096];
+ uint32_t len = sizeof exe;
+ if (_NSGetExecutablePath(exe, &len) != 0)
+ return;
+ posix_spawnattr_t attr;
+ posix_spawnattr_init(&attr);
+ short flags = 0;
+ posix_spawnattr_getflags(&attr, &flags);
+ posix_spawnattr_setflags(&attr, flags | 0x0100 /* _POSIX_SPAWN_DISABLE_ASLR */ | POSIX_SPAWN_SETEXEC);
+ char** oargv = *_NSGetArgv();
+ int argc = 0;
+ while (oargv[argc])
+ argc++;
+ std::vector nargv(oargv, oargv + argc + 1);
+ std::string tagged = std::string(oargv[0] ? oargv[0] : exe) + kReexecTag;
+ nargv[0] = tagged.data();
+ posix_spawn(nullptr, exe, nullptr, &attr, nargv.data(), *_NSGetEnviron()); // SETEXEC: only returns on failure
+ fprintf(stderr, "[snapshot] could not re-exec without ASLR; continuing slid (snapshot build/restore will not work)\n");
+#elif OS(LINUX)
+ // Linux: the executable is non-PIE and slid libraries are fixed up, so ASLR stays on; the re-exec only gets the allocator/JIT options into the env before startup reads them.
+ if (!needEnv)
+ return;
+ setenv("BUN_STARTUP_SNAPSHOT_REEXECED", "1", 1);
+ setSnapshotEnvDefaults();
+ setenv("BUN_STARTUP_SNAPSHOT_LIB_FIXUPS", "1", 0);
+ {
+ extern char** environ;
+ // argv: read our own cmdline
+ std::vector args;
+ {
+ FILE* f = fopen("/proc/self/cmdline", "r");
+ std::string cur;
+ int c;
+ while (f && (c = fgetc(f)) != EOF) {
+ if (!c) {
+ args.push_back(cur);
+ cur.clear();
+ } else
+ cur += (char)c;
+ }
+ if (f) fclose(f);
+ }
+ std::vector argv;
+ for (auto& a : args)
+ argv.push_back(a.data());
+ argv.push_back(nullptr);
+ execve("/proc/self/exe", argv.data(), environ);
+ }
+ fprintf(stderr, "[snapshot] could not re-exec to pass the allocator settings through; continuing (snapshot build/restore will not work)\n");
+#endif
+}
+
+// `.snapshot` next to the binary is used automatically (BUN_STARTUP_SNAPSHOT=0 opts out; BUN_STARTUP_SNAPSHOT_IN overrides).
+static uint64_t platformLibsBase();
+static uint64_t platformSystemLibsId();
+static uint64_t platformBuildId();
+// What a snapshot is valid for: this exact link of the executable, on a kernel with the page size its regions were cut to.
+static uint64_t snapshotEnvironmentId()
+{
+ return platformBuildId() ^ ((uint64_t)getpagesize() * 0x9E3779B97F4A7C15ull);
+}
+static bool snapshotOptedOut()
+{
+ const char* off = getenv("BUN_STARTUP_SNAPSHOT");
+ return off && (!strcmp(off, "0") || !strcmp(off, "false"));
+}
+static bool ownExecutablePath(char* exe, size_t cap)
+{
+#if OS(DARWIN)
+ uint32_t len = (uint32_t)cap;
+ return _NSGetExecutablePath(exe, &len) == 0;
+#else
+ ssize_t n = readlink("/proc/self/exe", exe, cap - 1);
+ if (n <= 0)
+ return false;
+ exe[n] = 0;
+ return true;
+#endif
+}
+static bool findSiblingSnapshot(char* out, size_t cap)
+{
+ char exe[4096];
+ if (snapshotOptedOut() || !ownExecutablePath(exe, sizeof exe))
+ return false;
+ snprintf(out, cap, "%s.snapshot", exe);
+ return access(out, R_OK) == 0;
+}
+static bool siblingSnapshotExists()
+{
+ char path[4300];
+ return findSiblingSnapshot(path, sizeof path);
+}
+
+extern "C" bool Bun__standaloneEmbeddedStartupSnapshot(const uint8_t** outPtr, size_t* outLen);
+static bool embeddedSnapshotExists()
+{
+ if (snapshotOptedOut())
+ return false;
+ const uint8_t* p;
+ size_t n;
+ return Bun__standaloneEmbeddedStartupSnapshot(&p, &n);
+}
+// "@" for a snapshot embedded in the __BUN/.bun section (in-memory pointer -> segment -> file offset).
+static bool findEmbeddedSnapshot(char* out, size_t cap)
+{
+ if (snapshotOptedOut())
+ return false;
+ const uint8_t* p = nullptr;
+ size_t n = 0;
+ if (!Bun__standaloneEmbeddedStartupSnapshot(&p, &n)) return false;
+ char exe[4096];
+ int64_t fileOff = -1;
+ uintptr_t a = (uintptr_t)p;
+#if OS(DARWIN)
+ uint32_t len = sizeof exe;
+ if (_NSGetExecutablePath(exe, &len) != 0) return false;
+ const struct mach_header_64* mh = &_mh_execute_header;
+ intptr_t slide = 0;
+ for (uint32_t i = 0; i < _dyld_image_count(); i++)
+ if ((const struct mach_header_64*)_dyld_get_image_header(i) == mh) {
+ slide = _dyld_get_image_vmaddr_slide(i);
+ break;
+ }
+ const uint8_t* lc = (const uint8_t*)(mh + 1);
+ for (uint32_t i = 0; i < mh->ncmds; i++) {
+ const struct load_command* c = (const struct load_command*)lc;
+ if (c->cmd == LC_SEGMENT_64) {
+ const struct segment_command_64* sc = (const struct segment_command_64*)c;
+ uintptr_t lo = sc->vmaddr + slide;
+ if (a >= lo && a < lo + sc->vmsize && (a - lo) < sc->filesize) {
+ fileOff = (int64_t)(sc->fileoff + (a - lo));
+ break;
+ }
+ }
+ lc += c->cmdsize;
+ }
+#elif OS(LINUX)
+ ssize_t r = readlink("/proc/self/exe", exe, sizeof exe - 1);
+ if (r <= 0) return false;
+ exe[r] = 0;
+ { // the appended payload is mapped by a PT_LOAD the ELF writer adds: pointer -> that segment's file offset
+ struct Ctx {
+ uintptr_t a;
+ int64_t off;
+ } ctx { a, -1 };
+ dl_iterate_phdr([](struct dl_phdr_info* info, size_t, void* arg) -> int {
+ if (info->dlpi_name && *info->dlpi_name) return 0; // main executable only
+ Ctx* c = (Ctx*)arg;
+ for (int i = 0; i < info->dlpi_phnum; i++) {
+ const ElfW(Phdr) & ph = info->dlpi_phdr[i];
+ if (ph.p_type != PT_LOAD) continue;
+ uintptr_t lo = info->dlpi_addr + ph.p_vaddr;
+ if (c->a >= lo && c->a < lo + ph.p_memsz && (c->a - lo) < ph.p_filesz) {
+ c->off = (int64_t)(ph.p_offset + (c->a - lo));
+ return 1;
+ }
+ }
+ return 0;
+ },
+ &ctx);
+ fileOff = ctx.off;
+ }
+#else
+ return false;
+#endif
+ if (fileOff < 0 || (fileOff & (getpagesize() - 1))) {
+ fprintf(stderr, "[snapshot] embedded snapshot is not page-aligned in the file (offset %lld); ignoring\n", (long long)fileOff);
+ return false;
+ }
+ snprintf(out, cap, "%s@%lld", exe, (long long)fileOff);
+ return true;
+}
+
+extern "C" void Bun__startupSnapshotMaybeRestore()
+{
+ const bool secondGeneration = getenv("BUN_STARTUP_SNAPSHOT_REEXECED");
+ unsetenv("BUN_STARTUP_SNAPSHOT_REEXECED"); // consumed: processes this one spawns must make their own re-exec decision
+ snapshotTimingMark(secondGeneration ? "main reached (second generation)" : "main reached (first generation)");
+ // Only compiled executables carry or sit next to snapshots; a plain `bun` takes part only when asked to through the environment.
+ if (!bun_is_compiled_executable() && !getenv("BUN_STARTUP_SNAPSHOT_IN") && !getenv("BUN_STARTUP_SNAPSHOT_OUT"))
+ return;
+ // No setenv()/heap use in a process that is about to restore: environ would be reallocated into memory the snapshot overlays.
+ bool wantSnapshot = getenv("BUN_STARTUP_SNAPSHOT_IN") || getenv("BUN_STARTUP_SNAPSHOT_OUT") || siblingSnapshotExists() || embeddedSnapshotExists();
+ if (wantSnapshot)
+ reexecWithoutASLRIfSlid(); // returns only once we are the unslid process with the snapshot env in place
+ char path[4200] = "";
+ if (const char* in = getenv("BUN_STARTUP_SNAPSHOT_IN"))
+ snprintf(path, sizeof path, "%s", in); // explicit file (debugging / dev loop)
+ else if (!getenv("BUN_STARTUP_SNAPSHOT_OUT")) {
+ if (!findSiblingSnapshot(path, sizeof path)) {
+ path[0] = 0;
+ findEmbeddedSnapshot(path, sizeof path);
+ } // a sibling .snapshot (debugging), else the one embedded in this executable
+ }
+ s_snapshotActive = path[0] || getenv("BUN_STARTUP_SNAPSHOT_OUT");
+ if (path[0])
+ snapshotRestoreAndRun(path); // returns only if the snapshot was declined (then we boot normally, still with snapshot-compatible options so a rebuild can snapshot)
+}
+extern "C" void Bun__startupSnapshotSetBuilding(bool);
+extern "C" void mi_prof_reinit_lock(void);
+extern "C" void mi_os_hint_floor(void*) noexcept;
+extern "C" bool mi_prof_lock_is_free(void);
+extern "C" void Bun__requestSnapshot(JSC::VM*, const char* path);
+static bool snapshotDump(JSC::VM& vm, const char* path);
+// envGate (take() option): NUL-separated names stored after the region data, hashed with their values so a launch that differs in any of them declines before mapping anything.
+static std::string s_envGateNames;
+extern "C" void Bun__startupSnapshotSetEnvGate(const uint8_t* names, size_t len) { s_envGateNames.assign((const char*)names, len); }
+static uint64_t envGateHash(const char* names, size_t len)
+{
+ uint64_t h = 1469598103934665603ull;
+ auto mix = [&](const char* p, size_t n) { for (size_t i = 0; i < n; i++) { h ^= (uint8_t)p[i]; h *= 1099511628211ull; } h ^= 0xff; h *= 1099511628211ull; };
+ for (size_t i = 0; i < len;) {
+ const char* name = names + i;
+ size_t nl = strnlen(name, len - i);
+ mix(name, nl);
+ if (const char* v = getenv(name))
+ mix(v, strlen(v));
+ else
+ mix("\x01unset", 6);
+ i += nl + 1;
+ }
+ return h ? h : 1;
+}
+extern "C" void Bun__startupSnapshotUnwindJS(JSC::VM* vm) { vm->notifyNeedTermination(); }
+extern "C" void Bun__startupSnapshotClearTerminationRequest(JSC::VM* vm) { vm->clearHasTerminationRequest(); }
+extern "C" bool Bun__startupSnapshotDumpNow(JSC::VM* vm, const char* path)
+{
+ mi_scavenger_stop(); // joins mimalloc's background thread: nothing may hold allocator locks while we freeze
+#if OS(DARWIN)
+ // Pool workers were told to exit; give them (bounded) time to actually be gone, and any straggler inside the allocator time to leave it.
+ for (int attempt = 0; attempt < 200; attempt++) {
+ thread_act_array_t threads;
+ mach_msg_type_number_t count = 0;
+ unsigned pool = 0;
+ if (task_threads(mach_task_self(), &threads, &count) == KERN_SUCCESS) {
+ for (mach_msg_type_number_t i = 0; i < count; i++) {
+ pthread_t pt = pthread_from_mach_thread_np(threads[i]);
+ char name[64] = "";
+ if (pt) pthread_getname_np(pt, name, sizeof name);
+ if (!strncmp(name, "Bun Pool", 8)) pool++;
+ mach_port_deallocate(mach_task_self(), threads[i]);
+ }
+ vm_deallocate(mach_task_self(), (vm_address_t)threads, count * sizeof(thread_act_t));
+ }
+ if (!pool && mi_prof_lock_is_free()) break;
+ usleep(10000);
+ }
+ { // who else is alive right now? every one of them is a potential holder of some lock we are about to freeze
+ thread_act_array_t threads;
+ mach_msg_type_number_t count = 0;
+ if (snapshotVerbose() && task_threads(mach_task_self(), &threads, &count) == KERN_SUCCESS) {
+ fprintf(stderr, "[snapshot] %u threads at snapshot time:", count);
+ for (mach_msg_type_number_t i = 0; i < count; i++) {
+ pthread_t pt = pthread_from_mach_thread_np(threads[i]);
+ char name[64] = "?";
+ if (pt) pthread_getname_np(pt, name, sizeof name);
+ fprintf(stderr, " [%s]", name[0] ? name : "unnamed");
+ mach_port_deallocate(mach_task_self(), threads[i]);
+ }
+ fprintf(stderr, "\n");
+ vm_deallocate(mach_task_self(), (vm_address_t)threads, count * sizeof(thread_act_t));
+ }
+ }
+#else
+ // Pool workers were told to exit; give them (bounded) time to actually be gone, and any straggler inside the allocator time to leave it.
+ for (int attempt = 0; attempt < 200; attempt++) {
+ unsigned pool = 0;
+ if (DIR* d = opendir("/proc/self/task")) {
+ while (struct dirent* e = readdir(d)) {
+ if (e->d_name[0] == '.') continue;
+ char pth[128], name[64] = "";
+ snprintf(pth, sizeof pth, "/proc/self/task/%s/comm", e->d_name);
+ if (FILE* f = fopen(pth, "r")) {
+ if (fgets(name, sizeof name, f) && !strncmp(name, "Bun Pool", 8)) pool++;
+ fclose(f);
+ }
+ }
+ closedir(d);
+ }
+ if (!pool && mi_prof_lock_is_free()) break;
+ usleep(10000);
+ }
+#endif
+ if (!mi_prof_lock_is_free()) fprintf(stderr, "[snapshot] WARNING: mimalloc profiler lock is held at snapshot time (some thread is mid-free)\n");
+ { // the termination that unwound JS to get us here is done with; none of it may persist into the snapshot (it would read as "terminating" forever on restore)
+ JSC::JSLockHolder lock(*vm);
+ vm->clearHasTerminationRequest();
+ auto scope = DECLARE_TOP_EXCEPTION_SCOPE(*vm);
+ scope.clearException();
+ vm->traps().clearTrap(JSC::VMTraps::NeedTermination);
+ }
+ return snapshotDump(*vm, path);
+}
+extern "C" uint32_t Bun__standaloneStartupSnapshotBuildFlags();
+extern "C" void Bun__startupSnapshotRunMain(JSC::JSGlobalObject*);
+extern "C" bool Bun__startupSnapshotHasMain();
+// `bun build --snapshot` marks the payload (Flags::TAKE_STARTUP_SNAPSHOT…); a marked run writes `.snapshot` instead of starting the app. Translated here into the runtime's internal variables, so the app's own env/argv are never involved.
+static void applySnapshotBuildMarking()
+{
+ if (!bun_is_compiled_executable() || getenv("BUN_STARTUP_SNAPSHOT_OUT"))
+ return;
+ uint32_t bits = Bun__standaloneStartupSnapshotBuildFlags();
+ if (!(bits & 1))
+ return;
+ char exe[4096];
+#if OS(DARWIN)
+ uint32_t len = sizeof exe;
+ if (_NSGetExecutablePath(exe, &len) != 0)
+ return;
+#else
+ ssize_t n = readlink("/proc/self/exe", exe, sizeof exe - 1);
+ if (n <= 0)
+ return;
+ exe[n] = 0;
+#endif
+ char out[4096 + 16];
+ snprintf(out, sizeof out, "%s.snapshot", exe);
+ setenv("BUN_STARTUP_SNAPSHOT_OUT", out, 1);
+ if (!(bits & 2))
+ setenv("BUN_STARTUP_SNAPSHOT_AUTO", "1", 1);
+ if (bits & 8)
+ setenv("BUN_STARTUP_SNAPSHOT_IO", "network", 1);
+ else if (bits & 4)
+ setenv("BUN_STARTUP_SNAPSHOT_IO", "local", 1);
+}
+
+extern "C" void Bun__startupSnapshotInit()
+{
+ applySnapshotBuildMarking();
+ if (getenv("BUN_STARTUP_SNAPSHOT_OUT")) {
+ if (!Bun__startupSnapshotSupported()) {
+ fprintf(stderr, "error: %s\n", "startup snapshots are not available in this build of bun (macOS with mimalloc as the process allocator, and glibc Linux)");
+ exit(1);
+ }
+ Bun__startupSnapshotSetBuilding(true);
+ }
+ startupSnapshotToolingInstall();
+}
+
+namespace Bun::StartupSnapshot {
+// Bun.startupSnapshot.reclean(): pages this process dirtied and then restored to their original bytes go back to the clean file mapping.
+void recleanFrozenPages(JSC::VM& vm)
+{
+#if OS(DARWIN) || OS(LINUX)
+ JSC::JSLockHolder lock(vm);
+ if (snapshotFd < 0)
+ return;
+ const size_t pg = getpagesize();
+ std::vector orig(pg);
+#if OS(DARWIN)
+ std::vector disp;
+#endif
+ size_t dirty = 0, remapped = 0;
+#if OS(DARWIN)
+ auto pageIsDirty = [&](size_t i) { return (disp[i] & (VM_PAGE_QUERY_PAGE_DIRTY | VM_PAGE_QUERY_PAGE_COPIED)) != 0; };
+#else
+ int pagemap = open("/proc/self/pagemap", O_RDONLY | O_CLOEXEC);
+ if (pagemap < 0)
+ return;
+ std::vector pm;
+ auto pageIsDirty = [&](size_t i) { return (pm[i] & (1ull << 63)) && !(pm[i] & (1ull << 61)); }; // present and no longer the file's page: a private copy
+#endif
+ auto pageIsPristine = [&](const FrozenRun& run, size_t i) {
+ return ipread(snapshotFd, orig.data(), pg, run.fileOff + i * pg) == (ssize_t)pg && !memcmp((const void*)(run.start + i * pg), orig.data(), pg);
+ };
+ for (auto& run : snapshotRuns) {
+ const size_t n = run.len / pg;
+#if OS(DARWIN)
+ disp.assign(n, 0);
+ mach_vm_size_t cnt = n;
+ if (mach_vm_page_range_query(mach_task_self(), run.start, run.len, (mach_vm_address_t)disp.data(), &cnt) != KERN_SUCCESS)
+ continue;
+#else
+ pm.assign(n, 0);
+ if (::pread(pagemap, pm.data(), n * sizeof(uint64_t), (off_t)(run.start / pg) * sizeof(uint64_t)) != (ssize_t)(n * sizeof(uint64_t)))
+ continue;
+#endif
+ for (size_t i = 0; i < n;) {
+ if (!pageIsDirty(i)) {
+ i++;
+ continue;
+ }
+ dirty++;
+ if (!pageIsPristine(run, i)) {
+ i++;
+ continue;
+ }
+ size_t j = i + 1; // coalesce a run of pristine dirty pages into one mapping
+ while (j < n && pageIsDirty(j) && pageIsPristine(run, j)) {
+ dirty++;
+ j++;
+ }
+ if (immap((void*)(run.start + i * pg), (j - i) * pg, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED, snapshotFd, run.fileOff + i * pg) != MAP_FAILED)
+ remapped += j - i;
+ i = j;
+ }
+ }
+#if OS(LINUX)
+ close(pagemap);
+#endif
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE"))
+ fprintf(stderr, "[snapshot] reclean: %zu dirty snapshot pages, %zu were pristine and are file-backed again\n", dirty, remapped);
+#else
+ UNUSED_PARAM(vm);
+#endif
+}
+} // namespace Bun::StartupSnapshot
+extern "C" void Bun__startupSnapshotRecleanPages(JSC::VM* vm) { Bun::StartupSnapshot::recleanFrozenPages(*vm); }
+
+struct us_loop_t;
+extern "C" void us_loop_reinit_for_snapshot(struct us_loop_t*);
+extern "C" struct us_loop_t* uws_get_loop();
+extern "C" void Bun__startupSnapshotContinueEventLoop();
+extern "C" void uws_adopt_loop_for_current_thread(struct us_loop_t*);
+void _mi_scavenger_forked_child(void); // C++-mangled (mimalloc is built as C++ here)
+void _mi_scavenger_start_if_forked(void);
+extern "C" void Bun__startupSnapshotAdoptMainThreadVM();
+struct BunLaunchContext {
+ size_t argc;
+ const char* const* argv;
+};
+extern "C" void bun_launch_context_capture(BunLaunchContext*);
+extern "C" void bun_launch_context_restore(const BunLaunchContext*);
+extern "C" void Bun__VM__refreshStackBoundsAfterSnapshotRestore(JSC::VM* vm)
+{
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE")) fprintf(stderr, "[snapshot] refreshing VM stack bounds: lastStackTop=%p thread stack=[%p,%p)\n", vm->lastStackTop(), WTF::Thread::currentSingleton().stack().end(), WTF::Thread::currentSingleton().stack().origin());
+ vm->refreshStackBoundsAfterSnapshotRestore();
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE")) fprintf(stderr, "[snapshot] refreshed: lastStackTop=%p\n", vm->lastStackTop());
+}
+// Snapshot-capable = a compiled executable (BUN_COMPILED is readable before main) or an explicit env request: drives the deterministic allocator hints and fixed JIT pool, so compiled apps need no environment.
+
+extern "C" char** environ;
+
+#if OS(LINUX)
+#include
+// Linker/loader-owned data in our own snapshot (.got, .got.plt, .init_array, .fini_array): process-specific, never program state — keep this process's copy across the overlay.
+static size_t platformLinkerOwnedRanges(uint64_t (*out)[2], size_t cap)
+{
+ size_t n = 0;
+ int fd = open("/proc/self/exe", O_RDONLY);
+ if (fd < 0) return 0;
+ Elf64_Ehdr eh;
+ if (::pread(fd, &eh, sizeof eh, 0) != (ssize_t)sizeof eh || !eh.e_shnum) {
+ close(fd);
+ return 0;
+ }
+ std::vector sh(eh.e_shnum);
+ ::pread(fd, sh.data(), eh.e_shnum * sizeof(Elf64_Shdr), eh.e_shoff);
+ std::vector names(sh[eh.e_shstrndx].sh_size);
+ ::pread(fd, names.data(), names.size(), sh[eh.e_shstrndx].sh_offset);
+ for (auto& sec : sh) {
+ if (sec.sh_name >= names.size() || !sec.sh_addr) continue;
+ const char* nm = names.data() + sec.sh_name;
+ if (!strcmp(nm, ".got") || !strcmp(nm, ".got.plt") || !strcmp(nm, ".init_array") || !strcmp(nm, ".fini_array") || !strcmp(nm, ".preinit_array")) {
+ if (n < cap) {
+ out[n][0] = sec.sh_addr;
+ out[n][1] = sec.sh_addr + sec.sh_size;
+ n++;
+ }
+ }
+ }
+ // Copy relocations: libc variables (__libc_stack_end, program_invocation_name, environ, ...) that a non-PIE executable hosts in its
+ // own .bss. They describe this process (glibc derives the main thread's stack bounds from __libc_stack_end), so they are kept too.
+ for (auto& sec : sh) {
+ if (sec.sh_type != SHT_RELA || sec.sh_link >= sh.size() || sh[sec.sh_link].sh_type != SHT_DYNSYM) continue;
+ std::vector relas(sec.sh_size / sizeof(Elf64_Rela));
+ std::vector syms(sh[sec.sh_link].sh_size / sizeof(Elf64_Sym));
+ if (::pread(fd, relas.data(), relas.size() * sizeof(Elf64_Rela), sec.sh_offset) != (ssize_t)(relas.size() * sizeof(Elf64_Rela))) continue;
+ if (::pread(fd, syms.data(), syms.size() * sizeof(Elf64_Sym), sh[sec.sh_link].sh_offset) != (ssize_t)(syms.size() * sizeof(Elf64_Sym))) continue;
+#if CPU(ARM64)
+ constexpr uint32_t copyType = R_AARCH64_COPY;
+#else
+ constexpr uint32_t copyType = R_X86_64_COPY;
+#endif
+ for (auto& r : relas) {
+ if (ELF64_R_TYPE(r.r_info) != copyType || n >= cap) continue;
+ uint32_t si = ELF64_R_SYM(r.r_info);
+ uint64_t size = si < syms.size() && syms[si].st_size ? syms[si].st_size : 8;
+ out[n][0] = r.r_offset;
+ out[n][1] = r.r_offset + ((size + 7) & ~7ull);
+ n++;
+ }
+ }
+ close(fd);
+ return n;
+}
+#else
+static size_t platformLinkerOwnedRanges(uint64_t (*out)[2], size_t cap) // Darwin: the malloc zone libsystem registered for this process (see alloc-override-zone.c)
+{
+#if !defined(BUN_MIMALLOC_ZONE_OVERRIDE)
+ (void)out;
+ (void)cap;
+ return 0;
+#else
+ uintptr_t tmp[8][2];
+ size_t n = mi_malloc_zone_process_owned_ranges(tmp, std::min(cap, 8));
+ for (size_t i = 0; i < n; i++) {
+ out[i][0] = tmp[i][0];
+ out[i][1] = tmp[i][1];
+ }
+ return n;
+#endif
+}
+#endif
+
+// Platform seam: region walk, residency, data segments, JIT copy.
+struct PlatformRegion {
+ uint64_t addr, size;
+ bool writable, executable, anon, shared, isStack, isMallocZone, isGuard;
+ int tag;
+ unsigned pagesResident, pagesDirtied, pagesSwapped;
+};
+#if OS(DARWIN)
+template static void platformEnumerateRegions(F&& f)
+{
+ mach_vm_address_t addr = 0;
+ for (;;) {
+ mach_vm_size_t size = 0;
+ vm_region_extended_info_data_t info;
+ mach_msg_type_number_t count = VM_REGION_EXTENDED_INFO_COUNT;
+ mach_port_t objName;
+ if (mach_vm_region(mach_task_self(), &addr, &size, VM_REGION_EXTENDED_INFO, (vm_region_info_t)&info, &count, &objName) != KERN_SUCCESS) break;
+ int tag = info.user_tag;
+ PlatformRegion r { addr, size, !!(info.protection & VM_PROT_WRITE), !!(info.protection & VM_PROT_EXECUTE), info.external_pager == 0, info.share_mode == SM_SHARED,
+ tag == VM_MEMORY_STACK, tag >= VM_MEMORY_MALLOC && tag <= VM_MEMORY_MALLOC_NANO, tag == VM_MEMORY_GUARD || tag == 22, tag, info.pages_resident, info.pages_dirtied, info.pages_swapped_out };
+ f(r);
+ addr += size;
+ }
+}
+static bool platformResidentPages(uint64_t addr, uint64_t size, std::vector& disp)
+{
+ size_t pg = getpagesize();
+ disp.assign(size / pg, 0);
+ mach_vm_size_t dispCount = disp.size();
+ return mach_vm_page_range_query(mach_task_self(), addr, size, (mach_vm_address_t)disp.data(), &dispCount) == KERN_SUCCESS;
+}
+template static void platformDataSegments(F&& f)
+{
+ const struct mach_header_64* mh = (const struct mach_header_64*)_dyld_get_image_header(0);
+ for (const char* seg : { "__DATA_CONST", "__DATA", "__DATA_DIRTY", "__AUTH", "__AUTH_CONST" }) {
+ unsigned long segSize = 0;
+ uint8_t* segData = getsegmentdata(mh, seg, &segSize);
+ if (segData && segSize) f((uint64_t)segData, (uint64_t)segSize);
+ }
+}
+static void platformWriteJIT(void* dst, const void* src, size_t len)
+{
+ pthread_jit_write_protect_np(0);
+ memcpy(dst, src, len);
+ pthread_jit_write_protect_np(1);
+ sys_icache_invalidate(dst, len);
+}
+static bool platformIsJITRegion(const PlatformRegion& r) { return r.tag == 64 && r.executable && r.anon; }
+static uint64_t platformTextBase() { return (uint64_t)&_mh_execute_header; }
+extern "C" const void* _dyld_get_shared_cache_range(size_t* length);
+extern "C" bool _dyld_get_shared_cache_uuid(uuid_t uuid);
+// System libraries' load address: snapshot words that point into them (ICU vtables, pthread main-thread handle, ...) are only valid while this matches.
+static uint64_t platformLibsBase()
+{
+ size_t len = 0;
+ return (uint64_t)_dyld_get_shared_cache_range(&len);
+}
+static uint64_t platformSystemLibsId()
+{
+ uuid_t u;
+ if (!_dyld_get_shared_cache_uuid(u)) return 0;
+ uint64_t h = 1469598103934665603ull;
+ for (size_t i = 0; i < sizeof u; i++) {
+ h ^= u[i];
+ h *= 1099511628211ull;
+ }
+ return h ? h : 1;
+} // identity of the OS's dyld shared cache: same across reboots (it only slides), different after an OS update
+// Identity of this exact executable (a snapshot is only valid for the binary that produced it): LC_UUID folded to 64 bits.
+static uint64_t platformBuildId()
+{
+ const struct mach_header_64* mh = &_mh_execute_header;
+ const uint8_t* p = (const uint8_t*)(mh + 1);
+ for (uint32_t i = 0; i < mh->ncmds; i++) {
+ const struct load_command* lc = (const struct load_command*)p;
+ if (lc->cmd == LC_UUID) {
+ uint64_t a, b;
+ memcpy(&a, ((const struct uuid_command*)lc)->uuid, 8);
+ memcpy(&b, ((const struct uuid_command*)lc)->uuid + 8, 8);
+ return a ^ b;
+ }
+ p += lc->cmdsize;
+ }
+ return 0;
+}
+#elif OS(LINUX)
+extern "C" char __executable_start[];
+template static void platformEnumerateRegions(F&& f)
+{
+ FILE* maps = fopen("/proc/self/maps", "r");
+ if (!maps) return;
+ char line[512];
+ while (fgets(line, sizeof line, maps)) {
+ unsigned long lo, hi, off, inode = 0;
+ char perms[8] = "", dev[16] = "";
+ char path[256] = "";
+ if (sscanf(line, "%lx-%lx %7s %lx %15s %lu %255s", &lo, &hi, perms, &off, dev, &inode, path) < 6) continue;
+ bool anon = inode == 0 && (path[0] == 0 || path[0] == '[');
+ PlatformRegion r { lo, hi - lo, perms[1] == 'w', perms[2] == 'x', anon, perms[3] == 's', !strncmp(path, "[stack", 6), false, perms[0] == '-' && perms[1] == '-', 0, 1, 1, 0 };
+ // Linux has no VM tags: callers identify "ours" by address windows; JIT by the fixed pool address.
+ f(r);
+ }
+ fclose(maps);
+}
+static bool platformResidentPages(uint64_t addr, uint64_t size, std::vector& disp)
+{
+ size_t pg = getpagesize();
+ std::vector vec(size / pg);
+ disp.assign(size / pg, 0);
+ if (mincore((void*)addr, size, vec.data())) return false;
+ for (size_t i = 0; i < vec.size(); i++)
+ disp[i] = vec[i] & 1;
+ return true;
+}
+extern "C" char _end[]; // linker-defined end of .bss: everything the injector appended to the segment (payload blocks, live or superseded) lies past it
+template static void platformDataSegments(F&& f)
+{
+ // The writable PT_LOADs (.data/.bss/GOT: -z norelro), cut at _end: what the injector appended past it (payload blocks) is file-backed and identical in every launch.
+ struct Ctx {
+ F* f;
+ uint64_t end;
+ } ctx { &f, (uint64_t)_end };
+ dl_iterate_phdr([](struct dl_phdr_info* info, size_t, void* arg) -> int {
+ if (info->dlpi_name && *info->dlpi_name) return 0; // main executable only
+ auto& ctx = *static_cast(arg);
+ size_t pg = getpagesize();
+ for (int i = 0; i < info->dlpi_phnum; i++) {
+ const ElfW(Phdr) & ph = info->dlpi_phdr[i];
+ if (ph.p_type != PT_LOAD || !(ph.p_flags & PF_W)) continue;
+ uint64_t lo = (info->dlpi_addr + ph.p_vaddr) & ~(uint64_t)(pg - 1);
+ uint64_t hi = (info->dlpi_addr + ph.p_vaddr + ph.p_memsz + pg - 1) & ~(uint64_t)(pg - 1);
+ if (ctx.end > lo && ctx.end < hi) hi = (ctx.end + pg - 1) & ~(uint64_t)(pg - 1);
+ if (hi > lo) (*ctx.f)(lo, hi - lo);
+ }
+ return 0;
+ },
+ &ctx);
+}
+static void platformWriteJIT(void* dst, const void* src, size_t len)
+{
+ memcpy(dst, src, len);
+ __builtin___clear_cache((char*)dst, (char*)dst + len);
+}
+static bool platformIsJITRegion(const PlatformRegion& r) { return r.executable && r.anon && r.addr >= 0x3c0000000ull && r.addr < 0x400000000ull; } // BUN_STARTUP_SNAPSHOT_JIT_ADDR window
+static uint64_t platformTextBase() { return (uint64_t)__executable_start; }
+static uint64_t platformLibsBase() { return (uint64_t)dlsym(RTLD_DEFAULT, "getpid"); } // libc's slide stands in for all system libs
+static uint64_t platformSystemLibsId() { return 0; } // Linux: per-library name+size matching in the fixup table is the identity
+extern "C" char __etext[] __attribute__((weak));
+extern "C" char etext[];
+static uint64_t platformBuildId() // the ELF NT_GNU_BUILD_ID note (identity of this exact link), folded to 64 bits; falls back to the text extent
+{
+ struct Ctx {
+ uint64_t id;
+ } ctx { 0 };
+ dl_iterate_phdr([](struct dl_phdr_info* info, size_t, void* arg) -> int {
+ if (info->dlpi_name && *info->dlpi_name) return 0; // main executable only
+ for (int i = 0; i < info->dlpi_phnum; i++) {
+ const ElfW(Phdr) & ph = info->dlpi_phdr[i];
+ if (ph.p_type != PT_NOTE) continue;
+ const uint8_t* p = (const uint8_t*)(info->dlpi_addr + ph.p_vaddr);
+ const uint8_t* end = p + ph.p_memsz;
+ while (p + sizeof(ElfW(Nhdr)) <= end) {
+ const ElfW(Nhdr)* nh = (const ElfW(Nhdr)*)p;
+ const uint8_t* name = p + sizeof *nh;
+ const uint8_t* desc = name + ((nh->n_namesz + 3) & ~3u);
+ if (nh->n_type == NT_GNU_BUILD_ID && nh->n_namesz == 4 && !memcmp(name, "GNU", 4) && desc + nh->n_descsz <= end) {
+ uint64_t h = 1469598103934665603ull;
+ for (uint32_t k = 0; k < nh->n_descsz; k++) {
+ h ^= desc[k];
+ h *= 1099511628211ull;
+ }
+ ((Ctx*)arg)->id = h;
+ return 1;
+ }
+ p = desc + ((nh->n_descsz + 3) & ~3u);
+ }
+ }
+ return 0;
+ },
+ &ctx);
+ return ctx.id ? ctx.id : (uint64_t)((char*)etext - (char*)__executable_start);
+}
+#endif
+// Resident pool pages the allocator has actually handed out (freed pages are MADV_FREE'd and may still read as present); its occupancy is page-granular, hence two samples per page.
+static bool jitLivePages(uint64_t addr, uint64_t size, size_t pg, std::vector& disp)
+{
+ if (!platformResidentPages(addr, size, disp)) return false;
+ Locker locker { JSC::ExecutableAllocator::singleton().getLock() };
+ for (size_t i = 0; i < disp.size(); i++)
+ if (disp[i] && !JSC::ExecutableAllocator::singleton().isValidExecutableMemory(locker, (void*)(addr + i * pg)) && !JSC::ExecutableAllocator::singleton().isValidExecutableMemory(locker, (void*)(addr + i * pg + pg / 2))) disp[i] = 0;
+ return true;
+}
+static size_t jitLivePageCount(size_t pg)
+{
+ size_t n = 0;
+ platformEnumerateRegions([&](const PlatformRegion& r) {
+ if (!platformIsJITRegion(r)) return;
+ std::vector disp;
+ if (jitLivePages(r.addr, r.size, pg, disp)) n += std::count(disp.begin(), disp.end(), 1);
+ });
+ return n;
+}
+
+// Loaded system libraries as (base, end, nameHash): snapshot words pointing into them are recorded at dump and rebased at restore.
+struct PlatformLib {
+ uint64_t base, end, nameHash;
+ uint64_t flags;
+ char path[232];
+ char seg[16];
+}; // flags bit 0: lives in the dyld shared cache (slides with it as a unit; needs no dlopen to know where it went) // path: what to dlopen when the restoring process has not loaded the library yet (apps dlopen e.g. libsqlite3 lazily); matching uses nameHash + size
+static void platformLibSetName(PlatformLib& l, const char* path, const char* seg)
+{
+ snprintf(l.path, sizeof l.path, "%s", path ? path : "");
+ snprintf(l.seg, sizeof l.seg, "%s", seg ? seg : "");
+}
+static uint64_t fnv1a(const char* p)
+{
+ uint64_t h = 1469598103934665603ull;
+ for (; *p; p++) {
+ h ^= (uint8_t)*p;
+ h *= 1099511628211ull;
+ }
+ return h;
+}
+#if OS(LINUX)
+static std::vector platformSystemLibs()
+{
+ std::vector libs;
+ dl_iterate_phdr([](struct dl_phdr_info* info, size_t, void* arg) -> int {
+ auto* libs = static_cast*>(arg);
+ const char* name = info->dlpi_name;
+ if (!name || !*name) return 0; // main executable: fixed (non-PIE)
+ uint64_t lo = UINT64_MAX, hi = 0;
+ for (int i = 0; i < info->dlpi_phnum; i++)
+ if (info->dlpi_phdr[i].p_type == PT_LOAD) {
+ uint64_t a = info->dlpi_addr + info->dlpi_phdr[i].p_vaddr;
+ lo = std::min(lo, a);
+ hi = std::max(hi, a + info->dlpi_phdr[i].p_memsz);
+ }
+ if (hi > lo) {
+ const char* slash = strrchr(name, '/');
+ const char* bn = slash ? slash + 1 : name;
+ PlatformLib l { lo, hi, fnv1a(bn), 0, {}, {} };
+ platformLibSetName(l, name, nullptr);
+ libs->push_back(l);
+ }
+ return 0;
+ },
+ &libs);
+ return libs;
+}
+#else
+static std::vector platformSystemLibs() // Darwin: every segment of every loaded dylib (they all live in the dyld shared cache, which slides as a unit per boot; per-segment ranges keep the pointer scan tight)
+{
+ std::vector libs;
+ for (uint32_t i = 0, n = _dyld_image_count(); i < n; i++) {
+ const struct mach_header_64* mh = (const struct mach_header_64*)_dyld_get_image_header(i);
+ if (!mh || mh == &_mh_execute_header) continue;
+ intptr_t slide = _dyld_get_image_vmaddr_slide(i);
+ const char* name = _dyld_get_image_name(i);
+ const char* slash = name ? strrchr(name, '/') : nullptr;
+ uint64_t nameHash = fnv1a(slash ? slash + 1 : (name ? name : "?"));
+ bool inCache = (mh->flags & MH_DYLIB_IN_CACHE) != 0;
+ const uint8_t* lc = (const uint8_t*)(mh + 1);
+ for (uint32_t j = 0; j < mh->ncmds; j++) {
+ const struct load_command* c = (const struct load_command*)lc;
+ if (c->cmd == LC_SEGMENT_64) {
+ const struct segment_command_64* sc = (const struct segment_command_64*)c;
+ if (sc->vmsize && strcmp(sc->segname, "__PAGEZERO")) {
+ uint64_t base = sc->vmaddr + (uint64_t)slide, end = base + sc->vmsize;
+ bool dup = false;
+ for (auto& l : libs)
+ if (l.base == base && l.end == end) {
+ dup = true;
+ break;
+ }
+ if (!dup) {
+ PlatformLib l { base, end, nameHash ^ fnv1a(sc->segname), inCache ? 1ull : 0ull, {}, {} };
+ platformLibSetName(l, name, sc->segname);
+ libs.push_back(l);
+ }
+ }
+ }
+ lc += c->cmdsize;
+ }
+ }
+ return libs;
+}
+#endif
+struct StartupSnapshotFixup {
+ uint64_t addr;
+ uint64_t lib;
+};
+struct SnapshotFixupHeader {
+ char magic[8];
+ uint64_t nlibs;
+ uint64_t nfixups;
+}; // then PlatformLib[nlibs] (base/end/nameHash as recorded), StartupSnapshotFixup[nfixups]
+
+// Header CPU-feature word: latched SIMD dispatch in vendored code means a snapshot is only used on a CPU with at least the builder's features.
+static uint64_t platformCpuFeatures()
+{
+ uint64_t f = 0;
+#if CPU(X86_64)
+ unsigned a, b, c, d;
+ auto cpuid = [&](unsigned leaf, unsigned sub) { __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(leaf), "c"(sub)); };
+ cpuid(1, 0);
+ f |= (uint64_t)(c & ((1u << 0) | (1u << 9) | (1u << 19) | (1u << 20) | (1u << 23) | (1u << 25) | (1u << 28))); // sse3 ssse3 sse4.1 sse4.2 popcnt aes avx
+ cpuid(7, 0);
+ f |= (uint64_t)(b & ((1u << 3) | (1u << 5) | (1u << 8) | (1u << 16) | (1u << 17) | (1u << 30) | (1u << 31))) << 32; // bmi1 avx2 bmi2 avx512f avx512dq avx512bw avx512vl
+ f |= 1ull << 30; // "x86-64" tag: a bit neither leaf uses (leaf 1 occupies 0-28, leaf 7 is shifted to 32-63)
+#elif CPU(ARM64)
+#if OS(DARWIN)
+ const char* keys[] = { "hw.optional.arm.FEAT_AES", "hw.optional.arm.FEAT_SHA256", "hw.optional.arm.FEAT_CRC32", "hw.optional.arm.FEAT_LSE", "hw.optional.arm.FEAT_DotProd", "hw.optional.arm.FEAT_SHA3", "hw.optional.arm.FEAT_I8MM", "hw.optional.arm.FEAT_BF16", "hw.optional.arm.FEAT_SME", "hw.optional.arm.FEAT_SVE" };
+ for (unsigned i = 0; i < sizeof keys / sizeof *keys; i++) {
+ int v = 0;
+ size_t n = sizeof v;
+ if (!sysctlbyname(keys[i], &v, &n, nullptr, 0) && v) f |= 1ull << i;
+ }
+#elif OS(LINUX)
+ f = getauxval(AT_HWCAP) & 0xffffffffull;
+ f |= (getauxval(AT_HWCAP2) & 0x7fffffffull) << 32;
+#endif
+ f |= 1ull << 63; // "arm64" tag: HWCAP2 is masked to 31 bits (32-62) and the Darwin keys use low bits, so nothing else reaches 63
+#endif
+ return f;
+}
+
+// A snapshot taken after the program ran is only valid for the argv it ran with (or `exe subcommand` would get the restored REPL); see main() for the exemption.
+static uint64_t snapshotArgvKey()
+{
+ uint64_t h = 1469598103934665603ull;
+ int argc = 0;
+ char** argv = nullptr;
+#if OS(DARWIN)
+ argc = *_NSGetArgc();
+ argv = *_NSGetArgv();
+#elif OS(LINUX)
+ static std::vector args;
+ static std::vector ptrs;
+ if (ptrs.empty()) {
+ FILE* f = fopen("/proc/self/cmdline", "r");
+ std::string cur;
+ int c;
+ while (f && (c = fgetc(f)) != EOF) {
+ if (!c) {
+ args.push_back(cur);
+ cur.clear();
+ } else
+ cur.push_back((char)c);
+ }
+ if (f) fclose(f);
+ for (auto& a : args)
+ ptrs.push_back(a.data());
+ }
+ argc = (int)ptrs.size();
+ argv = ptrs.data();
+#endif
+ for (int i = 1; i < argc; i++) {
+ for (const char* p = argv[i]; *p; p++) {
+ h ^= (uint8_t)*p;
+ h *= 1099511628211ull;
+ }
+ h ^= 0xff;
+ h *= 1099511628211ull;
+ }
+ return h ^ ((uint64_t)(argc > 0 ? argc - 1 : 0) << 56) ^ 0x5a5a; // never 0
+}
+
+struct StartupSnapshotHeader {
+ char magic[8];
+ uint64_t textBase;
+ uint64_t vm;
+ uint64_t globalObject;
+ uint64_t mainThread;
+ uint64_t nregions;
+ uint64_t reserved[8];
+ uint64_t libsBase;
+ uint64_t spare[7];
+}; // 176 bytes; region table follows
+struct StartupSnapshotRegion {
+ uint64_t addr;
+ uint64_t len;
+ uint64_t fileOff;
+ uint64_t kind;
+}; // kind: 0 heap(anon), 1 __DATA segment
+
+// First-writer trap: snapshot pages are made read-only; the fault handler records the writer's stack, unprotects the page and resumes.
+
+// A dup'd controlling-tty fd to recreate at restore: fd, the F_GETFL word, and source stdio+1 in disjoint fields (an overlapping layout let x86-64's O_LARGEFILE bleed into the fd number).
+static uint64_t ttyFdRecord(int fd, int flags, int src) { return ((uint64_t)(uint32_t)fd << 40) | ((uint64_t)(uint32_t)flags << 8) | (uint64_t)(src + 1); }
+static void ttyFdRecordUnpack(uint64_t v, int& fd, int& flags, int& src)
+{
+ fd = (int)(v >> 40);
+ flags = (int)((v >> 8) & 0xffffffffu);
+ src = (int)(v & 0xff) - 1;
+}
+
+static struct termios s_snapshotTermios;
+static int s_snapshotTermiosFd = -1; // lives in __DATA, so it travels inside the snapshot
+static uint64_t s_snapshotOpenFds[16]; // fds 0..1023 open in the build process: the restored process parks /dev/null on them so stale closes are harmless and new fds never alias them
+struct SnapshotFileFd {
+ int fd;
+ int flags;
+ char path[1024]; // F_GETPATH needs MAXPATHLEN
+};
+static SnapshotFileFd s_snapshotFileFds[32];
+static int s_snapshotFileFdCount = 0; // writable regular files (logs) get reopened O_APPEND at the same fd number
+static bool snapshotDump(JSC::VM& vm, const char* path)
+{
+#if OS(DARWIN) || OS(LINUX)
+ JSC::JSLockHolder lock(vm);
+ {
+ Vector gated;
+ for (size_t start = 0; start < s_envGateNames.size();) {
+ size_t end = s_envGateNames.find('\0', start);
+ if (end == std::string::npos)
+ end = s_envGateNames.size();
+ gated.append(String::fromUTF8(std::span { s_envGateNames.data() + start, end - start }));
+ start = end + 1;
+ }
+ Bun::printEnvReadsBeforeSnapshot(defaultGlobalObject(), gated);
+ }
+ s_snapshotTermiosFd = -1;
+ for (int fd = 0; fd < 3; fd++)
+ if (isatty(fd) && !tcgetattr(fd, &s_snapshotTermios)) {
+ s_snapshotTermiosFd = fd;
+ break;
+ }
+ memset(s_snapshotOpenFds, 0, sizeof s_snapshotOpenFds);
+ s_snapshotFileFdCount = 0;
+ for (int fd = 3; fd < 1024; fd++) {
+ if (fcntl(fd, F_GETFD) == -1) continue;
+ s_snapshotOpenFds[fd / 64] |= 1ull << (fd % 64);
+ struct stat st;
+ if (s_snapshotFileFdCount < 32 && !fstat(fd, &st) && S_ISREG(st.st_mode)) {
+ SnapshotFileFd& f = s_snapshotFileFds[s_snapshotFileFdCount];
+ f.fd = fd;
+ f.flags = fcntl(fd, F_GETFL);
+#if OS(DARWIN)
+ if ((f.flags & O_ACCMODE) != O_RDONLY && fcntl(fd, F_GETPATH, f.path) != -1) s_snapshotFileFdCount++;
+#else
+ {
+ char lnk[64];
+ snprintf(lnk, sizeof lnk, "/proc/self/fd/%d", fd);
+ ssize_t n = readlink(lnk, f.path, sizeof f.path - 1);
+ if ((f.flags & O_ACCMODE) != O_RDONLY && n > 0) {
+ f.path[n] = 0;
+ s_snapshotFileFdCount++;
+ }
+ }
+#endif
+ }
+ }
+ size_t settledStrings = 0;
+ { // Error objects keep raw StackFrames (CodeBlock pointers) until .stack is first read; resolve them now so nothing in the snapshot points at code we drop or re-link
+ JSC::HeapIterationScope scope(vm.heap);
+ vm.heap.objectSpace().forEachLiveCell(scope, [&](JSC::HeapCell* heapCell, JSC::HeapCell::Kind kind) {
+ if (isJSCellKind(kind)) {
+ JSC::JSCell* cell = static_cast(heapCell);
+ if (auto* error = dynamicDowncast(cell)) error->materializeErrorInfoIfNeeded(vm);
+ // Lazy one-time StringImpl header writes (hash, did-report-cost) would otherwise dirty snapshot pages the first time a string is used after restore.
+ if (auto* str = dynamicDowncast(cell)) {
+ if (!str->isRope())
+ if (auto* impl = str->tryGetValueImpl()) {
+ impl->settleLazyHeaderWritesForStartupSnapshot();
+ settledStrings++;
+ }
+ }
+ }
+ return IterationStatus::Continue;
+ });
+ }
+ if (auto* table = vm.atomStringTable())
+ for (auto& packed : table->table())
+ if (auto* impl = packed.get()) {
+ impl->settleLazyHeaderWritesForStartupSnapshot();
+ settledStrings++;
+ }
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE")) fprintf(stderr, "[snapshot] settled %zu StringImpl headers\n", settledStrings);
+ {
+ // Compiled JS is per-run hot state: a long-running program is 11-17 MB/process lighter re-creating it, a main() tool starts 15-20% faster keeping it. BUN_STARTUP_SNAPSHOT_DELETE_CODE=0|linked overrides.
+ const char* dc = getenv("BUN_STARTUP_SNAPSHOT_DELETE_CODE");
+ vm.completeAllJITPlansBeforeStartupSnapshot();
+ JSC::sanitizeStackForVM(vm);
+ bool keep = dc ? !strcmp(dc, "0") : Bun__startupSnapshotHasMain();
+ if (keep) {
+ } else if (dc && !strcmp(dc, "linked"))
+ vm.deleteAllLinkedCode(JSC::DeleteAllCodeIfNotCollecting);
+ else
+ vm.deleteAllCode(JSC::DeleteAllCodeIfNotCollecting);
+ }
+ vm.heap.freezeCurrentHeapAsImmortalStartupSnapshot(); // GC never writes snapshot blocks again (frozen marks = liveness, side remembered set)
+ mi_option_set(mi_option_purge_delay, 0);
+ mi_collect(true); // free spans get decommitted so "resident" below means "snapshot payload"
+ size_t pg = getpagesize();
+ startupSnapshotToolingIndexAtFreeze(vm, pg);
+ std::vector> freeRanges; // arena slices in no page: free memory, whatever the kernel says about residency
+ mi_arenas_visit_free_ranges(mi_heap_main(), [](void* start, size_t size, void* arg) { static_cast>*>(arg)->push_back({ (uintptr_t)start, (uintptr_t)start + size }); }, &freeRanges);
+ std::sort(freeRanges.begin(), freeRanges.end());
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE")) {
+ size_t fb = 0;
+ for (auto& r : freeRanges)
+ fb += r.second - r.first;
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] arena free ranges: %zu, %.1fMB\n", freeRanges.size(), fb / 1048576.0);
+ }
+ auto inFreeRange = [&](uintptr_t a) { auto it = std::upper_bound(freeRanges.begin(), freeRanges.end(), std::make_pair(a, UINTPTR_MAX)); return it != freeRanges.begin() && a < std::prev(it)->second; };
+ std::vector regions;
+ size_t jitPagesAtScan = 0;
+ // 1. anonymous writable regions we own (mimalloc arenas + page map, JSC/WTF OS allocations in the hint windows) + the JIT pool
+ platformEnumerateRegions([&](const PlatformRegion& r) {
+ uint64_t addr = r.addr, size = r.size;
+ int tag = r.tag;
+ // Only memory we own and place deterministically. Kernel-placed libSystem regions belong to the *new* process and must not be overlaid.
+ bool ours = tag == 240 || tag == 63 || tag == 65 || (addr >= 0x1f000000000ull && addr < 0x30000000000ull) || (addr >= 0x2e0000000000ull && addr < 0x2f0000000000ull);
+ if (platformIsJITRegion(r)) {
+ regions.push_back({ addr, size, 0, ((uint64_t)tag << 8) | 3 }); // reservation, no data
+ std::vector disp;
+ if (jitLivePages(addr, size, pg, disp)) {
+ jitPagesAtScan += std::count(disp.begin(), disp.end(), 1);
+ for (size_t i = 0; i < disp.size();) {
+ if (!disp[i]) {
+ i++;
+ continue;
+ }
+ size_t j = i;
+ while (j < disp.size() && disp[j])
+ j++;
+ regions.push_back({ addr + i * pg, (j - i) * pg, 0, ((uint64_t)tag << 8) | 2 });
+ i = j;
+ }
+ }
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE")) fprintf(stderr, "[snapshot] JIT region %llx+%llx resident=%u dirty=%u\n", (unsigned long long)addr, (unsigned long long)size, r.pagesResident, r.pagesDirtied);
+ } else if (ours && r.writable && !r.executable && r.anon && !r.isStack && !r.isMallocZone && !r.isGuard && !r.shared) {
+ // Emitted even when nothing in it is resident: pointers into it may exist (an allocator table that is still all zeros), so the mapping itself must come back.
+ regions.push_back({ addr, size, 0, ((uint64_t)tag << 8) | 4 }); // anonymous reserve, then resident runs as file-backed data
+ std::vector disp;
+ if (platformResidentPages(addr, size, disp)) {
+ auto live = [&](size_t k) { return disp[k] && !inFreeRange(addr + k * pg); }; // purged spans can still read as present; mimalloc knows they are free
+ for (size_t i = 0; i < disp.size();) {
+ if (!live(i)) {
+ i++;
+ continue;
+ }
+ size_t j = i;
+ while (j < disp.size() && live(j))
+ j++;
+ regions.push_back({ addr + i * pg, (j - i) * pg, 0, (uint64_t)tag << 8 });
+ i = j;
+ }
+ } else
+ regions.back().kind = (uint64_t)tag << 8;
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE")) fprintf(stderr, "[snapshot] region %llx+%llx tag=%d resident=%u dirty=%u\n", (unsigned long long)addr, (unsigned long long)size, tag, r.pagesResident, r.pagesDirtied);
+ }
+ });
+ // 2. main binary data segments (globals of Bun/JSC/WTF/mimalloc)
+ platformDataSegments([&](uint64_t a, uint64_t len) { regions.push_back({ a, (len + pg - 1) & ~(uint64_t)(pg - 1), 0, 1 }); });
+ // drop anon regions overlapping __DATA entries (region scan sees them as file-backed anyway) and our own stack
+ uintptr_t sp = (uintptr_t)__builtin_frame_address(0);
+ std::vector out;
+ for (auto& r : regions) {
+ if (r.kind == 0 && sp >= r.addr && sp < r.addr + r.len) continue;
+ // Address-adjacent heap runs (and reservations) restore as one mapping; the enumeration yields them in address order.
+ unsigned k = r.kind & 0xff;
+ if (!out.empty() && (k == 0 || k == 4) && (out.back().kind & 0xff) == k && out.back().addr + out.back().len == r.addr) {
+ out.back().len += r.len;
+ continue;
+ }
+ out.push_back(r);
+ }
+ int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
+ if (fd < 0) {
+ fprintf(stderr, "[snapshot] open %s failed\n", path);
+ return false;
+ }
+ StartupSnapshotHeader hdr {};
+ memcpy(hdr.magic, "BUNSNAP1", 8);
+ hdr.textBase = platformTextBase();
+ hdr.libsBase = platformLibsBase();
+ hdr.spare[0] = snapshotEnvironmentId();
+ hdr.spare[2] = platformCpuFeatures();
+ // With main() registered the program has not run yet, so the snapshot fits any invocation (0 = no argv check); otherwise it holds this invocation's state.
+ hdr.spare[3] = Bun__startupSnapshotHasMain() ? 0 : snapshotArgvKey();
+ hdr.spare[4] = platformSystemLibsId();
+ hdr.vm = (uint64_t)&vm;
+ hdr.globalObject = (uint64_t)defaultGlobalObject();
+ hdr.mainThread = (uint64_t)&WTF::Thread::currentSingleton();
+ hdr.reserved[0] = (uint64_t)mi_theap_get_default(); // main thread's mimalloc theap (TLS-referenced, lives in the heap)
+ hdr.reserved[7] = (uint64_t)uws_get_loop(); // main thread's uWS loop (TLS-referenced)
+ {
+ pthread_key_t k = 0;
+ if (!pthread_key_create(&k, nullptr)) {
+ hdr.reserved[1] = (uint64_t)k;
+ pthread_key_delete(k);
+ }
+ } // high-water mark of pthread TLS keys
+ { // fds that are the controlling TTY (dup'd stdin/stdout readers): the restoring process recreates them from its own 0/1/2
+ struct stat st[3];
+ bool have[3];
+ for (int i = 0; i < 3; i++)
+ have[i] = !fstat(i, &st[i]) && S_ISCHR(st[i].st_mode);
+ int n = 0;
+ for (int fd = 3; fd < 256 && n < 5; fd++) {
+ struct stat fs;
+ if (fstat(fd, &fs) || !S_ISCHR(fs.st_mode)) continue;
+ int fl = fcntl(fd, F_GETFL);
+ int src = -1;
+ for (int i = 0; i < 3; i++)
+ if (have[i] && fs.st_rdev == st[i].st_rdev) {
+ src = ((fl & O_ACCMODE) == O_RDONLY) ? 0 : (i == 0 ? 1 : i);
+ break;
+ }
+ if (src < 0) continue;
+ hdr.reserved[2 + n++] = ttyFdRecord(fd, fl, src);
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE")) fprintf(stderr, "[snapshot] tty fd %d (flags %x) <- std%d\n", fd, fl, src);
+ }
+ }
+ hdr.nregions = out.size();
+ size_t tableOff = sizeof(StartupSnapshotHeader);
+ size_t dataOff = (tableOff + out.size() * sizeof(StartupSnapshotRegion) + pg - 1) & ~(pg - 1);
+ size_t fileOff = dataOff, total = 0;
+ for (auto& r : out) {
+ size_t used = ((r.kind & 0xff) == 3 || (r.kind & 0xff) == 4) ? 0 : r.len;
+ r.fileOff = fileOff;
+ fileOff += used;
+ }
+ mi_arenas_freeze_pages(); // from here on nothing frees into a page that is going into the snapshot (this process's remaining frees are dropped too)
+ { // extern-library fixups: words in the snapshot that point into a loaded system library get rebased at restore (lets libraries slide)
+ std::vector libs = platformSystemLibs();
+ std::vector fixups;
+ if (!libs.empty()) {
+ uint64_t minB = UINT64_MAX, maxE = 0;
+ for (auto& l : libs) {
+ minB = std::min(minB, l.base);
+ maxE = std::max(maxE, l.end);
+ }
+ for (auto& r : out) {
+ unsigned k = r.kind & 0xff;
+ if (k == 2 || k == 3 || k == 4) continue;
+ const uint64_t* w = (const uint64_t*)r.addr;
+ size_t n = r.len / 8;
+ for (size_t i = 0; i < n; i++) {
+ uint64_t v = w[i];
+ if (v < minB || v >= maxE) continue;
+ for (size_t li = 0; li < libs.size(); li++)
+ if (v >= libs[li].base && v < libs[li].end) {
+ fixups.push_back({ r.addr + i * 8, li });
+ break;
+ }
+ }
+ }
+ }
+ { // Keep only the segments something actually points into (a process has ~1.7K loaded segments; a snapshot references a few dozen).
+ std::vector newIndex(libs.size(), UINT64_MAX);
+ std::vector used;
+ for (auto& f : fixups) {
+ if (newIndex[f.lib] == UINT64_MAX) {
+ newIndex[f.lib] = used.size();
+ used.push_back(libs[f.lib]);
+ }
+ f.lib = newIndex[f.lib];
+ }
+ libs.swap(used);
+ }
+ SnapshotFixupHeader fh {};
+ memcpy(fh.magic, "BUNFIX3", 8);
+ fh.nlibs = libs.size();
+ fh.nfixups = fixups.size();
+ size_t fixOff = (fileOff + 4095) & ~4095ull;
+ hdr.spare[1] = fixOff;
+ pwrite(fd, &fh, sizeof fh, fixOff);
+ pwrite(fd, libs.data(), libs.size() * sizeof(PlatformLib), fixOff + sizeof fh);
+ pwrite(fd, fixups.data(), fixups.size() * sizeof(StartupSnapshotFixup), fixOff + sizeof fh + libs.size() * sizeof(PlatformLib));
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE") || !fixups.empty()) {
+ size_t pages = 0;
+ uint64_t last = ~0ull;
+ for (auto& f : fixups) {
+ uint64_t pg = f.addr >> 14;
+ if (pg != last) {
+ pages++;
+ last = pg;
+ }
+ }
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] %zu extern-library fixups across %zu library segments, touching %zu 16K pages (%.1f MB dirtied at restore if libraries slid)\n", fixups.size(), libs.size(), pages, pages * 16384.0 / 1048576.0);
+ }
+ }
+ for (auto& r : out) {
+ // write region contents; non-resident anon pages read as zero which is what a fresh mapping would give anyway
+ size_t used = ((r.kind & 0xff) == 3 || (r.kind & 0xff) == 4) ? 0 : r.len;
+ if (pwrite(fd, (void*)r.addr, used, r.fileOff) != (ssize_t)used) {
+ fprintf(stderr, "[snapshot] pwrite failed for %llx+%llx errno %d\n", r.addr, (unsigned long long)used, errno);
+ }
+ total += used;
+ }
+ if (!s_envGateNames.empty()) {
+ struct stat cur;
+ fstat(fd, &cur);
+ size_t gateOff = ((size_t)cur.st_size + 4095) & ~4095ull;
+ pwrite(fd, s_envGateNames.data(), s_envGateNames.size(), gateOff);
+ hdr.spare[5] = (uint64_t)gateOff | ((uint64_t)s_envGateNames.size() << 40);
+ hdr.spare[6] = envGateHash(s_envGateNames.data(), s_envGateNames.size());
+ }
+ // Background compilers were quiesced before the walk; if code was installed anyway the snapshot points at code it lacks, and no snapshot beats that one.
+ if (size_t now = jitLivePageCount(pg); now != jitPagesAtScan) {
+ fprintf(stderr, "[snapshot] error: executable memory changed while the snapshot was being written (%zu pages live at the walk, %zu now): something was still compiling; not writing a snapshot\n", jitPagesAtScan, now);
+ close(fd);
+ unlink(path);
+ return false;
+ }
+ pwrite(fd, &hdr, sizeof hdr, 0);
+ pwrite(fd, out.data(), out.size() * sizeof(StartupSnapshotRegion), tableOff);
+ close(fd);
+ fprintf(stderr, "[snapshot] wrote %s: %zu regions, %.1fMB (vm=%p global=%p thread=%p text=%p)\n", path, out.size(), total / 1048576.0, (void*)hdr.vm, (void*)hdr.globalObject, (void*)hdr.mainThread, (void*)hdr.textBase);
+ return true;
+#else
+ UNUSED_PARAM(vm);
+ UNUSED_PARAM(path);
+ return false;
+#endif
+}
+
+// Restore: called from Bun__startupSnapshotMaybeRestore (very early in main) when BUN_STARTUP_SNAPSHOT_IN is set. Never returns.
+static void snapshotRestoreAndRun(const char* path)
+{
+ snapshotTimingMark("restore begins (after the re-exec, if any)");
+#if OS(DARWIN) || OS(LINUX)
+ char filePath[4200];
+ snprintf(filePath, sizeof filePath, "%s", path);
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE")) {
+ void* probe = malloc(64);
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] pre-restore heap probe=%p (expected >= 0x21000000000, above where snapshot regions go)\n", probe);
+ free(probe);
+ }
+ snapshotBaseOff = 0;
+ if (char* at = strrchr(filePath, '@')) {
+ char* end = nullptr;
+ long long o = strtoll(at + 1, &end, 10);
+ if (end && !*end && o > 0) {
+ *at = 0;
+ snapshotBaseOff = (off_t)o;
+ }
+ } // "@": snapshot embedded in a bigger file (our own executable)
+ int fd = open(filePath, O_RDONLY);
+ if (fd < 0) {
+ fprintf(stderr, "[snapshot] cannot open %s\n", path);
+ _exit(2);
+ }
+ StartupSnapshotHeader hdr;
+ ipread(fd, &hdr, sizeof hdr, 0);
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE")) fprintf(stderr, "[snapshot] source %s base=%lld magic=%.7s nregions=%llu text=%llx libs=%llx build=%llx\n", filePath, (long long)snapshotBaseOff, hdr.magic, (unsigned long long)hdr.nregions, (unsigned long long)hdr.textBase, (unsigned long long)hdr.libsBase, (unsigned long long)hdr.spare[0]);
+ if (memcmp(hdr.magic, "BUNSNAP1", 8) || hdr.spare[0] != snapshotEnvironmentId()) {
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] %s was not produced by this build of the executable (or by one on a different page size); booting normally\n", path);
+ close(fd);
+ return;
+ }
+ if (hdr.spare[5]) {
+ size_t gateOff = hdr.spare[5] & ((1ull << 40) - 1), gateLen = hdr.spare[5] >> 40;
+ char names[4096];
+ if (gateLen == 0 || gateLen > sizeof names || ipread(fd, names, gateLen, gateOff) != (ssize_t)gateLen) {
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] unreadable environment gate; booting normally\n");
+ close(fd);
+ return;
+ }
+ if (envGateHash(names, gateLen) != hdr.spare[6]) {
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE")) fprintf(stderr, "[snapshot] environment differs from the build in a gated variable; booting normally\n");
+ close(fd);
+ return;
+ }
+ }
+ if (hdr.spare[3] && hdr.spare[3] != snapshotArgvKey() && !getenv("BUN_STARTUP_SNAPSHOT_IN")) {
+ if (getenv("BUN_STARTUP_SNAPSHOT_VERBOSE")) fprintf(stderr, "[snapshot] argv differs from the build invocation; booting normally\n");
+ close(fd);
+ return;
+ }
+ {
+ uint64_t need = hdr.spare[2], have = platformCpuFeatures();
+ if (need && (have & need) != need) {
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] %s was built on a CPU with features this one lacks (%llx vs %llx); booting normally\n", path, (unsigned long long)need, (unsigned long long)have);
+ close(fd);
+ return;
+ }
+ }
+ if (hdr.textBase != platformTextBase()) {
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] ASLR slide differs (snapshot text %llx vs ours %llx); booting normally\n", (unsigned long long)hdr.textBase, (unsigned long long)platformTextBase());
+ close(fd);
+ return;
+ }
+ // Extern-library fixup table. Storage is anonymous mmap, not heap: the allocator's memory is about to be overlaid by the snapshot.
+ constexpr size_t kMaxPendingLibs = 64;
+ const char* pendingLibs[kMaxPendingLibs];
+ size_t nPendingLibs = 0;
+ bool haveFixups = false;
+ int64_t* libDelta = nullptr;
+ size_t nLibDelta = 0;
+ StartupSnapshotFixup* fixups = nullptr;
+ size_t nFixups = 0;
+ if (hdr.spare[1]) {
+ SnapshotFixupHeader fh;
+ if (ipread(fd, &fh, sizeof fh, hdr.spare[1]) == (ssize_t)sizeof fh && !memcmp(fh.magic, "BUNFIX3", 8) && fh.nlibs < 4096 && fh.nfixups < (1u << 24)) {
+ size_t bytes = (fh.nlibs * (sizeof(PlatformLib) + sizeof(int64_t)) + fh.nfixups * sizeof(StartupSnapshotFixup) + 16383) & ~16383ull;
+ uint8_t* buf = (uint8_t*)mmap(nullptr, bytes ? bytes : 16384, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
+ PlatformLib* recorded = (PlatformLib*)buf;
+ libDelta = (int64_t*)(recorded + fh.nlibs);
+ fixups = (StartupSnapshotFixup*)(libDelta + fh.nlibs);
+ nLibDelta = fh.nlibs;
+ nFixups = fh.nfixups;
+ ipread(fd, recorded, fh.nlibs * sizeof(PlatformLib), hdr.spare[1] + sizeof fh);
+ ipread(fd, fixups, fh.nfixups * sizeof(StartupSnapshotFixup), hdr.spare[1] + sizeof fh + fh.nlibs * sizeof(PlatformLib));
+ std::vector now = platformSystemLibs(); // heap use is fine up to here (before the overlay)
+ // Libraries the builder loaded during its run (dlopen: CoreFoundation/CoreServices for fs.watch, libsqlite3, …) whose
+ // initializers therefore never ran in this process. Their code is mapped either way (shared cache) and the snapshot's
+ // pointers into them are rebased below, but they must be dlopen'd here too — after the overlay, in the same
+ // position in process history the builder loaded them — so their per-process state (CF allocators, ObjC classes)
+ // exists when snapshotted code calls into them. Paths are collected now (heap is still ours), opened after the overlay.
+ for (size_t i = 0; i < fh.nlibs && nPendingLibs < kMaxPendingLibs; i++) {
+ recorded[i].path[sizeof recorded[i].path - 1] = 0;
+ if (!recorded[i].path[0]) continue;
+ bool present = false;
+ for (auto& l : now)
+ if (l.nameHash == recorded[i].nameHash) {
+ present = true;
+ break;
+ }
+ if (present) continue;
+ if (!(recorded[i].flags & 1)) { // not part of the shared cache: load it now so its segments have addresses to match against below (its initializers run here, before the overlay, exactly as they would have if it were linked)
+ bool used = false;
+ for (size_t k = 0; k < fh.nfixups; k++)
+ if (fixups[k].lib == i) {
+ used = true;
+ break;
+ }
+ if (used && dlopen(recorded[i].path, RTLD_NOW | RTLD_GLOBAL)) {
+ now = platformSystemLibs();
+ }
+ continue;
+ }
+ bool dup = false;
+ for (size_t j = 0; j < nPendingLibs; j++)
+ if (!strcmp(pendingLibs[j], recorded[i].path)) {
+ dup = true;
+ break;
+ }
+ if (!dup) pendingLibs[nPendingLibs++] = recorded[i].path; // points into `buf`, which stays mapped through the restore
+ }
+ haveFixups = true;
+ for (size_t i = 0; i < fh.nlibs; i++) {
+ libDelta[i] = 0;
+ bool found = false;
+ for (auto& l : now)
+ if (l.nameHash == recorded[i].nameHash && (l.end - l.base) == (recorded[i].end - recorded[i].base)) {
+ libDelta[i] = (int64_t)l.base - (int64_t)recorded[i].base;
+ found = true;
+ break;
+ }
+ if (!found && (recorded[i].flags & 1) && hdr.libsBase) {
+ libDelta[i] = (int64_t)platformLibsBase() - (int64_t)hdr.libsBase;
+ found = true;
+ } // not loaded here (the builder dlopen'd it); it is mapped with the cache regardless, at the cache's current slide
+ if (!found) {
+ bool used = false;
+ for (size_t k = 0; k < nFixups; k++)
+ if (fixups[k].lib == i) {
+ used = true;
+ break;
+ }
+ if (used) {
+ bool present = false;
+ for (auto& l : now)
+ if (l.nameHash == recorded[i].nameHash) {
+ present = true;
+ break;
+ }
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] system library %s (%s) the snapshot points into %s; booting normally\n", recorded[i].path, recorded[i].seg, present ? "changed size" : "could not be loaded");
+ close(fd);
+ return;
+ }
+ }
+ }
+ }
+ }
+ bool fixupsWanted = haveFixups && !(getenv("BUN_STARTUP_SNAPSHOT_LIB_FIXUPS") && !strcmp(getenv("BUN_STARTUP_SNAPSHOT_LIB_FIXUPS"), "0")); // system libraries may slide between boots (Darwin: the dyld shared cache; Linux: ASLR per exec)
+ if (fixupsWanted && hdr.spare[4] && hdr.spare[4] != platformSystemLibsId()) {
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] %s was built against a different OS build (system library contents changed); booting normally\n", path);
+ close(fd);
+ return;
+ }
+ if (!fixupsWanted && hdr.libsBase && hdr.libsBase != platformLibsBase()) {
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] %s was built against system libraries at %llx, now at %llx (reboot / OS update); booting normally\n", path, (unsigned long long)hdr.libsBase, (unsigned long long)platformLibsBase());
+ close(fd);
+ return;
+ }
+ mi_scavenger_stop(); // this process's scavenger thread must not touch allocator state while/after we overlay it
+ // No heap use from here until the overlay is done: with malloc routed to mimalloc, this process's heap sits at the same VA as the snapshot's.
+ if (hdr.nregions > 8192) {
+ fprintf(stderr, "[snapshot] too many regions\n");
+ _exit(2);
+ }
+ StartupSnapshotRegion* regionsBuf = (StartupSnapshotRegion*)mmap(nullptr, (hdr.nregions * sizeof(StartupSnapshotRegion) + 16383) & ~16383ull, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); // not heap, not __DATA: both get overlaid below
+ ipread(fd, regionsBuf, hdr.nregions * sizeof(StartupSnapshotRegion), sizeof(StartupSnapshotHeader));
+ std::span regions(regionsBuf, hdr.nregions);
+ { // this process's own (pre-overlay) allocator must not place anything where the snapshot goes: push mimalloc's hint pointer above the snapshot
+ uint64_t top = 0;
+ for (auto& r : regions)
+ if (r.addr >= 0x20000000000ull && r.addr < 0x2e0000000000ull) top = std::max(top, r.addr + r.len);
+ if (top) mi_os_hint_floor((void*)(top + (1ull << 30)));
+ }
+ const off_t savedBaseOff = snapshotBaseOff; // snapshotBaseOff lives in __DATA, which the overlay below rewrites with the builder's value
+ BunLaunchContext launch;
+ bun_launch_context_capture(&launch); // this process's raw argc/argv (our statics get the builder's below)
+ uint64_t hintFloorAfterOverlay = 0;
+ {
+ uint64_t top = 0;
+ for (auto& r : regions)
+ if (r.addr >= 0x20000000000ull && r.addr < 0x2e0000000000ull) top = std::max(top, r.addr + r.len);
+ hintFloorAfterOverlay = (top ? top : 0x20000000000ull) + (1ull << 30);
+ }
+ size_t mapped = 0, copied = 0;
+ struct DataSeg {
+ uint64_t* dst;
+ const uint64_t* src;
+ size_t words;
+ };
+ DataSeg dataSegs[16];
+ size_t nDataSegs = 0; // no heap here: the allocator's state is being overlaid
+ bool useLibFixups = fixupsWanted;
+ uint64_t linkerRanges[96][2];
+ size_t nLinkerRanges = platformLinkerOwnedRanges(linkerRanges, 96);
+ const bool deferDataCopy = useLibFixups || nLinkerRanges > 0; // words this process owns inside our data segments are skipped by the deferred copy
+ bool verbose = !!getenv("BUN_STARTUP_SNAPSHOT_VERBOSE");
+ for (auto& r : regions) {
+ if (verbose) {
+ fprintf(stderr, "[snapshot] restoring %llx+%llx kind=%llu tag=%llu\n", r.addr, r.len, r.kind & 0xff, r.kind >> 8);
+ }
+ if ((r.kind & 0xff) == 3) {
+ void* m = mmap((void*)r.addr, r.len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANON | MAP_JIT, -1, 0); // MAP_JIT|MAP_FIXED is EINVAL; rely on the hint
+ if (m != (void*)r.addr) {
+ fprintf(stderr, "[snapshot] mmap JIT %llx+%llx landed at %p errno %d\n", r.addr, r.len, m, errno);
+ _exit(3);
+ }
+ continue;
+ }
+ if ((r.kind & 0xff) == 2) {
+ void* buf = mmap(nullptr, r.len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
+ if (ipread(fd, buf, r.len, r.fileOff) != (ssize_t)r.len) {
+ fprintf(stderr, "[snapshot] pread JIT failed errno %d\n", errno);
+ _exit(3);
+ }
+ platformWriteJIT((void*)r.addr, buf, r.len);
+ munmap(buf, r.len);
+ copied += r.len;
+ continue;
+ }
+ if ((r.kind & 0xff) == 4) {
+ munmap((void*)r.addr, r.len);
+ void* m = mmap((void*)r.addr, r.len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0);
+ if (m == MAP_FAILED) {
+ fprintf(stderr, "[snapshot] mmap reserve %llx+%llx failed errno %d\n", r.addr, r.len, errno);
+ _exit(3);
+ }
+ continue;
+ }
+ if ((r.kind & 0xff) == 1) {
+ // __DATA is copied last: from then until the extern-library fixups the GOT is the builder's, so nothing in between may call into libc.
+ if (mprotect((void*)r.addr, r.len, PROT_READ | PROT_WRITE)) {
+ fprintf(stderr, "[snapshot] mprotect __DATA %llx failed errno %d\n", r.addr, errno);
+ _exit(3);
+ }
+ if (!deferDataCopy) { // copy in place now (nothing to skip or rebase)
+ if (ipread(fd, (void*)r.addr, r.len, r.fileOff) != (ssize_t)r.len) {
+ fprintf(stderr, "[snapshot] pread __DATA failed errno %d\n", errno);
+ _exit(3);
+ }
+ snapshotBaseOff = savedBaseOff; // just overwritten along with the rest of our __DATA
+ mi_os_hint_floor((void*)hintFloorAfterOverlay); // the builder's allocator hint pointer just arrived with __DATA; keep fresh OS memory above the snapshot
+ copied += r.len;
+ continue;
+ }
+ void* scratch = mmap(nullptr, r.len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
+ if (scratch == MAP_FAILED || ipread(fd, scratch, r.len, r.fileOff) != (ssize_t)r.len) {
+ fprintf(stderr, "[snapshot] pread __DATA failed errno %d\n", errno);
+ _exit(3);
+ }
+ if (nDataSegs < 16) dataSegs[nDataSegs++] = { (uint64_t*)r.addr, (const uint64_t*)scratch, r.len / 8 };
+ copied += r.len;
+ continue;
+ } else {
+ void* m = immap((void*)r.addr, r.len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED, fd, r.fileOff);
+ if (m == MAP_FAILED) {
+ // e.g. a reservation with restrictive max_prot already sits there: deallocate the range and retry
+ int e1 = errno;
+ munmap((void*)r.addr, r.len);
+ m = immap((void*)r.addr, r.len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED, fd, r.fileOff);
+ if (m == MAP_FAILED) {
+ fprintf(stderr, "[snapshot] mmap %llx+%llx (tag %llu) failed errno %d then %d — skipping\n", r.addr, r.len, r.kind >> 8, e1, errno);
+ continue;
+ }
+ }
+ mapped += r.len;
+ }
+ }
+ // Re-seat allocator TLS: this thread's default theap must be the snapshot's main theap, not whatever this process created before the overlay.
+ { // libc-free critical section: overwrite our data segments with the builder's, then rebase extern-library pointers. Plain loops only (no PLT calls).
+ // Process-owned libc globals that live in *our* data segment (copy relocations in a non-PIE executable): keep this process's values.
+ char** volatile* environSlot = (char** volatile*)&environ;
+ char** savedEnviron = *environSlot; // volatile: the overlay below rewrites it behind the compiler's back
+ for (size_t di = 0; di < nDataSegs; di++) {
+ DataSeg& d = dataSegs[di];
+ volatile uint64_t* dst = d.dst;
+ const uint64_t* src = d.src;
+ for (size_t k = 0; k < d.words; k++) {
+ uint64_t a = (uint64_t)(d.dst + k);
+ bool linkerOwned = false;
+ for (size_t q = 0; q < nLinkerRanges; q++)
+ if (a >= linkerRanges[q][0] && a < linkerRanges[q][1]) {
+ linkerOwned = true;
+ break;
+ }
+ if (!linkerOwned) dst[k] = src[k];
+ }
+ }
+ if (useLibFixups)
+ for (size_t k = 0; k < nFixups; k++) {
+ StartupSnapshotFixup& f = fixups[k];
+ bool linkerOwned = false;
+ for (size_t q = 0; q < nLinkerRanges; q++)
+ if (f.addr >= linkerRanges[q][0] && f.addr < linkerRanges[q][1]) {
+ linkerOwned = true;
+ break;
+ }
+ if (!linkerOwned && f.lib < nLibDelta && libDelta[f.lib]) *(volatile uint64_t*)f.addr += libDelta[f.lib];
+ }
+ if (deferDataCopy) *environSlot = savedEnviron;
+ *(volatile off_t*)&snapshotBaseOff = savedBaseOff;
+ mi_os_hint_floor((void*)hintFloorAfterOverlay);
+ }
+ for (size_t di = 0; di < nDataSegs; di++)
+ munmap((void*)dataSegs[di].src, dataSegs[di].words * 8);
+ bun_launch_context_restore(&launch); // everything derived from it (process.argv, Bun.argv, …) is ProcessDerived and recomputes this epoch
+ if (hdr.reserved[0]) {
+ mi_theap_set_default((mi_theap_t*)hdr.reserved[0]);
+ mi_theap_adopt_current_thread((mi_theap_t*)hdr.reserved[0]); // the fresh heap below binds to this thread state; it must name this thread
+ }
+ _mi_scavenger_forked_child(); // same situation as a fork child: the snapshot says a scavenger runs, but no such thread exists here
+ mi_prof_reinit_lock(); // and any allocator-internal lock a build-process thread was holding is nobody's now
+ { // park /dev/null on every fd number the snapshot thinks it owns (the snapshot file fd itself gets moved out of the way first)
+ int hi = 1023;
+ while (hi > 2 && !(s_snapshotOpenFds[hi / 64] & (1ull << (hi % 64))))
+ hi--;
+ if (fd <= hi) {
+ int moved = fcntl(fd, F_DUPFD_CLOEXEC, hi + 1);
+ if (moved >= 0) {
+ close(fd);
+ fd = moved;
+ }
+ }
+ for (int i = 0; i < s_snapshotFileFdCount; i++) {
+ SnapshotFileFd& f = s_snapshotFileFds[i];
+ if (fcntl(f.fd, F_GETFD) != -1) continue;
+ int nfd = open(f.path, (f.flags & ~(O_CREAT | O_TRUNC | O_EXCL)) | O_APPEND | O_CLOEXEC);
+ if (nfd < 0) continue;
+ if (nfd != f.fd) {
+ dup2(nfd, f.fd);
+ close(nfd);
+ }
+ if (verbose) fprintf(stderr, "[snapshot] reopened log fd %d -> %s\n", f.fd, f.path);
+ }
+ int devnull = open("/dev/null", O_RDWR | O_CLOEXEC);
+ int parked = 0;
+ for (int k = 3; k <= hi; k++)
+ if ((s_snapshotOpenFds[k / 64] & (1ull << (k % 64))) && fcntl(k, F_GETFD) == -1 && dup2(devnull, k) == k) parked++;
+ if (devnull > hi) close(devnull);
+ if (verbose) fprintf(stderr, "[snapshot] parked /dev/null on %d stale fd numbers (max %d)\n", parked, hi);
+ }
+ bun_refresh_stdio_after_snapshot_restore(); // this launch's terminal state becomes what exit restores, captured before the builder's mode goes on below
+ if (s_snapshotTermiosFd >= 0 && isatty(s_snapshotTermiosFd)) {
+ tcsetattr(s_snapshotTermiosFd, TCSANOW, &s_snapshotTermios);
+ if (s_snapshotTermiosFd < 3)
+ bun_stdio_modified[s_snapshotTermiosFd] = 1; // so exit puts the shell's state back even though this process never called setRawMode
+ } // raw mode etc. as the build process left it
+ for (int i = 2; i < 7 && hdr.reserved[i]; i++) { // recreate TTY fds at their old numbers from our own stdio
+ int fd, fl, src;
+ ttyFdRecordUnpack(hdr.reserved[i], fd, fl, src);
+ if (isatty(src) && dup2(src, fd) == fd) {
+ if (fl & O_NONBLOCK) fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
+ if (verbose) fprintf(stderr, "[snapshot] dup2(%d, %d) flags %x\n", src, fd, fl);
+ }
+ }
+ setvbuf(stderr, nullptr, _IONBF, 0);
+ setvbuf(stdout, nullptr, _IOLBF, 0); // stdio buffering mode was decided in the builder (whose fds may have been files)
+ { // Snapshot payload pages are immortal: never free into them (that would dirty a clean file-backed page for allocator metadata); allocate from fresh pages.
+ snapshotFd = fd; // stays open: reclean remaps pristine pages from it (and the tooling diffs against it)
+ { // Watchpoint.cpp asks whether an object is snapshotted; the snapshotted allocator arenas are one contiguous span
+ uintptr_t lo = UINTPTR_MAX, hi = 0;
+ for (auto& r : regions)
+ if ((r.kind & 0xff) == 0 && (r.kind >> 8) == 240) {
+ lo = std::min(lo, r.addr);
+ hi = std::max(hi, r.addr + r.len);
+ }
+ if (hi > lo) {
+ JSC::Heap::s_snapshotImmortalRangeLo = lo;
+ JSC::Heap::s_snapshotImmortalRangeSpan = hi - lo;
+ }
+ }
+ if (hdr.reserved[0]) mi_theap_freeze((mi_theap_t*)hdr.reserved[0]);
+ mi_arenas_seal_existing(); // every arena that exists now is snapshot memory: nobody (any thread) allocates into its free space again
+ uint64_t snapshottedTop = 0; // the overlay brought the builder's hint pointer (or none): fresh OS memory goes above everything snapshotted
+ for (auto& r : regions)
+ if (r.addr >= 0x20000000000ull && r.addr < 0x2e0000000000ull) snapshottedTop = std::max(snapshottedTop, r.addr + r.len);
+ if (snapshottedTop) mi_os_hint_floor((void*)(snapshottedTop + (1ull << 30)));
+ mi_heap_t* fresh = nullptr;
+#if OS(DARWIN)
+ { // This process's allocations get their own arena, placed explicitly 1GiB above the snapshot rather than wherever the allocator's
+ // (just overlaid) hint state would put them. Linux relies on the sealed arenas + hint floor until exclusive-arena binding is sorted out.
+ mi_arena_id_t freshArena = 0;
+ void* want = (void*)((snapshottedTop + (1ull << 30)) & ~((1ull << 30) - 1));
+ size_t sz = 1ull << 30;
+ void* got = mmap(want, sz, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0);
+ if (got != MAP_FAILED && got != want) {
+ munmap(got, sz);
+ got = MAP_FAILED;
+ }
+ if (got != MAP_FAILED && mi_manage_os_memory_ex(got, sz, /*committed*/ false, /*large*/ false, /*zero*/ true, /*numa*/ -1, /*exclusive*/ true, &freshArena))
+ fresh = mi_heap_new_in_arena(freshArena);
+ else if (mi_reserve_os_memory_ex(sz, false, false, true, &freshArena) == 0)
+ fresh = mi_heap_new_in_arena(freshArena);
+ mi_os_hint_floor((void*)((uintptr_t)want + 2 * sz));
+ }
+#endif
+ if (!fresh)
+ fresh = mi_heap_new();
+ freshHeap = fresh;
+ mi_theap_set_default(mi_heap_theap(fresh));
+ // Only now may anything allocate: these live in the fresh heap, not in the (frozen) snapshot pages they describe.
+ frozenRanges.clear();
+ snapshotRuns.clear();
+ for (auto& r : regions)
+ if ((r.kind & 0xff) == 0) {
+ frozenRanges.push_back({ r.addr, r.addr + r.len });
+ snapshotRuns.push_back({ (uintptr_t)r.addr, (size_t)r.len, (size_t)r.fileOff });
+ }
+ std::sort(frozenRanges.begin(), frozenRanges.end());
+ std::sort(snapshotRuns.begin(), snapshotRuns.end(), [](const FrozenRun& x, const FrozenRun& y) { return x.start < y.start; });
+ {
+ void* probe = mi_malloc(64);
+ if (verbose) fprintf(stderr, "[snapshot] fresh heap: own arena=%d probe=%p\n", (int)(fresh != nullptr), probe);
+ if (JSC::Heap::isInSnapshotImmortalRange(probe)) { // cannot happen with the floor above; if it ever does, misclassifying new objects as snapshotted would be worse than the rule it serves
+ fprintf(stderr, "[snapshot] fresh heap overlaps the snapshot span; not tracking snapshot objects\n");
+ JSC::Heap::s_snapshotImmortalRangeSpan = 0;
+ }
+ mi_free(probe);
+ }
+ }
+ // The scavenger thread died with the build process; without it nothing sweeps parked heaps and frees stay resident — restart it as a fork child would.
+ _mi_scavenger_start_if_forked();
+ startupSnapshotToolingAfterRestore();
+ startupSnapshotToolingArmTraps();
+ // pthread TLS keys created by the build process (WTF::ThreadSpecific etc.) must exist here too, or setspecific silently fails; burn keys up to the snapshot's high-water mark. The burned keys have no destructors: a thread that exits after storing into one leaks that value (accepted; the main thread never exits).
+ if (hdr.reserved[1]) {
+ for (int i = 0; i < 1024; i++) {
+ pthread_key_t k = 0;
+ if (pthread_key_create(&k, nullptr)) break;
+ if ((uint64_t)k + 1 >= hdr.reserved[1]) break;
+ }
+ }
+ for (size_t i = 0; i < nPendingLibs; i++) { // see the note where these were collected
+ if (!dlopen(pendingLibs[i], RTLD_NOW | RTLD_GLOBAL))
+ fprintf(stderr, "[snapshot] warning: could not load %s, which the snapshot uses: %s\n", pendingLibs[i], dlerror());
+ else if (verbose)
+ fprintf(stderr, "[snapshot] loaded %s (the builder had it loaded)\n", pendingLibs[i]);
+ }
+ if (useLibFixups && verbose) fprintf(stderr, "[snapshot] rebased %zu extern-library pointers\n", nFixups);
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] restored %zu regions: %.1fMB mapped clean, %.1fMB __DATA copied\n", regions.size(), mapped / 1048576.0, copied / 1048576.0);
+ snapshotTimingMark("regions mapped and library pointers rebased");
+ // From here on all globals/heap are the build process's. Adopt the snapshot's main Thread object for this OS thread.
+ WTF::Thread* mainThread = (WTF::Thread*)hdr.mainThread;
+ mainThread->adoptCurrentThreadForStartupSnapshot();
+ JSC::VM* vm = (JSC::VM*)hdr.vm;
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] thread: snapshot main=%p currentSingleton=%p currentMayBeNull=%p apiLock owner=%p held=%d\n", mainThread, &WTF::Thread::currentSingleton(), WTF::Thread::currentMayBeNull(), vm->apiLock().ownerThread() ? vm->apiLock().ownerThread()->get() : nullptr, (int)vm->apiLock().currentThreadIsHoldingLock());
+ JSC::JSGlobalObject* globalObject = (JSC::JSGlobalObject*)hdr.globalObject;
+ uws_adopt_loop_for_current_thread((struct us_loop_t*)hdr.reserved[8 - 1]); // main thread's uWS::Loop TLS -> the snapshot's loop object (else uws_get_loop() would make a second loop)
+ us_loop_reinit_for_snapshot(uws_get_loop());
+ __atomic_add_fetch(&bun_snapshot_epoch, 1, __ATOMIC_ACQ_REL);
+ snapshotReprobeCPUDispatch();
+ Bun__startupSnapshotAdoptMainThreadVM();
+ JSC::JSLockHolder restoreLock(*vm); // held until 'restore' has been emitted: releasing a JSLock drains microtasks, and snapshotted continuations must not run before the app hears about the restore
+ {
+ JSC::JSLockHolder lock(*vm);
+ vm->didRestoreFromStartupSnapshot();
+ if (snapshotVerbose()) fprintf(stderr, "[snapshot] termination state: request=%d pendingTermException=%d exception=%p trapsNeedTermination=%d\n", (int)vm->hasTerminationRequest(), (int)vm->hasPendingTerminationException(), vm->exceptionForInspection(), (int)vm->traps().needHandling(JSC::VMTraps::NeedTermination));
+ if (vm->hasPendingTerminationException() || vm->hasTerminationRequest()) {
+ vm->clearHasTerminationRequest();
+ {
+ auto scope = DECLARE_TOP_EXCEPTION_SCOPE(*vm);
+ scope.clearException();
+ }
+ vm->traps().clearTrap(JSC::VMTraps::NeedTermination);
+ if (verbose) fprintf(stderr, "[snapshot] cleared stale termination state\n");
+ }
+ }
+
+ {
+ JSC::JSLockHolder lock(*vm);
+ NakedPtr exception;
+ globalObject->weakRandom().setSeed(WTF::cryptographicallyRandomNumber()); // Math.random's stream came from the builder
+ // chdir('.') refreshes libc's cached cwd; after the app's post-restore burst settles, one full GC plus a reclean hands back what it only touched transiently.
+ JSC::evaluate(globalObject, JSC::makeSource("try { process.chdir('.'); } catch {} process.emit('restore');"_s, JSC::SourceOrigin {}, JSC::SourceTaintedOrigin::Untainted), JSC::JSValue(), exception);
+ if (exception) { // reported before main() runs: main() may end the process, and a listener's failure is the likelier cause of main()'s
+ fprintf(stderr, "[snapshot] a 'restore' listener threw: %s\n", exception->value().toWTFString(globalObject).utf8().data());
+ exception = nullptr;
+ }
+ JSC::evaluate(globalObject, JSC::makeSource("setTimeout(() => { Bun.gc(true); Bun.startupSnapshot.reclean(); }, 2000).unref();"_s, JSC::SourceOrigin {}, JSC::SourceTaintedOrigin::Untainted), JSC::JSValue(), exception);
+ Bun__startupSnapshotRunMain(globalObject); // the program registered with Bun.startupSnapshot.main(), if any
+ }
+ snapshotTimingMark("runtime refreshed, 'restore' emitted and main() run; entering the event loop");
+ Bun__startupSnapshotContinueEventLoop(); // never returns
+#endif
+}
+
+#else // !BUN_STARTUP_SNAPSHOT_SUPPORTED
+
+#include
+#include
+namespace JSC {
+class VM;
+}
+extern "C" int bun_is_compiled_executable(void);
+extern "C" bool Bun__isCompiledExecutable() { return bun_is_compiled_executable(); }
+extern "C" bool Bun__startupSnapshotMode() { return false; }
+extern "C" bool Bun__startupSnapshotActive() { return false; }
+extern "C" bool Bun__startupSnapshotSupported() { return false; }
+extern "C" void Bun__startupSnapshotMaybeRestore() {}
+extern "C" void Bun__startupSnapshotInit()
+{
+ if (getenv("BUN_STARTUP_SNAPSHOT_OUT")) {
+ fprintf(stderr, "error: %s\n", "startup snapshots are not available in this build of bun (macOS with mimalloc as the process allocator, and glibc Linux)");
+ exit(1);
+ }
+}
+extern "C" void Bun__startupSnapshotSetEnvGate(const uint8_t*, size_t) {}
+extern "C" void Bun__startupSnapshotRecleanPages(JSC::VM*) {}
+extern "C" void Bun__VM__refreshStackBoundsAfterSnapshotRestore(JSC::VM*) {}
+extern "C" void Bun__startupSnapshotUnwindJS(JSC::VM*) {}
+extern "C" void Bun__startupSnapshotClearTerminationRequest(JSC::VM*) {}
+extern "C" bool Bun__startupSnapshotDumpNow(JSC::VM*, const char*)
+{
+ fprintf(stderr, "error: snapshots are not supported on this platform\n");
+ exit(1);
+}
+
+#endif // BUN_STARTUP_SNAPSHOT_SUPPORTED
+#pragma clang diagnostic pop
diff --git a/src/jsc/bindings/StartupSnapshot.h b/src/jsc/bindings/StartupSnapshot.h
new file mode 100644
index 000000000000..5b2b5de8f0f2
--- /dev/null
+++ b/src/jsc/bindings/StartupSnapshot.h
@@ -0,0 +1,65 @@
+#pragma once
+// Snapshots: StartupSnapshot.cpp builds and restores them; StartupSnapshotTooling.cpp holds the attribution commands used while
+// putting an application on a diet (dirty-page maps, censuses, write traps), compiled only with -DBUN_STARTUP_SNAPSHOT_TOOLING=1.
+#include "root.h"
+
+#ifndef BUN_STARTUP_SNAPSHOT_TOOLING
+#define BUN_STARTUP_SNAPSHOT_TOOLING 0
+#endif
+
+#if defined(__has_feature)
+#if __has_feature(address_sanitizer)
+#define BUN_STARTUP_SNAPSHOT_ASAN 1
+#endif
+#endif
+#if defined(__SANITIZE_ADDRESS__)
+#define BUN_STARTUP_SNAPSHOT_ASAN 1
+#endif
+// ASAN owns the fixed address ranges the snapshot heap and JIT pool are placed in. Linux support is glibc for now: the
+// musl build crashes while writing the snapshot and has not been debugged yet.
+#if (OS(DARWIN) || (OS(LINUX) && defined(__GLIBC__))) && !defined(BUN_STARTUP_SNAPSHOT_ASAN)
+#define BUN_STARTUP_SNAPSHOT_SUPPORTED 1
+#else
+#define BUN_STARTUP_SNAPSHOT_SUPPORTED 0
+#endif
+
+namespace JSC {
+class VM;
+}
+
+#if BUN_STARTUP_SNAPSHOT_SUPPORTED
+#include
+#include
+#include
+struct mi_heap_s;
+
+namespace Bun::StartupSnapshot {
+struct FrozenRun {
+ uintptr_t start;
+ size_t len;
+ size_t fileOff;
+};
+// State of the restored snapshot (empty in a process that did not restore one).
+extern std::vector> frozenRanges; // sorted [start, end)
+extern std::vector snapshotRuns; // the same ranges with their file offsets, sorted by address
+extern int snapshotFd; // the snapshot file, kept open so pages can be compared with / remapped from it
+extern ::mi_heap_s* freshHeap; // where this process allocates after a restore (null before one, or if the general path was used)
+extern off_t snapshotBaseOff; // where the snapshot starts inside snapshotFd (non-zero when it is embedded in the executable)
+ssize_t ipread(int fd, void* buf, size_t n, off_t off);
+void* immap(void* addr, size_t len, int prot, int flags, int fd, off_t off);
+void recleanFrozenPages(JSC::VM&);
+}
+#endif
+
+#if BUN_STARTUP_SNAPSHOT_TOOLING
+void startupSnapshotToolingInstall();
+void startupSnapshotToolingIndexAtFreeze(JSC::VM&, size_t pageSize);
+void startupSnapshotToolingArmTraps();
+void startupSnapshotToolingAfterRestore();
+extern "C" void Bun__startupSnapshotToolingTick(JSC::VM*);
+#else
+inline void startupSnapshotToolingInstall() {}
+inline void startupSnapshotToolingIndexAtFreeze(JSC::VM&, size_t) {}
+inline void startupSnapshotToolingArmTraps() {}
+inline void startupSnapshotToolingAfterRestore() {}
+#endif
diff --git a/src/jsc/bindings/StartupSnapshotTooling.cpp b/src/jsc/bindings/StartupSnapshotTooling.cpp
new file mode 100644
index 000000000000..6c4f2c25dfe1
--- /dev/null
+++ b/src/jsc/bindings/StartupSnapshotTooling.cpp
@@ -0,0 +1,1778 @@
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wformat" // uint64_t is unsigned long on Linux, unsigned long long on Darwin; this file prints a lot of addresses
+#include "root.h"
+#include "StartupSnapshot.h"
+#if BUN_STARTUP_SNAPSHOT_TOOLING && BUN_STARTUP_SNAPSHOT_SUPPORTED
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include