add vs
Jaime Fournier
27a4be5b0edf1380d08498f7a292a725565c7387
new file mode 100644 --- /dev/null +++ b/vs-djbdns.md @@ -0,0 +1,131 @@ +# jerboa-dns vs djbdns (tinydns) — Security Comparison + +## Architecture Overview + +Both are authoritative-only UDP DNS servers using CDB for zone data. jerboa-dns is a faithful port of djbdns's core design (CDB, tdlookup, response builder, zone file format) into Chez Scheme, with a WASM sandboxing layer added on top. + +| | jerboa-dns | djbdns | +|---|---|---| +| Language | Chez Scheme + FFI + WASM | C | +| LOC (application) | ~2,200 (10 .sls files) | ~2,000 | +| LOC (runtime) | ~500K (Chez Scheme + JIT) | ~0 (reimplements string ops) | +| External deps | Chez Scheme, wasmi (optional) | None (not even libc string fns) | + +--- + +## Where jerboa-dns Is Stronger + +### 1. Memory safety in DNS parsing is enforced, not audited + +`protocol.sls` and `cdb.sls` operate entirely on Chez bytevectors. Every `bytevector-u8-ref` and `bytevector-copy!` is bounds-checked by the runtime. A malformed packet that would overflow a buffer in C raises a Scheme exception here. DJB's C code is *carefully* correct, but it's manually correct — the language doesn't enforce it. + +### 2. Integer overflow is impossible in application logic + +CDB hash computation (`cdb.sls:36`), packet length arithmetic, domain label walking — all use Scheme's arbitrary-precision integers with explicit `(bitwise-and h #xffffffff)` masking. In djbdns, these are `uint32` operations where wrapping is implicit and occasionally exploitable. + +### 3. Use-after-free and double-free are structurally impossible + +Response state, CDB reader contents, bytevectors — all garbage collected. djbdns uses a global response buffer and careful lifetime discipline; one mistake is a vulnerability. + +### 4. WASM sandboxing adds defense-in-depth for the parsing hot path + +`wasm-dns.sls` and `wasm-cdb.sls` compile DNS parsing and CDB lookups into WASM bytecode running in 128KB linear memory, completely isolated from the Chez heap. Even if there's a bug in the parser logic, the blast radius is confined to WASM's sandbox. djbdns has no equivalent — a bug in `dns_packet_getname()` is a bug in the same address space as everything else. The WASM fuel metering (10,000,000 fuel per query) also prevents algorithmic DoS via crafted compression pointer loops. + +### 5. Capsicum fd restrictions go beyond djbdns + +`server.sls` applies `cap_rights_limit` to stdin/stdout/stderr, constraining them to specific operations at the kernel level. djbdns does chroot + privdrop but doesn't restrict individual fd capabilities. jerboa-dns also has Landlock filesystem sandboxing on Linux and PID/mount namespace isolation. + +### 6. Atomic zone updates are correctly implemented + +`zone-compiler.sls` writes to `.tmp` then renames — same pattern as djbdns `tinydns-data`. Both get atomicity right (though see issue E below for a subtlety). + +--- + +## Where djbdns Is Stronger + +### 1. Trusted Computing Base is orders of magnitude smaller + +djbdns's TCB is ~2,000 lines of C with zero library dependencies. jerboa-dns's TCB includes the entire Chez Scheme runtime: JIT compiler, garbage collector, FFI bridge, port system, `eval`, and the Jerboa prelude. A vulnerability in any of these is a vulnerability in jerboa-dns. DJB deliberately avoided this tradeoff — his code doesn't even link libc for the critical path. + +### 2. The FFI boundary completely negates Scheme's memory safety + +`server.sls` has 44+ FFI operations: `foreign-alloc`, `foreign-ref`, `foreign-set!`, raw pointer arithmetic. Specific concerns: + +- **Unbounded C string read.** `foreign-cstring->string` (`server.sls:168-174`) scans memory until `\0`. If `inet_ntop` somehow didn't null-terminate, this is an unbounded read — same as C's `strlen`. +- **Manual struct packing.** `make-sockaddr-in` (`server.sls:136-154`) does manual struct packing with hardcoded offsets. One wrong offset is the same bug class as C struct misalignment. +- **Unchecked recv length.** `process-query!` (`server.sls:258-262`) copies from foreign memory to bytevector byte-by-byte. The `pkt-len` parameter comes from `recvfrom`'s return value, but there's no check that it doesn't exceed the 65536-byte recv buffer. +- **Manual foreign memory lifecycle.** `send-response!` (`server.sls:319-327`) allocates foreign memory, copies response into it, calls `sendto`, frees. This is manual C-style memory management with no GC protection. + +### 3. GC pauses make latency non-deterministic + +djbdns has bounded, deterministic response times — no allocator locks, no stop-the-world pauses. jerboa-dns allocates bytevectors per-query (CDB reader, response copies, domain copies). Under heavy load, GC pauses could cause packet drops. DNS amplification attacks could weaponize this. + +### 4. JIT + eval = massive ROP gadget surface + +A Chez Scheme process has a writable JIT code heap and `eval` capability. An attacker who achieves code execution has far more to work with. djbdns as a stripped static binary has a minimal gadget surface. The `enter-landlock-sandbox!` function (`server.sls:485-508`) actually uses `eval` with `interaction-environment` at runtime to load modules — this keeps the full Scheme evaluator live in the process. + +### 5. djbdns never falls back silently to weaker security + +jerboa-dns has extensive fallback logic: if chroot fails, it falls back to chdir (`server.sls:380-393`). If Capsicum fails, it warns and continues. If WASM sandbox fails, it falls back to Scheme parsing. If namespace isolation fails, it continues without. djbdns exits on failure — it won't run in a degraded security posture. This is a deliberate DJB design philosophy: fail hard, don't degrade. + +### 6. The recv buffer is oversized relative to the protocol limit + +`server.sls:425` allocates a 65536-byte recv buffer, but DNS UDP is limited to 512 bytes (`MAX-PACKET`). The buffer is fine for receiving, but the gap means processing code must consistently enforce the 512 limit. djbdns uses a 512-byte buffer — the limit is structural, not checked. + +--- + +## Specific Issues Found in Review + +### A. Silent fallback chain is a security smell + +The server happily runs with: no chroot (just chdir), no Capsicum, no Landlock, no namespaces, no WASM parser. An operator deploying this might assume they have sandboxing when they actually have none. djbdns either sandboxes or exits. + +### B. CNAME following has no depth limit + +`lookup.sls:118-119` says "one level only" in the comment, but `add-answer-records!` calls itself recursively via the CNAME branch. If a zone has `A CNAME B, B CNAME C, C CNAME A`, this is unbounded recursion leading to stack overflow. djbdns has an explicit depth counter. + +### C. No `setgroups()` before `setgid()` + +`server.sls:398` calls `setgid` but not `setgroups(0, NULL)`. Supplementary groups from the root-launched process survive privilege drop, potentially granting unintended filesystem access. djbdns calls `setgroups`. + +### D. Temp file for program loading + +`static/main.c:91-112` writes the compiled program to `/tmp/jdns-prog-XXXXXX`. This happens before chroot/privdrop. If an attacker can race or predict the tempfile, they could potentially swap it. The musl build (`build-jdns-musl.ss:254-269`) uses `memfd_create` which is better, but falls back to `/tmp/.jdns-program.so` if memfd is unavailable. + +### E. Zone compiler atomic swap has a TOCTOU window + +`zone-compiler.sls:318-320` does `delete-file` then `rename-file` instead of a direct `rename` (which atomically replaces the target on POSIX). The delete+rename sequence creates a brief window where `data.cdb` doesn't exist. djbdns uses a single `rename()`. + +### F. No way to require WASM parsing + +The `*use-wasm-parser*` flag is a mutable global. If WASM init fails, the server falls back to Scheme parsing silently. There's no way for an operator to require WASM parsing and fail if it's unavailable. + +--- + +## Summary Table + +| Dimension | jerboa-dns | djbdns | Edge | +|---|---|---|---| +| Buffer overflow in parsing | Impossible (bytevector bounds) | Manually prevented | jerboa-dns | +| Integer overflow | Impossible (bignums) | Manually prevented | jerboa-dns | +| Use-after-free | Impossible (GC) | Manually prevented | jerboa-dns | +| FFI/socket code safety | Same as C | Same | Tie | +| Trusted computing base | ~500K LOC runtime | ~2K LOC, no deps | djbdns | +| Fails-closed on error | Degrades silently | Exits | djbdns | +| Latency determinism | GC pauses | Deterministic | djbdns | +| Exploit gadget surface | JIT heap + eval | Stripped static | djbdns | +| WASM parsing sandbox | Yes (128KB isolate) | No equivalent | jerboa-dns | +| Capsicum/Landlock | Yes (beyond djbdns) | chroot + privdrop only | jerboa-dns | +| CNAME loop protection | Unbounded recursion | Depth-limited | djbdns | +| Supplementary groups | Not dropped | Dropped | djbdns | +| Atomic zone update | delete+rename (TOCTOU) | Single rename | djbdns | + +--- + +## Bottom Line + +jerboa-dns eliminates entire vulnerability classes (buffer overflow, integer overflow, use-after-free) in the DNS application logic. The WASM sandbox adds a layer djbdns simply doesn't have. But the massive runtime TCB, silent security degradation, FFI boundary that's as unsafe as C, and a few implementation gaps (CNAME depth, setgroups, atomic rename) mean it doesn't dominate djbdns on security. + +They're different tradeoffs: djbdns minimizes attack surface through extreme simplicity; jerboa-dns maximizes memory safety through language-level guarantees and sandboxing. + +The issues in A through F above are all fixable and would meaningfully close the gap.