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.
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.
file_save_directory_to_disk() applied the exact source mode (fchmod)
inline for explicit --dirs/STATUS_MKDIR entries. A restrictive source
mode (e.g. 0555) then made the directory read-only before its children
were written, so a non-root receiver failed each child with EACCES. The
recursive -a path never hit this because it defers directory metadata.
Remove the inline fchmod and let the existing deferred
dir_metadata_list_apply() stamp the exact mode at end of transfer, as the
recursive path does. Keep the inline ownership and xattrs (a direct
file_save_to_disk_full() caller has no deferred pass) and document the
resulting intentional ordering. Capture errno before output_escape() in
the inline timestamp diagnostic so strerror() reports the real error, and
add the missing trailing newline to tests/test_xattr.c.
Pure structural split of src/shared/file_receive.c into focused translation
units behind the unchanged file_receive.h facade:
- file_save.c : save-to-disk, special nodes, --delay-updates staging
- incremental_check.c : xattr/delta/basis/fuzzy receive + check state machine
- delete_commit.c : manifest receive + delete budget walkers
- file_receive.c : wire receive dispatch + deferred dir metadata
The shared receive_file_xattrs helper and MAX_FILE_DATA_SIZE are declared in
incremental_check.h. file_save_to_disk_full_ex is decomposed into static
helpers (validation, special dispatch, dir/symlink creation, path resolution,
pre-write policies, data install) routed through one cleanup epilogue.
No behavior change.
Move the filter rule-tree/context helpers and entry inspection into
scanner_filter.c, the parallel scanner into scanner_parallel.c, and keep
the sequential scanner in scanner.c. Shared internal declarations live in
the new scanner_internal.h; scanner.h stays the public façade.
Decompose directory_scanner_next into static helpers (skipped-entry,
selection-protection, mount/-x, directory-finish and per-entry handlers)
with no semantic change.
Move the stats/progress reporting, scanner-preparation/scan helpers and
manifest/list/dry-run senders out of the ~3.9k-line client_send.c into
client_report.c, client_scan.c and client_manifest.c, sharing declarations
through the new internal client_send_internal.h. client_send.c keeps the
transfer orchestration and is now ~2.1k lines.
Decompose the monolithic send_files into static phase helpers
(send_files_prepare/_prepare_delete/_run/_finalize/_cleanup) driven by a
single SendFilesState; ownership, ordering and exit codes are unchanged.
No behavior change.
Update the docs for the audit follow-up fixes:
- --filter merge modifiers e/n/w/- are now accepted-and-consumed on
merge/dir-merge rules (rejected on non-merge, x rejected everywhere);
their semantics stay unimplemented, so the --filter row moves to Caveat
and the tally becomes 119/11/27 = 157.
- --inplace + --partial-dir is rejected with rsync's message.
- secret_file_open() O_NOFOLLOW (symlinked credential paths fail closed;
fd-backed paths exempt) and ~3 s bound-wait on FIFO reads.
- AGENTS setpriv wording corrected to the collected instance count.
- CHANGELOG [Unreleased] audit section extended with the follow-ups.
The earlier modifier-rejection change rejected e/n/w on all rules, but rsync
3.4.1 accepts them (plus the '-' merge-only modifier) on merge and dir-merge
rules. Restrict the rejection to non-merge rules and consume the merge-file
modifiers (e/n/w/-) so they no longer leak into the merge filename.
- is_merge_rule()/is_merge_modifier_char() gate the merge-only modifiers.
- scan vs consume sets: e/n/w still count as modifier-run chars on every rule
(pure tokens like -new/-press stay rejected), but are only consumed on merge
rules, preserving mixed-token parsing such as H,!secret -> ecret.
- '-' is accepted/consumed only on merge/dir-merge (e.g. dir-merge,- .rules).
- x remains rejected everywhere with its dedicated message.
- e/n/w/- semantics remain unimplemented and are documented as accepted-but-
ignored in filter.h.
Tests: split the merge forms out of the rejection test into a new acceptance
test asserting the merge file is read and dir_merge_names keeps the modifier-
free basename; non-merge pure-modifier forms still rejected.
secret_file_open() opened secret files with O_NONBLOCK and only cleared it
for S_ISREG, so on a FIFO/process-substitution source (--password-file
<(...), --early-input <(...)) fgets() failed immediately with EAGAIN when
the writer had not yet produced data, breaking slow producers.
Keep O_NONBLOCK at open() (a writer-less FIFO must not block the open) and
route all three readers through a new secret_read_line() helper. It
accumulates a line across reads and, on EAGAIN/EWOULDBLOCK (or a partial
line) with no newline and no EOF, clearerr()s and polls for readability
against one overall CLOCK_MONOTONIC deadline of
CREDENTIAL_FIFO_READ_TIMEOUT_MS (3000 ms); on timeout or a real read error
it fails with a clear message. EOF finishes normally. Regular files are
left blocking and read exactly as before.
Handles a line split across several write()s and keeps the owner/mode
fstat gate, O_NOFOLLOW and the /dev/fd/N exception unchanged.
secret_file_open() previously opened --password-file/--early-input with
plain O_RDONLY, so a symlinked path was followed before the owner/mode
fstat gate ran, and an empty/planted FIFO could block fgets forever.
Open with O_NOFOLLOW|O_NONBLOCK|O_CLOEXEC (mirroring the dummy-key
sidecar): ELOOP now fails closed, and a writer-less FIFO yields EOF/EAGAIN
instead of hanging. Clear O_NONBLOCK again for regular files, where it is
a no-op, so their stdio read path is unchanged.
file.c: drop the duplicate <fcntl.h>/<unistd.h> includes (kept the first
occurrences).
Tests: a symlinked password file is rejected, and a writer-less named
FIFO fails cleanly without hanging.
Client: replace the non-async-signal-safe signal(3) call inside
client_signal_handler() with a precomputed SIG_DFL sigaction(2), which is
on the POSIX async-signal-safe list. The handler stays installed while a
transfer is armed so a repeated Ctrl-C still leads to a graceful abort
rather than a hard kill mid-cleanup.
Server: cleanup() now only calls _exit(2) (async-signal-safe). The former
server_delete()/daemon_conf_free()/credentials_free() teardown called
free()/close()/SSL_CTX_free() from signal context, which can deadlock or
corrupt the heap if the signal lands inside malloc/free. Handlers are
installed with sigaction(2) instead of signal(3). The normal shutdown
path in main() still performs the full teardown; the signal path relies on
process exit to reclaim the parent daemon's socket, anonymous shared
mapping and heap (no named/persistent parent resource is left behind).
The standalone filter_rule_parse() already rejected the rsync xattr-name
'x' modifier, but the list parser used by --filter/-f silently dropped the
flag for merge/dir-merge rules (and relied on a second parse for plain
rules). Reject it explicitly in filter_list_parse_append_depth() with the
same diagnostic, so '-x', 'merge,x' and 'dir-merge,x' all fail cleanly.
Also reject the unimplemented rsync merge modifiers 'e', 'n' and 'w'
instead of folding them into the pattern, which previously produced
misleading errors such as "could not read merge file 'n file'". Only a
token made up solely of modifier characters is treated as a modifier run,
so glued patterns ('-newfile', '-e2e') and mixed tokens ("H,!secret")
keep their historical parsing.
Adds tests/test_filter.c with focused rejection and supported-syntax
cases.