| Version | Supported |
|---|---|
| 0.6.x | ✅ (current: 0.6.2) |
| 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) |
Mostly fixed | Path-based syscalls have been migrated to *at variants routed through DirSandbox with openat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS) runtime detection. SEC-1.a..h, .k..n have shipped; SEC-1.i/.j remain in flight (see SEC-1 progress note below). Umbrella tracking issue #2516. |
| CVE-2026-43617 | Reverse-DNS lookup after daemon chroot causes hostname ACL bypass | Not vulnerable | module_peer_hostname runs in module_access::listing / request phases (crates/daemon/src/daemon/sections/module_access/listing.rs:52, request.rs:269) which complete before chroot/setuid in transfer.rs:346-360. |
| CVE-2026-43618 | Integer overflow in compressed-token decoder causes memory disclosure | Mitigated | crates/compress/src/zstd.rs:218,224 and crates/compress/src/zlib/decoder.rs:61,67 use saturating_add for byte counters; explicit regression test counting_writer_saturating_add_prevents_overflow (zlib/tests.rs:186-189). Rust bounds-checking would panic on any post-overflow OOB index, not leak memory. |
| CVE-2026-43619 | Symlink races on chmod/lchown/utimes/rename/unlink/mkdir/symlink/mknod/link/rmdir/lstat | Mostly fixed | Same root cause as CVE-2026-29518. The lstat / unlink / rmdir / mkdir / symlink / link surfaces have been migrated to fstatat / unlinkat / mkdirat / symlinkat / linkat routed through DirSandbox; chmod / lchown / utimes (SEC-1.i) and rename (SEC-1.j) remain in flight (see SEC-1 progress note below). 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 | Mitigated | 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 = 1024 bytes, matching upstream's establish_proxy_connection() ceiling (socket.c:53). 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. |
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) - umbrella issue #2516, decomposed into SEC-1.a..o. Most mitigations have shipped (see SEC-1 progress note below); SEC-1.i and SEC-1.j remain in flight. Beta-blocker status lifts once SEC-1.i / SEC-1.j land and all receiver call sites are wired throughDirSandbox. - SEC-2.b (cosmetic: align proxy-line cap from 4096 to upstream's 1024) - SEC-2.a confirmed the structural mitigation is already in place via the 4096-byte cap at
connect/proxy.rs:337-372; SEC-2.b is purely an upstream-parity tightening, not a security gap. - SEC-3 (confirm hyphen-prefixed hostname rejection in SSH operand parse) - SEC-3.a audit in flight.
- 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.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.
Remaining work:
- SEC-1.i - replace path-based
chmod/lchown/utimeswithfchmodat/fchownat/utimensat. In flight. - SEC-1.j - replace path-based
renamewithrenameat. In flight. - mknodat for device / FIFO nodes - deferred; not on the daemon-reachable surface.
- Receiver wiring for SEC-1.i helpers - deferred to a follow-up; the carrier-first staging lands
*atprimitives ahead of full call-site migration.
Target full-Fixed status: when SEC-1.i and SEC-1.j ship and all receiver call sites are wired through DirSandbox.
CI integration: as of 2026-05-21 the interop job (.github/workflows/_interop.yml) runs upstream rsync's own testsuite/*.test corpus against oc-rsync as $RSYNC, pinned to upstream 3.4.3 by default. The known-failures roster lives at tools/ci/upstream_testsuite_known_failures.conf.
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 cargo-fuzz targets for security-critical parsing and SIMD parity:
cd fuzz
cargo +nightly fuzz run fuzz_varint
cargo +nightly fuzz run fuzz_delta
cargo +nightly fuzz run fuzz_multiplex_frame
cargo +nightly fuzz run fuzz_legacy_greeting
cargo +nightly fuzz run simd_checksum_paritysimd_checksum_parity cross-validates the AVX2, SSE2, NEON, and scalar
rolling/strong checksum paths against random inputs (see #2103). 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 validates that buf_id falls within the registered buffer pool using debug_assert!. In release builds the assertion compiles out, so a corrupted or attacker-influenced buf_id reaching this code path would index out of range and either produce undefined behaviour inside the io_uring crate or — more likely — be caught by the kernel as an invalid SQE. A defense-in-depth fix to upgrade the check to a release-mode bound is tracked; until it lands, do not expose the io_uring path to untrusted protocol input.
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 an error rather than wrapping. Long-running daemons that churn ring groups should monitor for the bounded error and recycle.
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.
oc-rsync --daemon does not terminate TLS natively. To expose the daemon over an untrusted network, deploy it behind one of:
- stunnel in front of
rsync://-style daemon traffic - SSH tunnel (
ssh -Lto a localhost-bound daemon) - A reverse proxy that performs TLS termination (e.g., HAProxy in TCP mode)
Bind the daemon itself to 127.0.0.1 (or a private VPC interface) and route external clients exclusively through the TLS terminator.
See docs/deployment/daemon-tls.md for runnable recipes covering stunnel, ssh -L, and HAProxy TCP-mode configurations, hardened systemd unit excerpts, and nftables/iptables rules that deny external access to the daemon's loopback port.
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!)