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.
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).
## 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**.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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_VERSIONgoes 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)
ProtocolSessiongains aProtocolIoOpsfunction-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 onsession->ssl.WANT_READ/WANT_WRITEwait_eventsswitching,SSL_ERROR_SYSCALL && EINTRretry,SSL_pendingpoll gating,INT_MAXclamping, deadline handling, EOF-vs-error.--threadsfix:file_send's TLS-vs-sendfile decision now usesprotocol_current_ssl()(bound session preferred, thread-local fallback) instead of the thread-localio_get_ssl()alone, removing a thread-affinity hazard. Thexfailon 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.SSL_readis 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)
llistxattr/lgetxattr) and carried in an optional block appended toSTATUS_SYMLINKwhenuse_xattrs; the receiver decodes it and applies no-follow (lsetxattrthrough the confined parent dir). Never follows the link; whitelist enforced on both capture and apply.lsetxattr→EPERMfor 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-superdoes not contradict this (it materializes symlinks as regular files). Documented in the-Xrow, which stays ❌ for the never-preserved privileged namespaces.CMakeLists.txtproject version,README,CHANGELOG(new 2.29.0 entry),RSYNC_COMPAT--protocolrow, 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.