Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ js-sys = "0.3.82"
urlencoding = "2"
percent-encoding = "2"

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

# See https://github.com/leptos-rs/cargo-leptos for documentation of all the parameters.

# A leptos project defines which workspace members
Expand Down
2 changes: 1 addition & 1 deletion common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ serde = { workspace = true, features = ["derive"] }
thiserror.workspace = true
rss = { version = "2", default-features = false, features = ["builders", "atom"] }
atom_syndication = { version = "0.12", default-features = false }
quick-xml = "0.39"
quick-xml = "0.41"
serde_json = "1"
sha2 = "0.10"
unicode-normalization = "0.1"
Expand Down
15 changes: 5 additions & 10 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,6 @@ feature-depth = 1
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
# TEMPORARY SCAFFOLD (jaunder #193) — remove once the tree is on quick-xml >= 0.41
# via the atom_syndication/rss forks (ADR-0042). Present only so the verify gate is
# green repo-wide while that fix lands; the fork/[patch] PR removes these two lines.
# quick-xml 0.39.4 (transitive via atom_syndication 0.12.8 / rss 2.0.13) is DoS-only
# and reachable solely through authenticated app-password AtomPub clients. Both fixed
# in quick-xml >= 0.41.0.
{ 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)." },
{ 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)." },
{ id = "RUSTSEC-2024-0436", reason = "The issue seems overblown, original author simply sees no future development required." },
{ 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." },
#"RUSTSEC-0000-0000",
Expand Down Expand Up @@ -251,8 +243,11 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []

[sources.allow-org]
# github.com organizations to allow git sources for
github = []
# github.com organizations to allow git sources for.
# TEMPORARY (jaunder #193 / ADR-0043): jaunder-org hosts the atom_syndication/rss forks
# on quick-xml >= 0.41 that the root Cargo.toml [patch.crates-io] points at. Remove once
# upstream releases and the [patch] is dropped.
github = ["jaunder-org"]
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ convention). All are currently `accepted`.
| [0040](adr/0040-web-rendering-leptos-csr.md) | Web rendering: leptos-CSR (drop concurrent reactive SSR) | accepted |
| [0041](adr/0041-public-projector-and-csr-client.md) | Public projector and CSR client (SSR the data, not the components) | accepted |
| [0042](adr/0042-emacs-org-atom-mapping-struct-seam.md) | Emacs org→atom mapping: struct seam, `dom-print` serialization, Emacs 29.1 floor | accepted |
| [0043](adr/0043-quick-xml-fork-patch.md) | quick-xml advisory: fork + git-patch bridge (RUSTSEC-2026-0194/0195) | accepted |

## Archive

Expand Down
103 changes: 103 additions & 0 deletions docs/adr/0043-quick-xml-fork-patch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# ADR-0043 — quick-xml advisory: fork + git-patch bridge (RUSTSEC-2026-0194/0195)

**Status:** accepted **Date:** 2026-07-02 **Issue:**
[#193](https://github.com/jaunder-org/jaunder/issues/193)

## Context

RUSTSEC-2026-0194 (O(N²) duplicate-attribute check) and RUSTSEC-2026-0195
(unbounded namespace-declaration allocation) fail `cargo-deny`'s `advisories`
check repo-wide. Both are DoS advisories against **quick-xml 0.39.4**, fixed in
**quick-xml ≥ 0.41.0**. AtomPub POST/PUT bodies are client-supplied
(authenticated app-password clients), so the exposure is real if authenticated.

quick-xml 0.39.4 enters the graph three ways: transitively via
`atom_syndication 0.12.8` and `rss 2.0.13`, and as a direct dep of `common`
(`quick-xml = "0.39"`).

The obvious fixes are all blocked:

- **Bump the crates.** `atom_syndication 0.12.8` / `rss 2.0.13` are the _latest_
releases and upstream `master` on both still pins `quick-xml = "0.39"`. No
release exists that depends on quick-xml ≥ 0.41.
- **Lock-file bump.** `cargo update -p quick-xml` locks 0 packages; the registry
has no 0.39.x backport (0.39.4 → 0.41.0 directly), so `^0.39` can never reach
a fixed version.
- **`[patch.crates-io]` quick-xml directly.** A patch must satisfy the existing
requirement; 0.41 does not satisfy `^0.39`.

## Decision

Move the whole tree to quick-xml **0.41** by forking the two syndication crates,
raising _their_ quick-xml requirement, and wiring the forks in with a git
`[patch.crates-io]` — **not** by ignoring the advisories.

1. **Fork** `rust-syndication/atom` → `jaunder-org/atom` and
`rust-syndication/rss` → `jaunder-org/rss`. On a patch branch, change
`quick-xml = "0.39"` → `"0.41"` (keeping crate versions `0.12.8` / `2.0.13`
so `[patch]` applies). The touched quick-xml API subset is stable across
0.39→0.41 (see spec); expected delta is the version requirement plus at most
swapping one deprecated `decode_and_unescape_value` call.
2. **Patch** the workspace: root `Cargo.toml` `[patch.crates-io]` points
`atom_syndication` and `rss` at the forks at a **pinned rev**;
`common/Cargo.toml` raises its direct `quick-xml = "0.39"` → `"0.41"`.
Result: a single quick-xml `0.41.x` in `Cargo.lock`, no 0.39.x remaining,
advisories cleared with **no ignore**.
3. **Hermetic Nix build.** The flake builds with crane and runs `cargo-deny` as
a crane derivation; a sandboxed build cannot fetch a git `[patch]`. Each fork
is therefore added as a `flake = false` **flake input** (pinned in
`flake.lock`) and fed to crane's vendor step via `overrideVendorGitCheckout`,
so the git source is content-addressed and reproducible (cachix-friendly),
with no build-time network.
4. **Dependency policy.** `deny.toml` gains `jaunder-org` under
`[sources.allow-org].github` to authorize the git sources. No
`[advisories].ignore` entry is added.
5. **Upstream.** Open PRs against `rust-syndication/{atom,rss}` so the forks are
a temporary bridge, not a permanent maintenance burden.

## Consequences

- **Positive.** The advisories are cleared _correctly_ — the vulnerable code is
gone from the tree, not merely silenced. No accepted-risk window. The upstream
PRs, if merged, benefit the wider ecosystem and let us delete the whole
apparatus.
- **Negative / cost.** We carry two forks and a git `[patch]` until upstream
releases; the flake now has two extra inputs and a crane vendor override — the
first git `[patch]` in this repo, so it sets the pattern. `deny.toml`'s
sources policy is loosened for one org.
- **Reproducibility.** Because the forks are pinned flake inputs (rev + narHash
in `flake.lock`), the hermetic build stays deterministic and Cachix-cacheable;
the git patch does not reintroduce network into the sandbox.

## Exit / how to drop this

When `atom_syndication` and `rss` publish releases depending on quick-xml ≥
0.41: delete the `[patch.crates-io]` entries, the two flake inputs, and the
crane `overrideVendorGitCheckout`; raise the `atom_syndication`/`rss` version
requirements in `common/Cargo.toml` to the fixed releases; remove `jaunder-org`
from `[sources.allow-org]`; archive the forks. Tracked as a follow-up to #193.

## Staging: a temporary ignore bridges the repo-wide breakage

The advisory fails `cargo-deny` on `main` and every branch, so a scoped
`[advisories].ignore` of the two IDs is landed **first**, as a standalone hotfix
(PR #194), purely to make the gate green everywhere while this fix is built.
That ignore is a short-lived scaffold, not the resolution: the **final step of
this ADR's work removes it**, and the end state carries no advisory ignore. The
two phases are deliberately separate PRs so the unblock can merge in minutes
without waiting on the fork/patch/Nix work.

## Alternatives considered

- **Scoped `deny.toml` ignore as the _permanent_ answer** (referencing #193)
until upstream releases. Precedented (RUSTSEC-2024-0436, RUSTSEC-2026-0173)
and the issue's stated fallback; simplest, no Nix work. Rejected as the _end
state_ because it accepts the (authenticated-only) DoS risk rather than
eliminating it, and the fork patch is trivial on the code side — so we use it
only as the temporary bridge above, not the resolution.
- **Vendored-path `[patch]`** (patched crates checked into `vendor/`). Hermetic
without git-source handling, but carries two crate sources in-tree for a
throwaway bridge.
- **Replace `atom_syndication`/`rss`** with an alternative syndication library
on quick-xml ≥ 0.41. Largest change, real functional-regression risk in
AtomPub serialize/parse — disproportionate for a dependency-advisory task.
100 changes: 100 additions & 0 deletions docs/archive/2026-07-02-issue-193-quick-xml-advisory-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Plan — issue #193: quick-xml ≥ 0.41 via atom_syndication/rss forks

**Spec:**
[2026-07-02-issue-193-quick-xml-advisory.md](../specs/2026-07-02-issue-193-quick-xml-advisory.md)
**ADR:** [0043](../../adr/0043-quick-xml-fork-patch.md) **Branch:**
`worktree-issue-193-quick-xml-advisory`

Each task is one clean, gate-verified commit (`cargo xtask check` green) unless
marked **(external)** — GitHub/fork operations that produce no jaunder-repo
commit.

## Prerequisite (before any commit here)

- [x] **P0. Rebase onto main once hotfix PR #194 has merged.** #194 lands the
temporary `deny.toml` ignore of RUSTSEC-2026-0194/0195 on `main`. Rebase
this branch onto the updated `main` so the ignore is inherited and the
verify gate is green during development. (Until then, every commit here
would fail `cargo-deny`.) The design docs (spec, ADR-0043, README row) are
already staged and become Task 1's commit.

## Tasks

- [x] **1. Land the design docs.** Commit the staged spec,
`docs/adr/0043-quick-xml-fork-patch.md`, and the `docs/README.md`
ADR-table row. Pure docs; gate green.

- [x] **2. (external) Create + patch the forks.**
- `gh repo fork rust-syndication/atom --org jaunder-org` and
`… rust-syndication/rss --org jaunder-org`.
- On each fork, branch `quick-xml-0.41`; set `quick-xml = "0.41"` (keep
`features = ["encoding"]`); keep crate versions `0.12.8` / `2.0.13` so
`[patch]` applies. `rss`'s `atom_syndication` dep already tracks `0.12.8` —
leave it (the `[patch]` swaps the source, versions still satisfy).
- Build + run each fork's own test suite (`cargo test`) against quick-xml
0.41; fix any hard breakage. Expected delta: the version bump only;
optionally swap the single deprecated `decode_and_unescape_value` call →
`decoded_and_normalized_value` to keep the upstream PR warning-clean. **Do
not** open upstream PRs yet (Task 6).
- Push the branches; **record the exact commit rev of each** (needed for
pinning).

- [x] **3. Wire the forks into the workspace + hermetic Nix vendoring (one
atomic commit).** This must be a single commit: the Cargo-level `[patch]`
alone would break the hermetic Nix build until the vendoring lands, so
they land together to keep the gate green.
- Root `Cargo.toml`: `[patch.crates-io]` `atom_syndication` and `rss` → the
git forks at the pinned revs from Task 2.
- `common/Cargo.toml`: `quick-xml = "0.39"` → `"0.41"`.
- Regenerate `Cargo.lock`; confirm a **single** quick-xml `0.41.x` and **no**
0.39.x (`cargo tree -i quick-xml`).
- `flake.nix`: add each fork as a `flake = false` input; feed both to crane's
vendor step via `overrideVendorGitCheckout` so the `git+https://…?rev=…`
sources resolve from the pinned flake inputs (no build-time network).
`flake.lock`: pin the inputs.
- `deny.toml`: add `jaunder-org` under `[sources.allow-org].github`.
- **Verify (risk-retirement — this is the spike):** `cargo xtask check` green
**and** the hermetic Nix path resolves the git patch — e.g.
`nix build .#checks.<sys>.deny` and the app derivation build succeed with no
network in the sandbox. If crane vendoring of the git source proves
intractable, STOP and reassess (fallback options in ADR-0043: vendored-path
`[patch]`, or scoped-ignore-as-end-state) before proceeding.

- [x] **4. Verify AtomPub round-trip (no functional regression).** Run
`common`'s atompub tests and the elisp live-integration suite; confirm
serialize/parse behaviour is unchanged on quick-xml 0.41. If green with no
code change, this may fold into Task 3's verification rather than a
separate commit; otherwise commit any fixups.

- [x] **5. Remove the temporary advisory ignore (the climax).** Delete the
RUSTSEC-2026-0194/0195 entries (and their scaffold comment) from
`deny.toml` `[advisories].ignore` — the tree is now on quick-xml 0.41, so
the advisories genuinely no longer apply. Verify
`cargo deny check advisories` passes **with no ignore**, then full
`cargo xtask check` / `validate --no-e2e` green. Commit. End state: no
advisory ignore, single quick-xml 0.41.x.

- [x] **6. (external, user-gated) File follow-up + open upstream PRs.**
- File a fresh GitHub issue (`jaunder-issues`): _"Drop quick-xml git
`[patch]` + forks once atom_syndication/rss publish releases on quick-xml ≥
0.41"_ — the drop-fork tracker (per resolved decision #3). It records the
exit steps from ADR-0043.
- **After** the user reviews the local green result, open PRs to
`rust-syndication/{atom,rss}` from the fork branches (outward-facing —
confirm first).

## Ship (jaunder-ship, after plan execution + user go)

Final review, archive spec/plan, push branch, open the #193 PR (closes #193),
merge (halt). The follow-up issue from Task 6 stays open as the drop-fork
tracker.

## Notes / risks

- **The one real unknown is Task 3's Nix git-vendoring** (first git `[patch]` in
this repo). Everything else is mechanical. Task 3 is deliberately the spike.
- Coverage cache: `.ts` edits aren't involved, but Cargo.lock/flake changes will
rebuild Nix derivations (cachix-warm). Run `cargo clean` on cadence if the
sweep is long.
- After #194 merges, other branches also rebase; no coordination needed beyond
P0.
Loading