# Dynamic libraries on danos A design note and milestone plan for shared objects: building them, loading them with `dlopen`, and — the part that needs kernel work — actually *sharing* them between processes. Directional, post-P5 of [python-on-danos-milestones.md](python-on-danos-milestones.md); nothing on the CPython bring-up path depends on it. ## Reconciling the earlier "rejected" Dynamic libraries were evaluated once before and rejected — but as an answer to a *different question*: whether they could claw back ReleaseSafe's measured ~2× code size. They cannot (the safety checks inline at every call site; no library scheme dedups them), and that verdict stands for that question. The reasons to build them now are the ones that investigation never weighed: - **`ctypes` and runtime FFI** — Python calling into a danos library without rebuilding the interpreter. This is the piece that makes Python prototyping self-serve: drop a `.so` on the image, `ctypes.CDLL` it, iterate. - **Loadable CPython extension modules** — today every C extension means relinking the interpreter (`Modules/Setup`); with `dlopen`, an extension is a file. - **One interpreter image, many Python services** — a statically-linked CPython is tens of megabytes *per process*. A shared `libpython` mapped read-only once (milestone D3 below) makes Python services cheap enough to be the default way to prototype one. - **Plugin-shaped applications** — the UI toolkit and the terminal will want them eventually. The scoping that dissolves the apparent contradiction is the **size doctrine**: leanness is an *operating-system* property — the kernel and system services stay small and statically linked, and none of them ever link the loader — while *applications* have their own budget and may be big. Dynamic libraries are an **application-layer facility**, full stop. What also does **not** change: the public ABI stays the vDSO + the IPC protocols. Shared objects are artifacts *within* one system image, versioned by the build — not a new stable ABI surface for the OS. ## Design - **Format and codegen are free.** ELF shared objects with position-independent code; `zig cc -fPIC -shared` against the [libdanos-c](c-library-compatibility.md) sysroot already emits them. The work is entirely on the loading side. - **The loader lives in userspace, inside the libc.** `dlopen` reads the `.so` through the VFS, maps its segments, applies relocations, resolves symbols against the process and the `DT_NEEDED` dependency graph, runs constructors, returns a handle. No kernel loader changes in v1 — segments land in anonymous `mmap` as private copies. - **Bind-now, always.** All relocations resolved at `dlopen` time (`RTLD_NOW` semantics only). Lazy PLT binding buys startup latency danos does not care about, at the price of a writable GOT dance and a much subtler loader. Not worth it; keep it out permanently. - **W^X from day one.** Map, relocate, then flip text pages read-execute — which requires memory-protection change (`mprotect`-shaped) in the danos `mmap` surface if it is not already there. No page is ever writable and executable at once. - **TLS in shared objects is deferred.** Thread-local storage models (initial-exec vs. general-dynamic) are the deep end of every dynamic linker. v1 refuses a `.so` with a TLS segment; revisit alongside the post-P5 pthread subset, which is when it could matter. - **Executables stay static until D4.** v1 is "a static binary that can `dlopen`" — no `PT_INTERP`, no program interpreter, no dynamically-linked `main` binaries. That keeps process startup untouched. ## Milestones 1. **D1 — dlopen in-process.** The `.so` build target; the loader in libdanos-c: map, relocate (`RELATIVE`/`GLOB_DAT`/`JUMP_SLOT`), resolve, constructors; `dlopen`/`dlsym`/`dlerror`/`dlclose`; private anonymous mappings; no TLS. *Test:* QEMU `dlopen-hello` — load a `.so`, call a symbol, unload, reload. 2. **D2 — the FFI payoff.** `DT_NEEDED` dependency graphs; a **libffi port** (x86-64 SysV assembly is upstream; the port is its closure-allocation paths, which must respect W^X); CPython's `ctypes` enabled; extension modules loadable from file. *Test:* QEMU — a Python script `ctypes.CDLL`s a danos `.so` and round-trips a call; a `.so` extension module imports. 3. **D3 — actual sharing (the kernel milestone).** Shared read-only file-backed mappings — a page-cache-shaped facility so N processes mapping `libpython` hold one physical copy. This is the memory-win milestone and the only one touching the kernel; design it with the existing shm machinery in view (the shared-fate walks already locked the relevant paths). *Test:* N Python services up; measure physical pages against N× the static baseline. 4. **D4 — dynamically-linked executables** (optional, evaluate after D3): `PT_INTERP`, a danos program interpreter, and the spawn path teaching the loader about it. Only worth it if the image-size or update story demands it. ## Risks and gotchas - **Scope creep is the failure mode.** Every dynamic linker grows toward glibc. The fences: bind-now only, no lazy binding ever, no TLS until pthreads demand it, no dlopen-from-memory, no versioned symbols. Each fence removed is a design discussion, not a patch. - **Code loading is a security event.** `dlopen` turns file bytes into executable code, so W^X discipline is table stakes and *what may be dlopened* is a capability question — the natural danos answer is that loadability follows VFS readability of the `.so`, and services' images are supervised like any other artifact. Revisit explicitly at D3 when mappings become shared. - **`dlclose` is where loaders go to die.** Constructors/destructors, dangling function pointers, re-open identity. Keep v1 semantics honest and simple: `dlclose` runs destructors and unmaps; holding pointers past it is undefined; no reference-counted deferral cleverness. - **The ReleaseSafe fact still applies to `.so`s** — a ReleaseSafe shared object carries its inlined checks like any static code; D3's sharing saves *copies*, not check overhead. Size expectations should be set accordingly. ## Decisions needing sign-off - Dynamic libraries join the roadmap at all (this note exists because the earlier size-motivated rejection was re-opened for ABI/sharing reasons). - **Bind-now only; no lazy binding, permanently.** - **Loader in userspace libc; kernel involvement only at D3** (shared read-only mappings). - **Static executables until D4**, and D4 only on demonstrated need. ## Related - [c-library-compatibility.md](c-library-compatibility.md) — the sysroot the loader ships in; its absence table gains `dlfcn.h` at D1. - [python-on-danos.md](python-on-danos.md) — the `ctypes` story this unlocks. - [python-on-danos-milestones.md](python-on-danos-milestones.md) — sequencing; this work is post-P5. - [os-development/memory-map.md](os-development/memory-map.md) / [os-development/paging.md](os-development/paging.md) — where W^X and shared mappings land.