| Version | Supported |
|---|---|
| 0.6.x | ✅ (current: 0.6.4) |
| 0.5.x | |
| < 0.5 | ❌ |
Fixes land on master first and ship in the next 0.6.x release. Changes merged since 0.6.4 are listed under Unreleased in CHANGELOG.md.
If you discover a security vulnerability in oc-rsync, please report it responsibly:
- Do not open a public GitHub issue for security vulnerabilities
- Email the maintainer directly at: skewers.irises.3b@icloud.com
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact assessment
- Any suggested fixes (optional)
You can expect:
- Initial acknowledgment within 48 hours
- Regular updates on the fix progress
- Credit in the security advisory (unless you prefer anonymity)
Safe Rust rules out out-of-bounds access, use-after-free, reads of uninitialised memory and data races in the code it covers. It does not rule out logic flaws: a fail-open access check, an unconfined path or an unbounded peer-supplied count is as reachable in safe Rust as in C. OS-level races at filesystem boundaries (TOCTOU) are also outside what the language prevents, which is why path handling goes through the confined *at walk described below.
Every crate root sets #![deny(unsafe_code)] (engine outside tests only), so each unsafe block needs a targeted #[allow(unsafe_code)] and a SAFETY comment. Unsafe code belongs to two owning crates, split by subject, that expose safe APIs:
platform- process, identity, environment and signals: daemonization, per-connectionfork/waitpid, name and group resolution, privilege transitions,chroot, environment access, local time, signal disposition and Windows service dispatch.fast_io- I/O syscalls: io_uring,copy_file_range,sendfile, mmap, reflink and copy dispatch, IOCP and Winsock, and the confined-path resolver.
New unsafe code goes first to a safe wrapper crate (nix, rustix, socket2, exacl, windows), then to a safe API on platform or fast_io, and only then to a new #[allow(unsafe_code)] function whose SAFETY comment says why the first two could not express it.
That is the target, not yet the whole tree. Several crates outside the two owners still declare FFI crates such as libc or nix directly, and four hold production unsafe from before the split, permitted until their sites migrate: metadata (ownership, identity, xattr, timestamp and Windows ACL FFI), checksums (SIMD intrinsics, with scalar fallbacks and parity tests), engine (platform FFI and the buffer pool) and protocol (one site in multiplex frame parsing). windows-gnu-eh holds Windows GNU exception-handling shims, and the oc-rsync binary has one #[unsafe(no_mangle)] static that sets jemalloc's decay options. Other crates allow unsafe only in tests.
The reference version is upstream rsync 3.5.1. Its release notes name no CVE ids; its fixes are tracked in the 3.5.1 table below. Each CVE row carries one of five statuses:
- Fixed: the code closes it, and a named test or PR shows it.
- Partially fixed: some affected paths are closed and shown by a test or PR, but at least one path is still exposed.
- Open: oc-rsync is still exposed, or a test that covers it fails.
- Not applicable: the affected code has no counterpart in oc-rsync.
- Unverified: not yet established either way from the code or a test.
"Passes on every leg that runs it" refers to the upstream 3.5.1 test suite, run against oc-rsync on the legs listed in the README. Some cells skip on some legs, for example when they need root.
| Upstream batch | oc-rsync status |
|---|---|
| 2024 (CVE-2024-12084 to 12088, CVE-2024-12747) | Not vulnerable or mitigated |
| rsync 3.4.2 fixes | Audited in v0.6.2; equivalent paths verified safe |
| rsync 3.4.3 (six CVEs) | Fixed or not vulnerable |
| rsync 3.5.0 (33 CVEs) | 27 fixed, 1 partially fixed, 5 not applicable; one row each below |
| rsync 3.5.1 (no CVE ids) | One row per security-relevant fix below |
| CVE | Upstream Issue | oc-rsync Status | Reason |
|---|---|---|---|
| CVE-2024-12084 | Heap overflow in checksum parsing | Not vulnerable | Rust Vec handles dynamic sizing |
| CVE-2024-12085 | Uninitialized stack buffer leak | Not vulnerable | Rust requires initialization |
| CVE-2024-12086 | Server leaks client files | Not vulnerable | Strict path validation |
| CVE-2024-12087 | Path traversal via --inc-recursive | Not vulnerable | Unless --trust-sender is given, the receiver drops every file-list entry with a .. component, or an absolute path outside --relative, before any disk operation (crates/transfer/src/receiver/file_list/sanitize.rs, after upstream's clean_fname(CFN_REFUSE_DOT_DOT_DIRS)), and a sub-list's parent index is bounds-checked against the received directory list (DirFlist::resolve, pinned by out_of_range_sublist_dir_ndx_is_rejected_fail_closed in crates/transfer/src/receiver/file_list/on_demand.rs) |
| CVE-2024-12088 | --safe-links bypass | Mitigated | --safe-links uses a port of upstream's unsafe_symlink() (crates/transfer/src/symlink_safety.rs), including the 3.4.1 rule that rejects an inner /../, so a target that could later be redirected through a replaced parent is refused; symlinks are then created with symlinkat beneath the pinned destination descriptor |
| CVE-2024-12747 | Symlink race condition | Mitigated | Receiver path syscalls resolve beneath the pinned destination descriptor: temp files are created with openat(O_CREAT | O_EXCL | O_NOFOLLOW) and committed with renameat, so a path component swapped for a symlink cannot redirect the write. The Windows receiver uses the reparse-point-safe equivalents (crates/fast_io/src/win_atomic_commit.rs). On Linux the daemon receiver is additionally confined to the module path by Landlock |
| CVE-2026-29518 | TOCTOU symlink race in daemon receiver (use chroot = no) |
Fixed | All path-based syscalls have been migrated to *at variants routed through DirSandbox with openat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS) runtime detection. Device/FIFO node creation uses mknodat/mkfifoat. A Landlock LSM defense-in-depth layer (PR #4702) allowlists module.path on the daemon receiver via Landlock 0.4 (requests up to ABI v5 with best-effort downgrade; v1 on kernel 5.13+). Umbrella tracking issue #2516. |
| CVE-2026-43617 / GHSA-rjfm-3w2m-jf4f | Reverse-DNS lookup after daemon chroot causes hostname ACL bypass | Fixed | Hostname resolution runs before any per-module chroot: per session in session_runtime.rs::handle_session at accept time, and per module in module_access/request.rs::respond_with_module_request during ACL evaluation (module_peer_hostname()). The per-module chroot is applied later, after authentication (module_access/transfer/sandbox.rs::apply_privilege_restrictions_with_upstream_errors). The global daemon chroot is applied once at startup, before the accept loop (accept_loop.rs::serve_connections), so a lookup inside it can fail when the chroot lacks NSS files. ModuleDefinition::permits therefore fails closed when reverse DNS returns nothing and any hosts deny rule is hostname-based: a peer that controls or blackholes its PTR record cannot bypass a hostname deny rule. Regression tests: module_peer_hostname_resolution_before_chroot_denies_unknown (allow side), module_hostname_deny_fails_closed_when_dns_unresolved (deny side under daemon chroot), module_ip_deny_unaffected_by_dns_failure (IP-only rules unchanged). |
| CVE-2026-43618 | Integer overflow in compressed-token decoder causes memory disclosure | Fixed | The upstream C vulnerability uses a multiplexed signed-integer return from recv_deflated_token() where a negative rx_token is misinterpreted as a literal length, leaking memory via a stale *data pointer. oc-rsync's decoder returns a typed CompressedToken enum (Literal(Vec<u8>) / BlockMatch(u32) / End), structurally eliminating the return-value misinterpretation vector. The residual risk - a malicious sender injecting a negative absolute token via TOKEN_LONG that wraps to a valid-looking block index after as u32 cast - is closed by an explicit sign check in all three wire decoders (zlib, zstd, lz4). Regression tests: zlib_decoder_rejects_negative_absolute_token, zstd_decoder_rejects_negative_absolute_token, lz4_decoder_rejects_negative_absolute_token, zlib_decoder_rejects_i32_min_token in crates/protocol/src/wire/compressed_token/tests.rs. Audit doc: docs/audits/upstream-3.4.2-token-decoder-parity.md. |
| CVE-2026-43619 | Symlink races on chmod/lchown/utimes/rename/unlink/mkdir/symlink/mknod/link/rmdir/lstat | Fixed | Same root cause as CVE-2026-29518. All *at helpers shipped and receiver call sites fully wired: lstat / unlink / rmdir / mkdir / symlink / link migrated to fstatat / unlinkat / mkdirat / symlinkat / linkat; chmod / lchown / utimes migrated to fchmodat / fchownat / utimensat (PR #4690); rename migrated to renameat (PR #4693); mknod / mkfifo migrated to mknodat / mkfifoat. Deletion routes through the same sandbox, and directory trees are removed with unlinkat throughout. The Landlock LSM defense-in-depth layer (PR #4702) confines the daemon receiver to the configured module.path via Landlock 0.4 (kernel 5.13+). Umbrella tracking issue #2516. |
| CVE-2026-43620 | OOB read in recv_files via negative parent_ndx → client SIGSEGV |
Not vulnerable | oc-rsync consumes the parent reference as Option<usize> and resolves it against a bounds-checked directory list. A sub-list whose directory index is out of range is refused with a protocol error before any entry is used (crates/transfer/src/receiver/file_list/receive.rs, DirFlist::resolve). Regression coverage on the live receiver path: out_of_range_sublist_dir_ndx_is_rejected_fail_closed in crates/transfer/src/receiver/file_list/on_demand.rs. The receiver reaches this code only when incremental recursion is negotiated. |
| CVE-2026-45232 | Off-by-one stack write in HTTP CONNECT proxy response handler | Fixed | read_proxy_line() in crates/core/src/client/module_list/connect/proxy.rs reads byte-by-byte into a heap Vec<u8> and explicitly caps the response line at MAX_PROXY_LINE_BYTES = 1023 bytes, matching upstream's 1024-byte establish_proxy_connection() stack buffer (socket.c:86). The C off-by-one stack-write is structurally impossible (bounds-checked Vec::push), and indefinite buffering is bounded by the explicit cap. Audit PR #4609; upstream-parity alignment PR #4812. |
3.5.0 kept protocol 32, so none of these stem from a wire-format change. They are implementation flaws in code oc-rsync reimplements independently. Re-derive the id list from the 3.5.0 section of the 3.5.1 NEWS.md and check that every id has a row (the section also names two 3.4.3 ids, CVE-2026-43617 and CVE-2026-43620, which have rows above):
awk '/^# NEWS for rsync 3.5.0/,/^# NEWS for rsync 3.4/' \
target/interop/upstream-src/rsync-3.5.1/NEWS.md \
| grep -o 'CVE-2026-[0-9]*' | sort -u > /tmp/batch
grep -oE '^\| CVE-2026-[0-9]+' SECURITY.md | cut -c3- | sort -u > /tmp/rows
comm -23 /tmp/batch /tmp/rows # ids with no row: prints nothing
grep -cE '^\| CVE-2026-(5|7)[0-9]{4} \|' SECURITY.md # 3.5.0 rows: 33| CVE | Severity | Upstream issue | Status | Evidence |
|---|---|---|---|---|
| CVE-2026-53791 | CRITICAL | proxy protocol = true let a direct client forge a PROXY header and spoof its address past hosts allow / hosts deny. |
Fixed | proxy protocol hosts mirrors upstream's allow_proxy_protocol_peer() (access.c:311-317): an unset or empty list refuses every PROXY header, and the daemon warns at startup as upstream does (PR #7648). The stdio entry points no longer fabricate 127.0.0.1 as the peer (PR #7303). proxy-protocol-trusted-peer passes on every leg that runs it. |
| CVE-2026-53783 | HIGH | rrsync restricted-directory escape through a TOCTOU window between validation and exec. |
Not applicable | oc-rsync ships no rrsync wrapper. Upstream's rrsync-* cells, which wrap oc-rsync as the rsync binary, pass on every leg that runs them. |
| CVE-2026-53784 | HIGH | Daemon module-root chdir followed a planted parent symlink under use chroot = no. |
Fixed | The module root is entered with plain chdir/openat and the peer tail through a confined walk (PR #7304). daemon-module-chdir-symlink passes on every leg that runs it. |
| CVE-2026-53785 | HIGH | Under --relative, implied-parent creation followed a planted parent symlink. |
Fixed | --relative implied parents resolve through the ownership walk (PR #7393). relative-implied-symlink, relative-mkpath-symlink and relative-mkpath-dir-symlink pass on every leg that runs them. |
| CVE-2026-53786 | MEDIUM | A client --filter merge file bypassed the daemon module filter. |
Fixed | daemon-filter-merge-bypass passes on every leg. |
| CVE-2026-53788 | MEDIUM | A newline in a peer-controlled name injected requests into the name-converter protocol. | Fixed | daemon-namecvt-newline-token passes on every leg that runs it. |
| CVE-2026-53789 | MEDIUM | A daemon sender widened --delete scope by omitting the "no content dir" flag on an implied parent. |
Fixed | malicious-sender-delete-scope, malicious-dot-dir-delete-scope, malicious-dot-file-delete-scope and peer-legacy-implied-delete-scope pass on every leg that runs them. |
| CVE-2026-53790 | HIGH | Command and argument injection through unquoted peer- or host-controlled values. | Fixed | Hook-variable expansion refuses shell metacharacters (PR #7465), the RSYNC_CONNECT_PROG %H substitution is validated and quoted (PR #7430), and the batch replay script quotes its arguments (PR #7445). The connect-prog-*, daemon-exec-* and write-batch-quoting cells pass on every leg that runs them. The rsync-ssl arm does not apply: oc-rsync ships no rsync-ssl. |
| CVE-2026-53792 | MEDIUM | A checksum header with blocks but a zero block length drove the sender's match arithmetic negative. | Fixed | checksum-zero-blocklen passes on every leg that runs it. |
| CVE-2026-53793 | HIGH | A symlinked parent inside a chroot inner module reached a sibling outside it through /./. |
Fixed | The module identity is resolved before the chroot (PR #7585), and the destination is entered through a port of secure_relative_dirfd() (PR #8010). The six chroot-*-inner-module cells pass on every leg that runs them. |
| CVE-2026-53794 | MEDIUM | --max-alloc=0 disabled the per-allocation cap. |
Fixed | Follows 3.5.1, which accepts 0 again as the maximum limit rather than as "no limit" (PR #8011). max-alloc-zero passes on every leg that runs it. |
| CVE-2026-53795 | HIGH | An absolute --temp-dir or --link-dest disabled rename and link confinement. |
Fixed | The staging family, --log-file, the alt-dest basis, --relative implied parents and absolute rename endpoints resolve through one ownership walk (PRs #7393, #7398, #7404, #7415, #7419), as do the operator-named auxiliary files (PRs #7421-#7426, #7439, #7441-#7443, #7459, #7463). The operator-path-*, link-dest-* and rename-mixed-parent-* cells pass on every leg that runs them. |
| CVE-2026-53796 | MEDIUM | A non-daemon receiver's chdir() into a relative destination was not confined. |
Partially fixed | Every non-daemon receiver (local copy, the pull client and the rsh --server receiver) enters an existing destination through the ownership walk, refuses an untrusted destination symlink with exit 3 and upstream's diagnostic, and pins the walked descriptor, so data writes stay in the directory the walk accepted even if the destination is swapped afterwards (PRs #8071, #8201). Three residuals remain. After a swap the network receiver still applies about 60 metadata calls (stat, chmod, utime) by path; they fail closed with exit 23 instead of reaching the swapped-in target, but they are not anchored on the pinned directory the way upstream's fchdir() makes them (task 546). A local destination that names a single file, not a directory, is not pinned. On a push, the walk's refusal line is not relayed from the server to the client. tests/destination_root_owner_walk.rs and crates/fast_io/tests/destination_root_pin.rs pin the refusal, the trusted-symlink and --insecure-links cases and the post-walk swap on all three receivers. symlink-race-dest, symlink-race-relative-dest and chdir-symlink-race pass on every leg that runs them. |
| CVE-2026-53797 | MEDIUM | A non-daemon sender opened file content by path, following a raced parent symlink. | Fixed | Explicit source roots are held by dev/ino and content opens run beneath the held root; a replaced root is refused with ELOOP (PR #8120). An rsh-server-sender e2e swaps a listed directory mid-scan and is refused with exit 23, and relative-source-ancestor, symlink-race-source and chdir-symlink-race pass on every leg that runs them. |
| CVE-2026-53798 | MEDIUM | The daemon name converter mapped an unknown name to uid/gid 0. | Fixed | oc-rsync reads a converter answer strictly, as upstream's namecvt_call() does (clientserver.c:1340-1354): an empty or non-numeric answer is an unknown name, and the receiver keeps the sender's id (uidlist.c:273-276). Id 0 is never resolved by name, matching recv_add_id()'s *name && id guard (PR #8108). A root e2e test (tests/daemon_name_converter_unknown_answer.rs) pushes a nobody-owned file through converters that answer empty, notanumber and 4242, and gets the sender's ids, the sender's ids and 4242:4242, the same as the upstream 3.5.1 control; making the parse read those answers as 0 fails both refusal cells. The upstream daemon-namecvt-empty-response cell reads a --fake-super xattr, which oc-rsync does not implement, so it passes without exercising the mapping. |
| CVE-2026-53799 | MEDIUM | ACL and xattr application followed a symlink race. | Fixed | Upstream parity on supported platforms (PRs #8167, #8201). The network receiver and the local copy write xattrs through a leaf pinned beneath the destination root, and skip them when the pin is refused (rsync.c:555-599,800). On Linux, ACLs take the same pin: they are serialised into the kernel's system.posix_acl_* xattr format and written with fsetxattr on the pinned fd (crates/metadata/src/acl_exacl/pinned.rs, configure.ac:1510-1514, acls.c:1266-1322); a device or socket is never opened and takes the confined parent dirfd through /proc/self/fd (lib/acl.c:188-255), and --version reports ACL-at. Special files get their ACL through the same pin (receiver_applies_the_acl_to_a_fifo_entry). macOS and FreeBSD apply ACLs by path, as upstream 3.5.1 does (acls.c:1316-1322, 1352); a daemon can refuse them there with refuse options = acls. Upstream's Solaris facl arm (acls.c:1325-1344) does not apply, because oc-rsync does not support Solaris or illumos. commit_path_acl_apply_is_pinned_beneath_the_destination_root and a_local_copy_acl_sync_is_pinned_beneath_the_destination_root drive both receivers through a swapped parent. |
| CVE-2026-53800 | MEDIUM | --remove-source-files unlink followed a parent-symlink race. |
Fixed | sender-remove-source-secure and sender-remove-source-relative-anchor pass on every leg that runs them. |
| CVE-2026-53801 | MEDIUM | Directory-scan enumeration escaped the transfer root or module. | Fixed | sender-scan-dir-escape and daemon-scan-dir-escape pass on every leg that runs them. |
| CVE-2026-53802 | HIGH | Symlinked operator input files: merge files, per-directory merges and .cvsignore. |
Fixed | Merge and --*clude-from files open through the ownership walk (PRs #7421-#7426). filter-merge-symlink passes on every leg that runs it. |
| CVE-2026-53803 | HIGH | Symlinked operator output paths: --log-file, batch files, and the daemon motd, lock, early-input and config opens. |
Fixed | The --config, log file / --log-file, lock file, motd file, --write-batch and its .sh script, --read-batch and --early-input opens go through the ownership walk (PRs #7439, #7441, #7443). The oc-only --motd-file / --motd flags now take the same walk instead of a plain read, and the pid file leaf is opened as upstream's create_pid_file() does (clientserver.c:1636-1654): a symlink is replaced, never written through (PR #8108). log-file-symlink, operator-path-write-batch, batch-file-symlink, daemon-config-symlink (config, motd and lock file) and early-input-symlink pass on every leg that runs them. A root e2e test (tests/daemon_operator_path_symlinks.rs) plants a nobody-owned symlink at the config, log file, pid file, --motd-file and --read-batch paths; reverting the motd or pid-file fix fails its cell. |
| CVE-2026-70452 | HIGH | hosts deny failed open when a configured hostname did not resolve. |
Fixed | Lookup failure and non-match are now distinct results, and matching is case-insensitive as in upstream iwildmatch (access.c:57) (PR #7314). daemon-deny-dns-failopen passes on every leg that runs it. |
| CVE-2026-70453 | HIGH | Quadratic CPU in hash_search() from a long equal-weak-checksum chain. |
Fixed | The chain walk is bounded (PR #7293). hashsearch-chain passes on every leg that runs it. |
| CVE-2026-70454 | MEDIUM | rsync-ssl made unauthenticated TLS connections. |
Not applicable | oc-rsync ships no rsync-ssl. |
| CVE-2026-70455 | HIGH | A daemon client could request any Zstandard worker count. | Fixed | daemon-zstd-thread-exhaustion and daemon-refuse-compress-threads-alias pass on every leg that runs them. |
| CVE-2026-70456 | HIGH | Heap write one past the end in read_args() at exactly maxargs. |
Not applicable | The daemon crate denies unsafe code, so an out-of-bounds write cannot happen. The argument count is still bounded at upstream's MAX_DAEMON_ARGS (read_client_arguments_refuses_past_the_daemon_ceiling, run_daemon_logs_an_oversized_client_argv), and daemon-argv-limit passes on every leg that runs it. Each argument and each pre-authentication line is read into upstream's fixed buffer size, so a single long argument cannot grow memory either (PR #8186). |
| CVE-2026-70457 | MEDIUM | Attacker-chosen-offset write in parse_size_arg() error formatting. |
Fixed | daemon-size-arg-overflow passes on every leg. |
| CVE-2026-70458 | HIGH | Out-of-bounds write from FLAG_HLINKED accepted without -H. |
Not applicable | The file list is decoded in safe Rust (protocol, transfer), so an out-of-bounds write cannot happen. The gnum that follows the flag is range-checked, and a negative reference is refused with exit 2 as upstream does (PR #8173). proto-hlink-flag-oob needs an ASan build and skips on every leg. |
| CVE-2026-70459 | MEDIUM | Wild-pointer read from a crafted first incremental file list. | Fixed | A non-directory transfer root is refused with upstream's rejecting non-directory transfer-root entry diagnostic (read_entry_rejects_non_directory_transfer_root in crates/protocol/src/flist/read/tests.rs), and an empty directory list yields no parent index. proto-parent-ndx-empty-dirflist passes on every leg that runs it. |
| CVE-2026-70460 | HIGH | A peer --partial-dir or --backup-dir resolved by pathname escaped the module. |
Fixed | operator-path-partial-dir-daemon, operator-path-backup-dir-daemon, their -exclude-daemon variants and the three operator-path-traversal-*-daemon cells pass on every leg. |
| CVE-2026-70461 | HIGH | One-byte heap write in add_implied_include() from a trailing backslash. |
Not applicable | The filters crate denies unsafe code, so an out-of-bounds write cannot happen. Whether the rule is handled like upstream is unverified: exclude-implied-trailing-backslash skips on every leg. |
| CVE-2026-70462 | MEDIUM | A peer MSG_IO_TIMEOUT overflowed or disabled the client's I/O timeout. |
Fixed | msg-io-timeout-overflow and msg-io-timeout-zero pass on every leg that runs them. |
| CVE-2026-70463 | HIGH | auth users split on whitespace despite the leading-comma form. |
Fixed | The comma-only form is honoured for auth users and gid (PR #7345). daemon-auth-users-comma-only passes on every leg. |
| CVE-2026-70464 | HIGH | An unauthenticated peer could stall the daemon after the greeting. | Fixed | The pre-authentication phase has a deadline (crates/daemon/src/daemon/handshake_deadline.rs), and a peer @RSYNCD: OPTION line cannot override daemon parameters: a line sent before the module name is answered as an unknown module, as upstream does (PRs #7754, #8002). daemon-handshake-timeout passes on every leg that runs it. |
Count the statuses:
grep -E '^\| CVE-2026-(5|7)[0-9]{4} \|' SECURITY.md | cut -d'|' -f5 | sort | uniq -cHardening gaps without a CVE. The coverage matrix in docs/audits/cve-uts351-matrix.md found these next to the rows above. All four are now fixed:
| Gap | Upstream behaviour | Fixed in |
|---|---|---|
| A single daemon argument or pre-module line had no byte cap | read into a fixed line[1024] or BIGPATHBUFLEN buffer (clientserver.c, io.c) |
PR #8186 |
--max-alloc bounded only xattr allocations; peer-driven file-list growth was unbounded |
my_alloc() exits 22 (util2.c) |
PR #8171 |
| A negative hard-link gnum was accepted; the xattr count was unbounded on the live path | exits 2 (flist.c, xattrs.c) | PR #8173 |
| The zstd token decoder inflated a whole packet into one buffer | fixed out_buffer_size per step (token.c) |
PR #8172 |
New options in 3.5.0. --confine-root=DIR, --drop-D / --no-drop-D, --insecure-links / --no-insecure-links, and the auth digest, insecure links and proxy protocol hosts daemon directives are implemented (PRs #7396, #7299, #7350, #7484, #7648). Like upstream, --confine-root and --drop-D are not forwarded to the remote side.
--drop-D makes a receiver refuse to create device and special files. It exists because --no-D on one end only desynchronises the file list: a -D sender writes rdev fields that a --no-D receiver never reads. --drop-D refuses the creation and leaves the wire format alone.
3.5.1 raises the protocol to 33 and names no CVE ids. Its security-relevant fixes, from its NEWS.md:
| 3.5.1 fix | Status | Evidence |
|---|---|---|
Explicit sender paths traverse symlinked ancestors without weakening scan confinement; --files-from entries are operator paths |
Fixed | --files-from entries resolve through the ownership walk and are refused outside --confine-root (PR #8012). relative-source-ancestor passes on every leg that runs it. |
/dev/stdin, /dev/stdout, /dev/stderr and /dev/fd/N work for pipes; --read-batch accepts a FIFO |
Fixed | pseudo-paths, read-batch-pipe and pseudo-paths-daemon pass on every leg that runs them (PRs #8096, #8109). |
--max-alloc=0 means the maximum limit, not no limit |
Fixed | PR #8011. max-alloc-zero passes on every leg that runs it. |
rrsync restricted-root paths |
Not applicable | oc-rsync ships no rrsync. |
| inetd mode only for a network stream on stdin | Fixed | PR #8011. daemon-stdin-local-socket passes on every leg. |
--contimeout applies to daemon connections over --rsh only |
Fixed | PR #8011. contimeout-rsh passes on every leg. |
| Partial-directory state is validated | Fixed | A block match with no basis file errors as upstream does (PR #8016), and a sender NDX_DONE ends the phase as in upstream recv_files() (PR #8075). strict-basis passes on every leg. |
| An alternate-destination leaf symlink is not followed as a basis | Fixed | alt-dest-symlink-race passes on every leg. |
| Undefined shifts in the bundled zlib | Not applicable | oc-rsync does not build upstream's bundled zlib. |
No 3.5.1 testsuite cell fails on any leg. A future fail row would be listed, with cause and owner, by awk '!/^#/ && $2=="fail" {print $1}' tools/ci/upstream-3.5.1-expect.*.txt | sort -u.
The rsync 3.4.3 defense-in-depth audit, the path-syscall (*at) migration record for CVE-2026-29518 / CVE-2026-43619, and the rsync 3.4.2 audit list are kept in docs/audits/security-upstream-audit-history.md.
Sources:
- rsync-announce: https://lists.samba.org/mailman/listinfo/rsync-announce
- NVD: https://nvd.nist.gov/vuln/search?query=rsync
- GitHub Security Advisories for this repository
- Scheduled watcher:
upstream-release-watch.ymlrunstools/ci/check_upstream_release.shevery Monday and opens a tracking issue when a new upstream rsync release ships
For each new upstream CVE:
- Analyse the root cause (memory corruption, logic error, etc.)
- Check whether oc-rsync has an equivalent code path
- Check whether Rust's guarantees actually cover it (logic flaws are not covered)
- Record the analysis in this file
- If oc-rsync is affected, issue a security advisory and a fix
Three cargo-fuzz workspaces hold 36 targets. They are excluded from the main workspace and do not commit a Cargo.lock, so each fuzz build resolves a fresh lockfile.
| Workspace | Targets |
|---|---|
fuzz/ (26) |
Protocol parsing: varint_decode, multiplex_frame_parse, legacy_greeting, daemon_greeting, ndx_codec, protocol_wire, flist_entry_decode, incremental_flist, capability_flags, vstring, auth_response. Files and options: batch_reader, bwlimit, acl_xattr_wire, filter_list_wire, buffered_map. Compression: decompressor_zlib, decompressor_zstd, zlib_token_decode. SIMD parity: simd_checksum_parity. Concurrency: parallel_receive_delta_adversarial. Differential against upstream: filter_rules_vs_upstream, filter_differential, differential_outcome, differential_multiplex, differential_flist |
crates/protocol/fuzz/ (8) |
fuzz_varint, fuzz_delta, fuzz_multiplex_frame, fuzz_legacy_greeting, file_entry_roundtrip, multiplex_frame_roundtrip, varint_roundtrip, negotiation_prologue |
crates/filters/fuzz/ (2) |
fuzz_filter_parse, fuzz_filter_chain |
Every target runs every day. fuzz-coverage-report.yml ("Fuzz Daily Sweep") fuzzes 31 of them for 120 seconds each, built from the workspaces' [[bin]] tables by tools/ci/fuzz_target_matrix.py, and fails on any crash, sanitizer report, timeout or OOM. The five differential targets run for longer in differential-fuzzer-overnight.yml and filter-fuzzer-overnight.yml. See fuzz/README.md for how to run a target locally.
recycle_buffer(buf_id) in the io_uring path (crates/fast_io/src/io_uring/buffer_ring/mod.rs) validates that buf_id falls within the registered buffer pool and returns BufferRingError::BufferIdOutOfRange when it does not. The check runs in both debug and release builds, and the recycle is refused before any state is mutated, so a corrupted or attacker-influenced buf_id cannot advance the ring tail or write into kernel-shared memory.
io_uring buffer-group IDs (bgid) live in a 16-bit namespace. Allocation is capped at this bound, and exhaustion returns BgidAllocError::Exhausted rather than wrapping. Released ids go back to a free list. No production caller allocates one today: BufferRing and BgidAllocator appear only inside fast_io and its tests, so exhaustion is not a live operational condition (docs/audits/bgid-lifecycle.md, section 5). Peak-occupancy telemetry becomes relevant once per-session rings are wired in.
- Transport. The daemon protocol is plaintext, like upstream: it authenticates but does not encrypt. On an untrusted network, bind the daemon to
127.0.0.1or a private interface and reach it through an SSH tunnel (ssh -L) or the SSH transport, or through a TLS terminator such asstunnelor HAProxy / nginx in TCP mode. oc-rsync has no built-in TLS client (the former--ssl/client-tlspath was removed to match upstream), so clients use an external wrapper such asrsync-sslorstunnel. - Confinement.
use chroot = yes, and expose only the paths you need. Preferread only = yeswhere possible. - Access control.
hosts allow/hosts denyrun before authentication;auth userswith asecrets fileof mode0600, owned by the daemon user only. Withproxy protocol = true, list the trusted proxies inproxy protocol hosts; left unset, every PROXY header is refused. - Identity mapping.
numeric ids = yeskeeps uid/gid mapping independent of the daemon'spasswd/group. - Options.
refuse options = delete *for read-only mirrors.
- Verify the server's identity: use SSH for transport when possible
- Check
--deletetargets: make sure you are syncing to the intended destination - Review exclude patterns: avoid transferring sensitive files by accident
- Confine untrusted transfers:
--confine-root=DIRconfines every operator- and peer-supplied path beneathDIR, and--drop-Dmakes a receiver refuse device and special files
Security researchers who have contributed to oc-rsync's security:
- (Your name could be here - report responsibly!)