|
| 1 | +# ADR-0043 — quick-xml advisory: fork + git-patch bridge (RUSTSEC-2026-0194/0195) |
| 2 | + |
| 3 | +**Status:** accepted **Date:** 2026-07-02 **Issue:** |
| 4 | +[#193](https://github.com/jaunder-org/jaunder/issues/193) |
| 5 | + |
| 6 | +## Context |
| 7 | + |
| 8 | +RUSTSEC-2026-0194 (O(N²) duplicate-attribute check) and RUSTSEC-2026-0195 |
| 9 | +(unbounded namespace-declaration allocation) fail `cargo-deny`'s `advisories` |
| 10 | +check repo-wide. Both are DoS advisories against **quick-xml 0.39.4**, fixed in |
| 11 | +**quick-xml ≥ 0.41.0**. AtomPub POST/PUT bodies are client-supplied |
| 12 | +(authenticated app-password clients), so the exposure is real if authenticated. |
| 13 | + |
| 14 | +quick-xml 0.39.4 enters the graph three ways: transitively via |
| 15 | +`atom_syndication 0.12.8` and `rss 2.0.13`, and as a direct dep of `common` |
| 16 | +(`quick-xml = "0.39"`). |
| 17 | + |
| 18 | +The obvious fixes are all blocked: |
| 19 | + |
| 20 | +- **Bump the crates.** `atom_syndication 0.12.8` / `rss 2.0.13` are the _latest_ |
| 21 | + releases and upstream `master` on both still pins `quick-xml = "0.39"`. No |
| 22 | + release exists that depends on quick-xml ≥ 0.41. |
| 23 | +- **Lock-file bump.** `cargo update -p quick-xml` locks 0 packages; the registry |
| 24 | + has no 0.39.x backport (0.39.4 → 0.41.0 directly), so `^0.39` can never reach |
| 25 | + a fixed version. |
| 26 | +- **`[patch.crates-io]` quick-xml directly.** A patch must satisfy the existing |
| 27 | + requirement; 0.41 does not satisfy `^0.39`. |
| 28 | + |
| 29 | +## Decision |
| 30 | + |
| 31 | +Move the whole tree to quick-xml **0.41** by forking the two syndication crates, |
| 32 | +raising _their_ quick-xml requirement, and wiring the forks in with a git |
| 33 | +`[patch.crates-io]` — **not** by ignoring the advisories. |
| 34 | + |
| 35 | +1. **Fork** `rust-syndication/atom` → `jaunder-org/atom` and |
| 36 | + `rust-syndication/rss` → `jaunder-org/rss`. On a patch branch, change |
| 37 | + `quick-xml = "0.39"` → `"0.41"` (keeping crate versions `0.12.8` / `2.0.13` |
| 38 | + so `[patch]` applies). The touched quick-xml API subset is stable across |
| 39 | + 0.39→0.41 (see spec); expected delta is the version requirement plus at most |
| 40 | + swapping one deprecated `decode_and_unescape_value` call. |
| 41 | +2. **Patch** the workspace: root `Cargo.toml` `[patch.crates-io]` points |
| 42 | + `atom_syndication` and `rss` at the forks at a **pinned rev**; |
| 43 | + `common/Cargo.toml` raises its direct `quick-xml = "0.39"` → `"0.41"`. |
| 44 | + Result: a single quick-xml `0.41.x` in `Cargo.lock`, no 0.39.x remaining, |
| 45 | + advisories cleared with **no ignore**. |
| 46 | +3. **Hermetic Nix build.** The flake builds with crane and runs `cargo-deny` as |
| 47 | + a crane derivation; a sandboxed build cannot fetch a git `[patch]`. Each fork |
| 48 | + is therefore added as a `flake = false` **flake input** (pinned in |
| 49 | + `flake.lock`) and fed to crane's vendor step via `overrideVendorGitCheckout`, |
| 50 | + so the git source is content-addressed and reproducible (cachix-friendly), |
| 51 | + with no build-time network. |
| 52 | +4. **Dependency policy.** `deny.toml` gains `jaunder-org` under |
| 53 | + `[sources.allow-org].github` to authorize the git sources. No |
| 54 | + `[advisories].ignore` entry is added. |
| 55 | +5. **Upstream.** Open PRs against `rust-syndication/{atom,rss}` so the forks are |
| 56 | + a temporary bridge, not a permanent maintenance burden. |
| 57 | + |
| 58 | +## Consequences |
| 59 | + |
| 60 | +- **Positive.** The advisories are cleared _correctly_ — the vulnerable code is |
| 61 | + gone from the tree, not merely silenced. No accepted-risk window. The upstream |
| 62 | + PRs, if merged, benefit the wider ecosystem and let us delete the whole |
| 63 | + apparatus. |
| 64 | +- **Negative / cost.** We carry two forks and a git `[patch]` until upstream |
| 65 | + releases; the flake now has two extra inputs and a crane vendor override — the |
| 66 | + first git `[patch]` in this repo, so it sets the pattern. `deny.toml`'s |
| 67 | + sources policy is loosened for one org. |
| 68 | +- **Reproducibility.** Because the forks are pinned flake inputs (rev + narHash |
| 69 | + in `flake.lock`), the hermetic build stays deterministic and Cachix-cacheable; |
| 70 | + the git patch does not reintroduce network into the sandbox. |
| 71 | + |
| 72 | +## Exit / how to drop this |
| 73 | + |
| 74 | +When `atom_syndication` and `rss` publish releases depending on quick-xml ≥ |
| 75 | +0.41: delete the `[patch.crates-io]` entries, the two flake inputs, and the |
| 76 | +crane `overrideVendorGitCheckout`; raise the `atom_syndication`/`rss` version |
| 77 | +requirements in `common/Cargo.toml` to the fixed releases; remove `jaunder-org` |
| 78 | +from `[sources.allow-org]`; archive the forks. Tracked as a follow-up to #193. |
| 79 | + |
| 80 | +## Staging: a temporary ignore bridges the repo-wide breakage |
| 81 | + |
| 82 | +The advisory fails `cargo-deny` on `main` and every branch, so a scoped |
| 83 | +`[advisories].ignore` of the two IDs is landed **first**, as a standalone hotfix |
| 84 | +(PR #194), purely to make the gate green everywhere while this fix is built. |
| 85 | +That ignore is a short-lived scaffold, not the resolution: the **final step of |
| 86 | +this ADR's work removes it**, and the end state carries no advisory ignore. The |
| 87 | +two phases are deliberately separate PRs so the unblock can merge in minutes |
| 88 | +without waiting on the fork/patch/Nix work. |
| 89 | + |
| 90 | +## Alternatives considered |
| 91 | + |
| 92 | +- **Scoped `deny.toml` ignore as the _permanent_ answer** (referencing #193) |
| 93 | + until upstream releases. Precedented (RUSTSEC-2024-0436, RUSTSEC-2026-0173) |
| 94 | + and the issue's stated fallback; simplest, no Nix work. Rejected as the _end |
| 95 | + state_ because it accepts the (authenticated-only) DoS risk rather than |
| 96 | + eliminating it, and the fork patch is trivial on the code side — so we use it |
| 97 | + only as the temporary bridge above, not the resolution. |
| 98 | +- **Vendored-path `[patch]`** (patched crates checked into `vendor/`). Hermetic |
| 99 | + without git-source handling, but carries two crate sources in-tree for a |
| 100 | + throwaway bridge. |
| 101 | +- **Replace `atom_syndication`/`rss`** with an alternative syndication library |
| 102 | + on quick-xml ≥ 0.41. Largest change, real functional-regression risk in |
| 103 | + AtomPub serialize/parse — disproportionate for a dependency-advisory task. |
0 commit comments