Skip to content

Security: oferchen/rsync

SECURITY.md

Security Policy

Supported Versions

Version Supported
0.6.x ✅ (current: 0.6.2)
0.5.x ⚠️ critical fixes only
< 0.5

Reporting a Vulnerability

If you discover a security vulnerability in oc-rsync, please report it responsibly:

  1. Do not open a public GitHub issue for security vulnerabilities
  2. Email the maintainer directly at: skewers.irises.3b@icloud.com
  3. 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)

Security Design

oc-rsync leverages Rust's memory safety to eliminate entire vulnerability classes:

Memory Safety Guarantees

  • 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

Unsafe Code Policy

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. embedding carries one #[cfg(test)]-only #[allow(unsafe_code)] on a tests::EnvGuard helper that wraps std::env::set_var under 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 via getpwuid_r/getgrnam_r, setuid/setgid, setattrlist)
  • protocol - One isolated #[allow] in multiplex::helpers for performance-critical frame parsing
  • engine - 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 into fast_io::signal::install_signal_handler; the handlers themselves are defined in core::signal under 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 tests
  • fast_io - Platform I/O syscalls (sendfile, io_uring, mmap, copy_file_range, IOCP, WSARecv/WSASend, setsockopt) and the signal::install_signal_handler FFI wrapper, with standard I/O / no-op fallbacks
  • windows-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.

CVE Monitoring

Upstream rsync CVEs

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.

Upstream rsync 3.4.3 audits (2026-05-20)

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 a MAX_* ceiling.
  • Length-underflow guard in cumulative snprintf() callers - oc-rsync uses format!()/write!() which do not underflow; the equivalent risk is usize subtraction, audited cleanly.
  • Parent block-index bounds check on receiver - addressed by CVE-2026-43620 entry above.
  • NULL check in read_delay_line() - oc-rsync uses Option<&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.rs and surrounding) uses bounded u32 varints 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.rs already had hostname validation; verify it includes leading-hyphen rejection).
  • NULL-check on localtime_r() in timestring() - oc-rsync uses chrono/time for timestamp formatting; out-of-range timestamps return Err rather 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 through DirSandbox.
  • 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_idx per CVE-2026-43620 mitigation) - closed. DirectoryTree::try_add_directory validates the wire-supplied parent index and returns DirTreeError::OutOfBoundsParent; three regression tests in crates/protocol/src/flist/dir_tree.rs pin down both the graceful-reject path and the worst-case controlled-panic path (no SIGSEGV).

SEC-1 progress (CVE-2026-29518 / CVE-2026-43619)

Shipped:

  • SEC-1.a/b/c/d/e: DirSandbox carrier 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_metadata path resolves via fstatat(AT_SYMLINK_NOFOLLOW) routed through DirSandbox.
  • SEC-1.g (PR #4671): receiver remove_file / remove_dir path uses unlinkat routed through DirSandbox.
  • SEC-1.h (PR #4683): receiver mkdir / symlink / hard_link creation paths use mkdirat / symlinkat / linkat routed through DirSandbox.
  • SEC-1.k: macOS verified - the *at syscall 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 *at paths.

Remaining work:

  • SEC-1.i - replace path-based chmod / lchown / utimes with fchmodat / fchownat / utimensat. In flight.
  • SEC-1.j - replace path-based rename with renameat. 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 *at primitives 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.

Upstream rsync 3.4.2 audits

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 qsort element-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::Local pre-init before chroot (#2234)
  • --open-noatime propagation through sender source-file opens (#2236)
  • AVX2 get_checksum1 mul_one uninitialised-regression audit (#2222)
  • MD4 get_checksum2 buf1 uninitialised-regression audit (#2223)
  • SIMD vs scalar self-test that cross-validates AVX2/SSE2/NEON paths at startup (#2224)

Monitoring Process

  1. Subscribe to rsync-announce: https://lists.samba.org/mailman/listinfo/rsync-announce
  2. Monitor NVD: https://nvd.nist.gov/vuln/search?query=rsync
  3. GitHub Security Advisories: Watch this repository for security advisories
  4. Scheduled CI watcher: tools/ci/check_upstream_release.sh runs weekly via GitHub Actions and opens a tracking issue when a new upstream rsync release ships, so new CVEs are surfaced automatically

When New CVEs Are Published

For each new upstream rsync CVE:

  1. Analyze the root cause (memory corruption, logic error, etc.)
  2. Check if oc-rsync has equivalent code paths
  3. Verify Rust's safety guarantees apply
  4. Document the analysis in this file
  5. If vulnerable, issue a security advisory and patch

Fuzzing

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_parity

simd_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.

Hardening Notes

These cover operationally relevant trade-offs in the current code base and how to mitigate them.

Buffer pool bounds checks

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 ID namespace

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.

SSH double compression

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.

Daemon TLS

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 -L to 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.

Daemon module hardening

In addition to use chroot = yes, prefer:

  • numeric ids = yes so uid/gid mapping does not depend on the daemon's passwd/group
  • refuse options = delete * for read-only mirrors
  • hosts allow / hosts deny ACLs at the daemon layer (these run before authentication)
  • secrets file permissions of 0600, owned by the daemon user only

Security Best Practices for Users

Daemon Mode

When running oc-rsync --daemon:

  1. Use chroot: Configure use chroot = yes in rsyncd.conf
  2. Restrict modules: Only expose necessary paths
  3. Authentication: Use auth users and secrets file for access control
  4. Network security: Run behind a firewall, use SSH tunneling for remote access
  5. Read-only modules: Use read only = yes where possible

Client Mode

  1. Verify server identity: Use SSH for transport when possible
  2. Careful with --delete: Ensure you're syncing to the intended destination
  3. Review exclude patterns: Avoid accidentally transferring sensitive files

Acknowledgments

Security researchers who have contributed to oc-rsync's security:

  • (Your name could be here - report responsibly!)

There aren't any published security advisories