The Launcher (stub)
The Launcher (stub)
The launcher ("stub") is the small program embedded at the head of every
.daedalus. It's the ELF the kernel runs when the user executes
./my_app.daedalus.
- Code:
stub/src/main.rs+daedalus-core/src/format.rs(shared format parser) - Language: Rust, statically compiled for
x86_64-unknown-linux-musl→ zero dynamic dependencies, runs everywhere. - Size: ~600 KB (musl static, opt-level=z, LTO, strip, panic=abort).
Why Rust + musl?
Rust: the launcher executes before any verification, manipulates binary files and makes system calls. A memory error here = vulnerability. Rust eliminates use-after-free, buffer overflows and null dereferences by construction, with no runtime or GC.
musl: glibc is dynamically linked and its versions vary between distros
(GLIBC_2.35 not found...). A static musl binary has zero dynamic
dependencies — it runs on any Linux kernel ≥ 3.8. This is exactly what daedalus
must guarantee for its own launcher.
ldd stub/target/x86_64-unknown-linux-musl/release/daedalus-stub
# → statically linked
Execution flow
./my_app.daedalus
│
1. open("/proc/self/exe") ← reliable self-location
2. read footer (last 92 bytes for v3), validate magic
3. read JSON metadata
4. read payload, verify SHA-256 ← integrity
5. cache hit? → ~/.cache/daedalus/{sha256}/.ready
yes → reuse
no → extract (zstd → tar) to tmp, atomic rename()
6. build argv + env (inject LD_LIBRARY_PATH + PATH)
7. execvp(entrypoint) ← replaces the process
Why /proc/self/exe and not argv[0]?
argv[0] is caller-controlled: a malicious process could launch the launcher
with a fake argv[0]. /proc/self/exe is provided by the kernel and
always points to the real running executable. We always read the correct
file.
Decompression: why ruzstd and not the zstd crate?
The zstd crate binds the C library libzstd, requiring a C compiler for
the musl target. ruzstd is a 100% Rust zstd decompressor: no C
toolchain, trivial static musl build. The launcher only decompresses —
that's exactly ruzstd's scope. Compression (more CPU-intensive) stays
on the builder side via the zstd CLI.
The ${ROOTFS} token in the environment
The builder doesn't know in advance where the cache will be materialized
(~/.cache/daedalus/{sha256}/rootfs). To still declare paths (e.g. PYTHONPATH),
it writes the ${ROOTFS} token in the manifest's environment variables, and
the launcher replaces it with the real path at exec time:
manifest : PYTHONPATH = ${ROOTFS}/app/site-packages
execution: PYTHONPATH = /home/user/.cache/daedalus/f342.../rootfs/app/site-packages
LD_LIBRARY_PATH doesn't need this token: the launcher computes it directly
from the lib* directories present in the rootfs.
PATH injection for bundled binaries
The launcher also injects PATH with rootfs binary directories, using the
same logic as LD_LIBRARY_PATH:
- Pivot mode (isolation ≥ 2):
PATH=usr/bin:bin:usr/local/bin - Non-pivot mode:
PATH={rootfs}/usr/bin:{rootfs}/bin:{rootfs}/usr/local/bin:{existing_PATH}
Bundled entries go before the existing PATH. This is intentional: if
the builder packages ffmpeg 6.1 into the rootfs but the host has
ffmpeg 4.4, the app uses the bundled version. The same priority logic
applies to LD_LIBRARY_PATH — rootfs libs come first.
CWD handling after pivot_root
When isolation level 2 is active, after pivot_root + umount2, the process's
current working directory still points to the old root — which was just
detached. The launcher calls set_current_dir("/") just before execve()
so the app process starts in the new root, ensuring correct resolution of
relative paths and symlinks.
Concurrent access: flock()
If two instances of the same .daedalus start simultaneously on a cold cache,
an exclusive flock() (on ~/.cache/daedalus/{hash}.lock) guarantees only one
performs the extraction; the other waits and then finds the cache ready.
Extraction is already atomic via rename() — flock simply avoids duplicated
work.
What the launcher does at runtime (annotated)
Excerpt from main.rs — the sequence is deliberately linear and readable:
// 1. Locate ourselves reliably (not argv[0], which is caller-controlled).
let mut exe = File::open("/proc/self/exe")?;
let footer = Footer::read_from(&mut exe)?;
// 2-3. Metadata then payload, with integrity verification.
let meta: Metadata = serde_json::from_slice(&meta_bytes)?;
verify_sha256(&payload, &footer.payload_sha256)?;
// 4. Cache: extract once, atomically.
if !ready_marker.exists() {
extract_atomic(&payload, &cache_root, &rootfs)?;
}
// 5. Replace the current process with the app.
exec_app(&meta, &rootfs) // uses execvp for single-service, execve in supervisor