CI / lint (push) Successful in 1m25s
CI / lint (pull_request) Successful in 1m25s
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
CI / sanitizers (address) (push) Successful in 1m6s
CI / sanitizers (undefined) (push) Successful in 1m0s
CI / fuzz-build (push) Successful in 34s
CI / coverage (push) Successful in 55s
CI / build-and-test (pull_request) Successful in 1m49s
CI / valgrind (push) Successful in 3m19s
CI / build-and-test (push) Successful in 5m23s
- Protocol 2.21.0: STATUS_ERROR_DETAIL rejection reasons and server-contacting --dry-run - Daemon per-module/per-host caps and cross-process auth lockout - Config X-macro serialization, authorized_root single-owner, Data charge ownership, receiver pipeline move - Security audit hardening (SSH injection, FIFO/inplace, zstd DoS, TLS, dry-run oracle, bounds) - Pre-auth basis_count NULL-deref fix; benchmark and nix-shell improvements - Tested: unit, integration, ASan/UBSan, valgrind, fuzz, coverage (CI green)
11 KiB
11 KiB
Changelog
All notable changes to FastSync are documented here. Versions match
PROTOCOL_VERSION (printed by fastsync --version); the client and server must
run the same version because the handshake is strict.
[2.21.0] - 2026-09-14
Added
- Optional server→client rejection detail (protocol 2.21.0). A rejected
operation may now carry a bounded human-readable reason via
STATUS_ERROR_DETAILinstead of a bareSTATUS_ERROR, so the client can report why the server refused (daemon module gate, config validation, receiver-side path/node validation).receive_status()transparently maps the new status back toSTATUS_ERRORfor every existing call site and captures the reason into a thread-local buffer exposed byprotocol_last_error(). The detail body is always consumed, so the stream cannot desynchronize, and messages are sliced toMAX_ERROR_DETAIL_BYTES(4096) on send. - Server-contacting
--dry-run(protocol 2.21.0).--dry-runnow performs a real handshake with a remote/daemon receiver and reports exactly what WOULD change based on receiver state (existing destination files, mtimes, checksums, basis dirs). The wire config carries the dry-run intent (Config.dry_run) and the receiver answers each per-file check withSTATUS_DRY_RUN_TRANSFER(would transfer) orSTATUS_OK(already up to date); the sender prints the would-transfer set and its trailer without sending any file data. The receiver performs the normal read-only incremental decision but mutates nothing: no temp files, writes, renames, deletes, metadata/xattr/chown, or directory creation. A plain local destination (no explicit--server-port/remote) keeps the original client-side dry-run. Would-delete reporting for--delete*is deferred to a follow-up; dry-run never deletes. - Daemon
max connections per host(per-source-IP concurrent cap, default 0 = unlimited),auth lockout threshold(default 10; 0 disables) andauth lockout duration(default 300 s) config keys. fastsync-server --allow-superopt-in for a privileged standalone TCP server; without it a root standalone receiver forces super-user activities off (device nodes,--write-devices, ownership). The--stdioSSH argv is client-composed, so super activities always stay off there.
Changed
- Config wire fields are now declared once in an X-macro table
(
CONFIG_WIRE_FIELDSinsrc/shared/config.h) that generates the struct members, defaults, and the send/receive sequence, removing the manual six-site field sync. Wire bytes andPROTOCOL_VERSIONare unchanged. receive_incremental_check()(the per-fileSTATUS_CHECKfast path) is split into small static helpers with a short linear orchestrator. Pure refactor: the wire byte stream and all cleanup are unchanged.authorized_rootstate has a single owner (utils.c) with read accessors; the duplicated statics infile.cand the server were removed.Datarecords its owningProtocolSessionso its memory charge is returned to the session that reserved it, regardless of the destroying thread.- The receiver pipeline moved out of
sharedintoserver/receiver_pipeline.[ch]; the build now uses explicitfastsync_shared/fastsync_client_core/fastsync_server_coretargets instead of a GLOB, and the client no longer links server code. - The benchmark tool generates the requested random/compressible data mix
accurately, verifies each transfer before recording it, computes correct
percentiles, adds a MB/s column, handles
tc/netem without requiringsudowhen already root, builds into a dedicatedbuild-bench/directory, and adds a--warmincremental-transfer mode. - The
nix-shelldev environment provides the full toolchain (clang-format, cppcheck, pytest-xdist, OpenSSH, rsync, iproute2, valgrind, lcov) and no longer builds on entry.
Security
- Enforce the daemon's per-module
max connectionscap (0 = unlimited) and add the shared per-sourcemax connections per hostcap plus a cross-processauth lockout. Because the listener forks one child per connection, the counters live in an anonymous shared mapping created before the accept loop and reclaimed by the parent'sSIGCHLDhandler, so the per-module, per-source and auth-failure state is shared across every child (including afterSIGKILL). The per-source table has a bounded lifetime (expired/idle entries are reclaimed, with a rate-limited warning when genuinely full), and the occupancy counters are re-derived from the shared slot table on every child exit. Trusted loopback peers are exempt (they share one address); clients behind a shared NAT/proxy share a single per-host budget and lockout, which is documented. - Hardening from a full security audit:
- Fail a truncated zstd frame instead of spinning forever (remote DoS).
- Open receiver destination/basis/hard-link entries
O_NONBLOCKso a client-planted FIFO cannot block a worker indefinitely. - Require a regular file before
--inplacewrites, closing a FIFO-hang and a raw-device write that bypassed the--write-devicesgate. - Reject SSH destinations whose user/host begins with
-and insert--before the host token, closing-o ProxyCommand=…argument injection (RCE). - Gate client
--forcerecursive removal behind the server--allow-deletepolicy. - Reject empty
hosts allow/hosts deny/auth usersvalues instead of silently meaning "unrestricted". - Restrict TLS 1.2 to AEAD suites and set server cipher preference; load the
private key TOCTOU-safely from an
O_NOFOLLOWfd; verify IP literals against IP SANs; guard client-cert CN truncation. - Make
--dry-runcontent-blind: it neither reads destination files nor hashes basis files, removing a 1-bit content oracle againstread onlymodules. - Bound glob matching (iterative DP, no exponential backtracking) and bound
line reads for filter/
--files-from/pattern files. - Gate
system.posix_acl_*xattrs on--aclsand charge decompression/chunk allocations against the per-connection memory budget.
Fixed
- Pre-auth NULL dereference in
config_delete()when an over-longbasis_count(and the analogous count fields) was received and then failed validation; received counts are now validated before being published. - Leaked inherited
Datain the forked compression-truncation unit test (valgrind definite leak). receive_status()no longer loses a captured rejection reason when owed keepalives are drained.
[2.20.0] - 2026-09-13
Security
- Cap cumulative
DirTimeListgrowth and bound pre-auth config-string memory (remote memory-exhaustion DoS). - Daemon host access control (
hosts allow/hosts deny, IPv4/IPv6/CIDR), configurable globalmax connections, connection audit logging, and a boundedauth failure delaythrottle. IPv4-mapped peers are normalized and invalid patterns are rejected at parse time (no silent fail-open). - Honor
--timeoutfor protocol I/O and bound idle/session time to defeat keepalive slowloris; child-safe signal handling in the forked daemon. - Compiler/linker hardening (
_FORTIFY_SOURCE, stack protector, PIE, RELRO) and pinned build dependencies.
Fixed
- Use-after-free in the basis-dir oversize preflight.
- Placeholder
Dataleaks,missing_argsleak, scanner chunk leak. - Thread-safe logging; single fd owner and cleanup epilogue in the server handler.
Performance
- Metadata now crosses the wire as one packed frame (protocol 2.20.0).
- Delete keep-set and
--files-fromlookups indexed (O(n*m) → O(n)). - Reused per-thread zstd contexts;
TCP_NODELAYby default. - Byte-bounded sender queues; removed a redundant scanner
stat().
[2.19.0] - 2026-09-12
Security
- Daemon authentication rewritten as SCRAM-SHA-256 challenge/response
(
STATUS_AUTH_CHALLENGE→STATUS_AUTH_RESPONSE→STATUS_AUTH_OK/STATUS_AUTH_FAILED), replacing the old replayable staticSHA-256(password)bearer credential. Each proof is bound to a fresh per-connection server nonce plus a client nonce, so a captured response can never be reused. - Salted verifier store.
--password-file/--early-inputnow holduser:$fastsync$1$pbkdf2-sha256$<iters>$<salt>$<stored_key>$<server_key>(PBKDF2-HMAC-SHA256, default 600000 iterations, range 100000–10000000). The legacyuser:SHA256HEXform is hard-rejected; there is no auto-upgrade. Generate stores offline withfastsync-server --hash-credentials FILE [--iterations N]. - Username-enumeration hardening. Unknown/off-list users are answered with a
dummy verifier whose salt is a deterministic per-username value
(
HMAC-SHA256(dummy_key, username)), using the store-wide uniform iteration count and a constant-time full-length membership scan. The dummy key is persisted in an owner-only<store>.dummykeysidecar (atomic publish, exact mode 0600) so challenges are stable across restarts. - Verified transport for auth-required modules. A module with
auth usersaccepts credentials only over verified TLS whose client certificate matches--client-cn, or — when--allow-unauthenticatedis explicitly set — plaintext from a loopback peer. Remote plaintext is refused before any challenge. Clients must use--tlsto send--password-filecredentials to a non-loopback daemon;--client-cnis mandatory with--tls. - Secret hygiene. The plaintext password, derived keys, nonces/proofs and the dummy key are wiped from memory on every path and never logged.
- Carried-over hardening:
-KTOCTOU-safe directory walk (openat(O_NOFOLLOW)per component), always shell-quoted SSH remote path, TLS compression/renegotiation disabled, race-free (open-then-fstat)--password-file/--early-inputchecks, log-injection escaping, and lazy protocol debug escaping.
Added
fastsync-server --hash-credentials FILE [--iterations N]offline tool.<store>.dummykeysidecar (auto-created, owner-only, 0600).- Integration tests for auth replay rejection, malformed frames, legacy-store refusal, and the loopback/TLS transport policy; fuzz targets for config receive and daemon-auth parsing.
Changed
- Protocol version 2.18.0 → 2.19.0 (breaking). The config-frame auth block
is now
[present][username](digest removed) and the auth challenge/response frames are interleaved between the config frame and itsSTATUS_OK. A 2.19.0 client and a 2.18.0 server (or vice versa) fail cleanly at the handshake. - Daemon modules declaring
auth usersrequire a configured credential store at startup (fail closed); operators regenerate stores from plaintext with--hash-credentials.
Notes
- First tagged release. FastSync implements rsync-compatible file synchronization over TCP and SSH with TLS (OpenSSL), streaming zstd compression, multithreaded transfers, and incremental sync. See RSYNC_COMPAT.md for the flag-parity matrix.