Runtime Launcher
Runtime Launcher: SISR self-update
Status: implemented (
daedalus-coresisr::engine+sisr::swap, wired intostub/src/main.rs). Describes how the launcher rebuilds the running binary from a signed delta manifest before extracting and executing it. Since mission 8 the launcher also supervises the first run of a freshly updated version and rolls back automatically if it crashes — see Rollback & Resilience.
The launcher flow in stub/src/main.rs is deliberately linear: locate self
via /proc/self/exe, read the footer and metadata, apply a SISR update if one
is requested, then extract the payload and exec. The SISR step sits between
metadata parsing and the cache/extract phase, so the same run executes the
new version.
Two trigger paths exist:
- Local staging (mission 6):
$DAEDALUS_SISR_MANIFESTpoints at a signed manifest whose chunks are staged in<manifest-dir>/chunks/. The launcher stays network-free; this path is unchanged. - Remote update (mission 7):
./app.daedalus --daedalus-update [URL]— the launcher intercepts the flag before the app sees it, downloads the manifest and the changed chunks over HTTPS, applies the delta, prints reuse/fetch statistics, and exits. The URL resolution order is--daedalus-update <URL>argument >$DAEDALUS_UPDATE_URL> theupdate_urlembedded at build time (daedalus build --update-url).--daedalus-versionprints version info and exits.
./app.daedalus (DAEDALUS_SISR_MANIFEST=/updates/app.daedalus.manifest — local staging)
(./app.daedalus --daedalus-update — remote update)
│
1. open /proc/self/exe → footer → metadata
2. update requested?
no → skip to 6
yes ↓
3a. local: read + parse remote manifest (XBMR) from $DAEDALUS_SISR_MANIFEST
3b. remote: GET {base}/manifest (HTTPS) and parse it
4. verify Ed25519 signature against trusted keys (~/.daedalus/trusted-keys/)
verify Merkle root against the chunk table
5. SisrEngine::apply_update(/proc/self/exe, manifest, fetcher)
- reuse unchanged chunks from the current binary
- fetch the rest — local: <manifest-dir>/chunks/<hex-hash>
remote: GET {base}/chunks/<hex-hash> (HTTPS)
- SHA-256-verify every chunk before writing
- write to .tmp, fsync, rename → atomic swap
failure at any point ⇒ binary untouched, launcher exits with error
6. re-open the *canonical real path* returned by the engine (not
/proc/self/exe, which can still resolve to the pinned pre-update inode)
and re-read footer + metadata — now the new version
7. health gate (mission 8): snapshot `./app.daedalus.bak` taken before the
swap; the new version is supervised for its startup window
- healthy ⇒ confirmed, `.bak` discarded
- crashing ⇒ recorded, `.bak` restored atomically, previous version runs
- quarantined target ⇒ refused before the swap
8. cache check → extract → exec as usual
The --daedalus-update / --daedalus-version paths are terminal: after the
update the launcher prints statistics on stderr and exits without exec'ing the
app, so those flags never reach the host application.
Trigger and chunk location
$DAEDALUS_SISR_MANIFEST— path to a signed remote manifest (RemoteManifest::from_bytes); when unset the launcher is stock. Chunk files are read from thechunks/directory next to the manifest (<manifest-dir>/chunks/<64-hex-sha256>), served byDirectoryChunkFetcher.--daedalus-update [URL]— fetches<URL>/manifestand<URL>/chunks/<hex>over HTTPS viaHttpChunkFetcher, resolving the base URL from the positional argument, then$DAEDALUS_UPDATE_URL, then the embeddedupdate_url. The transport is never a trust anchor: the manifest is signature-verified and every chunk is SHA-256-verified by the engine before it is written.- When every chunk is already present in the running binary, no chunk file is touched and the server may not even be asked for chunks.
Security ordering (non-negotiable)
The manifest is authenticated before a single byte is written:
RemoteManifest::verify_any(trusted_keys)— the Ed25519 signature overmerkle_root ‖ manifest_bytesmust verify against at least one key in the trusted-keys directory (same directory the embedded binary signature uses, seeload_trusted_keys).RemoteManifest::verify_merkle()— the stated Merkle root must match the chunk table, so a signer error cannot smuggle a mismatched root.- Per-chunk, inside the engine: every byte written — whether reused from
the current binary or fetched — must SHA-256 to its
ChunkEntry.hash. A fetched chunk of the wrong length or hash is rejected on the spot. - At rest, on every cold start: the rebuilt binary embeds the manifest
signature from step 1 in its
SisrFooterExt; the launcher re-verifies it offline (signature → Merkle root → per-chunk payload hashes) before extracting. An updated binary therefore keeps its at-rest authenticity instead of degrading to integrity-only. Unsigned SISR sections are refused unlessDAEDALUS_SISR_ALLOW_UNSIGNED=1is set explicitly.
Only then does the engine assemble and atomically swap.
Atomicity guarantees
sisr::swap::AtomicWriter writes to .<tag>.tmp-<pid> in the same directory,
fsyncs, then rename(2)s over the destination:
- an error or an interruption before commit leaves the original binary byte-for-byte intact (the temp file is removed on drop);
- the engine opens and hashes from the canonical path, and the swap is a single atomic rename — there is no "half-updated" state;
- the source file's mode is copied onto the temp file before the rename, so a
replaced binary keeps its executable bit (
File::createalone would yield0o644); - after the swap the launcher re-opens the canonical real path returned by
the engine —
/proc/self/execan keep resolving to the pinned pre-update inode — and continues with the new footer and payload.
Post-update health gate
Atomicity alone does not protect against a valid but broken update. Before
the swap the launcher snapshots the running binary to ./app.daedalus.bak
(same filesystem → atomic restore); after the swap it supervises the new
version for its startup window (DAEDALUS_HEALTH_TIMEOUT_MS, default 10 s):
- the new version exits 0, or is still running when the window closes →
health_storemarks it healthy and the snapshot is discarded; - the new version crashes or exits non-zero → a failure is recorded; once
attempts >= DAEDALUS_HEALTH_MAX_ATTEMPTS(default 3) the version is quarantined and the snapshot is restored, after which the previous version runs; - a quarantined target is refused at the top of the update path — before any snapshot or engine I/O — so a broken release cannot be re-installed in a loop.
Health records are JSON files in ~/.cache/daedalus/health/, keyed by the target
version's content hash. See
Rollback & Resilience for the full
state machine.
What is copied, what is rebuilt
| Piece | Handling |
|---|---|
| stub (0..payload_offset) | copied verbatim from the current binary |
| payload chunks | reused when the current binary already has hash-verified bytes, else fetched + hash-verified |
| metadata | copied verbatim from the current binary (deltas never change metadata) |
DeltaManifest + SisrFooterExt |
rebuilt from the fetched manifest and chunk table |
| footer | rebuilt: flags = (old & ~FLAG_SIGNED) | FLAG_SISR, payload SHA-256 = SHA-256(payload ‖ meta), signature offset zeroed |
SisrFooterExt.signature |
carries over the remote manifest's Ed25519 signature (verified in step 1), so the rebuilt binary stays authentic at rest |
A pre-SISR (legacy) binary is handled too: there is no embedded chunk index, so the engine falls back to fetching every chunk — correct, just not incremental.
Failure table
| Situation | Outcome |
|---|---|
DAEDALUS_SISR_MANIFEST unreadable / bad magic |
launcher exits, binary untouched |
| Manifest URL unreachable / non-2xx | update refused, binary untouched |
No update URL resolvable (--daedalus-update alone) |
update refused with guidance |
| Signature fails (no trusted key verifies) | update refused before any write |
| Merkle root mismatch | update refused before any write |
| Fetched chunk wrong length or SHA-256 | engine errors, binary untouched |
| Embedded SISR section unsigned / untrusted / payload mismatch at cold start | launcher exits (override: DAEDALUS_SISR_ALLOW_UNSIGNED=1) |
Chunk missing from chunks/ (local) or 404s (remote) |
engine errors, binary untouched |
rename fails (read-only dir) |
engine errors, binary untouched |
Power loss / SIGKILL mid-write |
.tmp may remain, binary untouched |
| Update applies, new version crashes at startup | .bak restored atomically, previous version runs, failure recorded |
Version fails DAEDALUS_HEALTH_MAX_ATTEMPTS times |
quarantined; previous version runs |
| Re-install of a quarantined version | refused before any snapshot or write |
Related
- Builder Pipeline — where the manifest and chunks are produced and signed.
- SISR: Self-Incremental Sovereign Reconstruction — trust model and invariants.
.daedalusFormat v2 — SISR extension — byte layout of the remote manifest and the footer extension.- Incremental Updates (SISR) — the end-to-end workflow the launcher completes.