feat: transport I/O vtable + symlink-xattr wire block (protocol 2.29.0) #311

Merged
TapTap merged 10 commits from feat/transport-xattr into dev 2026-09-23 01:50:51 +02:00
Owner

Deferred structural cycle — transport vtable + symlink-xattr wire block

Lands the two deliberately-deferred structural items from the earlier cycles. This is a wire-breaking change: PROTOCOL_VERSION goes 2.28.0 → 2.29.0, so 2.28.0 and 2.29.0 peers are incompatible under the strict handshake. The config-frame layout is unchanged (golden length still 886; hash updated).

Transport I/O vtable (wire-identical)

  • ProtocolSession gains a ProtocolIoOps function-pointer vtable (send/recv/has_pending), selected once at session init / set_ssl. The three wire loops (protocol_send_n_data, protocol_receive_n_data_until, protocol_read_status_until) and the keepalive poll gate now dispatch through the ops instead of branching on session->ssl.
  • Semantics preserved exactly: WANT_READ/WANT_WRITE wait_events switching, SSL_ERROR_SYSCALL && EINTR retry, SSL_pending poll gating, INT_MAX clamping, deadline handling, EOF-vs-error.
  • TLS + --threads fix: file_send's TLS-vs-sendfile decision now uses protocol_current_ssl() (bound session preferred, thread-local fallback) instead of the thread-local io_get_ssl() alone, removing a thread-affinity hazard. The xfail on the TLS+multithreading test is gone; a discriminating unit test (real in-memory TLS handshake in a worker thread) fails on the pre-fix code and passes now.
  • Review follow-ups fixed: an EINTR busy-spin on an unexpected TLS EOF (0-byte SSL_read is now classified as CLOSED before any EINTR retry), and the resolver no longer lets a bound plaintext session mask a live thread-local TLS transport.

Symlink-xattr wire block (protocol 2.29.0)

  • A symlink's own xattrs are captured no-follow (llistxattr/lgetxattr) and carried in an optional block appended to STATUS_SYMLINK when use_xattrs; the receiver decodes it and applies no-follow (lsetxattr through the confined parent dir). Never follows the link; whitelist enforced on both capture and apply.
  • Honest limitation: Linux refuses to associate xattrs with a symlink at all (lsetxattr → EPERM for every namespace, even as root — verified), so this block is a no-op on Linux and is carried for correctness on platforms/filesystems that support it. rsync 3.4.1's --fake-super does not contradict this (it materializes symlinks as regular files). Documented in the -X row, which stays ❌ for the never-preserved privileged namespaces.
  • Version pins updated: CMakeLists.txt project version, README, CHANGELOG (new 2.29.0 entry), RSYNC_COMPAT --protocol row, the release skill, and the preflight reject-list (2.28.0 now rejected).

Verification

Strict -Werror, clang-format 18, cppcheck clean; unit 45/45 (ASan with leak detection, UBSan, valgrind); integration 900 passed; differential parity 62 passed; preflight/README-consistency 13 passed.

## Deferred structural cycle — transport vtable + symlink-xattr wire block Lands the two deliberately-deferred structural items from the earlier cycles. **This is a wire-breaking change: `PROTOCOL_VERSION` goes 2.28.0 → 2.29.0**, so 2.28.0 and 2.29.0 peers are incompatible under the strict handshake. The config-frame layout is unchanged (golden length still 886; hash updated). ### Transport I/O vtable (wire-identical) - `ProtocolSession` gains a `ProtocolIoOps` function-pointer vtable (`send`/`recv`/`has_pending`), selected once at session init / `set_ssl`. The three wire loops (`protocol_send_n_data`, `protocol_receive_n_data_until`, `protocol_read_status_until`) and the keepalive poll gate now dispatch through the ops instead of branching on `session->ssl`. - Semantics preserved exactly: `WANT_READ`/`WANT_WRITE` `wait_events` switching, `SSL_ERROR_SYSCALL && EINTR` retry, `SSL_pending` poll gating, `INT_MAX` clamping, deadline handling, EOF-vs-error. - **TLS + `--threads` fix**: `file_send`'s TLS-vs-sendfile decision now uses `protocol_current_ssl()` (bound session preferred, thread-local fallback) instead of the thread-local `io_get_ssl()` alone, removing a thread-affinity hazard. The `xfail` on the TLS+multithreading test is gone; a discriminating unit test (real in-memory TLS handshake in a worker thread) fails on the pre-fix code and passes now. - Review follow-ups fixed: an EINTR busy-spin on an unexpected TLS EOF (0-byte `SSL_read` is now classified as CLOSED before any EINTR retry), and the resolver no longer lets a bound plaintext session mask a live thread-local TLS transport. ### Symlink-xattr wire block (protocol 2.29.0) - A symlink's **own** xattrs are captured no-follow (`llistxattr`/`lgetxattr`) and carried in an optional block appended to `STATUS_SYMLINK` when `use_xattrs`; the receiver decodes it and applies no-follow (`lsetxattr` through the confined parent dir). Never follows the link; whitelist enforced on both capture and apply. - **Honest limitation:** Linux refuses to associate xattrs with a symlink at all (`lsetxattr` → `EPERM` for every namespace, even as root — verified), so this block is a no-op on Linux and is carried for correctness on platforms/filesystems that support it. rsync 3.4.1's `--fake-super` does not contradict this (it materializes symlinks as regular files). Documented in the `-X` row, which stays ❌ for the never-preserved privileged namespaces. - Version pins updated: `CMakeLists.txt` project version, `README`, `CHANGELOG` (new 2.29.0 entry), `RSYNC_COMPAT` `--protocol` row, the release skill, and the preflight reject-list (2.28.0 now rejected). ### Verification Strict `-Werror`, clang-format 18, cppcheck clean; unit 45/45 (ASan with leak detection, UBSan, valgrind); integration **900 passed**; differential parity **62 passed**; preflight/README-consistency **13 passed**.
TapTap added 10 commits 2026-09-23 01:45:41 +02:00
file_send.c chose between sendfile() and the TLS-aware buffered path by
calling io_get_ssl(), which reads the thread-local io_ssl. A worker thread
that bound a TLS ProtocolSession via protocol_session_bind() never ran the
handshake in that thread, so io_ssl is NULL there and a TLS + --threads
transfer took the raw sendfile() path on an encrypted socket.

Add protocol_current_ssl(), which prefers the bound session's SSL and falls
back to io_ssl on the fd-shim path, and use it in file_send.c. Un-xfail
test_tls_with_multithreading.
Introduce ProtocolIoOps (send/recv/has_pending), selected once by
protocol_session_init() and protocol_session_set_ssl(), and dispatch the
send, receive and status-read loops through session->ops instead of
branching on session->ssl at runtime.

Each op performs one transfer attempt and classifies the result
(PROTOCOL_IO_RETRY/CLOSED/ERROR), preserving the WANT_READ/WANT_WRITE
wait_events switching, the SSL_ERROR_SYSCALL/EINTR retry, the
SSL_pending poll gating and the deadline handling. The raw read()/write()
fallback lives in the plaintext ops.

Add unit tests: a socketpair session with a counting ops wrapper proving
the loops dispatch through the vtable, and a worker-thread test that
protocol_current_ssl() resolves the bound session's SSL when io_ssl is NULL.
docs: bump release version to 2.29.0 and reconcile protocol docs
CI / lint (pull_request) Successful in 1m48s
CI / parity-full (pull_request) Skipped
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 / parity-fast (pull_request) Successful in 20s
CI / build-and-test (pull_request) Successful in 57s
f3d7672694
TapTap merged commit 13b257d1dd into dev 2026-09-23 01:50:51 +02:00
TapTap deleted branch feat/transport-xattr 2026-09-23 01:50:51 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: TapTap/FastSync#311