3.4 KiB
ADR-0003 — Memory safety posture for the parser layers
- Status: Accepted. The Rust-vs-C++ decision deferred here is resolved by ADR-0005 (Rust for the image-codec leaf decoders; C++ elsewhere).
- Date: 2025-07-25
- Plan reference: §7.2, §15
- Superseded by: the deferred-decision portion is superseded by ADR-0005; the hardening posture below stands.
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 viacmake/FreePDFEditorHardening.cmaketo 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.