Skip to content

Commit 25333b0

Browse files
authored
Merge pull request #200 from jaunder-org/worktree-issue-193-quick-xml-advisory
deps(#193): quick-xml >= 0.41 via atom_syndication/rss forks (clear RUSTSEC-2026-0194/0195)
2 parents f5a62b4 + 34c0345 commit 25333b0

10 files changed

Lines changed: 465 additions & 20 deletions

Cargo.lock

Lines changed: 4 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ js-sys = "0.3.82"
7878
urlencoding = "2"
7979
percent-encoding = "2"
8080

81+
# TEMPORARY (jaunder #193 / ADR-0043): route atom_syndication and rss to forks that
82+
# depend on quick-xml >= 0.41, clearing RUSTSEC-2026-0194/0195. Upstream's latest
83+
# releases (atom_syndication 0.12.8 / rss 2.0.13) hard-pin quick-xml ^0.39 and no
84+
# 0.39.x backport exists, so [patch] to quick-xml directly is impossible; the forks
85+
# raise the requirement instead. The fork checkouts are vendored hermetically into the
86+
# Nix build via flake inputs + crane overrideVendorGitCheckout (see flake.nix). Remove
87+
# this block once upstream publishes releases on quick-xml >= 0.41.
88+
[patch.crates-io]
89+
atom_syndication = { git = "https://github.com/jaunder-org/atom.git", rev = "2462e3798295047ba35078b9634bcd129e887ffe" }
90+
rss = { git = "https://github.com/jaunder-org/rss.git", rev = "60b2a81445160af85ab94a23774c74e6616decfe" }
91+
8192
# See https://github.com/leptos-rs/cargo-leptos for documentation of all the parameters.
8293

8394
# A leptos project defines which workspace members

common/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ serde = { workspace = true, features = ["derive"] }
1616
thiserror.workspace = true
1717
rss = { version = "2", default-features = false, features = ["builders", "atom"] }
1818
atom_syndication = { version = "0.12", default-features = false }
19-
quick-xml = "0.39"
19+
quick-xml = "0.41"
2020
serde_json = "1"
2121
sha2 = "0.10"
2222
unicode-normalization = "0.1"

deny.toml

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,6 @@ feature-depth = 1
7070
# A list of advisory IDs to ignore. Note that ignored advisories will still
7171
# output a note when they are encountered.
7272
ignore = [
73-
# TEMPORARY SCAFFOLD (jaunder #193) — remove once the tree is on quick-xml >= 0.41
74-
# via the atom_syndication/rss forks (ADR-0042). Present only so the verify gate is
75-
# green repo-wide while that fix lands; the fork/[patch] PR removes these two lines.
76-
# quick-xml 0.39.4 (transitive via atom_syndication 0.12.8 / rss 2.0.13) is DoS-only
77-
# and reachable solely through authenticated app-password AtomPub clients. Both fixed
78-
# in quick-xml >= 0.41.0.
79-
{ id = "RUSTSEC-2026-0194", reason = "TEMPORARY (jaunder #193): quick-xml 0.39.4 O(N^2) attribute check; removed once the atom_syndication/rss forks move the tree to quick-xml >= 0.41 (ADR-0042)." },
80-
{ id = "RUSTSEC-2026-0195", reason = "TEMPORARY (jaunder #193): quick-xml 0.39.4 unbounded namespace-declaration allocation; removed once the atom_syndication/rss forks move the tree to quick-xml >= 0.41 (ADR-0042)." },
8173
{ id = "RUSTSEC-2024-0436", reason = "The issue seems overblown, original author simply sees no future development required." },
8274
{ id = "RUSTSEC-2026-0173", reason = "proc-macro-error2 is unmaintained but is a compile-time-only (proc-macro) transitive dep pulled in solely by leptos 0.8.x macro crates (leptos_macro, leptos_router_macro, reactive_stores_macro, rstml); it never ships in the binary. No leptos 0.8.x release drops it. Remove this ignore once leptos drops the dependency (likely a 0.9 upgrade) — see jaunder-0eir." },
8375
#"RUSTSEC-0000-0000",
@@ -251,8 +243,11 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"]
251243
allow-git = []
252244

253245
[sources.allow-org]
254-
# github.com organizations to allow git sources for
255-
github = []
246+
# github.com organizations to allow git sources for.
247+
# TEMPORARY (jaunder #193 / ADR-0043): jaunder-org hosts the atom_syndication/rss forks
248+
# on quick-xml >= 0.41 that the root Cargo.toml [patch.crates-io] points at. Remove once
249+
# upstream releases and the [patch] is dropped.
250+
github = ["jaunder-org"]
256251
# gitlab.com organizations to allow git sources for
257252
gitlab = []
258253
# bitbucket.org organizations to allow git sources for

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ convention). All are currently `accepted`.
6868
| [0040](adr/0040-web-rendering-leptos-csr.md) | Web rendering: leptos-CSR (drop concurrent reactive SSR) | accepted |
6969
| [0041](adr/0041-public-projector-and-csr-client.md) | Public projector and CSR client (SSR the data, not the components) | accepted |
7070
| [0042](adr/0042-emacs-org-atom-mapping-struct-seam.md) | Emacs org→atom mapping: struct seam, `dom-print` serialization, Emacs 29.1 floor | accepted |
71+
| [0043](adr/0043-quick-xml-fork-patch.md) | quick-xml advisory: fork + git-patch bridge (RUSTSEC-2026-0194/0195) | accepted |
7172

7273
## Archive
7374

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Plan — issue #193: quick-xml ≥ 0.41 via atom_syndication/rss forks
2+
3+
**Spec:**
4+
[2026-07-02-issue-193-quick-xml-advisory.md](../specs/2026-07-02-issue-193-quick-xml-advisory.md)
5+
**ADR:** [0043](../../adr/0043-quick-xml-fork-patch.md) **Branch:**
6+
`worktree-issue-193-quick-xml-advisory`
7+
8+
Each task is one clean, gate-verified commit (`cargo xtask check` green) unless
9+
marked **(external)** — GitHub/fork operations that produce no jaunder-repo
10+
commit.
11+
12+
## Prerequisite (before any commit here)
13+
14+
- [x] **P0. Rebase onto main once hotfix PR #194 has merged.** #194 lands the
15+
temporary `deny.toml` ignore of RUSTSEC-2026-0194/0195 on `main`. Rebase
16+
this branch onto the updated `main` so the ignore is inherited and the
17+
verify gate is green during development. (Until then, every commit here
18+
would fail `cargo-deny`.) The design docs (spec, ADR-0043, README row) are
19+
already staged and become Task 1's commit.
20+
21+
## Tasks
22+
23+
- [x] **1. Land the design docs.** Commit the staged spec,
24+
`docs/adr/0043-quick-xml-fork-patch.md`, and the `docs/README.md`
25+
ADR-table row. Pure docs; gate green.
26+
27+
- [x] **2. (external) Create + patch the forks.**
28+
- `gh repo fork rust-syndication/atom --org jaunder-org` and
29+
`… rust-syndication/rss --org jaunder-org`.
30+
- On each fork, branch `quick-xml-0.41`; set `quick-xml = "0.41"` (keep
31+
`features = ["encoding"]`); keep crate versions `0.12.8` / `2.0.13` so
32+
`[patch]` applies. `rss`'s `atom_syndication` dep already tracks `0.12.8`
33+
leave it (the `[patch]` swaps the source, versions still satisfy).
34+
- Build + run each fork's own test suite (`cargo test`) against quick-xml
35+
0.41; fix any hard breakage. Expected delta: the version bump only;
36+
optionally swap the single deprecated `decode_and_unescape_value` call →
37+
`decoded_and_normalized_value` to keep the upstream PR warning-clean. **Do
38+
not** open upstream PRs yet (Task 6).
39+
- Push the branches; **record the exact commit rev of each** (needed for
40+
pinning).
41+
42+
- [x] **3. Wire the forks into the workspace + hermetic Nix vendoring (one
43+
atomic commit).** This must be a single commit: the Cargo-level `[patch]`
44+
alone would break the hermetic Nix build until the vendoring lands, so
45+
they land together to keep the gate green.
46+
- Root `Cargo.toml`: `[patch.crates-io]` `atom_syndication` and `rss` → the
47+
git forks at the pinned revs from Task 2.
48+
- `common/Cargo.toml`: `quick-xml = "0.39"``"0.41"`.
49+
- Regenerate `Cargo.lock`; confirm a **single** quick-xml `0.41.x` and **no**
50+
0.39.x (`cargo tree -i quick-xml`).
51+
- `flake.nix`: add each fork as a `flake = false` input; feed both to crane's
52+
vendor step via `overrideVendorGitCheckout` so the `git+https://…?rev=…`
53+
sources resolve from the pinned flake inputs (no build-time network).
54+
`flake.lock`: pin the inputs.
55+
- `deny.toml`: add `jaunder-org` under `[sources.allow-org].github`.
56+
- **Verify (risk-retirement — this is the spike):** `cargo xtask check` green
57+
**and** the hermetic Nix path resolves the git patch — e.g.
58+
`nix build .#checks.<sys>.deny` and the app derivation build succeed with no
59+
network in the sandbox. If crane vendoring of the git source proves
60+
intractable, STOP and reassess (fallback options in ADR-0043: vendored-path
61+
`[patch]`, or scoped-ignore-as-end-state) before proceeding.
62+
63+
- [x] **4. Verify AtomPub round-trip (no functional regression).** Run
64+
`common`'s atompub tests and the elisp live-integration suite; confirm
65+
serialize/parse behaviour is unchanged on quick-xml 0.41. If green with no
66+
code change, this may fold into Task 3's verification rather than a
67+
separate commit; otherwise commit any fixups.
68+
69+
- [x] **5. Remove the temporary advisory ignore (the climax).** Delete the
70+
RUSTSEC-2026-0194/0195 entries (and their scaffold comment) from
71+
`deny.toml` `[advisories].ignore` — the tree is now on quick-xml 0.41, so
72+
the advisories genuinely no longer apply. Verify
73+
`cargo deny check advisories` passes **with no ignore**, then full
74+
`cargo xtask check` / `validate --no-e2e` green. Commit. End state: no
75+
advisory ignore, single quick-xml 0.41.x.
76+
77+
- [x] **6. (external, user-gated) File follow-up + open upstream PRs.**
78+
- File a fresh GitHub issue (`jaunder-issues`): _"Drop quick-xml git
79+
`[patch]` + forks once atom_syndication/rss publish releases on quick-xml ≥
80+
0.41"_ — the drop-fork tracker (per resolved decision #3). It records the
81+
exit steps from ADR-0043.
82+
- **After** the user reviews the local green result, open PRs to
83+
`rust-syndication/{atom,rss}` from the fork branches (outward-facing —
84+
confirm first).
85+
86+
## Ship (jaunder-ship, after plan execution + user go)
87+
88+
Final review, archive spec/plan, push branch, open the #193 PR (closes #193),
89+
merge (halt). The follow-up issue from Task 6 stays open as the drop-fork
90+
tracker.
91+
92+
## Notes / risks
93+
94+
- **The one real unknown is Task 3's Nix git-vendoring** (first git `[patch]` in
95+
this repo). Everything else is mechanical. Task 3 is deliberately the spike.
96+
- Coverage cache: `.ts` edits aren't involved, but Cargo.lock/flake changes will
97+
rebuild Nix derivations (cachix-warm). Run `cargo clean` on cadence if the
98+
sweep is long.
99+
- After #194 merges, other branches also rebase; no coordination needed beyond
100+
P0.

0 commit comments

Comments
 (0)