From 3cf2e2c91f4bbec3831e2c2a26e4bbc6c079a5f0 Mon Sep 17 00:00:00 2001 From: TapTap Date: Sun, 13 Sep 2026 05:01:15 +0200 Subject: [PATCH] docs(version): align 2.20.0 artifacts; fix protocol-bump rationale --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ CMakeLists.txt | 2 +- RSYNC_COMPAT.md | 10 ++++++---- src/shared/config.h | 25 ++++++++++++------------- src/shared/utils.c | 4 +++- 5 files changed, 51 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcdf3a4..66132a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,35 @@ 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.20.0] - 2026-09-13 + +### Security + +- Cap cumulative `DirTimeList` growth and bound pre-auth config-string memory + (remote memory-exhaustion DoS). +- Daemon host access control (`hosts allow`/`hosts deny`, IPv4/IPv6/CIDR), + configurable global `max connections`, connection audit logging, and a + bounded `auth failure delay` throttle. IPv4-mapped peers are normalized and + invalid patterns are rejected at parse time (no silent fail-open). +- Honor `--timeout` for 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 `Data` leaks, `missing_args` leak, 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-from` lookups indexed (O(n*m) → O(n)). +- Reused per-thread zstd contexts; `TCP_NODELAY` by default. +- Byte-bounded sender queues; removed a redundant scanner `stat()`. + ## [2.19.0] - 2026-09-12 ### Security diff --git a/CMakeLists.txt b/CMakeLists.txt index ff92672..f384f62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.22) -project(FastFileTransfer VERSION 2.19.0) +project(FastFileTransfer VERSION 2.20.0) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_C_STANDARD 11) diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index 830605e..6fb91c8 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -850,10 +850,12 @@ now sends the metadata as ONE packed frame: a single `int32` present flag `FILE_METADATA_WIRE_SIZE`-byte (68-byte) field record already emitted by the shared `metadata_to_buf()`/`metadata_from_buf()` chunk codec. Absent metadata is a lone `int32` zero. The encoded field layout is unchanged (only the framing -collapses), so chunk-serialized blobs remain byte-identical; `PROTOCOL_VERSION` -was bumped `2.19.0 → 2.20.0` because a 2.19 peer would desynchronize on the -removed frames. The strict same-version handshake rejects any mismatch before a -byte of the frame is parsed. +collapses), so chunk-serialized blobs remain byte-identical. Protocol data is an +unframed byte stream, so the packed encoding is byte-for-byte identical to the +old field-by-field writes; `PROTOCOL_VERSION` was bumped `2.19.0 → 2.20.0` as a +deliberate lockstep-release marker rather than because of a +desynchronization. The strict same-version handshake rejects any mismatch before +a byte of the frame is parsed. ### Recommended Delivery Order diff --git a/src/shared/config.h b/src/shared/config.h index 23b811b..29b7baf 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -646,19 +646,18 @@ typedef struct Config { * * Packed Metadata Wave: 2.19.0 -> 2.20.0. * - * WHY the bump, grounded in the wire: metadata_send()/metadata_receive() no - * longer emit/consume the metadata as up to 12 separate per-field framed - * writes. A file's metadata now crosses the wire as ONE packed frame: a - * single int32 present flag (0 = absent, 1 = present) followed, when present, - * by the fixed FILE_METADATA_WIRE_SIZE-byte (68-byte) field record produced by - * metadata_to_buf(). A 2.19 peer would desynchronize on the removed frames - * (it would read the packed record's bytes as a stream of separate field - * frames), so the strict same-version handshake (config_receive rejects a - * mismatched version before parsing anything else) is what keeps a 2.20 client - * and a 2.19 server from ever reaching that state. The encoded field layout - * itself is unchanged (only its framing collapses), so the chunk codec, which - * already used the packed metadata_to_buf()/metadata_from_buf() codec, is - * byte-identical to before. */ + * WHY the bump: metadata_send()/metadata_receive() no longer emit/consume the + * metadata as up to 12 separate per-field writes. A file's metadata now + * crosses the wire as ONE packed frame: a single int32 present flag (0 = + * absent, 1 = present) followed, when present, by the fixed + * FILE_METADATA_WIRE_SIZE-byte (68-byte) field record produced by + * metadata_to_buf(). Protocol data is an unframed byte stream, so the packed + * encoding is byte-for-byte identical to the old field-by-field writes (same + * fields, same order, same widths); the change only removes per-field syscalls. + * The bump is therefore a deliberate lockstep-release marker, not a + * desynchronization fix — the strict same-version handshake still rejects a + * mixed 2.19/2.20 deployment. The chunk codec, which already used the packed + * metadata_to_buf()/metadata_from_buf() form, is unchanged. */ #define PROTOCOL_VERSION "2.20.0" #define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024) /* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */ diff --git a/src/shared/utils.c b/src/shared/utils.c index 17b1d50..080785d 100644 --- a/src/shared/utils.c +++ b/src/shared/utils.c @@ -175,7 +175,9 @@ bool str_hash_set_insert_ref(StrHashSet* set, const char* key) { return false; if (!str_hash_set_grow(set)) return false; - return str_hash_set_put(set, key, strlen(key)) >= 0; + /* put() returns 1 for a new slot and 0 for a duplicate; both are success. */ + (void)str_hash_set_put(set, key, strlen(key)); + return true; } static const StrHashSetSlot* str_hash_set_find_n(const StrHashSet* set, const char* key,