7.0 KiB
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; 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:
ctypesand runtime FFI — Python calling into a danos library without rebuilding the interpreter. This is the piece that makes Python prototyping self-serve: drop a.soon the image,ctypes.CDLLit, iterate.- Loadable CPython extension modules — today every C extension means
relinking the interpreter (
Modules/Setup); withdlopen, an extension is a file. - One interpreter image, many Python services — a statically-linked CPython
is tens of megabytes per process. A shared
libpythonmapped 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 -sharedagainst the libdanos-c sysroot already emits them. The work is entirely on the loading side. - The loader lives in userspace, inside the libc.
dlopenreads the.sothrough the VFS, maps its segments, applies relocations, resolves symbols against the process and theDT_NEEDEDdependency graph, runs constructors, returns a handle. No kernel loader changes in v1 — segments land in anonymousmmapas private copies. - Bind-now, always. All relocations resolved at
dlopentime (RTLD_NOWsemantics 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 danosmmapsurface 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
.sowith 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" — noPT_INTERP, no program interpreter, no dynamically-linkedmainbinaries. That keeps process startup untouched.
Milestones
- D1 — dlopen in-process. The
.sobuild 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: QEMUdlopen-hello— load a.so, call a symbol, unload, reload. - D2 — the FFI payoff.
DT_NEEDEDdependency graphs; a libffi port (x86-64 SysV assembly is upstream; the port is its closure-allocation paths, which must respect W^X); CPython'sctypesenabled; extension modules loadable from file. Test: QEMU — a Python scriptctypes.CDLLs a danos.soand round-trips a call; a.soextension module imports. - D3 — actual sharing (the kernel milestone). Shared read-only file-backed
mappings — a page-cache-shaped facility so N processes mapping
libpythonhold 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. - 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.
dlopenturns 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. dlcloseis where loaders go to die. Constructors/destructors, dangling function pointers, re-open identity. Keep v1 semantics honest and simple:dlcloseruns destructors and unmaps; holding pointers past it is undefined; no reference-counted deferral cleverness.- The ReleaseSafe fact still applies to
.sos — 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 — the sysroot the
loader ships in; its absence table gains
dlfcn.hat D1. - python-on-danos.md — the
ctypesstory this unlocks. - python-on-danos-milestones.md — sequencing; this work is post-P5.
- os-development/memory-map.md / os-development/paging.md — where W^X and shared mappings land.