1# Subsecond hot-patching: what `dx` does, and how to reimplement it under buck2 2 3Source: `~/src/github.com/DioxusLabs/dioxus` at tag `v0.7.10` (commit 4`57d6794ad60b949e5bd8aa282f6f8c3dc97a365e`), a blobless clone (`git show` 5fetches blobs on demand). All cited files are dual `MIT OR Apache-2.0` 6(`packages/subsecond/subsecond/Cargo.toml:7`, 7`packages/subsecond/subsecond-types/Cargo.toml:6`, 8`packages/cli/Cargo.toml:8`, `packages/devtools/Cargo.toml:6`, 9`packages/devtools-types/Cargo.toml:6`; repo root carries `LICENSE-MIT` + 10`LICENSE-APACHE`), so copying code/logic from them and keeping the licence 11notice is fine. 12 13Scope note: `subsecond-cli`/a standalone harness binary does **not exist** in 14this checkout — I grepped `git ls-tree -r --name-only HEAD | grep subsecond` 15and the only subsecond dirs are `subsecond/`, `subsecond-types/`, and 16`subsecond-tests/{cross-tls-crate,cross-tls-crate-dylib,cross-tls-test}` 17(cross-TLS regression fixtures, not a build-tool example). All "how does the 18patch get built" logic lives in `packages/cli/src/build/{request.rs,link.rs, 19patch.rs}` and `packages/cli/src/{rustcwrapper.rs,cli/link.rs}`, not in a 20separate crate. 21 22## The mechanism dx uses to see rustc/linker invocations at all 23 24`dx` never parses `cargo`'s own progress output for build flags — it makes 25**itself** both the rustc wrapper and the linker, for every workspace-member 26crate, on **every** build (fat or thin): 27 28- `request.rs:1646`: `cmd.env("RUSTC_WORKSPACE_WRAPPER", Workspace::path_to_dx()?);` 29 — this wraps only workspace-member crates (not external deps), letting cargo 30 invoke `dx rustc <real-args>` for each one. 31- `request.rs:1637-1641`: `DX_RUSTC_WRAPPER_ENV_VAR` (`"DX_RUSTC"`, defined 32 `rustcwrapper.rs:36`) points at a per-build-mode scope directory where 33 captured args get written. 34- `rustcwrapper.rs:46-48`: `is_wrapping_rustc()` just checks that env var is set. 35- `rustcwrapper.rs:53-94` (`run_rustc`): captures `args()` + `vars()` into a 36 `RustcArgs{args, envs}`, **always** writes it to 37 `{crate_name}.{lib|bin}.json` (`write_rustc_args`, `rustcwrapper.rs:96-136`) 38 even for link steps ("the tip crate's bin target is typically only observed 39 during the final link invocation" — comment at `link.rs:68`), then either 40 delegates to the linker-interception path (`has_linking_args()`, 41 `rustcwrapper.rs:138-170`, which checks for a `.o` arg or `-flavor`, including 42 inside `@responsefile` command files) or actually execs `rustc` with the 43 captured args/envs (`rustcwrapper.rs:79-93`). 44- Simultaneously `dx` sets itself as the **linker** too 45 (`cargo_build_arguments`, `request.rs:1748-1753`: 46 `-Clinker={path to dx}`, gated on `BuildMode::Thin | BuildMode::Fat`), and 47 `LinkAction::write_env_vars` (`cli/link.rs:85-109`) exports `DX_LINK=1`, 48 `DX_LINK_ARGS_FILE`, `DX_LINK_ERR_FILE`, `DX_LINK_TRIPLE`, optionally 49 `DX_LINK_CUSTOM_LINKER`. 50- `LinkAction::run_link_inner` (`cli/link.rs:131-242`) is "NoLink": it writes 51 the linker's argv to `DX_LINK_ARGS_FILE` and then, if no real linker was 52 given, **does not link at all** — it writes a dummy empty object file 53 (`object::write::Object::new(...).write()`) at the expected `-o`/`/OUT:` 54 path "to satisfy rustc's use of llvm-objcopy" (comment `cli/link.rs:191-196`). 55 The doc comment on `LinkAction` (`cli/link.rs:7-28`) is explicit about why: 56 rustc has no supported way to stop short of a full link, so this is a hack 57 to get the *arguments* rustc would have used without paying for (or 58 trusting) a real link. 59 60This two-pronged interception (`RUSTC_WORKSPACE_WRAPPER` + `-Clinker=dx`) is 61how `dx` gets a reliable, per-crate, per-platform record of every rustc 62invocation's exact args/envs **and** the exact final link line, without 63depending on cargo's own diagnostic JSON (comment `link.rs:1-16` explains the 64alternative — parsing cargo's printed link args — was rejected as unreliable 65across platforms, esp. Windows response files). 66 67## 1. The "fat" (initial) build 68 69Fat is a normal `cargo rustc` invocation (`request.rs:1613-1649`, 70`cargo_build_command`), with these additions layered on top of the user's own 71profile/rustflags: 72 73- **`-Csave-temps=true`** and **`-Clink-dead-code`** 74 (`request.rs:1799-1801`), added whenever `build_mode` is `Thin` or `Fat` 75 (`request.rs:1795`). Comment: "link-dead-code: prevents rust from passing 76 `-dead_strip` to the linker since that's the default" / "save-temps=true: 77 keeps the incremental object files around, which we need for manually 78 linking" (`request.rs:1796-1798`). These are captured by the rustc wrapper 79 and so get **replayed automatically** on every future thin build too 80 (comment `request.rs:1792-1794`). 81- **`-Clinker={dx}`** (`request.rs:1748-1753`) — always for `Thin`/`Fat`, so 82 the "NoLink" capture above fires. 83- Platform rpath flags (`-Wl,-rpath,$ORIGIN` etc., `request.rs:1775-1778` for 84 Linux) and, for wasm only, PIC/export flags (`request.rs:1852-1866`) — not 85 relevant to native Linux. 86- **No `-Zunstable-options`, no nightly requirement, and no explicit 87 `-Crelocation-model=pic` on native (non-wasm) targets.** Proof of absence: 88 `grep -n "relocation-model" packages/cli/src/build/*.rs` returns exactly two 89 hits, both gated on wasm/wasi (`request.rs:1594-1596`, 90 `link.rs:573-575`, `link.rs:358-363` doc comment "I think we can make 91 relocation-model=pic work for non-wasm platforms" — i.e. **not done today** 92 for native). Nightly is used **only** for an unrelated unit-count estimate 93 (`cargo +nightly unit-graph`, `request.rs:2349-2360`), not for hotpatch 94 compilation; `grep -rn nightly` over `packages/cli/src` and 95 `packages/subsecond` turns up nothing else load-bearing. 96- No `--export-dynamic`/`-rdynamic` blanket flag on Linux at the `cargo_args` 97 level; instead the **fat link step** (next section) surgically exports just 98 `main` via `-Wl,--export-dynamic-symbol,main` (`link.rs:995`). 99- Debug-assertions/opt-level are **not forced** by dx — they come from 100 whatever cargo profile the user picked. But `subsecond::call`/`try_call` 101 are `cfg!(debug_assertions)`-gated no-ops in release 102 (`subsecond/src/lib.rs:250-254`, `:411-414`), so hot-patching is a no-op 103 unless the profile has `debug-assertions = true` (dev profile, by default). 104 105**What's captured, and by what:** the rustc-wrapper writes one JSON per 106workspace crate (`{crate}.lib.json` / `{crate}.bin.json`) containing 107`{args: Vec<String>, envs: Vec<(String,String)>}` (`rustcwrapper.rs:26-30`); 108the linker-wrapper writes the raw final link argv, one arg per line, to 109`link_args_file` (`cli/link.rs:145`). Both are read back after the `cargo 110build` child process exits: `workspace_rustc_args = self.load_rustc_argset()` 111(`request.rs:1152`) assembles a `WorkspaceRustcArgs{link_args, rustc_args: 112HashMap<String,RustcArgs>}` (`rustcwrapper.rs:11-24`) from the on-disk JSONs 113plus the plain-text link-args file. 114 115For **Fat** specifically, `run_fat_link` (`link.rs:788-1142`) is then invoked 116(`request.rs:1157-1160`) to do the *real* link, because the wrapper only wrote 117a dummy object file: 118 119- Takes the tip crate's captured `.bin.json` args + the captured `link_args`. 120- Filters `link_args` down to the `.rlib` entries (`link.rs:809-815`). 121- Builds (and caches, keyed on rlib name+size+mtime+dx's own git commit hash, 122 `link.rs:817-849`) a **"fat archive"**: an `ar` archive 123 (`libdeps-{hash}.a`) containing every `.rcgu.o`/`.obj` member pulled out of 124 every workspace `.rlib` (external/toolchain rlibs are kept as normal `.rlib` 125 refs instead, `link.rs:865-922` — "Skip compiler rlibs since they're 126 missing bitcode"). 127- Splices that archive into the original link command with 128 `-Wl,--whole-archive … -Wl,--no-whole-archive` on the `Gnu` flavor 129 (`link.rs:957-965`) — i.e. it forces every object, not just what the linker 130 would keep, into the final binary, so those symbols exist to be jumped back 131 to later. 132- Appends `-Wl,--export-dynamic-symbol,main` (Gnu/Linux, `link.rs:993-996`; 133 `_main`+`-Wl,-exported_symbol,_main` on Darwin, `/EXPORT:main` on MSVC) — 134 this is **the only blanket "export a symbol" flag on Linux**, and it's 135 `main` specifically, used purely as the ASLR reference point (see §3), not 136 a general `--export-dynamic`. 137- Runs the real linker (`select_linker`, `link.rs:1240-1277`: `cc` on 138 Gnu/Linux) directly (`link.rs:1084-1089`), i.e. dx invokes `cc` itself here, 139 bypassing rustc entirely for this final link. 140 141## 2. The "thin" (patch) build 142 143Trigger: `BuildMode::Thin{ modified_crates, workspace_rustc_args, aslr_reference, cache, .. }` 144(`compile_workspace_hotpatch`, `link.rs:132-335`). 145 146**Which crates get recompiled:** every crate in the *cumulative* (since the 147last fat build) `modified_crates` set gets replayed — not just the tip. 148`AppBuilder::patch_rebuild` (`builder.rs:364-456`) BFS-walks 149`workspace_dependents_of` from the changed files' crates to add the cascade 150(comment `builder.rs:353-358`: "editing a leaf crate forces parent crates' 151generic instantiations to change too"), then keeps that set forever until the 152next full/fat rebuild (`builder.rs:466-467`: "A full rebuild resets all 153accumulated hotpatch state"). This **contradicts** the subsecond crate's own 154doc comment ("Subsecond currently only patches the 'tip' crate… We plan to 155add full workspace support in the future," `subsecond/src/lib.rs:67-75`) — 156that doc appears stale relative to the CLI's actual v0.7.10 implementation. 157UNVERIFIED: whether this workspace-replay path is fully reliable for 158arbitrary dependency-crate changes (the subsecond doc's caveats about 159non-deterministic build graphs and generic-forwarding cascades may still 160apply in edge cases) — I found no test exercising a multi-crate thin build 161in this checkout. 162 163**How non-tip crates are recompiled:** `workspace_hotpatch_replay_order` 164(`link.rs:612-662`, Kahn's-algorithm topological sort over the *modified* 165subgraph only) determines order; for each, `workspace_hotpatch_replay_args` 166(`link.rs:586-603`) looks up its **original captured `.lib.json`** args, and 167`compile_dep_crate` (`link.rs:518-584`) runs `rustc` **directly** (not 168`cargo`) with those exact args verbatim (env-cleared, replaying the captured 169`envs`, stripping `-Clinker=…` so it doesn't recurse into dx's link 170interception, and adding `-Crelocation-model=pic` only for wasm/wasi). This 171overwrites the crate's `.rlib` **in place** at its original path 172(`link.rs:46-47` doc comment). 173 174**The tip crate** is then rebuilt via the normal `cargo_build` path 175(`link.rs:161`, i.e. `cargo rustc` again) but under `BuildMode::Thin`, which 176routes `cargo_build_command` into the *other* branch (`request.rs:1571-1601`): 177runs `rustc` directly (not `cargo`) with the tip's **captured fat-build args** 178(`rustc_args.args[1..]`), env-cleared and wrapper vars removed, plus 179`-Clinker={dx}` again so this recompile also gets intercepted for object 180capture. **No new flags are added for the tip crate on native platforms** in 181this thin path — it reuses exactly what was captured during the fat build 182(the `-Csave-temps`/`-Clink-dead-code` already baked in). 183 184**What gets linked into the patch, and into what:** 185 186- `temp_objects`: every `.rcgu.o` from the *tip's* fresh link args 187 (`link.rs:190-197`) — these are per-codegen-unit object files straight from 188 rustc's `-Csave-temps` output, i.e. **only the tip crate's own new/changed 189 translation units**, not whole-crate objects. 190- `workspace_rlibs`: the (now freshly overwritten) `.rlib` files for every 191 *other* replayed crate (`link.rs:199-200`, `workspace_hotpatch_link_rlibs` 192 at `link.rs:672-723`), resolved by reconstructing the exact rlib filename 193 from each crate's captured `--out-dir` + `-C extra-filename` 194 (`find_rlib_for_crate`, `link.rs:1285-1349`). 195- On non-wasm (our case): a **generated stub object** — 196 `create_undefined_symbol_stub` (`patch.rs:852-1259`) — is appended 197 (`link.rs:219-231`). This is the "resolve missing symbols against the 198 running binary" step (§ next paragraph). 199- Everything is linked with `thin_link_args` (`link.rs:341-511`), which for 200 `LinkerFlavor::Gnu` (Linux) starts from **`-shared`** 201 (`link.rs:429-436`: `-shared -Wl,--eh-frame-hdr -Wl,-z,noexecstack 202 -Wl,-z,relro,-z,now -nodefaultlibs -Wl,-Bdynamic`), i.e. **the patch output 203 is a shared object (`.so`)**, not an executable — confirmed by 204 `patch_exe` (`link.rs:737-756`): extension is `"so"` for `LinkerFlavor::Gnu`, 205 filename `lib{name}-patch-{timestamp_ms}.so` next to the main exe. Original 206 args are filtered down to just `-L`, `-l*`, `-m*`, `-fuse-ld*`/`-Wl,-fuse-ld*`, 207 `-B*` (needed because "Rust 1.86+ injects `-B/gcc-ld` + `-fuse-ld=lld`", 208 comment `link.rs:442-444`), and `-ld-path` (`link.rs:447-463`) — i.e. **no 209 rlibs/objects from the original fat link are reused as linker inputs 210 here**, only search-path/toolchain-selection flags. The actual linker 211 binary is `select_linker()` → `self.workspace.cc()` for Gnu (`link.rs:1255`), 212 i.e. the same `cc` used for the fat link, invoked directly (not through 213 rustc), env-cleared but with `PATH` restored on Linux specifically 214 (`link.rs:277-280`, `compile_workspace_hotpatch`'s own linker call at 215 `link.rs:287-292`). 216 217**How symbols that live in the ORIGINAL binary are resolved (Linux/native, 218not the WASM ifunc path):** `create_undefined_symbol_stub` 219(`patch.rs:852-1259`) — **not `--just-symbols`, not an asm/ELF diff, a 220hand-generated object file of absolute-address jump stubs**: 221 2221. Collect every undefined symbol across the objects about to be linked 223 (`collect_stub_symbols_from_path`/`_bytes`, `patch.rs:1262-1308`, via the 224 `object` crate reading `.o`/`.rlib`/`.a` member symbol tables), minus 225 anything also defined among them (`patch.rs:867-870`). 2262. Compute `aslr_offset = aslr_reference - aslr_ref_address` where 227 `aslr_ref_address` is the **original fat binary's on-disk `main` symbol 228 address** (from the cached symbol table, `patch.rs:926-939`) and 229 `aslr_reference` is the **live process's actual `main` address**, reported 230 by the running app over the devtools websocket (§4) and threaded in from 231 `BuildMode::Thin{ aslr_reference, .. }`. 2323. For each undefined symbol found in the original binary's cached symbol 233 table (`HotpatchModuleCache.symbol_table`, populated once per fat build via 234 the `object` crate, `patch.rs:263-291`, and reused across thin builds for 235 speed — "dropped the patch time from 3s to 1.1s", `patch.rs:66`): 236 `abs_addr = sym.address + aslr_offset` (`patch.rs:966`), then **hand-emit 237 machine code that jumps to that absolute address** and register it as a 238 new defined symbol with the *undefined* symbol's exact name in a freshly 239 built `object::write::Object`: 240 - Linux/x86_64 text symbols: `SymbolKind::Text` branch 241 (`patch.rs:1083-1091`): `FF 25 00 00 00 00` (`jmp [rip+0]`) followed by 242 the raw 8-byte absolute address appended right after — i.e. a RIP-relative 243 indirect jump whose target slot is the very next 8 bytes. 244 - Data symbols: written as `SymbolSection::Absolute` at `abs_addr` directly 245 (`patch.rs:1245-1254`) — no jump stub needed, the symbol just *is* that 246 address. 247 - TLS symbols: handled specially by copying real init bytes out of the 248 cached `.tdata`/`__thread_data` section into a new TLS symbol 249 (`patch.rs:1163-1226`) rather than writing a bogus absolute address. 2504. This generated object (`stub.o`) is linked in alongside the tip's 251 `.rcgu.o`s and the workspace `.rlib`s. 252 253So on Linux/x86_64 the whole "resolve against the live process" trick is: 254**one small hand-built ELF object of `jmp [rip+0]; <8-byte address>` stubs**, 255computed from (a) the fat build's own captured symbol table and (b) the 256process-reported ASLR reference — no `--just-symbols`, no dlopen-time symbol 257patching, no linker feature at all beyond ordinary symbol resolution against 258this synthetic object. 259 260## 3. The jump table 261 262`subsecond_types::JumpTable` (`subsecond-types/src/lib.rs:8-47`): 263 264```rust 265pub struct JumpTable { 266 pub lib: PathBuf, // the patch .so/.dylib/.dll/.wasm path 267 pub map: AddressMap, // old_addr -> new_addr, u64 keys, identity-hashed (no re-hash of an address) 268 pub aslr_reference: u64, // "main" symbol's address in the OLD (running) binary 269 pub new_base_address: u64, // "main" symbol's address as recorded IN the patch's own symbol table 270 pub ifunc_count: u64, // wasm-only: size to grow the indirect function table by 271} 272``` 273`AddressMap = HashMap<u64, u64, BuildHasherDefault<AddressHasher>>` where 274`AddressHasher` (`subsecond-types/src/lib.rs:53-92`) is a **no-op/identity** 275hasher (`write_u64` just stores the value) — addresses are unique by 276construction so hashing them is wasted work. 277 278**How "changed" is decided — it isn't, at the jump-table level.** There is no 279symbol-diffing step anywhere I could find (grepped `patch.rs`/`link.rs` for 280"diff"/"changed" — only comments describing intent, e.g. `link.rs:177` 281"ThinLink… Diffing of object files for function invalidation" is aspirational 282doc-prose, not implemented code found in this checkout). What actually 283happens: `create_native_jump_table` (`patch.rs:403-443`) parses the **whole** 284old binary's symbol table (cached) and the **whole** freshly-linked patch 285`.so`'s symbol table (`obj2.symbol_map()`), and inserts an entry for 286**every** name present in both (`patch.rs:420-424`): 287```rust 288for (new_name, new_addr) in new_name_to_addr.iter() { 289 if let Some(old_addr) = old_name_to_addr.get(*new_name) { 290 map.insert(old_addr.address, *new_addr); 291 } 292} 293``` 294"Changed" is decided **upstream**, structurally: only the tip crate's 295`.rcgu.o`s (i.e. whatever codegen units rustc actually re-emitted) plus 296replayed dependency crates make it into the patch `.so` at all; anything not 297recompiled simply isn't in `new_name_to_addr` and gets no entry (its old 298address is used unchanged, since callers only redirect through entries that 299exist in `jump_table.map`, `subsecond/src/lib.rs:396-403,919`/`936`). So the 300"diff" is really "what did rustc choose to recompile", not a post-hoc 301comparison — every symbol common to old and new gets remapped whether or not 302its bytes actually changed, which is why `subsecond/src/lib.rs:118-120` notes 303`ptr_address` "will always return the most up-to-date version… equivalent to 304a version of the function where every function is considered 'new'." 305 306`sentinel = main_sentinel(triple)` (referenced `patch.rs:426-434`, "main" on 307Linux) is used as the reference: `new_base_address` = patch's own `main` 308address, `aslr_reference` = the **original** binary's cached `main` address 309(not yet corrected for the live process — that correction happens at apply 310time, see below). 311 312**What `apply_patch` does on Linux/unix** (`subsecond/src/lib.rs:498-547`, 313`pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError>`): 3141. `libloading::Library::new(&table.lib)` — an ordinary `dlopen` 315 (`subsecond/src/lib.rs:507-512`), leaked forever (never `dlclose`'d — 316 comment explains dropping could run destructors mid-crash-prone-state). 3172. `old_offset = aslr_reference() - table.aslr_reference` — `aslr_reference()` 318 (`subsecond/src/lib.rs:716-750`) is `dlsym(RTLD_DEFAULT, "main")` in the 319 **currently running process**, so this is the ASLR slide of the *original* 320 binary between compile time and now. 3213. `new_offset = dlopen'd_lib.get::<*const ()>("main") - table.new_base_address` 322 — same idea for the just-loaded patch `.so`. 3234. Every entry in `table.map` gets rebased: 324 `(old_key + old_offset, new_val + new_offset)` (`subsecond/src/lib.rs:535-544`). 3255. `commit_patch(table)` — atomically swaps a leaked, `Box`-owned `JumpTable` 326 into a global `AtomicPtr` (`Relaxed` ordering, `subsecond/src/lib.rs:279`, 327 `308-321`) and runs any registered hot-reload handlers. 328 329Callers then look up `known_fn_ptr` (the function's **compile-time** address, 330taken via `transmute` of the function item, `subsecond/src/lib.rs:396`/`430`) 331in `jump_table.map` and, if present, call through the mapped address instead 332(`HotFn::try_call`, `subsecond/src/lib.rs:411-441`; the macro-generated 333`call_as_ptr`, `subsecond/src/lib.rs:902-941`, handles Android pointer-tagging 334specially but is a no-op branch on Linux). 335 336## 4. The wire protocol 337 338Types: `packages/devtools-types/src/lib.rs`. 339```rust 340pub enum DevserverMsg { 341 HotReload(HotReloadMsg), HotPatchStart, FullReloadStart, 342 FullReloadFailed, FullReloadCommand, Shutdown, 343} // lines 9-28 344pub struct HotReloadMsg { 345 pub templates: Vec<HotReloadTemplateWithLocation>, 346 pub assets: Vec<PathBuf>, 347 pub ms_elapsed: u64, 348 pub jump_table: Option<JumpTable>, 349 pub for_build_id: Option<u64>, 350 pub for_pid: Option<u32>, 351} // lines 42-50 352``` 353Both derive plain `Serialize, Deserialize` with no `#[serde(tag=...)]` 354override, so `DevserverMsg` serializes as serde's default externally-tagged 355JSON, e.g. `{"HotReload":{"templates":[],"assets":[],"ms_elapsed":0, 356"jump_table":{...JumpTable fields...},"for_build_id":1,"for_pid":12345}}`. 357Transport is a plain `tungstenite` WebSocket carrying `Message::Text(json)` 358(`devtools/src/lib.rs:98-109`), explicitly **not authenticated**: "This 359doesn't use any form of security or protocol, so it's not safe to expose to 360the internet" (`devtools/src/lib.rs:59`). 361 362**Connection URL** (`connect_at`, `devtools/src/lib.rs:89-101`): 363``` 364{endpoint}?aslr_reference={subsecond::aslr_reference()}&build_id={dioxus_cli_config::build_id()}&pid={std::process::id()} 365``` 366— the client (the running app) reports its **own live `main` address** as a 367query param when it connects; this is exactly the `aslr_reference` value 368`patch_rebuild` threads into `BuildMode::Thin` (`builder.rs:380-395` — dx 369*waits* for a client to be connected before it will do a thin build on 370non-wasm targets, since it needs this value: "Ignoring hotpatch since there 371is no ASLR reference. Is the client connected?", `builder.rs:390-391`). 372 373**Receiving/applying** (`connect_subsecond`, `devtools/src/lib.rs:76-86`): 374```rust 375pub fn connect_subsecond() { 376 connect(|msg| { 377 if let DevserverMsg::HotReload(hot_reload_msg) = msg { 378 if let Some(jumptable) = hot_reload_msg.jump_table { 379 if hot_reload_msg.for_pid == Some(std::process::id()) { 380 unsafe { subsecond::apply_patch(jumptable).unwrap() }; 381 } 382 } 383 } 384 }); 385} 386``` 387`for_pid` gates whether *this* process applies it (a dev server can be 388talking to several connected clients/processes at once); `for_build_id` is 389checked similarly in the dioxus-VirtualDom-aware `try_apply_changes` 390(`devtools/src/lib.rs:19-55`, checks `msg.for_build_id == 391Some(dioxus_cli_config::build_id())`) but **not** in the bare 392`connect_subsecond` path — for a non-Dioxus app (jev is eframe/egui, not 393Dioxus), `connect_subsecond`'s simpler pid-only check is the relevant 394precedent. 395 396**Confirmed: `subsecond::apply_patch` is `pub unsafe fn apply_patch(table: 397JumpTable) -> Result<(), PatchError>`** (`subsecond/src/lib.rs:498`) — it can 398be called directly with a hand-constructed `JumpTable`, with no dependency on 399the websocket protocol, `dx`, or Dioxus at all. A buck2-driven tool can build 400its own `JumpTable` (e.g. by shelling out to the same `create_native_jump_table` 401logic, or a from-scratch equivalent using the `object` crate) and either (a) 402open its own tiny websocket/HTTP server that `jev`'s MCP server dials into and 403speaks a `{jump_table: JumpTable}`-shaped JSON, mirroring `DevserverMsg`, or 404(b) skip the network entirely and have the MCP `hot_patch` tool call 405`subsecond::apply_patch` **in-process**, since jev's own binary already links 406`subsecond` and already has an MCP server loop (`apps/native/src/mcp.rs`) that 407can receive a file path + deserialize a `JumpTable` from disk/stdin and call 408`apply_patch` directly on the UI/tool thread. This avoids reimplementing the 409websocket handshake and the `aslr_reference` query-param dance — the tool call 410itself can read `subsecond::aslr_reference()` in-process before invoking the 411build. 412 413## 5. Hard requirements/limits for the buck2 design 414 415- **No nightly rustc requirement.** (See §1 — the only nightly use is an 416 unrelated build-progress estimate.) Stable rustc + stable `-C`/`-Z`-free 417 flags (`-Csave-temps=true`, `-Clink-dead-code`) suffice for native/Linux. 418- **`debug_assertions` must be on** in the profile being patched — `call`/ 419 `try_call` collapse to a plain call in release (`subsecond/src/lib.rs:250-254`, 420 `:411-414`); patching a release-profile binary compiles but every 421 `subsecond::call` site is a dead no-op, so `apply_patch` succeeds but 422 nothing ever redirects. jev's buck2 `system_rust_toolchain` sets 423 `rustc_flags = ["-Copt-level=3", "-Cdebuginfo=1"]` (`toolchains/BUCK:16`) 424 with no explicit `-Cdebug-assertions`. **VERIFIED 2026-09-20, measured**: 425 built `//apps/hotdemo:hotdemo` with `println!("{}", 426 cfg!(debug_assertions))` at the top of `main` and no `-Cdebug-assertions` 427 flag on the target (toolchain's `-Copt-level=3` only) — ran it and it 428 printed `hotdemo debug_assertions=false`. So buck2's `system_rust_toolchain` 429 at `opt-level=3` does **not** imply debug-assertions on; it must be set 430 explicitly per hot-patch target. `apps/hotdemo/BUCK` does this 431 (`-Cdebug-assertions=yes` in `rustc_flags`). 432- **Thread-locals in the tip crate reset on every patch** — "not currently 433 bind[ing] thread-locals in the patches to their original addresses" 434 (`subsecond/src/lib.rs:88-92`). Not a buck2-specific concern, but a 435 correctness limit to document for whoever writes jev's hot-patched code. 436- **No struct-layout/ABI safety net.** Layout or size changes to structs 437 referenced across the patch boundary crash outright — frameworks are 438 expected to throw away and rebuild affected state (`subsecond/src/lib.rs:94-109`). 439- **"Tip crate only" in the subsecond crate's *doc comment* is stale** 440 relative to the CLI's actual v0.7.10 code: `compile_workspace_hotpatch` + 441 `workspace_hotpatch_replay_order`/`_link_rlibs` (`link.rs:132-335, 442 586-723`) DO recompile and relink modified **library** crates (topologically 443 replayed via direct `rustc` invocations reusing captured args), not just the 444 tip. So `//packages/panels` (or `//packages/console`, `//packages/alttp`) 445 changing **can** be folded into a patch, *provided* the buck2 tool replays 446 that crate's rustc invocation with `--out-dir`/`extra-filename` unchanged 447 (so the resulting `.rlib` lands exactly where the tip's link line already 448 expects it) and the tip crate itself gets relinked against the new `.rlib`. 449 UNVERIFIED beyond what's in this checkout: no test in this repo exercises 450 a multi-crate thin build, so treat this as "the code path exists and is 451 structured to support it" rather than "empirically proven robust." 452- **The stub-symbol resolver (`create_undefined_symbol_stub`) is unimplemented 453 for WASM** ("this function is not defined to run on WASM binaries", 454 `patch.rs:849-850`) — irrelevant to jev's native build but worth noting if a 455 wasm target is ever added; that path instead rewrites the `.wasm` module's 456 ifunc/GOT imports directly (`create_wasm_jump_table`, `patch.rs:458-761`). 457- **A live client connection is required before any thin build can even 458 start**, because `aslr_reference` (the process's real `main` address) has 459 to be known (`builder.rs:380-395`). For jev's own tool, the MCP server 460 already running inside the app window is the natural source of this value 461 — it can report `subsecond::aslr_reference()` directly to the `hot_patch` 462 tool caller instead of over a websocket query param. 463 464## 6. Buck2 prelude: what's available, and a concrete design 465 466Prelude location (this repo): `~/nixos-config`-independent, resolved via 467`nix develop -c buck2 audit cell prelude` → `/home/nixos/jev/prelude` (which 468is itself a redirect to the bundled cell at 469`/home/nixos/jev/buck-out/v2/external_cells/bundled/prelude`, where the actual 470`.bzl` files live). 471 472Relevant prelude machinery (paths relative to that prelude root): 473 474- `rust/build_params.bzl:22-30` — `CrateType = enum("bin","rlib","dylib", 475 "proc-macro","cdylib","staticlib")`. **`cdylib` already exists** as a 476 first-class crate type — the natural output type for a hand-built 477 `rust_binary`-like "patch" rule, since it's exactly a `.so` with C-style 478 exports, matching what `dx`'s thin link produces by hand. 479 `crate_type_linked` (`build_params.bzl:35-36`) includes `"cdylib"`. 480- `rust/build.bzl:950` (`_rustc_flags`) and the `emit`-keyed dispatch starting 481 `build.bzl:1012` — this is where per-target `rustc_flags` (a plain 482 `attrs.list(attrs.arg())` on `rust_library`/`rust_binary`, standard prelude 483 attr) get assembled with toolchain-level `rustc_flags`/`extra_rustc_flags` 484 (`build.bzl:1226-1232`). A buck2 rule can add `-Csave-temps=true 485 -Clink-dead-code` (and, if confirmed necessary per §5, 486 `-Cdebug-assertions=yes`) the same way jev's `toolchains/BUCK:16` already 487 adds `-Copt-level=3 -Cdebuginfo=1` — no new mechanism needed, just more 488 flags on the existing `rustc_flags` toolchain attr or a target-level 489 override. 490- `rust/build.bzl:933-947` (`_check_restricted_rustc_flags`) — the prelude can 491 *restrict* certain rustc flags per toolchain (`uses_restricted_rustc_flags` 492 escape hatch); worth checking jev's toolchain doesn't block `-Csave-temps`/ 493 `-Clink-dead-code` (it doesn't set `restricted_rustc_flags` today — 494 `toolchains/BUCK` has no such field). 495- `rust/rust_library.bzl:557-558,627-628,636-637` — `exported_linker_flags`/ 496 `exported_post_linker_flags` (plus the plain non-exported `linker_flags` 497 attr inherited from the common Rust rule attrs) is the existing plumbing 498 for arg like `-Wl,--export-dynamic-symbol,main`. 499- `rust/rust_binary.bzl:148` — `link_style`/`LinkStrategy` attr, confirming 500 buck2's rust rules already have a first-class notion of static vs 501 shared-dependency linking, relevant to deciding whether the "fat" analogue 502 (a binary linking every workspace `.rcgu.o` unconditionally, undead-code 503 stripped) is better modeled as a `link_style = "static"` `rust_binary` with 504 the save-temps/link-dead-code flags, vs. a wholly custom `genrule`/action. 505- Object files: `--emit`/temp objects come out of the ordinary rustc action 506 buck2 already runs per crate (`build.bzl` `Emit("link")` etc, 507 `build.bzl:1012` on); with `-Csave-temps=true` those `.rcgu.o` files land in 508 rustc's own temp dir the same way they do under cargo — buck2's rust rules 509 don't need new emit plumbing, just the flag, though the tool will need to 510 locate rustc's actual (buck2-managed, hashed) `--out-dir`/temp dir rather 511 than cargo's, since buck2 controls that path differently than cargo does. 512 **VERIFIED 2026-09-20, measured — see §7.1**: they land in a normal 513 *declared* buck2 output directory, not ephemeral scratch, at a path 514 mechanically derivable from the target's own main-output path. 515 516**Proposed design**, given all of the above: 517 5181. **A `hotpatch` Rust binary** (new crate, e.g. `//tools/hotpatch`) that 519 reimplements, against the `object` crate directly (no dioxus/cli 520 dependency needed — `subsecond-types` is a tiny, license-compatible, 521 directly-vendorable crate for the `JumpTable`/`AddressMap` types): 522 - `create_native_jump_table`-equivalent (`patch.rs:403-443`): parse old 523 binary + new `.so` symbol tables via `object`, build the old→new address 524 map keyed on shared names, using `main` as the ASLR sentinel. 525 - `create_undefined_symbol_stub`-equivalent (`patch.rs:852-1259`, Linux 526 x86_64 branch only needed: `patch.rs:1083-1091` for the `jmp [rip+0]` 527 text stub, `patch.rs:1245-1254` for data, TLS can likely be deferred 528 given jev is a single-binary eframe app unlikely to lean on TLS 529 initializers). 530 - A `hot_patch` mode driven by a small CLI: given the base exe path (buck2 531 output), the new `.rcgu.o`/`.rlib` set, and the live process's 532 `aslr_reference` (queried from jev's own MCP server), emit the stub 533 object, link (`cc -shared -Wl,--eh-frame-hdr … -nodefaultlibs` per 534 `link.rs:429-436`, plus `-Wl,--export-dynamic-symbol,main` carried over 535 from the base build) and print/serialize the resulting `JumpTable` as 536 JSON. 5372. **Buck2 side**: two new targets alongside `apps/native:native` — 538 - `native` itself gains `-Csave-temps=true -Clink-dead-code 539 -Wl,--export-dynamic-symbol,main` (via `rustc_flags`/`linker_flags`) only 540 under a `hotpatch`-flavored build (e.g. a buck2 `select()` on a 541 `config_setting`, so ordinary release/dev builds are unaffected) — the 542 buck2 analogue of dx's "fat" build. No archive-splicing step is 543 necessary the way `run_fat_link` does it (`link.rs:788-1142`): `object` 544 files inside a `bin` crate that are reachable from `main` are always 545 linked (the compiler wouldn't have generated them otherwise), and 546 `-Clink-dead-code` alone is what stops the linker from then dropping 547 *dependency-rlib* symbols that are compiled in (rlibs retain every 548 public item, cargo/buck2-reachability rules) but not called from 549 anywhere in the current build. **VERIFIED 2026-09-20, measured — see 550 §7.2**: without `-Clink-dead-code`, roughly two-thirds of all 551 Rust-mangled symbols in `hotdemo`'s own binary were gone from `nm`, 552 including half of the `subsecond`/`subsecond-types`-related symbols. dx's 553 fat-archive dance is unnecessary for this design, but the flag is not 554 optional. 555 - A per-crate (or per-changed-crate) **thin recompile** path: since buck2 556 already tracks per-crate build graphs and caches, there is no need to 557 replay captured rustc args the way `compile_dep_crate` does 558 (`link.rs:518-584`) — buck2 already knows how to rebuild exactly the 559 crates whose inputs changed, with the *same* flags, because that's 560 buck2's whole job. The buck2-native replacement for `workspace_hotpatch_replay_order` 561 is simply: ask buck2 to build the (now on-disk-cached, per-`cdylib`- or 562 per-`rlib`-target) outputs for the changed target and its buck2-graph 563 dependents, then hand *those* `.rlib`/`.rcgu.o` paths to the `hotpatch` 564 tool for the final ad-hoc link — i.e. buck2's own incrementality 565 subsumes dx's rustc-arg-replay machinery entirely; only the *final, 566 ad-hoc, non-buck2-native* stub-object-and-link step needs the custom 567 tool, mirroring `compile_workspace_hotpatch`'s tail 568 (`link.rs:186-334`) but sourcing its object/rlib inputs from buck2 569 outputs instead of a hand-rolled JSON cache. 5703. **Delivery to the running window**: extend `apps/native/src/mcp.rs`'s 571 dev-mode tool set (already gated by `DevMode`, per its module doc, 572 `mcp.rs:8-12`) with a `hot_patch` tool that (a) on request, reports 573 `subsecond::aslr_reference()` back to the caller (buck2/the hotpatch tool 574 invocation) so it can compute `aslr_offset`, and (b) accepts a path to a 575 just-built `JumpTable` (JSON) or patch `.so` + JSON sidecar, and calls 576 `unsafe { subsecond::apply_patch(table) }` on it — in-process, no 577 websocket, no `dioxus_cli_config`/`dioxus-devtools` dependency at all. This 578 is strictly simpler than reimplementing `DevserverMsg`/`tungstenite` (§4), 579 since jev already has a live, bidirectional, already-authenticated 580 (loopback-only) channel into the running window via MCP. 581 582## 7. The two UNVERIFIED points from §6, resolved by measurement (2026-09-20) 583 584Both measured against `//apps/hotdemo:hotdemo` (`apps/hotdemo/BUCK`, 585`rustc_flags = ["-Csave-temps=true", "-Clink-dead-code", 586"-Cdebug-assertions=yes"]`), in this repo's own buck2/nix devshell, not the 587dioxus checkout. 588 589### 7.1 Where `-Csave-temps=true` leaves the `.rcgu.o` objects 590 591**They land in a normal declared buck2 output directory, not scratch — 592no `ar x` fallback needed.** 593 594`buck2 build //apps/hotdemo:hotdemo --show-full-output` reports the main 595binary at 596`buck-out/v2/art/root/<cfg-hash>/apps/hotdemo/__hotdemo__/hotdemo`. 597`buck2 log what-ran` (after touching a source line to force a rebuild) shows 598the actual rustc invocation's args file 599(`…/apps/hotdemo/__hotdemo__/<hash>/XIPL/hotdemo-link-diag.args`), which 600contains: 601``` 602-Csave-temps=true 603-Clink-dead-code 604-Cdebug-assertions=yes 605--emit=link=buck-out/v2/art/root/<cfg-hash>/apps/hotdemo/__hotdemo__/hotdemo 606--out-dir=buck-out/v2/art/root/<cfg-hash>/apps/hotdemo/__hotdemo__/XIPL/extras/hotdemo 607``` 608The prelude explains why that `--out-dir` exists at all, independent of 609`-Csave-temps`: `rust/build.bzl:1451-1453` always declares an "extras" 610directory as a real buck2 output (`extra_dir = subdir + "/extras/" + 611base`; `ctx.actions.declare_output(extra_dir, dir = True, …)`) for 612per-codegen-unit side artifacts (originally split-DWARF `.dwo` files, 613`build.bzl:1173-1180`'s comment). `-Csave-temps=true` doesn't need new buck2 614plumbing — it just makes rustc *also* drop its `.rcgu.o`/`.rcgu.bc` files 615into whatever `--out-dir` it's already given, and that directory happens to 616be a persistent, declared output rather than the action's `TMPDIR` (a 617separate, genuinely-ephemeral path under `buck-out/v2/tmp/…/rustc/_buck_…` 618that the args file also references, for the compiler's own scratch use). 619Confirmed by listing the directory after a clean build — it survives, with 620one `.rcgu.o` per codegen unit (`hotdemo.hotdemo.<hash>-cgu.<N>.rcgu.o`) plus 621one for the crate's monomorphization glue (`hotdemo.<hash>.rcgu.o`). 622 623So the relationship a caller needs is entirely mechanical, from 624`--show-full-output`'s own answer: 625``` 626extras_dir = dirname(main_output) + "/XIPL/extras/<crate-file-stem>" 627``` 628`tools/hotpatch/patch.sh` computes it this way rather than hard-coding a 629path. 630 631### 7.2 Whether the plain static link keeps every symbol a patch will need 632 633**No — `-Clink-dead-code` is doing real, necessary work, not a defensive 634no-op.** 635 636Built `hotdemo` twice, flipping only `-Clink-dead-code` 637(`-Csave-temps=true`/`-Cdebug-assertions=yes` held fixed), and compared 638`nm` on the resulting binary: 639 640| | total symbols (`nm \| wc -l`) | `subsecond`/`subsecond-types`-mangled symbols | 641| --- | --- | --- | 642| with `-Clink-dead-code` | 3847 | 67 | 643| without `-Clink-dead-code` | 1394 | 36 | 644 645Without the flag, the plain static link (buck2's default, no 646`--gc-sections` override of its own) drops roughly two-thirds of every 647Rust-mangled symbol in the binary, including about half of the 648`subsecond`/`subsecond-types` ones — dependency rlibs (subsecond, 649subsecond-types, hashbrown, serde, …) retain every public item at 650compile time (a `rlib`'s reachability set is "visible from the crate 651root", not "called by this program"), and the linker's default dead-code 652elimination then strips whichever of those never end up referenced by 653`hotdemo`'s own call graph. A function a *future* patch needs to jump back 654into — one `hotdemo` doesn't call today but a later edit will — is exactly 655the kind of thing this strips if not held open. `apps/hotdemo`'s own 656`tick`/`main`/`apply_pending_patch` were unaffected either way (directly 657reachable from `main`, so always kept), which is why this had to be 658measured against the *dependency* rlibs' symbol counts, not `hotdemo`'s own 659three functions, to be a real test. This matches dx's own rationale 660(`request.rs:1796-1798`'s comment, quoted in §1) exactly, and confirms the 661flag stays on every hot-patch-flavored buck2 target — including, later, 662`apps/native` and any of its library deps whose changes should be 663patchable. 664 665### 7.3 Bonus: the `-Cdebug-assertions` default at `-Copt-level=3`, resolved 666 667§5 flagged this as unconfirmed too. Built `hotdemo` with the toolchain's 668plain `-Copt-level=3 -Cdebuginfo=1` and *no* explicit `-Cdebug-assertions` 669flag, printing `cfg!(debug_assertions)` at startup: it printed 670`hotdemo debug_assertions=false`. So buck2's `system_rust_toolchain` does 671**not** imply debug-assertions from `opt-level=3` — every hot-patch target 672needs `-Cdebug-assertions=yes` explicitly, which `apps/hotdemo/BUCK` now 673carries. 674 675## 8. Two more traps found getting the end-to-end proof to actually redirect (2026-09-20) 676 677`tools/hotpatch`/`apps/hotdemo` built and linked cleanly, `apply_patch` 678returned `Ok(())`, and "patch applied" printed - and the log kept printing 679the OLD string anyway. Both root causes below are `nm`/disassembly-verified, 680not guessed, and both are silent: nothing errors, the patch mechanism just 681does nothing. 682 683### 8.1 `subsecond` needs its OWN `-Cdebug-assertions=yes`, not just the tip crate's 684 685`subsecond::call`/`try_call` are `cfg!(debug_assertions)`-gated no-ops (§5). 686`apps/hotdemo/BUCK` had `-Cdebug-assertions=yes` from the start. The redirect 687still never fired. 688 689`cfg!()` is resolved against **the compilation that contains the macro 690invocation**, not the crate that later monomorphizes a generic function. 691Under cargo, a single profile's `debug-assertions` setting applies 692uniformly to the whole dependency graph — `subsecond` gets built with 693whatever flag the TIP crate's profile carries, because cargo compiles 694every crate in that one profile. Under buck2/reindeer, each third-party 695crate is its own independent `cargo.rust_library` target, built with 696whatever `rustc_flags` ITS OWN target carries — which, absent a fixup, is 697just the toolchain default (`-Copt-level=3 -Cdebuginfo=1`, no 698debug-assertions, confirmed false in §7.3). `apps/hotdemo`'s own 699`-Cdebug-assertions=yes` therefore had **zero effect on `subsecond`'s 700internal gate** — verified by `nm` on the built binary: no call to 701`subsecond`'s own (real, addressable — `_RNvCs41PE3uYsSey_9subsecond14get_jump_table`) 702`get_jump_table` function appears anywhere inside `main`'s compiled code. 703The fast `if !cfg!(debug_assertions) { return f() }` path was compiled in 704as unconditionally true, because subsecond's OWN build never turned 705debug-assertions on. 706 707Fix: `third-party/rust/fixups/subsecond/fixups.toml` — 708```toml 709rustc_flags = ["-Cdebug-assertions=yes"] 710``` 711reindeer's `FixupConfig` supports `rustc_flags` per crate directly; no 712custom buck2 plumbing needed. **Any first-party crate that hot-patches 713under buck2/reindeer needs this same fixup on `subsecond`** — it is not 714specific to `apps/hotdemo` and will apply equally to `apps/native`. 715 716### 8.2 `subsecond::call` needs a bare `fn` item, not a closure — for two independent reasons 717 718The first working version of `tick` was `fn tick(n: u64)`, called as 719`subsecond::call(|| tick(n))`. Even after fixing §8.1, the redirect still 720never fired. Two separate, unrelated causes, found by disassembling `main` 721(`objdump -d`, `nm`) and reading `HotFn::try_call`/`call_as_ptr` 722(`subsecond/src/lib.rs:395-424`, `:877-943`) line by line: 723 7241. **The redirect key depends on `size_of::<F>()`.** `try_call` branches: 725 if the closure/fn type `F` is exactly pointer-width 726 (`size_of::<F>() == size_of::<fn() -> ()>()`, 8 bytes on x86_64), it 727 calls `call_as_ptr`, which does 728 `std::mem::transmute_copy::<Self, Self::Real>(&self)` — reinterpreting 729 `F`'s own in-memory bytes AS a function pointer. This is correct **only** 730 when `F` genuinely IS a bare fn pointer value (then `Self == 731 Self::Real` and the transmute is an identity). Our closure captured 732 `n: u64` — also 8 bytes — so it took the SAME branch, and the transmute 733 reinterpreted `n`'s VALUE as a function address: garbage, not a lookup 734 key subsecond's map would ever contain. Any closure whose captured 735 environment happens to be pointer-sized (one `u64`, one reference, …) 736 hits this, silently. `apps/hotdemo`'s fix: no closure at all — `tick` 737 takes no parameters, reads a `static AtomicU64` for its counter, and 738 `main` calls `subsecond::call(tick)` directly, so `F` is a genuine 739 zero-sized function-item type and the *other* branch 740 (`<F as HotFunction<A,M>>::call_it as *const ()`, subsecond/src/lib.rs:428-435) 741 runs instead. 7422. **`-Copt-level=3`'s default cross-CGU ThinLTO inlines small, 743 single-call-site functions across the whole chain.** Fixing (1) alone 744 was not enough: `nm` on the resulting binary still showed no `tick` 745 symbol and no `call_it`/`HotFunction` symbol anywhere — the entire 746 `subsecond::call` → `HotFn::try_call` → `HotFunction::call_it` → `tick` 747 chain had been inlined into one block inside `main`, leaving nothing to 748 redirect a call to. `subsecond`'s own source carries **no 749 `#[inline(never)]` anywhere** (confirmed by grep) — it is written 750 assuming a dev-profile-style build (`opt-level = 0`), where LLVM 751 performs little to no inlining by default and this is a non-issue. This 752 project forces `-Copt-level=3` for every target unconditionally 753 (`toolchains/BUCK`, a real requirement — the emulator core is unusable 754 below realtime at `-O0`), which is a mode `subsecond` was never designed 755 against. Fix (for the tip crate's OWN patchable functions): 756 `#[inline(never)]` on `tick`. This does not, by itself, force `call_it` 757 to remain a distinct out-of-line symbol too — but with `tick` pinned, 758 `call_it`'s inlined copy contains a genuine `call` instruction to a 759 real, address-stable `tick`, which turned out to be sufficient in this 760 build (confirmed by the working v1→v2→v3 proof, §8.3) even though 761 `nm` still shows no separate `call_it` symbol. Treat this as "empirically 762 sufficient for a trivial single-statement function," not as a proof that 763 inlining of the wrapper machinery itself can never matter — a much 764 larger patchable function, or a different LLVM version's inlining 765 heuristics, could still behave differently. 766 767**Consequence for `apps/native`:** every function meant to be hot-patchable 768needs `#[inline(never)]`, and the call site needs to pass it as a bare `fn` 769item (or a zero-sized, non-capturing closure) — not a closure over any 770pointer-sized local state. A method taking `&mut self` is already safe from 771the size-8 coincidence in the common case (`&mut self` alone is exactly one 772pointer *is* 8 bytes, so this needs re-checking per call site, not assumed 773safe by shape). 774 775### 8.3 The actual end-to-end proof, once both were fixed 776 777`tools/hotpatch/patch.sh //apps/hotdemo:hotdemo`, twice in a row against a 778single running process (pid unchanged throughout, `run/hotdemo.log`): 779``` 780hotdemo pid=42801 aslr_reference=0x5555555adf20 781hotdemo v1 tick=0 782... 783hotdemo v1 tick=14 784hotdemo: patch applied 785hotdemo v2 tick=0 786... 787hotdemo v2 tick=45 788hotdemo: patch applied 789hotdemo v3 tick=0 790... 791``` 792`TICKS` (a `static`) resets to 0 on each patch rather than continuing its 793count — expected, not a bug: it lives in the tip crate, which gets fully 794recompiled every patch, so the freshly-`dlopen`'d `.so` gets its own fresh 795`.bss` copy rather than being rebased onto the original process's static 796(subsecond's "globals are tracked" claim, `subsecond/src/lib.rs`'s module 797doc, applies to statics that live in an UNCHANGED dependency crate reused 798via its cached `.rlib` — not to one recompiled as part of the tip). Same 799family of limitation as the already-documented thread-local reset (§5). 800 801Files most load-bearing for this design, for a follow-up implementer to open 802directly: `packages/cli/src/build/link.rs` (thin/fat link logic), 803`packages/cli/src/build/patch.rs` (jump table + stub generation), 804`packages/cli/src/rustcwrapper.rs` + `packages/cli/src/cli/link.rs` (why/how 805dx wraps rustc+linker — buck2 needs none of this, since it already owns its 806own build graph), `packages/subsecond/subsecond/src/lib.rs` (`apply_patch`, 807`aslr_reference`, the `HotFn`/`call` runtime), `packages/subsecond/subsecond-types/src/lib.rs` 808(`JumpTable`), `packages/devtools/src/lib.rs` + `packages/devtools-types/src/lib.rs` 809(wire protocol, useful only as a fallback if in-process MCP delivery turns 810out to be insufficient), and, in this repo, `/home/nixos/jev/toolchains/BUCK` 811+ `/home/nixos/jev/apps/native/BUCK` + `/home/nixos/jev/apps/native/src/mcp.rs` 812as the integration points.