From 78949d82863c0bf3680c97b5466a93e7823c300b Mon Sep 17 00:00:00 2001 From: Ofer Chen Date: Fri, 4 Sep 2026 09:29:52 +0300 Subject: [PATCH] docs: re-derive the testsuite figures from the manifests that produce them `README.md` ships the exact `awk` command that reads a leg's expected-outcome manifest and prints its pass/fail/skip counts. Running it disagrees with every row of the table directly beneath it. | row | documented | re-derived | |---|---|---| | non-root, pipe | 257 / 3 / 85 | **259 / 1 / 85** | | root, pipe | 286 / 3 / 56 | **288 / 1 / 56** | | non-root, tcp | 101 / 21 / 33 | **108 / 14 / 33** | | root, tcp | 113 / 27 / 15 | **120 / 20 / 15** | `SECURITY.md` carries the same four rows and the same two summary figures, so both files drifted together. Corrected throughout, along with: - **"3 of 345 tests currently diverge"** across the full-corpus legs. It is **one** - `filter-merge-content-echo` - and the README names it now rather than leaving the reader to count. - **"29 across all four"** distinct failures. It is **23**, and across **five** manifests, not four: the README's own glob `tools/ci/upstream-3.5.0-expect.*.txt` has always matched the macOS file its prose excluded. - **The macOS leg was missing from both tables.** It runs on every PR as `upstream-testsuite-macos` (`ci.yml`), on the full 345-cell corpus, with its own committed manifest. It is the only leg that can observe a platform-conditional divergence - one of its three remaining failures *skips* on Linux, so it had never executed in this repository's CI before the leg existed. Added as a fifth row, with its non-required status stated. - **"the required checks being the two stdio-pipe legs."** All four Linux legs have been required contexts since #7408 wired the TCP pair into PR CI; the ruleset returns ten contexts, not eight. - **`proxy protocol hosts` described as "not yet implemented"** and "still under audit" in two places. It shipped in #7648: parsed into a `ProxyProtocolPolicy` that mirrors upstream's `allow_proxy_protocol_peer()`, rejecting every peer when the trusted list is empty or unset, and warning at startup on the combination upstream warns about. - **The `MAX_PROXY_LINE_BYTES` citation** pointed at `connect/proxy.rs:344`. The constant moved and, more usefully, stopped being a typed literal: it is now `PROXY_BUF_SIZE - 1`, so the doc records the derivation rather than a line number that will drift again. The two macOS-leg rationale comments in the workflows carried the same pre-fix counts and a failure list six entries out of date. Both are recounted from the manifests. `CHANGELOG.md` stopped at #7632, leaving the 26 PRs merged since then unrecorded. Added under Security / Fixed / Testing and CI / Documentation. ## Why the numbers were wrong in a way reading could not catch Every figure here is the outcome column of a committed manifest. The previous values were transcribed once and then maintained by hand, so they decayed as fixes landed - and a reader checking the table against the prose beside it would find them perfectly consistent with each other. The check that finds this class is re-running the derivation, not re-reading the text, so that is what was done: a script recomputes all five legs and both summary figures from `tools/ci/upstream-3.5.0-expect*.txt` and asserts the two documents contain the results, with the superseded strings blacklisted so a partial edit cannot pass. It also caught a live error in this changeset - #7659 landed mid-review and flipped `operator-path-partial-dir-daemon`, taking macOS from 236/4 to 237/3 and the distinct-failure count from 24 to 23. The figures here are derived from master with that merge in place. --- .../workflows/_upstream-testsuite-macos.yml | 29 ++++--- .github/workflows/ci.yml | 14 ++-- CHANGELOG.md | 79 ++++++++++++++++++- README.md | 21 ++--- SECURITY.md | 21 ++--- 5 files changed, 123 insertions(+), 41 deletions(-) diff --git a/.github/workflows/_upstream-testsuite-macos.yml b/.github/workflows/_upstream-testsuite-macos.yml index 09c8d37739..724d99c9cd 100644 --- a/.github/workflows/_upstream-testsuite-macos.yml +++ b/.github/workflows/_upstream-testsuite-macos.yml @@ -21,23 +21,26 @@ name: Upstream Testsuite (macOS) # nothing to do with macOS. Duplicating ~60 lines of YAML is the cheaper and # safer half of that trade. # -# WHAT THIS LEG BUYS (measured 2026-09-03, full 345-cell nonroot/pipe run) +# WHAT THIS LEG BUYS (counted from the committed manifests, not transcribed: +# `awk '!/^#/ && NF {c[$NF]++} END {...}' tools/ci/upstream-3.5.0-expect.*.txt`) # --------------------------------------------------------------------------- -# 230 passed / 10 failed / 105 skipped, against 257 / 3 / 85 for the Linux -# nonroot/pipe leg. Only TWO of the ten failures are shared with Linux; the -# other EIGHT are divergences no existing required context can observe: +# 237 passed / 3 failed / 105 skipped, against 259 / 1 / 85 for the Linux +# nonroot/pipe leg. Only ONE of the three failures is shared with Linux +# (filter-merge-content-echo); the other TWO are divergences no existing +# required context can observe: # -# chmod-setid, daemon-scan-dir-escape, operator-path-partial-dir-daemon, -# sender-flist-symlink-leak, symlink-race-source (pass on Linux) -# crtimes, macos-setgid-ordinary-mode-regression, -# partial-protected-regular-retry-policy (SKIP on Linux) +# chmod-setid (passes on Linux) +# partial-protected-regular-retry-policy (SKIPS on Linux) # -# The last three skip on Linux, so they had never executed in this repo's CI at -# all - including `macos-setgid-ordinary-mode-regression`, a cell named for the -# platform that no leg was running. +# The leg opened at 10 failures; seven have since been fixed, each in its own +# PR (#7654, #7655, #7656, #7659 and the pipe-leg work before them). Two cells +# that SKIP on Linux now PASS here, so this leg is the only place they have +# ever run. # -# None of the ten appears in upstream's own `testsuite/skiplist/macos.txt`, so -# none is a platform artefact upstream itself waives. +# None of the three appears in upstream's own `testsuite/skiplist/macos.txt`, +# so none is a platform artefact upstream itself waives. `chmod-setid` is the +# one cell where the real rsync 3.5.0 binary lands on the same outcome on this +# runner class, so it is recorded `fail` rather than re-baselined away. # # COVERAGE THIS LEG DOES *NOT* BUY # --------------------------------------------------------------------------- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cd10528a1..e276b7de8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1018,12 +1018,12 @@ jobs: # Upstream testsuite - macOS (3.5.0, nonroot/pipe) # --------------------------------------------------------------------------- # The four Linux legs ({nonroot,root} x {pipe,tcp}) cannot observe a - # platform-conditional divergence. MEASURED on a full 345-cell run: this leg - # produces 10 failures, and only TWO are shared with the Linux nonroot/pipe - # leg. The other EIGHT are divergences no existing context sees - five pass on - # Linux, and three (crtimes, macos-setgid-ordinary-mode-regression, - # partial-protected-regular-retry-policy) SKIP there, so they had never - # executed in CI at all. + # platform-conditional divergence. Counted from the committed manifest: this + # leg carries 3 expected failures, and only ONE + # (filter-merge-content-echo) is shared with the Linux nonroot/pipe leg. The + # other TWO are divergences no existing context sees - chmod-setid passes on + # Linux, and partial-protected-regular-retry-policy SKIPS there, so it had + # never executed in CI at all. # # Uses its own reusable workflow rather than a `runs-on` input on # `_upstream-testsuite.yml`; the reasoning is in that file's header. The @@ -1031,7 +1031,7 @@ jobs: # # Not yet a required status check: registering a context in branch protection # is a repo-admin action. The manifest makes it non-vacuous from the first - # run regardless - it records the 10 failures as EXPECTED, so the gate fires + # run regardless - it records those failures as EXPECTED, so the gate fires # on drift, exactly as the Linux legs do with their own `fail` rows. # =========================================================================== upstream-testsuite-macos: diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf10499d7..6e3c5737a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,31 @@ families, and putting the 3.5.0 test suite in front of every pull request. ### Security +**Daemon and client input hardening** +- The `proxy protocol hosts` daemon directive now gates who may supply a PROXY + header. A direct peer that is not on the trusted list is refused, and an + empty or unset list rejects every peer rather than accepting any - upstream's + `allow_proxy_protocol_peer()` fails closed the same way, and warns at startup + on the `proxy protocol = true`-with-no-list combination (#7648) +- Refuse an over-long proxy CONNECT request, authorization header and response + header before writing them. Upstream has four length refusals where oc-rsync + had one, and an over-long *header* was reporting the *status-line* wording; + the 1023-byte bound is now derived from `PROXY_BUF_SIZE` rather than typed + twice (#7650) +- Confine a peer-supplied alternate-basis xname to its basedir, with the + sanitiser and its wire-driven consumer landing together so the guard cannot + ship inert (#7651) +- The macOS `clonefile` fast path bypassed the confined source open; it now + inherits it, so the fast path cannot resolve a component the slow path would + refuse (#7653) +- Bound the client argument vector the daemon accepts, and stop logging it + verbatim (#7633) +- Confine the `--delay-updates` staging path to the module. The staging path + was resolved to its canonical spelling while the module root was carried + unresolved, so a module under a symlinked ancestor escaped the guard - on + Linux only Landlock stopped it, leaving any pre-Landlock kernel, + `OC_RSYNC_NO_LANDLOCK`, or non-Linux platform exposed (#7659) + **Path confinement (CVE-2026-53795 family)** - Confine the destination write and the source read against symlink races, and route the confined source open onto one shared per-component resolver instead @@ -231,8 +256,9 @@ families, and putting the 3.5.0 test suite in front of every pull request. - rsync 3.5.0 now runs the full interop scenario matrix as a gating peer, alongside 3.0.9, 3.1.3 and 3.4.4 (#7290, #7337) - The required upstream-testsuite gate runs the rsync 3.5.0 Python corpus - instead of the 3.4.4 shell corpus, as four legs: privilege {non-root, root} x - daemon transport {stdio pipe, loopback TCP}. Each leg carries an expected- + instead of the 3.4.4 shell corpus, as four Linux legs: privilege + {non-root, root} x daemon transport {stdio pipe, loopback TCP}, plus a + non-root/pipe leg on macOS. Each leg carries an expected- outcome manifest generated from a real run, so only a change in outcome - a regression, or an unexpected pass - fails the gate (#7387, #7339, #7405, #7408, #7392, #7391) @@ -278,6 +304,27 @@ families, and putting the 3.5.0 test suite in front of every pull request. ### Fixed +**3.5.0 testsuite divergences** +- `keep_backup failed` named the source file rather than the backup + destination it failed to create - at both emitting sites (#7658) +- A directory's creation time is preserved under `--crtimes` (#7656) +- Apply the set-group-ID mode `chmod(2)` would apply: macOS `fchmodat` refuses + the bit that `chmod` silently masks, so the two calls disagreed (#7655) +- Anchor the sender's directory scan on the transfer root. The daemon's flist + walk read a process-global root that nothing installs; the anchor is now a + parameter, which fixed two macOS cells with one change (#7654) +- A trailing `/.` must not pivot the `--relative` root - two upstream + decisions had been conflated into one (#7652) +- Keep the DOTDIR marker on a module operand ending in `/.` (#7635) +- Reject an unknown `--info` / `--debug` item the way upstream does, instead + of accepting it silently (#7637) +- Keep a cleared directory's `dir_flist` slot and refuse it, and refuse a + transfer-phase NDX naming a cleared file entry (#7641, #7642) +- Report an oversized xattr datum and a zero block length with upstream's own + wording (#7647, #7646) +- Clear the master red: a rustdoc link, a racing-converter cause, and a + pre-mangled log operand (#7639) + **Filters** - Reject invalid filter-rule modifiers instead of stopping at the first one (#7361) @@ -532,6 +579,19 @@ families, and putting the 3.5.0 test suite in front of every pull request. ### Testing and CI +- The rsync 3.5.0 upstream testsuite now runs on **macOS** as a fifth leg + (non-root, stdio pipe, full 345-cell corpus), with its own committed + expect-manifest. It is the only leg that can observe a platform-conditional + divergence: three of its cells *skip* on Linux, so they had never executed in + this repository's CI at all (#7638) +- Build the old-rsync oracle binaries the 3.5.0 testsuite asserts against, + rather than silently falling back to a weaker substitute (#7636) +- Bound the interop smoke harness's readiness probe so its deadline is real + (#7640) +- Pin the `want_i` adjacent-match length re-check, the `preserve_hard_links` + gate on both wire encodings, and the reserved slot below the daemon-argument + ceiling (#7643, #7644, #7645) + - The 3.5.0 expect-manifest gate is proven non-vacuous on outcomes, with a whole-suite coverage guard so a silently deleted row cannot pass (#7387) - Guard-page over-read harness for the SIMD rolling checksum, carrying its own @@ -624,6 +684,21 @@ families, and putting the 3.5.0 test suite in front of every pull request. ### Documentation +- The upstream-testsuite figures in `README.md` and `SECURITY.md` were stale in + every row. Both files are re-derived from the committed manifests - the pipe + legs are at one divergence, not three; the distinct-failure count across all + manifests is 23, not 29; all four Linux legs are required contexts, not two; + and the macOS leg was missing from both tables entirely. `SECURITY.md` also + described `proxy protocol hosts` as unimplemented after it had shipped. The + two macOS-leg rationale comments in the workflows carried the same pre-fix + figures and are recounted from the same source +- Replace the INC_RECURSE gate's stale rationale with the measured one. The + comment named a mechanism that cannot apply - every call site of the function + it blamed is inside `cfg(test)` - while its conclusion was nonetheless + correct: the deadlock boundary is exactly upstream's `MIN_FILECNT_LOOKAHEAD` + of 1000, bisected by entry count (#7649) +- Document the public items rustdoc could not see (#7634) + - Corrected stale upstream-version and CI-gate claims across the contributor docs: five required checks where the ruleset returns eight, a claim of one approving review where the count is zero, three sites naming rsync 3.4.1 as diff --git a/README.md b/README.md index c311ada1d6..e687d4d877 100644 --- a/README.md +++ b/README.md @@ -19,27 +19,30 @@ Binary name: **`oc-rsync`** - installs alongside system `rsync` without conflict **Release:** 0.6.4 - Wire-compatible drop-in replacement for rsync 3.5.0 and the 3.4.x series (protocols 28-32). -All transfer modes (local, SSH, daemon), delta algorithm, metadata preservation, incremental recursion, and compression are complete. Interop scenarios run in CI against the peer releases enumerated by `versions=` in [`tools/ci/run_interop.sh`](./tools/ci/run_interop.sh), plus the build-only peers in `extra_build_versions=`; that script is the source of truth, and re-listing it here is what let the list drift before. Upstream rsync's own testsuite runs in CI against `oc-rsync` as `$RSYNC` on the 3.5.0 corpus, where **3 of 345 tests currently diverge** across the two full-corpus legs (see below). +All transfer modes (local, SSH, daemon), delta algorithm, metadata preservation, incremental recursion, and compression are complete. Interop scenarios run in CI against the peer releases enumerated by `versions=` in [`tools/ci/run_interop.sh`](./tools/ci/run_interop.sh), plus the build-only peers in `extra_build_versions=`; that script is the source of truth, and re-listing it here is what let the list drift before. Upstream rsync's own testsuite runs in CI against `oc-rsync` as `$RSYNC` on the 3.5.0 corpus, where **1 of 345 tests currently diverges** across the two full-corpus Linux legs (see below). **Tracking rsync 3.5.0.** Upstream released 3.5.0 on 13 Aug 2026. It is wire-identical to 3.4.4 - `PROTOCOL_VERSION` 32, `SUBPROTOCOL_VERSION` 0, unchanged `errcode.h` - so protocol compatibility carries over unchanged and is what the "wire-compatible" claim above rests on. What 3.5.0 changes is *behaviour*: 33 CVEs concentrated in path handling and the daemon, a rewritten path resolver, five new options (`--confine-root`, `--drop-D`, `--no-drop-D`, `--insecure-links`, `--no-insecure-links`), three new daemon directives (`proxy protocol hosts`, `auth digest`, `insecure links`), and a test suite rebuilt from shell scripts into Python. Aligning oc-rsync to those behaviours is in progress and tracked openly. -The 3.5.0 **release** testsuite runs as four flows, one per cell of privilege x daemon transport. `runtests.py` offers two transports - the secure stdio-pipe default, which opens no listening socket, and `--use-tcp`, which binds a real `rsyncd` on 127.0.0.1 - and the root/non-root split decides whether the root-only tests (chown, device nodes, xattrs, dir-sgid, protected-regular) execute or self-skip: +The 3.5.0 **release** testsuite runs as five flows. Four are the cells of privilege x daemon transport on Linux: `runtests.py` offers two transports - the secure stdio-pipe default, which opens no listening socket, and `--use-tcp`, which binds a real `rsyncd` on 127.0.0.1 - and the root/non-root split decides whether the root-only tests (chown, device nodes, xattrs, dir-sgid, protected-regular) execute or self-skip: | | pipe (secure default) | tcp (127.0.0.1) | |----------|-----------------------|-----------------| | non-root | [nonroot, pipe](https://github.com/oferchen/rsync/actions/workflows/upstream-testsuite.yml) | [nonroot, tcp](https://github.com/oferchen/rsync/actions/workflows/upstream-testsuite-tcp.yml) | | root | [root, pipe](https://github.com/oferchen/rsync/actions/workflows/upstream-testsuite-root.yml) | [root, tcp](https://github.com/oferchen/rsync/actions/workflows/upstream-testsuite-root-tcp.yml) | -The pipe legs run the whole 345-test corpus. The tcp legs add `--daemon-tests-only`, which is what upstream ships that option for - the tests it drops never call `start_test_daemon()`, so they cannot observe the transport - and so run the 155 tests that can. The `upstream testsuite` and `upstream testsuite (root)` checks that gate every PR run this same 3.5.0 corpus. +The fifth is the non-root/pipe corpus on **macOS**, wired in [`ci.yml`](./.github/workflows/ci.yml) as the `upstream-testsuite-macos` job. It has no badge and is not yet a required context - registering one is a repo-admin action - but it carries its own committed manifest, so it gates on drift exactly as the Linux legs do. It is the only leg that observes several divergences at all: three of its cells *skip* on Linux and so had never executed in this repository's CI before it existed. + +The pipe legs run the whole 345-test corpus. The tcp legs add `--daemon-tests-only`, which is what upstream ships that option for - the tests it drops never call `start_test_daemon()`, so they cannot observe the transport - and so run the 155 tests that can. All four Linux legs are required status checks on every PR: `upstream-testsuite / upstream testsuite{,(root)}` and `upstream-testsuite-tcp / upstream testsuite{,(root)}`. Current outcomes, as recorded in each leg's committed manifest: | leg | pass | fail | skip | corpus | |---|---:|---:|---:|---:| -| non-root, pipe | 257 | 3 | 85 | 345 | -| root, pipe | 286 | 3 | 56 | 345 | -| non-root, tcp | 101 | 21 | 33 | 155 | -| root, tcp | 113 | 27 | 15 | 155 | +| non-root, pipe | 259 | 1 | 85 | 345 | +| root, pipe | 288 | 1 | 56 | 345 | +| non-root, tcp | 108 | 14 | 33 | 155 | +| root, tcp | 120 | 20 | 15 | 155 | +| non-root, pipe (macOS) | 237 | 3 | 105 | 345 | Every figure above is the outcome column of a committed manifest, not a transcribed run log. Re-derive any row with: @@ -49,7 +52,7 @@ awk '!/^#/ && NF {c[$NF]++; t++} END {print t, c["pass"], c["fail"], c["skip"]}' tools/ci/upstream-3.5.0-expect.nonroot.txt ``` -**3 distinct tests** diverge across the two full-corpus legs, and 29 across all four (`awk '!/^#/ && $NF=="fail" {print $1}' tools/ci/upstream-3.5.0-expect.*.txt | sort -u`). Every leg carries its own expected-outcome manifest, generated from a real run rather than hand-written, so only a *change* in outcome turns a badge red - and that includes an unexpected **pass**, which is what stops a divergence being quietly re-baselined instead of fixed. A fix flips its manifest rows in the same commit. The divergences are genuine and tracked openly: each was re-run against the real upstream 3.5.0 binary as a negative control, so they are oc-rsync behaviour gaps, not harness artefacts. +**One test** diverges across the two full-corpus Linux legs - `filter-merge-content-echo` - and 23 across all five manifests (`awk '!/^#/ && $NF=="fail" {print $1}' tools/ci/upstream-3.5.0-expect.*.txt | sort -u`). Every leg carries its own expected-outcome manifest, generated from a real run rather than hand-written, so only a *change* in outcome turns a badge red - and that includes an unexpected **pass**, which is what stops a divergence being quietly re-baselined instead of fixed. A fix flips its manifest rows in the same commit. The divergences are genuine and tracked openly: each was re-run against the real upstream 3.5.0 binary as a negative control, so they are oc-rsync behaviour gaps, not harness artefacts - except where that control shows upstream landing on the same outcome, which is recorded as such rather than counted against oc-rsync. Separately, [Upstream Testsuite 3.5.0dev](https://github.com/oferchen/rsync/actions/workflows/track-3.5.0dev-testsuite.yml) is a **development** tracker, not a gate and not the 3.5.0 release: it builds RsyncProject git master (`version.h` == `3.5.0dev`), a moving target, and is deliberately non-blocking so an upstream-side break can never fail a PR here. @@ -236,7 +239,7 @@ oc-rsync is wire-compatible with upstream rsync 3.5.0, but a few architectural c - **SSH compression interaction.** When the SSH transport already compresses the stream (e.g., `Compression yes` in `ssh_config`), running `oc-rsync -z` compresses payloads twice. oc-rsync warns when it detects `-C` / `-o Compression=yes` in the SSH argv it builds, and - with the default `ssh-config-parse` feature - when a `Compression yes` directive applies via `~/.ssh/config` / `-F` / `/etc/ssh/ssh_config`; it does not auto-disable either layer, so operators should pick one. - **Daemon encryption.** The daemon protocol is plaintext, matching upstream rsync (authentication only, no encryption). oc-rsync has no built-in TLS client - the former `--ssl` / `client-tls` path was removed to match upstream. Encrypt with the SSH transport, or place the daemon behind a TLS-terminating proxy (`stunnel`, HAProxy, nginx) and reach it through an external wrapper such as `rsync-ssl` or `stunnel`, the same model as upstream. - **Windows IOCP scope.** IOCP is wired for socket I/O (daemon and SSH transports) and for the receive-side disk-write pipeline (`transfer::disk_commit` dispatches `Writer::Iocp` when the IOCP backend is selected on Windows). An IOCP file *reader* also exists, but the per-file dispatch that would select it is behind the experimental, non-default `adaptive-basis-dispatch` feature, so default builds still read files via standard buffered I/O. -- **`.rsync-filter` per-directory inheritance.** Nested merges and `!`-clear semantics work; `merge`, `filter-merge-recursion`, `filter-depth`, `exclude` and `exclude-lsh` pass in the gating 3.5.0 corpus. `filter-merge-content-echo` is one of the three divergences tabulated above, and the deepest anchored-vs-unanchored corner cases remain an area of ongoing hardening. +- **`.rsync-filter` per-directory inheritance.** Nested merges and `!`-clear semantics work; `merge`, `filter-merge-recursion`, `filter-depth`, `exclude` and `exclude-lsh` pass in the gating 3.5.0 corpus. `filter-merge-content-echo` is the single divergence tabulated above, and the deepest anchored-vs-unanchored corner cases remain an area of ongoing hardening. - **`--checksum-seed` / `--fuzzy`.** Both are implemented and honoured on the common path (`--fuzzy` ports upstream's `fuzzy_distance` basis selection); deeper corner-case conformance audits against upstream rsync 3.5.0 are tracked separately. --- diff --git a/SECURITY.md b/SECURITY.md index 273d0fb441..0ec777fc20 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -81,14 +81,15 @@ rsync 3.5.0 is a major security release closing **33 CVEs**, concentrated in pat **What is established.** 3.5.0 is wire-identical to 3.4.4 (`PROTOCOL_VERSION` 32, `SUBPROTOCOL_VERSION` 0, unchanged `errcode.h`), so none of these CVEs stem from a protocol change and none require a wire-format response. They are implementation vulnerabilities in areas oc-rsync reimplements independently, which means neither "inherited" nor "not applicable" can be assumed for any of them - each needs its own evidence. -**What is measured.** Upstream's 3.5.0 test suite runs against oc-rsync as a gate on every pull request, in four legs - privilege {non-root, root} x daemon transport {stdio pipe, loopback TCP}. Each leg carries an expected-outcome manifest generated from a real run, so the divergence set is a committed, per-test ledger rather than an estimate: +**What is measured.** Upstream's 3.5.0 test suite runs against oc-rsync as a gate on every pull request, in five legs - privilege {non-root, root} x daemon transport {stdio pipe, loopback TCP} on Linux, plus the non-root/pipe corpus on macOS. Each leg carries an expected-outcome manifest generated from a real run, so the divergence set is a committed, per-test ledger rather than an estimate: | leg | pass | fail | skip | corpus | |---|---:|---:|---:|---:| -| non-root, pipe | 257 | 3 | 85 | 345 | -| root, pipe | 286 | 3 | 56 | 345 | -| non-root, tcp | 101 | 21 | 33 | 155 | -| root, tcp | 113 | 27 | 15 | 155 | +| non-root, pipe | 259 | 1 | 85 | 345 | +| root, pipe | 288 | 1 | 56 | 345 | +| non-root, tcp | 108 | 14 | 33 | 155 | +| root, tcp | 120 | 20 | 15 | 155 | +| non-root, pipe (macOS) | 237 | 3 | 105 | 345 | Each row is the outcome column of the corresponding `tools/ci/upstream-3.5.0-expect.*.txt`, counted rather than transcribed: @@ -98,13 +99,13 @@ awk '!/^#/ && NF {c[$NF]++; t++} END {print t, c["pass"], c["fail"], c["skip"]}' tools/ci/upstream-3.5.0-expect.nonroot.txt ``` -The two pipe legs run the whole 345-test corpus; the tcp legs add `--daemon-tests-only`, so they re-run the 155 tests that can observe the transport. **3 distinct tests** diverge across the two full-corpus legs, and **29** across all four (`awk '!/^#/ && $NF=="fail" {print $1}' tools/ci/upstream-3.5.0-expect.*.txt | sort -u`). That set is the triage input, not a vulnerability count: it mixes real behavioural gaps, harness differences, and probes for C-level memory errors that have no Rust analogue. Classification requires per-test evidence, and a fix flips its manifest rows in the same commit - re-baselining a row without a fix would be a waiver, and the gate fails on an unexpected *pass* for exactly that reason. +The two pipe legs run the whole 345-test corpus; the tcp legs add `--daemon-tests-only`, so they re-run the 155 tests that can observe the transport. **One test** diverges across the two full-corpus Linux legs, and **23** across all five (`awk '!/^#/ && $NF=="fail" {print $1}' tools/ci/upstream-3.5.0-expect.*.txt | sort -u`). That set is the triage input, not a vulnerability count: it mixes real behavioural gaps, harness differences, and probes for C-level memory errors that have no Rust analogue. Classification requires per-test evidence, and a fix flips its manifest rows in the same commit - re-baselining a row without a fix would be a waiver, and the gate fails on an unexpected *pass* for exactly that reason. **Highest-severity items and their oc-rsync bearing:** | CVE | Severity | Upstream issue | oc-rsync bearing | |-----|----------|----------------|------------------| -| CVE-2026-53791 | CRITICAL | `proxy protocol = true` let a directly-connecting client forge a PROXY header and spoof its source address, defeating `hosts allow`/`hosts deny`. Fixed by a new `proxy protocol hosts` allow-list that fails **closed** when unset. | The `proxy protocol hosts` directive itself is still **under audit**. The related prerequisite is **fixed**: the daemon's two stdio entry points fabricated `127.0.0.1` as the peer address, which made every `hosts allow` / `hosts deny` rule evaluate against a synthetic localhost. Both now mirror upstream `client_addr()` - `getpeername` under inetd, the `REMOTE_HOST` / `SSH_CONNECTION` / `SSH_CLIENT` / `SSH2_CLIENT` environment chain under a remote shell - and abort with `RERR_SOCKETIO` rather than inventing a value (PR #7303). | +| CVE-2026-53791 | CRITICAL | `proxy protocol = true` let a directly-connecting client forge a PROXY header and spoof its source address, defeating `hosts allow`/`hosts deny`. Fixed by a new `proxy protocol hosts` allow-list that fails **closed** when unset. | **Fixed.** `proxy protocol hosts` is parsed into a `ProxyProtocolPolicy` that mirrors upstream's `allow_proxy_protocol_peer()` (access.c:300-306): an empty or unset trusted-proxy list makes the policy reject **every** peer rather than accept any, and the daemon warns at startup on the `proxy protocol = true` with no list combination exactly as upstream does (clientserver.c:1750-1751). A PROXY header from a direct peer that is not on the list is refused (PR #7648). The prerequisite was fixed earlier: the daemon's two stdio entry points fabricated `127.0.0.1` as the peer address, which made every `hosts allow` / `hosts deny` rule evaluate against a synthetic localhost. Both now mirror upstream `client_addr()` - `getpeername` under inetd, the `REMOTE_HOST` / `SSH_CONNECTION` / `SSH_CLIENT` / `SSH2_CLIENT` environment chain under a remote shell - and abort with `RERR_SOCKETIO` rather than inventing a value (PR #7303). | | CVE-2026-70452 | HIGH | `hosts deny` failed **open** when a configured hostname would not resolve. | **Fixed.** The root cause was a missing distinction, not a missing check: `forward_resolve` collapsed "lookup failed" and "resolved but did not match" into one empty result, which is what made the deny side fail open. The resolver now returns `Option>` so the two are separable, and hostname matching is case-insensitive against a lowercased list per upstream `iwildmatch` (access.c:46) (PR #7314). | | CVE-2026-70464 | HIGH | An unauthenticated peer could complete the `@RSYNCD` handshake. | Under audit. The related `auth digest` minimum-digest floor has **shipped** (PR #7350); `Md4Old` must rank as md4 or the floor locks out the very clients it exists to reason about. | | CVE-2026-53784 / 53793 | HIGH | Daemon module-root chdir escape under `use chroot`, and a `/./` inner-module escape via a symlinked path. | Under audit. Related and fixed: the daemon fused the operator module root and the peer-supplied tail into one absolute string and applied `RESOLVE_NO_SYMLINKS` to the whole thing. Upstream keeps the two in different mechanisms - plain `chdir`/`openat` for the root, a confined `RESOLVE_BENEATH` walk for the tail - and oc-rsync now does the same (PR #7304). | @@ -115,7 +116,7 @@ The two pipe legs run the whole 345-test corpus; the tcp legs add `--daemon-test | CVE-2026-70463 | HIGH | `auth users` separator handling. | **Fixed** (PR #7345). Upstream's leading-comma separator form is honoured at both affected sites - `auth users` and `gid`, not just the one named in the advisory. | | CVE-2026-70455 / 70461 / 70458 / 70456 | HIGH | Peer-controlled Zstandard thread count; three out-of-bounds heap writes. | The memory-safety trio has no direct Rust analogue, but each also has an observable half - whether malformed input is **rejected** rather than accepted with wrong state - which is being checked rather than assumed. | -**New defensive surfaces in 3.5.0.** `--confine-root=DIR`, `--drop-D` / `--no-drop-D`, `--insecure-links` / `--no-insecure-links` and the `auth digest` daemon directive have **shipped** (PRs #7396, #7299, #7350). Like upstream, `--confine-root` and `--drop-D` are deliberately **not forwarded** to the remote side: both are meant to be applied to one end of a connection by itself. The `insecure links` daemon directive has **shipped** as well; `proxy protocol hosts` is **not yet implemented**. +**New defensive surfaces in 3.5.0.** `--confine-root=DIR`, `--drop-D` / `--no-drop-D`, `--insecure-links` / `--no-insecure-links` and the `auth digest` daemon directive have **shipped** (PRs #7396, #7299, #7350). Like upstream, `--confine-root` and `--drop-D` are deliberately **not forwarded** to the remote side: both are meant to be applied to one end of a connection by itself. The `insecure links` and `proxy protocol hosts` daemon directives have **shipped** as well (PRs #7484, #7648). `--drop-D` tells a receiver to refuse to create device and special files whatever the transfer requested. It exists because the obvious alternative does not work: `--no-D` also frames the file list's rdev fields, and only one end of a connection receives the option, so a `-D` sender writes rdev that a `--no-D` receiver never reads. That desynchronises the list - a FIFO hangs the transfer at protocol 29 and corrupts it at 30, and a device node breaks every protocol. `--drop-D` refuses the creation while leaving the wire format untouched. Like `--confine-root`, it is deliberately **not forwarded** to the remote side: both are meant to be applied to one end of a connection by itself. @@ -135,7 +136,7 @@ rsync 3.4.3 (released 2026-05-20) is a major security release closing six CVEs a Open follow-ups: - **SEC-1** (TOCTOU on path-based daemon syscalls under `use_chroot=false`) - **Fixed.** Umbrella issue #2516, decomposed into SEC-1.a..s. All `*at` helpers shipped (SEC-1.a..n), receiver call-site wiring completed (SEC-1.q/q2), `DeleteFs` trait sandbox refactor shipped (SEC-1.q), `recursive_unlinkat` helper shipped (SEC-1.s), and `mknodat`/`mkfifoat` migration completed (SEC-MK.a..h). The SEC-1.p Landlock LSM defense-in-depth layer shipped (PR #4702). -- **SEC-2.b** (align proxy-line cap to upstream's ceiling) - **Fixed** (PR #4812). SEC-2.a confirmed the structural mitigation (bounds-checked `Vec::push`); SEC-2.b tightened the numeric cap to `MAX_PROXY_LINE_BYTES = 1023` at `connect/proxy.rs:344`, matching upstream's 1024-byte `establish_proxy_connection()` stack buffer. +- **SEC-2.b** (align proxy-line cap to upstream's ceiling) - **Fixed** (PR #4812). SEC-2.a confirmed the structural mitigation (bounds-checked `Vec::push`); SEC-2.b tightened the numeric cap to 1023 bytes, matching upstream's 1024-byte `establish_proxy_connection()` stack buffer. The cap is now **derived** rather than typed - `connect/proxy.rs` names `PROXY_BUF_SIZE = 1024` after socket.c:52 and defines `MAX_PROXY_LINE_BYTES = PROXY_BUF_SIZE - 1` - so the two cannot drift apart (PR #7650). - **SEC-3** (confirm hyphen-prefixed hostname rejection in SSH operand parse) - **Fixed.** SEC-3.a audit, SEC-3.b validation, and SEC-3.c regression coverage all completed. - **SEC-4** (regression test for malformed `parent_node_idx` per CVE-2026-43620 mitigation) - closed. `DirectoryTree::try_add_directory` validates the wire-supplied parent index and returns `DirTreeError::OutOfBoundsParent`; three regression tests in `crates/protocol/src/flist/dir_tree.rs` pin down both the graceful-reject path and the worst-case controlled-panic path (no SIGSEGV). @@ -164,7 +165,7 @@ Additionally shipped since the last update: **Status: Fixed.** All receiver call sites are wired through `DirSandbox`, and the SEC-1.m / SEC-1.n regression suites pass against the fully-wired pipeline. The SEC-1.p Landlock layer provides defense-in-depth. -CI integration: upstream rsync's own testsuite runs against oc-rsync as `$RSYNC` on every pull request. The gating corpus has been the rewritten **3.5.0 Python suite** since 2026-08-19 (PR #7387), with the two loopback-TCP legs added on 2026-08-20 (PR #7408); it runs as the four legs tabulated above, the required checks being the two stdio-pipe legs. Each leg is gated by its committed expected-outcome manifest (`tools/ci/upstream-3.5.0-expect.{nonroot,root}{,.tcp}.txt`), generated from a real run and never hand-typed. The 3.4.4 shell corpus is no longer run on any pull request or schedule: `.github/workflows/_interop.yml` states plainly that it does not drive it, and `tools/ci/run_upstream_testsuite.sh` now defaults to `UPSTREAM_VERSION=3.5.0`. The one workflow that still pins 3.4.4, `validate-daemon-environmental.yml`, is `workflow_dispatch`-only and exists to decide whether a single named test fails environmentally. The roster it would consult, `tools/ci/upstream_testsuite_known_failures.conf`, is empty and reaches only the shell-script path, so it is a historical record rather than a live measurement. +CI integration: upstream rsync's own testsuite runs against oc-rsync as `$RSYNC` on every pull request. The gating corpus has been the rewritten **3.5.0 Python suite** since 2026-08-19 (PR #7387), with the two loopback-TCP legs added on 2026-08-20 (PR #7408); it runs as the five legs tabulated above. All four Linux legs are required status checks - `upstream-testsuite / upstream testsuite{,(root)}` and `upstream-testsuite-tcp / upstream testsuite{,(root)}` - since PR #7408 wired the TCP pair into PR CI. The macOS leg runs on every PR as the `upstream-testsuite-macos` job but is not yet a required context, because registering one is a repo-admin action; its manifest gates it on drift regardless. Each leg is gated by its committed expected-outcome manifest (`tools/ci/upstream-3.5.0-expect.{nonroot,root}{,.tcp}.txt` and `-expect.macos.nonroot.txt`), generated from a real run and never hand-typed. The 3.4.4 shell corpus is no longer run on any pull request or schedule: `.github/workflows/_interop.yml` states plainly that it does not drive it, and `tools/ci/run_upstream_testsuite.sh` now defaults to `UPSTREAM_VERSION=3.5.0`. The one workflow that still pins 3.4.4, `validate-daemon-environmental.yml`, is `workflow_dispatch`-only and exists to decide whether a single named test fails environmentally. The roster it would consult, `tools/ci/upstream_testsuite_known_failures.conf`, is empty and reaches only the shell-script path, so it is a historical record rather than a live measurement. ### Upstream rsync 3.4.2 audits