io_timeout can reach INT_MAX -- from an operator --timeout (options.c parses it
as a plain int, unbounded and even negative) or a peer's MSG_IO_TIMEOUT (now
also capped at 86400 in read_a_msg). Several signed computations then misbehave:
* allowed_lull = (io_timeout + 1) / 2 overflows to a negative allowed_lull /
select_timeout; select() then returns EINVAL on the negative tv_sec, which
isn't EBADF, so the read loop spins at 100% CPU forever (io_timeout ~= 68
years never fires check_timeout), plus a keepalive flood. Compute
ceil(io_timeout/2) in a wider type so "+ 1" cannot overflow.
* the generator and sender derive an int loop-check limit as allowed_lull * 5
(generator.c, sender.c), which overflows for a large allowed_lull. Cap
allowed_lull so that product stays in range -- invisible to real use, as
allowed_lull is the keep-alive half-interval and INT_MAX/5 seconds is over
13 years.
* a negative --timeout drove allowed_lull / select_timeout negative the same
way; treat secs < 0 as "no timeout" up front.
The overflows are undefined behaviour, so plain -O2 gcc/clang happen to keep
select_timeout at 60, but -fwrapv / -fno-strict-overflow (common hardening) wrap
to the spin and -ftrapv aborts.
Reported by z3r0s.
A malicious server can send MSG_IO_TIMEOUT with val near INT_MAX
(0x7FFFFFFF). The existing val <= 0 guard prevents timeout disabling,
but a large positive value passes through to set_io_timeout() where
(io_timeout + 1) / 2 overflows signed int, wrapping allowed_lull and
select_timeout negative. Every subsequent select() returns EINVAL
immediately, trapping the client in a tight CPU loop.
Cap the accepted timeout at 86400 seconds (24 hours), which is well
above any practical timeout and avoids the overflow in set_io_timeout().
Reported-by: z3r0s <https://github.com/z3r0s6>
A peer-supplied MSG_IO_ERROR value was OR'd into the local io_error
without masking, allowing a malicious peer to set arbitrary bits that
propagate to exit codes and get re-forwarded to other peers.
Fix: mask the incoming value with IOERR_VALID_MASK (IOERR_GENERAL |
IOERR_VANISHED | IOERR_DEL_LIMIT) before OR'ing.
Test: testsuite/ki62-io-error-mask_test.py
sigusr2_handler called output_summary() (rprintf/vsnprintf/fwrite/iconv/malloc)
and close_all() (fstat/shutdown/close) directly from signal context before
_exit(). SIGUSR2 is sent by the generator/parent to tell the receiver child to
print its summary and exit; if it interrupted the receiver while it was inside
malloc/stdio, the handler re-entered those and could deadlock or corrupt.
The handler now only sets a flag (got_sigusr2, a volatile sig_atomic_t); the
actual summary + shutdown moves to receive_sigusr2(), run from a safe point in
the receiver's post-transfer wait paths:
- the perform_io() flag checks (next to got_kill_signal);
- the safe_read()/safe_write() loops (which a --read-batch / --write-batch fd
uses without going through perform_io);
- the whine_about_eof() kluge loop, where the receiver waits out the race of
the sender dying before the kill-signal arrives -- this loop polls for the
signal, so without the flag check it slept the full 10s and then errored with
RERR_STREAMIO instead of exiting cleanly;
- the trailing `while (!got_sigusr2) msleep()` loop in do_recv().
(Leonid Bugaev May-2026 re-audit, KI-14.)
A peer may use MSG_IO_TIMEOUT only to ask us to adopt a SHORTER I/O timeout
(a stricter cap). A crafted server sending val <= 0 would instead zero the
client's --timeout via set_io_timeout(0), disabling it entirely and letting the
server hang the client indefinitely. Ignore a non-positive value.
(Leonid Bugaev May-2026 re-audit, KI-47; pre-existing since 2009.)
io.c only treats a short read as the EOF sentinel when the fd is still
open; xattrs.c never stores a -1 from find_matching_xattr() and guards
ndx < 0 in set_xattr; rsync-ssl refuses the gnutls backend without
RSYNC_SSL_CA_CERT.
Co-authored-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
The run4 0002 fix deferred the in_multiplexed re-arm past the call-out only for
MSG_NOOP and MSG_IO_ERROR (the two fuzzed tags), but the same anti-pattern --
re-arming the re-entry guard before a write-side call-out that can reach
perform_io() -> read_a_msg() -- remained in the sibling cases (Codex review).
Defer it past the call-out in those too:
- MSG_NO_SEND (!am_generator -> send_msg_int): exploitable at a daemon sender,
the same stack-exhaustion recursion as MSG_NOOP.
- MSG_DELETED (!am_generator -> log_delete -> send_msg when am_server): same.
- MSG_DELETED (am_generator -> send_msg): not currently exploitable (the
generator is not am_sender, so send_msg doesn't perform_io), deferred for
uniformity.
MSG_SUCCESS/MSG_REDO are left as-is: successful_send() returns early unless
--remove-source-files and does not reach send_msg()->perform_io(), and the
generator-status path runs only in the (non-am_sender) generator.
(cherry picked from commit f4f1905f5cfd83bd10d4db3b734da34cc43d33b0)
After the '.' separator, read_args delegates argv growth to
glob_expand_module() -> glob_expand()/glob_match(), each of which reserves
glob.argc+1 slots -- room for the entry being added but not for the trailing
NULL that read_args writes after the loop. A daemon client can send a single
post-dot request line whose " mod/" splits land argc on exactly maxargs
(initially MAX_ARGS=1000, then any ENSURE_MEMSPACE doubling boundary), so the
'argv[argc] = NULL' at the end is an 8-byte NULL write one slot past the argv
heap allocation. Reachable from an unauthenticated client; runs after
chroot/setuid in rsync_module so the corruption is in the privilege-dropped
per-connection child.
Ensure the +1 slot before the store. The pre-dot growth check at
'argc == maxargs-1' already leaves the slot but never fires once dot_pos is set.
(cherry picked from commit ec4f7a03c42e93a48fce20d0b7667235564f9248)
read_a_msg() sets iobuf.in_multiplexed = -1 on entry so that any perform_io()
reached while the message body is being consumed will not re-enter read_a_msg()
(IN_MULTIPLEXED_AND_READY tests > 0). The MSG_NOOP and MSG_IO_ERROR handlers
reset it to 1 *before* calling maybe_send_keepalive() / send_msg_int(), both of
which can hit perform_io(PIO_NEED_MSGROOM). With more frames already buffered
(iobuf.in.len > 512), perform_io() then loops back into read_a_msg() -- each
frame stacks a fresh BIGPATHBUFLEN local, so a hostile peer that floods MSG_NOOP
at a daemon sender (or MSG_IO_ERROR at a daemon receiver child) exhausts the
stack.
Move the reset after the call-out so the guard stays armed across the write-side
flush. The outer caller continues draining input once we return.
(cherry picked from commit 5d2794fca309dd558f68b21bff6d3e9728e58237)
- io: bound daemon argument lists so a malicious daemon client cannot grow
argv without limit (DoS);
- socket: bound the PROXY CONNECT request and proxy response header lines;
- daemon: require a trusted-proxy host list for "proxy protocol = true"
(reject untrusted proxy peers, fail-closed), and warn at startup when the
trusted-proxy list is unset so the fail-closed behaviour is not silent.
Tests: daemon-argv-limit, proxy-connect-request-too-long,
proxy-response-header-too-long, proxy-protocol-trusted-peer.
Backport of the 3.5.0 wire-input validation:
- reject an out-of-range file index from the wire in read_ndx();
- reject a zero-length checksum block.
Test: checksum-zero-blocklen.
Without --secluded-args, the client's safe_arg() backslash-escapes shell
and wildcard chars in option values before sending them to the server, so
--chown's --usermap=*:user is transmitted as --usermap=\*:user. Over ssh a
remote shell removes the backslashes before rsync parses the args, but a
daemon has no shell and read_args() stored option args verbatim -- so the
receiver saw the literal "\*", the usermap/groupmap wildcard never matched,
and the module's configured uid/gid won instead. A regression from the
secluded-args hardening; rsync 3.2.3 (protocol 31) worked.
Un-backslash option args in read_args() on the daemon's first
(non-protected) read, mirroring what the ssh-side shell does. File args
after the dot are already handled by glob_expand(); the protected (NUL,
already-unescaped) re-read and the server's stdin read pass unescape=0 so
their raw args are left untouched.
Thanks to @elcamlost for the report (#829).
Fixes: #829
(cherry picked from commit 8dc5fd1408)
Multiple receiver-side fields read from the wire were trusted
without upper-bound checks. A hostile peer could either request
extreme allocations (DoS via --max-alloc) or, on platforms where
read_varint returned a negative value, push ~SIZE_MAX through the
size_t conversion to wrap downstream length checks.
Introduce read_int_bounded(), read_varint_bounded() and
read_varint_size() in io.c so wire-derived integer ranges are
checked at the read site rather than scattered across each
caller, with RERR_PROTOCOL on out-of-range input.
Apply the bounded primitives to:
- sum->count (checksum count -- previously could overflow
(size_t)count * xfer_sum_len on 32-bit with raised max-alloc)
- xattrs: count, name_len, datum_len, plus rel_pos overflow
detect to stop chain wrapping the num accumulator
- acls: ida-entry count
- flist: file mode S_IFMT validation, modtime_nsec range check
- delete-stat counters in main: per-summand cap so the total
can't overflow a signed 32-bit accumulator
Reporters include Joshua Rogers (checksum-count overflow finding).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commit 797e17f ("fixed an invalid access to files array") added a
parent_ndx < 0 guard to send_files() in sender.c, but the visually-
identical block in recv_files() in receiver.c was not updated. A
malicious rsync:// server can therefore drive any connecting client
into the same out-of-bounds dir_flist->files[-1] read followed by a
file_struct dereference in f_name() one line later.
Reach: protocol-30+ default (inc_recurse) makes flist.c:2745 set
parent_ndx = -1 on the first received flist when the sender omits a
leading "." entry; rsync.c flist_for_ndx() does not reject ndx == 0
in that state because the range check evaluates 0 < 0 = false; and
read_ndx_and_attrs() only validates ndx with the ITEM_TRANSFER bit
set, so iflags=ITEM_IS_NEW (or any other non-transfer iflag word)
bypasses the check.
Apply the same guard receiver-side. Confirmed: the same PoC (a
minimal Python rsyncd that handshakes with CF_INC_RECURSE, sends a
no-leading-"." flist, and emits ndx=0 with ITEM_IS_NEW) crashes
unpatched 3.4.2 with SEGV_MAPERR si_addr=0x4101a-class in the
receiver child; with this guard it exits cleanly with code 2
(RERR_PROTOCOL).
The attack surface delta over the sender variant is large:
the original was malicious-client -> daemon, this is
malicious-server -> any rsync client doing a normal rsync://
or remote-shell pull.
Reported by Pratham Gupta (alchemy1729).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Glibc 2.43 added C23 const-preserving overloads to various string functions,
which change the return type depending on the constness of the argument(s).
Currently this leads to warnings from calls to strtok() or strchr().
Fix this by properly declaring the respective variable types.
Signed-off-by: Holger Hoffstätte <holger@applied-asynchrony.com>
- Put sum2_array into sum_struct to hold an array of sum2 checksums
that are each xfer_sum_len bytes.
- Remove sum2 buf from sum_buf.
- Add macro sum2_at() to access each sum2 array element.
- Throw an error if a sums header has an s2length larger than
xfer_sum_len.
When conversing with a protocol 29 or earlier rsync, the modtime values
are arriving as 4-byte integers. This change interprets these short
values as unsigned integers, allowing the time that can be conveyed to
range from 1-Jan-1970 to 7-Feb-2106 instead of the signed range of
13-Dec-1901 to 19-Jan-2038. Given that we are fast approaching 2038,
any old-protocol transfers will be better served using the unsigned
range rather than the signed.
It is important to keep in mind that protocol 30 & 31 convey the full
8-byte mtime value (plus nanoseconds), allowing for a huge span of time
that is not affected by this change.
A local_server copy now includes the dev+ino info from the destination
file so that the sender can make sure that it is not going to delete
the destination file. Fixes mistakes such as:
rsync -aiv --remove-source-files dir .
The `--stop-at`, `--stop-after`, and `--time-limit`` options should have their
limit checked when receiving and sending data, not just when receiving.
Fixes#177.
- Rename daemon_over_rsh -> daemon_connection since it is also used to
indicate if a non-rsh daemon connection is active.
- Move the daemon-over-rsh exception out of server_options() to the one
caller that needs that behavior.
- Don't allow noop_io_until_death() to be short-circuited when talking
to a daemon over a socket, because it can't send errors via stderr.
- All the memory-allocation macros now auto-check for failure and exit
with a failure message that incudes the caller's file and lineno
info. This includes strdup().
- Added the `--max-alloc=SIZE` option to be able to override the memory
allocator's sanity-check limit. It defaults to 1G (as before).
Fixes bugzilla bug 12769.
Allow the receiver to increase their iobuf.msg xbuf if it fills up. This
ensures that the receiver will never block trying to output a message,
and thus it will always drain the data from the sender and keep the
whole thing from clogging up.
- Add checksum negotiation to the protocol so that we can easily add new
checksum algorithms and each will be used when both sides support it.
- Increase the size of the compat_flags value in the protocol from a
byte to an int.
If both sides support the "V" compatibility flag, we send the file-list
flags as a varint instead of a 1-or-2 byte value. This upgrades the
number of reserved flag bits from 1 to 17 with very few extra bytes in
typical file-list data.