| Version | Supported |
|---|---|
| 0.6.x | ✅ (current: 0.6.4) |
| 0.5.x | |
| < 0.5 | ❌ |
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)
oc-rsync leverages Rust's memory safety to eliminate entire vulnerability classes:
- No buffer overflows: Rust's bounds checking prevents out-of-bounds memory access
- No use-after-free: Rust's ownership system prevents dangling pointer access
- No uninitialized memory: All memory must be initialized before use
- No data races: Rust's type system prevents concurrent memory access bugs
Crates that enforce #![deny(unsafe_code)] with no allow-listed exceptions in production code:
daemon,cli,core,transfer,batch,filters,signature,matching,bandwidth,logging,logging-sink,branding,rsync_io,compress,apple-fs,flist,embedding,test-support- business logic, parsers, orchestration, and high-level I/O wrappers.embeddingcarries one#[cfg(test)]-only#[allow(unsafe_code)]on atests::EnvGuardhelper that wrapsstd::env::set_varunder a process-wide mutex.
Crates with #![deny(unsafe_code)] and targeted #[allow(unsafe_code)] for documented FFI/SIMD boundaries:
metadata- Ownership and privilege FFI (UID/GID lookup viagetpwuid_r/getgrnam_r,setuid/setgid,setattrlist)protocol- One isolated#[allow]inmultiplex::helpersfor performance-critical frame parsingengine- Denies unsafe outside tests (#![cfg_attr(not(test), deny(unsafe_code))]) with targeted#[allow(unsafe_code)]on platform FFI (prefetch, buffer pool,CopyFileExW)platform- Daemonization, name resolution (getpwnam_r/getgrnam_r), privilege transitions (setuid/setgid/initgroups), and chroot syscalls. Signal-handler installation has been hoisted out intofast_io::signal::install_signal_handler; the handlers themselves are defined incore::signalunder a plain#![deny(unsafe_code)].checksums- SIMD intrinsics for MD4/MD5 and rolling checksums (AVX2, AVX-512, SSE2, SSSE3, SSE4.1, NEON, WASM), with scalar fallbacks and parity testsfast_io- Platform I/O syscalls (sendfile, io_uring, mmap,copy_file_range, IOCP,WSARecv/WSASend,setsockopt) and thesignal::install_signal_handlerFFI wrapper, with standard I/O / no-op fallbackswindows-gnu-eh- Windows GNU exception handling shims (properly documented)
Long-term direction. Unsafe code is being consolidated into fast_io as the single crate permitted to wrap platform FFI directly; new unsafe code goes there and is exposed via safe public APIs. New #[allow(unsafe_code)] annotations in any other crate require explicit review.
Note: OS-level race conditions (TOCTOU) remain possible at filesystem boundaries; Rust's memory safety does not prevent them.
oc-rsync monitors upstream rsync CVEs to verify continued non-applicability. Recent CVEs and our status:
| 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 | Path sanitization |
| CVE-2024-12088 | --safe-links bypass | Mitigated | Rust path handling |
| CVE-2024-12747 | Symlink race condition | Mitigated | TOCTOU is OS-level |
| 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 (SEC-1.a..q2). Device/FIFO node creation uses mknodat/mkfifoat (SEC-MK.a..h). A Landlock LSM defense-in-depth layer (SEC-1.p, 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 chroot at two levels: session-level resolve_peer_hostname() in session_runtime.rs::handle_session (at accept time, before any module selection), and module-level module_peer_hostname() in module_access::request.rs::respond_with_module_request / listing.rs::respond_with_module_list (during ACL evaluation). Per-module chroot is applied later in transfer.rs::apply_privilege_restrictions_with_upstream_errors (after auth and argument reading). The daemon chroot global directive is applied once at startup in accept_loop.rs::serve_connections (before the accept loop), so per-connection DNS post-chroot can fail when the chroot lacks NSS configuration. To close that path without depending on the chroot containing /etc/resolv.conf//etc/nsswitch.conf//etc/hosts/NSS shared objects, ModuleDefinition::permits fails closed when reverse DNS returns None and any hosts deny rule is hostname-based - an attacker who controls their PTR record (or simply blackholes reverse DNS) cannot bypass a hostname-pattern deny rule. Regression tests: module_peer_hostname_resolution_before_chroot_denies_unknown (allow-side fail-closed), module_hostname_deny_fails_closed_when_dns_unresolved (GHSA scenario A: deny-side fail-closed under daemon chroot), module_ip_deny_unaffected_by_dns_failure (scope guard: IP-only rules retain their original semantics). |
| 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 (if self.rx_token < 0 { return Err(...) }) in all three wire decoders: zlib_codec.rs:419, zstd_codec.rs:460, lz4_codec.rs:346. 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 (SEC-1.i, PR #4690); rename migrated to renameat (SEC-1.j, PR #4693); mknod / mkfifo migrated to mknodat / mkfifoat (SEC-MK.a..h). Receiver wiring for SEC-1.i/j helpers completed (SEC-1.q/q2). The recursive_unlinkat helper shipped (SEC-1.s). The SEC-1.p 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 indexes into a bounds-checked Vec (crates/protocol/src/flist/dir_tree.rs). The validating entry point DirectoryTree::try_add_directory returns DirTreeError::OutOfBoundsParent on a malformed wire index; the unchecked add_directory aborts via Rust's bounds-check panic. Regression coverage: try_add_directory_rejects_out_of_range_parent_idx, try_add_directory_rejects_boundary_off_by_one, add_directory_panics_safely_on_oob_parent_idx in crates/protocol/src/flist/dir_tree.rs. SEC-4 closed. |
| 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: SEC-2.a (PR #4609); upstream-parity alignment SEC-2.b (PR #4812). |
rsync 3.5.0 is a major security release closing 33 CVEs, concentrated in path handling and the daemon. Unlike the 3.4.2/3.4.3 batches below, this set is not yet fully audited against oc-rsync, and this section states that plainly rather than implying coverage that does not exist.
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:
| leg | pass | fail | skip | corpus |
|---|---|---|---|---|
| non-root, pipe | 239 | 21 | 85 | 349 |
| root, pipe | 262 | 29 | 54 | 349 |
| non-root, tcp | 93 | 29 | 33 | 158 |
| root, tcp | 104 | 38 | 13 | 158 |
The two pipe legs run the whole corpus; the tcp legs add --daemon-tests-only, so they re-run the 158 tests that can observe the transport. 31 distinct tests diverge across the two full-corpus legs, and 55 across all four. 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-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<Vec<IpAddr>> 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). |
| CVE-2026-53795 | HIGH | An absolute --temp-dir or --link-dest disabled path confinement entirely. |
Directly applicable, and largely closed. oc-rsync's confinement was narrower than 3.5.0's and has been widened onto one shared per-component resolver: the staging family (--temp-dir, --partial-dir, --backup-dir), --log-file, the alt-dest basis leaf, --relative implied parents, and absolute rename endpoints all now resolve through the ownership walk (PRs #7398, #7404, #7415, #7419, #7393). The operator-named auxiliary file family followed: the daemon config, motd, secrets and lock file, --password-file, --log-file, --files-from, --early-input, the --*clude-from and merge files, and the batch files all open through the walk, and the alt-dest basis entry is stat-ed through it rather than opened (PRs #7421-#7426, #7439, #7441-#7443, #7459, #7463). What remains is the per-option-family confinement for the residual operator-path-* divergences, tracked openly against the 3.5.0 manifests. |
| CVE-2026-53783 | HIGH | rrsync restricted-directory escape - each path was validated, but the validation could be walked out of. |
oc-rsync ships no restricted-shell wrapper today; one is being added, enforcing through the same path resolver rather than a separate validator. The upstream rule set is extracted as a spec (PR #7283) - note it forces --drop-D, not --no-D. |
| CVE-2026-53790 | HIGH | Command / argument injection via unquoted peer- or config-supplied values. | Largely closed. Upstream has four sinks and three remedies; oc-rsync has two of the same shape, one structurally different, and one that does not apply. The live sink here is oc-rsync's own single-character config expansion, which upstream does not have - so an upstream-shaped port alone would have been inert, and the refusal was applied at that sink instead: shell metacharacters are now rejected when expanding a hook variable for pre-xfer exec / post-xfer exec (PR #7465), and the RSYNC_CONNECT_PROG %H substitution is validated and quoted rather than interpolated verbatim (PR #7430). The batch replay script closed two further injections in its generated .sh (PR #7445). |
| CVE-2026-70453 | HIGH | Quadratic CPU exhaustion in hash_search() from a long equal-weak-checksum chain. |
Fixed (PR #7293). oc-rsync's block matcher differs from upstream's, so the bound was derived independently rather than copied. |
| 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 and proxy protocol hosts daemon directives are not yet implemented.
--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.
A Rust reimplementation is immune to the memory-corruption half of several of these by construction. It is not immune to the logic half - a fail-open access check, an unconfined path, or an unbounded peer-supplied count is equally reachable in safe Rust. This section will be updated per-CVE as each is closed or evidenced as non-applicable.
rsync 3.4.3 (released 2026-05-20) is a major security release closing six CVEs and a defense-in-depth batch. Per-CVE applicability is captured in the table above (CVE-2026-29518 / 43617 / 43618 / 43619 / 43620 / 45232). The defense-in-depth items were audited as follows:
- Bounded wire-supplied counts and lengths in flist/io/acls/xattrs - oc-rsync already validates these at decode (
crates/protocol/src/flist/read/,xattr/cache.rs:123,141,acl/). Re-audit confirmed no path accepts an unbounded length without aMAX_*ceiling. - Length-underflow guard in cumulative
snprintf()callers - oc-rsync usesformat!()/write!()which do not underflow; the equivalent risk isusizesubtraction, audited cleanly. - Parent block-index bounds check on receiver - addressed by CVE-2026-43620 entry above.
- NULL check in
read_delay_line()- oc-rsync usesOption<&str>so the C null-dereference is impossible. - Lower ceiling on
MAX_WIRE_DEL_STAT- audit confirmed our delete-stats reader (crates/protocol/src/flist/delete_stats.rsand surrounding) uses boundedu32varints capped well below the upstream lowered ceiling. - Reject hyphen-prefixed remote-shell hostnames - tracked under SEC-3 (
crates/rsync_io/src/ssh/operand.rs+parse.rsalready had hostname validation; verify it includes leading-hyphen rejection). - NULL-check on
localtime_r()intimestring()- oc-rsync useschrono/timefor timestamp formatting; out-of-range timestamps returnErrrather than dereferencing a null pointer.
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*athelpers shipped (SEC-1.a..n), receiver call-site wiring completed (SEC-1.q/q2),DeleteFstrait sandbox refactor shipped (SEC-1.q),recursive_unlinkathelper shipped (SEC-1.s), andmknodat/mkfifoatmigration 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 toMAX_PROXY_LINE_BYTES = 1023atconnect/proxy.rs:344, matching upstream's 1024-byteestablish_proxy_connection()stack buffer. - 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_idxper CVE-2026-43620 mitigation) - closed.DirectoryTree::try_add_directoryvalidates the wire-supplied parent index and returnsDirTreeError::OutOfBoundsParent; three regression tests incrates/protocol/src/flist/dir_tree.rspin down both the graceful-reject path and the worst-case controlled-panic path (no SIGSEGV).
Shipped:
- SEC-1.a/b/c/d/e:
DirSandboxcarrier with in-tree dirfd cache,openat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS)runtime detection, and receiver pipeline wiring (PRs #4643, #4650 and prior). - SEC-1.f (PR #4668): receiver
lstat/symlink_metadatapath resolves viafstatat(AT_SYMLINK_NOFOLLOW)routed throughDirSandbox. - SEC-1.g (PR #4671): receiver
remove_file/remove_dirpath usesunlinkatrouted throughDirSandbox. - SEC-1.h (PR #4683): receiver
mkdir/symlink/hard_linkcreation paths usemkdirat/symlinkat/linkatrouted throughDirSandbox. - SEC-1.i (PR #4690):
fchmodat/fchownat/utimensatsandbox helpers replace path-basedchmod/lchown/utimes. - SEC-1.j (PR #4693):
renameatsandbox helper replaces path-basedrename. - SEC-1.k: macOS verified - the
*atsyscall family is available and behaves consistently with the Linux migration. - SEC-1.l: Windows audited - NTFS handle-based APIs naturally sidestep the TOCTOU window, so Windows is not affected by either CVE.
- SEC-1.m (PR #4675): comprehensive symlink-swap attack regression coverage against the daemon receiver.
- SEC-1.n (PR #4678): interop regression coverage confirming legitimate symlinks still transfer correctly under the new
*atpaths. - SEC-1.p (PR #4702, shipped 2026-05-22): Landlock LSM defense-in-depth for the daemon receiver.
crates/fast_io/src/landlock.rswraps Landlock 0.4 (requests up to ABI v5 with best-effort downgrade; v1 on kernel 5.13+);crates/daemon/src/daemon/sections/module_access/transfer.rs::engage_landlock_sandboxallowlistsmodule.pathimmediately before the receiver pipeline starts, so any residual unconverted path-based syscall is bounded by a kernel-enforced filesystem allowlist.
Additionally shipped since the last update:
- SEC-1.q (DeleteFs trait sandbox refactor): deletion operations route through the
DirSandbox-backedDeleteFstrait. - SEC-1.q2 (Receiver-deletion sandbox wiring): all receiver deletion call sites fully wired through
DirSandbox. - SEC-1.s (
recursive_unlinkathelper): recursive directory removal usesunlinkatthroughout, closing the last TOCTOU window in directory tree deletion. - SEC-MK.a..h (
mknodat/mkfifoatsandbox migration): device and FIFO node creation migrated to*atvariants routed throughDirSandbox. Previously deferred (closure doc #4694); now complete. - SEC-1.p Landlock LSM defense-in-depth - Linux 5.13+ kernel-side allowlist over the module root, engaged per-connection after
apply_module_privilege_restrictionsreturns. Even a future regression that calls a path-based syscall directly (bypassingDirSandbox) is rejected by the kernel withEACCES. Client-supplied--temp-dir/--partial-dir/--backup-dir/--compare-dest/--copy-dest/--link-destpaths that resolve outside the module root are rejected at the wire-protocol layer (PR #5568, URV-5.b.1); the in-module subset is admitted to the Landlock allowlist alongside the module root so a default-on Landlock posture (URV-5.c.5) does not EACCES legitimate writes (URV-5.b.REOPEN). Best-effort ABI downgrade picks the highest level the running kernel exposes (v3 on 6.2+, v2 on 5.19+, v1 on 5.13+). Stub returnsUnavailableon non-Linux targets so the SEC-1*atchain remains the sole defense there. - SEC-1.t (receiver pre-flight dest_root symlink refusal):
ensure_dest_root_existsincrates/transfer/src/receiver/mod.rsusessymlink_metadata()(lstat) rather thanmetadata()(stat) so a symlink at the destination root is observed directly. The helper refuses withInvalidInputfor any symlinked dest - broken or pointing at an existing directory, inside or outside the module - becausecreate_dir_allagainst a stat-NotFound result would otherwise resolve through the link and materialize the directory at the symlink target, sidestepping the SEC-1*atchain that protects every subsequent per-entry write. The receiver never auto-creates through a symlink; operators that genuinely need a symlinked dest must materialize the real directory themselves. Follow-up to PR #5567 which added the pre-flight mkdir path.
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. As of 2026-08-21 the gating corpus is the rewritten 3.5.0 Python suite, run as the four legs tabulated above; the required checks are the two stdio-pipe legs, and the two loopback-TCP legs run alongside them. 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 older 3.4.4 shell corpus still runs in the interop job with the roster at tools/ci/upstream_testsuite_known_failures.conf, which is currently empty - against 3.4.4 all tests pass.
In v0.6.2 the codebase was audited against every fix that landed in upstream rsync 3.4.2 (released 2026). The equivalent code paths were verified safe in oc-rsync:
- Compressed-stream negative-token decoder bounds (#2225)
- Xattr
qsortelement-count parity (#2226) clean_fname()buffer-underflow parity (#2227)- Allocator zeroing pattern (calloc + realloc-expand) (#2228)
- Y2038 safety in syscall paths (Int32x32To64 equivalent) (#2229)
- ACL ID mapping for non-root users (#2230, closes #618)
- FreeBSD many-xattrs handling parity (#2231)
- "Directory has vanished" error path (#2232)
- Removal of multiple leading slashes (#2233)
- Daemon
chrono::Localpre-init beforechroot(#2234) --open-noatimepropagation through sender source-file opens (#2236)- AVX2
get_checksum1mul_oneuninitialised-regression audit (#2222) - MD4
get_checksum2buf1uninitialised-regression audit (#2223) - SIMD vs scalar self-test that cross-validates AVX2/SSE2/NEON paths at startup (#2224)
- Subscribe to rsync-announce: https://lists.samba.org/mailman/listinfo/rsync-announce
- Monitor NVD: https://nvd.nist.gov/vuln/search?query=rsync
- GitHub Security Advisories: Watch this repository for security advisories
- Scheduled CI watcher:
tools/ci/check_upstream_release.shruns weekly via GitHub Actions and opens a tracking issue when a new upstream rsync release ships, so new CVEs are surfaced automatically
For each new upstream rsync CVE:
- Analyze the root cause (memory corruption, logic error, etc.)
- Check if oc-rsync has equivalent code paths
- Verify Rust's safety guarantees apply
- Document the analysis in this file
- If vulnerable, issue a security advisory and patch
The repo ships 26 cargo-fuzz targets covering security-critical parsing, SIMD parity, concurrency, and differential fuzzing against upstream rsync:
Core protocol parsing:
varint_decode- variable-length integer codecmultiplex_frame_parse- multiplexMSG_*frame parsinglegacy_greeting- daemon@RSYNCD:greeting parserndx_codec- file-list index codecprotocol_wire- generic protocol wire formatflist_entry_decode- file-list entry decoderincremental_flist- incremental file-list segmentscapability_flags(FCV-10) - negotiation prologue capability flagsvstring(FCV-14) - vstring parserauth_response- daemon authentication response
Daemon and configuration:
daemon_greeting- daemon greeting generationbatch_reader(FCV-12) - rsync batch file headerbwlimit(FCV-15) - bwlimit CLI string parser
Metadata and extensions:
acl_xattr_wire(FCV-11) - ACL/xattr wire formatfilter_list_wire- filter list wire formatbuffered_map- buffered map decoder
Compression:
decompressor_zlib- zlib decompressiondecompressor_zstd- zstd decompressionzlib_token_decode- zlib compressed-token decoder
SIMD parity:
simd_checksum_parity- cross-validates AVX2, SSE2, NEON, and scalar rolling/strong checksum paths against random inputs (see #2103)
Concurrency:
parallel_receive_delta_adversarial- adversarial scheduling against the parallel receive-delta pipeline
Filter rules:
filter_rules_vs_upstream- filter rule evaluation vs upstream behaviorfilter_differential- differential filter testing
Differential fuzzing (upstream wire parity):
differential_outcome(WDF-3) - outcome-based differential fuzzing against upstream rsyncdifferential_multiplex(WDF-4) - multiplex frame differential fuzzingdifferential_flist(WDF-5) - flist wire format differential fuzzing
See fuzz/README.md for detailed fuzzing instructions.
These cover operationally relevant trade-offs in the current code base and how to mitigate them.
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. Callers may log and ignore the error or surface it through the From<BufferRingError> for io::Error conversion.
io_uring buffer-group IDs (bgid) live in a 16-bit namespace. The provided-buffer ring helpers in fast_io cap allocation at this bound, and exhaustion returns a typed error rather than wrapping - it is never silent. IDs are recycled rather than accumulated: a measured run of 100,000 sessions consumed a single bgid, so exhaustion is not an expected operational condition and no monitoring is required for it.
If the SSH transport itself compresses the stream (Compression yes in ssh_config or a cipher with built-in compression), running oc-rsync -z will compress payloads twice. The amplification surface is small in practice but adds CPU and can mask compressor-specific bugs. Disable one layer; the canonical choice is to leave compression to rsync (-z / --compress) and disable it in SSH.
The daemon protocol is plaintext, matching upstream rsync: the daemon provides authentication but not encryption. To expose a daemon over an untrusted network, deploy it behind one of:
- SSH tunnel (
ssh -Lto a localhost-bound daemon), or use the ssh transport directly - stunnel in front of
rsync://-style daemon traffic - A reverse proxy that performs TLS termination (e.g., HAProxy in TCP mode, or nginx)
Bind the daemon to 127.0.0.1 (or a private VPC interface) and route external clients exclusively through the TLS terminator. oc-rsync has no built-in TLS client (the former --ssl / client-tls path was removed to match upstream), so clients reach an SSL-proxied daemon through an external wrapper such as rsync-ssl or stunnel, the same model as upstream.
In addition to use chroot = yes, prefer:
numeric ids = yesso uid/gid mapping does not depend on the daemon'spasswd/grouprefuse options = delete *for read-only mirrorshosts allow/hosts denyACLs at the daemon layer (these run before authentication)secrets filepermissions of0600, owned by the daemon user only
When running oc-rsync --daemon:
- Use chroot: Configure
use chroot = yesin rsyncd.conf - Restrict modules: Only expose necessary paths
- Authentication: Use
auth usersandsecrets filefor access control - Network security: Run behind a firewall, use SSH tunneling for remote access
- Read-only modules: Use
read only = yeswhere possible
- Verify server identity: Use SSH for transport when possible
- Careful with --delete: Ensure you're syncing to the intended destination
- Review exclude patterns: Avoid accidentally transferring sensitive files
Security researchers who have contributed to oc-rsync's security:
- (Your name could be here - report responsibly!)