72 lines
3.4 KiB
Markdown
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. |