# ADR-0003 — Memory safety posture for the parser layers * **Status**: Accepted (decision on Rust-vs-C++ for L1/L2 leaf decoders is *deferred to end of M0* per §15; this ADR records the posture that holds regardless of that decision) * **Date**: 2025-07-25 * **Plan reference**: §7.2, §15 ## Context We are writing a new PDF parser in C++. The threat model (§7.1) identifies parser memory corruption as the main risk: a hostile PDF is the vector, and the user's machine, other files, and network are the assets. "Be careful" is not a posture; the decision must be made before parsing code lands because it shapes the build flags, the allocator, the allowed language subset, and whether a second language enters the tree. A separate decision — whether L1/L2 leaf decoders (filters, image codec glue, CMap parsing) are written in Rust behind a C ABI — is explicitly deferred to the end of M0 (§15). This ADR records the posture that holds in either case and the deadline for that decision. ## Decision **Hardening on in release builds, not just debug.** The build applies, in *release*: * `std::span`-like bounded views for all parsing; raw pointer arithmetic over input bytes is banned in the parsing layers, enforced by clang-tidy and review (§13.2 rule 7). * `-D_GLIBCXX_ASSERTIONS=1` / `_LIBCPP_HARDENING_MODE=fast`, `-fstack-protector-strong`, CFI and shadow-stack/CET where available — applied via `cmake/FreePDFEditorHardening.cmake` to every target. * A hardened allocator (scudo or hardened_malloc) in the document process. * Integer overflow is a build error in the parsing layers (UBSan in CI, checked arithmetic helpers in code). * Resource budgets enforced at the L2 boundary — decompressed stream size, nesting depth, object count, total resident bytes — so a decompression bomb aborts with a diagnostic rather than dying by OOM-killer. **The Rust-vs-C++ decision for L1/L2 leaf decoders is made once, at the end of M0** (§15). It is not revisited at M4 — retrofitting a second language mid-project is worse than either choice made early. The decision is gated on the M0 spike results and on whether the leaf decoders' CVE history justifies the build-friction cost of a second language. ## Consequences **Positive.** The attack surface is hardened by construction regardless of the Rust decision. The build flags are committed (in `cmake/FreePDFEditorHardening.cmake`) so they cannot drift quietly. **Negative.** Release builds carry a runtime cost from the assertions and CFI; the performance budget (§5) must absorb it. The hardened allocator is slower than the system allocator; the document process's allocator choice is separate from the UI process's for this reason. **Neutral.** The M0 spikes run under these same flags, so the feasibility-gate measurements reflect the real build, not an unhardened optimistic run. ## Alternatives considered * **Hardening in debug only** — rejected; the release build is what users run and what an attacker targets. * **Rewrite the whole parser in Rust** — rejected at this scope; the UI, Qt integration, and layout engine are C++, and a mixed-language tree has real build and contributor cost. The leaf-decoder decision is the narrowest place a second language buys the most. * **Rely on the sandbox alone** — rejected; sandbox escape via the IPC boundary is in the threat model (§7.1), so defence in depth is required.