| 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..n). A Landlock LSM defense-in-depth layer (SEC-1.p, PR #4702) allowlists module.path on the daemon receiver via Landlock 0.4 (v1/v2/v3 best-effort, kernel 5.13+), so even residual receiver call sites that have not yet been wired through DirSandbox are bounded by a kernel-enforced filesystem allowlist. Receiver call-site wiring for the SEC-1.i / SEC-1.j helpers is the remaining gap (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. All *at helpers shipped: 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). 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+), capping the blast radius of any residual unconverted call site. Receiver wiring follow-up tracked separately (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..p. All*athelpers have shipped (SEC-1.a..n, including SEC-1.i in #4690 and SEC-1.j in #4693), and the SEC-1.p Landlock LSM defense-in-depth layer has shipped (PR #4702). The remaining work is receiver call-site wiring throughDirSandboxfor the deferred SEC-1.i / SEC-1.j sites (see SEC-1 progress note below). Beta-blocker status lifts once that receiver wiring lands. - 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.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 (v1/v2/v3 best-effort, 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.
Remaining work:
- mknodat for device / FIFO nodes - DEFERRED (closure doc #4694; not on the daemon-reachable surface).
- Receiver wiring for SEC-1.i helpers - DEFERRED (carrier-first staging; metadata crate does not yet carry
DirSandbox). Bounded by the SEC-1.p Landlock layer in the interim. - Receiver wiring for SEC-1.j deferred sites - DEFERRED (
disk_commit,transfer_ops/response,local_copy/executor; cross-thread carrier plumbing required). Bounded by the SEC-1.p Landlock layer in the interim. - 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-dirpaths that resolve outside the module root are rejected at the wire-protocol layer rather than widening the allowlist. 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.
Target full-Fixed status: when receiver wiring for both SEC-1.i and SEC-1.j callers is complete and the SEC-1.m / SEC-1.n regression suites continue to pass against the fully-wired pipeline. The SEC-1.p Landlock layer is already in place; it complements, but does not substitute for, completing the *at chain.
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!)