freepdfeditor/docs/adr/0003-memory-safety-posture.md

72 lines
3.4 KiB
Markdown

# 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 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.