jevsnes.git / tools / hotpatch

For agents, on top of README.md, which they read first.

  • Linux/x86_64 only, on purpose. No target_lexicon, no per-OS branches in src/main.rs - this ports the logic of dioxus-cli's patch.rs, not its full cross-platform surface. Adding another OS means adding the real branch (Mach-O headers, Windows stub asm, …), not guessing at one.
  • TLS symbols are handled by copying init bytes, not by refusing them. create_undefined_symbol_stub's SymbolKind::Tls arm copies the base binary's own .tdata bytes (CachedSymbol::address/size are the OFFSET and length into that section, not a virtual address - the one symbol kind here aslr_offset does not apply to) into a fresh TLS symbol in the patch, porting patch.rs's ELF branch (patch.rs:1163-1226 in the dioxus checkout). This was not a theoretical risk: proof against the real window (apps/native) hit it immediately, on a plain recompile with NO source changes to packages/panels at all - moving logic/ui's bodies into free functions shifted codegen-unit boundaries enough that a thread-local internal to std or tokio came out newly-undefined in the fresh link. apps/hotdemo never exercises this (no tokio, a two-crate dependency graph) - do not treat it passing as evidence this branch is unneeded for anything bigger. What is still genuinely unhandled: a symbol that is TLS in the NEW code but has no counterpart in the base binary's cache at all (nothing to copy init bytes from) - that still exits loudly, correctly, since there is nothing to port it from.
  • patch.sh's crate name comes from the target label's own suffix (//apps/hotdemo:hotdemo -> hotdemo), and that name has to match: the log file (run/<crate>.log), the --out-dir XIPL/extras/<crate> buck2 produces, and the patch-json path the running binary watches (run/<crate>.patch.json) all derive from it. A target whose crate name differs from its label's tail (an alias, an unusual crate attr) needs patch.sh taught about the difference, not a rename to force the convention.
  • Re-run research/subsecond-patch-build.md §7's measurements (the XIPL/extras path shape, the -Clink-dead-code symbol-count diff) if this is ever pointed at a different buck2 prelude version - both are read from the prelude's .bzl/measured against a real build, not documented upstream API.
  • Never remove -Csave-temps=true/-Clink-dead-code from apps/native:native's own BUCK rule, even though main.rs has no patch points. patch.sh reads that object off disk (never rebuilding it) as the "main" sentinel subsecond::apply_patch looks up by name in every patch's own link - see README's "the executable is never rebuilt" section. Removing the flags "cleans up" a real dependency, silently: hotpatch fails loudly ("the patch has no 'main' symbol") the next time anyone patches apps/native, not when the flags are removed.
  • A tip's first-party dependencies are walked TRANSITIVELY (all_first_party_deps_of), not one level. apps/native:native -> apps/native:app -> packages/panels is two hops; a one-level walk finds app and stops, so a packages/panels edit builds correct fresh objects that never reach hotpatch - the patch "succeeds" and silently keeps running the OLD panels code via the undefined-symbol stub. No error anywhere; this is the same family of silent failure as the TLS trap above, found the same way (proving against the real window, 2026-09-20). Adding another hop to the dependency graph needs no patch.sh change - the walk is already general - but REMOVING the transitive walk to "simplify" it reintroduces this exact bug.
  • A dependency is checked with has_save_temps BEFORE it is built, not after. Building it first to find out costs real time for nothing: packages/mcp depends on packages/panels, so a panels edit invalidates mcp's own (proc-macro-heavy) compile too, for a result that was always going to be thrown away (~10s measured 2026-09-20, before this check moved earlier in patch.sh's dependency loop).
  • --base is /proc/$PID/exe, never a path buck2 build just produced. Asking buck2 to build //apps/native:native for --base is what forces the relink this whole design exists to avoid the moment app has diverged from what shipped in the last real build of the executable - /proc/$PID/exe is the OS's own answer to "what is this pid actually running", free and correct by construction, whether or not buck2's on-disk copy at that path has since changed underneath it.