VYPR
High severity8.1NVD Advisory· Published May 20, 2026· Updated May 20, 2026

CVE-2026-43618

CVE-2026-43618

Description

Rsync version 3.4.2 and prior contain an integer overflow vulnerability in the compressed-token decoder where a 32-bit signed counter is not checked for overflow, allowing a malicious sender to trigger an overflow that causes the receiver process to read and return data from outside the intended buffer bounds. Attackers can exploit this vulnerability to disclose process memory contents including environment variables, passwords, heap and stack data, and library memory pointers, significantly reducing ASLR effectiveness and facilitating further exploitation.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Rsync 3.4.2 and prior have an integer overflow in compressed-token decoding, allowing a malicious sender to read process memory including passwords and pointers.

Vulnerability

In rsync versions 3.4.2 and prior, the compressed-token decoder uses a 32-bit signed counter without overflow checking [1][3]. A malicious sender can send crafted compressed tokens that cause an integer overflow, leading the receiver to read data beyond the intended buffer boundaries. The vulnerability is reachable when compression is enabled.

Exploitation

An attacker acting as a sender must have network access to the rsync receiver and be able to transmit malicious compressed tokens [1]. By carefully manipulating the token stream to trigger the overflow, the attacker forces the receiver to read out-of-bounds memory and return it as part of the decompressed data.

Impact

Successful exploitation allows the attacker to read arbitrary process memory of the rsync receiver, including environment variables, passwords, heap and stack data, and memory pointers from libraries [1][3]. This information disclosure reduces the effectiveness of ASLR and may facilitate further attacks.

Mitigation

The vulnerability is fixed in rsync version 3.4.3 [1][2]. For systems that cannot be immediately updated, a workaround is to disable compression on the rsync daemon by adding refuse options = compress to rsyncd.conf [1]. No EOL or KEV listing is indicated in the references.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

6
a0b9a8e989f6

NEWS: prepare 3.4.3 release entry with six CVEs

https://github.com/rsyncproject/rsyncAndrew TridgellMay 7, 2026Fixed in 3.4.3via llm-release-walk
1 file changed · +140 6
  • NEWS.md+140 6 modified
    @@ -1,7 +1,122 @@
    -# NEWS for rsync 3.4.3 (UNRELEASED)
    +# NEWS for rsync 3.4.3 (20 May 2026)
     
     ## Changes in this version:
     
    +### SECURITY FIXES:
    +
    +Six CVEs are fixed in this release.  All six are assigned by
    +VulnCheck as CNA.  Affected versions are 3.4.2 and earlier in every
    +case.  Three of the six (CVE-2026-29518, CVE-2026-43617,
    +CVE-2026-43619) require non-default daemon configuration to reach:
    +the first and third need `use chroot = no` for a module, the second
    +needs `daemon chroot = ...` set in rsyncd.conf.  Two (CVE-2026-43618,
    +CVE-2026-43620) are reachable from a normal pull or a normal
    +authenticated daemon connection.  The sixth (CVE-2026-45232) is
    +reachable only when `RSYNC_PROXY` is set and the proxy (or a MITM)
    +returns a pathological response.  Many thanks to the external
    +researchers who reported these issues.
    +
    +- CVE-2026-29518 (CVSS v4.0 7.3, HIGH): TOCTOU symlink race condition
    +  allowing local privilege escalation in daemon mode without chroot.
    +  An rsync daemon configured with "use chroot = no" was exposed to a
    +  time-of-check / time-of-use race on parent path components: a local
    +  attacker with write access to a module could replace a parent
    +  directory component with a symlink between the receiver's check and
    +  its open(), redirecting reads (basis-file disclosure) and writes
    +  (file overwrite) outside the module.  Default "use chroot = yes" is
    +  not exposed.  `secure_relative_open()` (added in 3.4.0 for
    +  CVE-2024-12086) was previously unused in the daemon-no-chroot
    +  case; the fix enables it there and reroutes the sender's
    +  read-path opens through it.  Reported by Nullx3D (Batuhan Sancak),
    +  Damien Neil and Michael Stapelberg.
    +
    +- CVE-2026-43617 (CVSS v3.1 4.8, MEDIUM): Hostname/ACL bypass on an
    +  rsync daemon configured with `daemon chroot = /X` in rsyncd.conf
    +  when the chroot tree lacks DNS resolution support.  The
    +  reverse-DNS lookup of the connecting client was performed *after*
    +  the daemon chroot had been entered; if /X did not contain the
    +  libc resolver fixtures (`/etc/resolv.conf`, `/etc/nsswitch.conf`,
    +  `/etc/hosts`, NSS service modules) the lookup failed and the
    +  connecting hostname was set to "UNKNOWN", causing hostname-based
    +  deny rules to silently fail open.  IP-based ACLs are unaffected.
    +  The per-module `use chroot` setting is unrelated to this issue.
    +  The fix performs the lookup before entering the daemon chroot.
    +  Reported by MegaManSec.
    +
    +- CVE-2026-43618 (CVSS v3.1 8.1, HIGH): Integer overflow in the
    +  compressed-token decoder enabling remote memory disclosure to an
    +  authenticated daemon peer.  The receiver accumulated a 32-bit
    +  signed counter without overflow checking; a malicious sender could
    +  trigger an overflow that, with careful manipulation, leaked process
    +  memory contents to the attacker -- environment variables,
    +  passwords, heap and library pointers -- significantly weakening
    +  ASLR.  The fix bounds the counter and adds wire-input validation in
    +  several adjacent places (defence-in-depth).  Workaround for older
    +  releases: `refuse options = compress` in rsyncd.conf.  Reported by
    +  Omar Elsayed.
    +
    +- CVE-2026-43619 (CVSS v3.1 6.3, MEDIUM): Symlink races on path-based
    +  system calls in "use chroot = no" daemon mode (generalisation of
    +  CVE-2026-29518).  Earlier fixes for symlink races on the receiver's
    +  open() call missed the same race class on every other path-based
    +  system call: chmod, lchown, utimes, rename, unlink, mkdir, symlink,
    +  mknod, link, rmdir and lstat.  The fix routes each affected
    +  path-based syscall through a parent dirfd opened under
    +  RESOLVE_BENEATH-equivalent kernel-enforced confinement (openat2 on
    +  Linux 5.6+, O_RESOLVE_BENEATH on FreeBSD 13+ and macOS 15+,
    +  per-component O_NOFOLLOW walk elsewhere).  Default "use chroot =
    +  yes" is not exposed.  Reported by Andrew Tridgell as a follow-on
    +  audit of CVE-2026-29518.
    +
    +- CVE-2026-43620 (CVSS v3.1 6.5, MEDIUM): Out-of-bounds read in the
    +  receiver's recv_files() enabling remote denial-of-service of any
    +  client pulling from a malicious server (incomplete fix of commit
    +  797e17f).  The earlier parent_ndx<0 guard added to send_files() was
    +  not applied to the visually-identical block in recv_files().  A
    +  malicious rsync server can drive any connecting client into a
    +  deterministic SIGSEGV by setting CF_INC_RECURSE in the
    +  compatibility flags and sending a crafted file list and transfer
    +  record.  inc_recurse is the protocol-30+ default, so no special
    +  options are required on the victim.  Workaround for older
    +  releases: `--no-inc-recursive` on the client.  Reported by Pratham
    +  Gupta.
    +
    +- CVE-2026-45232 (CVSS v3.1 3.1, LOW): Off-by-one out-of-bounds stack
    +  write in the rsync client's HTTP CONNECT proxy handler
    +  (`establish_proxy_connection()` in `socket.c`).  After issuing the
    +  CONNECT request, rsync read the proxy's first response line one
    +  byte at a time into a 1024-byte stack buffer with the bound
    +  `cp < &buffer[sizeof buffer - 1]`.  If the proxy (or a MITM in
    +  front of it) returned 1023+ bytes on that first line without a
    +  newline terminator, `cp` exited the loop pointing at a buffer slot
    +  the loop never wrote, leaving `*cp` holding stale stack data from
    +  the earlier `snprintf()` of the outgoing CONNECT request.  The
    +  post-loop logic then wrote a single `\0` one byte past the end of
    +  the buffer on the stack.  Reach is client-side only, and only when
    +  `RSYNC_PROXY` is set so rsync tunnels an `rsync://` connection
    +  through an HTTP CONNECT proxy.  The written byte is always `\0`
    +  and the offset is fixed by the buffer size, not attacker-chosen,
    +  so this is not an arbitrary-write primitive: practical impact is
    +  corruption of one adjacent stack byte and possible later
    +  misbehaviour or crash.  The fix detects the "buffer filled without
    +  finding `\n`" case explicitly by position and refuses the response
    +  with "proxy response line too long".  Reported by Aisle Research
    +  via Michal Ruprich (rsync-3.4.1-2.el10 QE).
    +
    +In addition to the six CVE fixes, this release adds defence-in-depth
    +hardening on several adjacent paths: bounded wire-supplied counts and
    +lengths in flist/io/acls/xattrs, a guard against length underflow in
    +cumulative `snprintf()` callers, a parent block-index bounds check on
    +the receiver, a NULL check in `read_delay_line()`, a lower ceiling on
    +`MAX_WIRE_DEL_STAT` to avoid signed-int overflow in the
    +`read_del_stats()` accumulator, rejection of hyphen-prefixed
    +remote-shell hostnames (defence-in-depth against argv-injection in
    +tooling that forwards untrusted input into the hostspec position;
    +reported by Aisle Research via Michal Ruprich), and a NULL-check on
    +`localtime_r()` in `timestring()` to keep a malicious server from
    +crashing the client by advertising a file with an out-of-range
    +modtime.
    +
     ### BUG FIXES:
     
     - Fixed a regression introduced by the 3.4.0 secure_relative_open()
    @@ -37,14 +152,33 @@
       with protocol < 29, top-level files). The test skips on
       platforms without a RESOLVE_BENEATH equivalent.
     
    -- runtests.py now errors early with a clear message when the test
    -  helper programs (`tls`, `trimslash`, `t_unsafe`, `wildtest`,
    -  `getgroups`, `getfsdev`) are missing, instead of letting many
    -  tests fail with confusing "not found" errors.
    +- Added regression tests for the new security fixes:
    +  `chmod-symlink-race.test`, `chdir-symlink-race.test`,
    +  `bare-do-open-symlink-race.test`, `alt-dest-symlink-race.test`,
    +  `copy-dest-source-symlink.test`, `sender-flist-symlink-leak.test`,
    +  `secure-relpath-validation.test`, `daemon-chroot-acl.test` and
    +  `daemon-refuse-compress.test`. The symlink-race tests skip on
    +  Cygwin, Solaris, OpenBSD and NetBSD (no RESOLVE_BENEATH
    +  equivalent on those platforms).
    +
    +- runtests.py now errors early with a clear message when any of
    +  the test helper programs (`tls`, `trimslash`, `t_unsafe`,
    +  `t_chmod_secure`, `t_secure_relpath`, `wildtest`, `getgroups`,
    +  `getfsdev`) are missing, instead of letting many tests fail with
    +  confusing "not found" errors.
     
     - Added OpenBSD and NetBSD CI jobs that run `make check` on those
       platforms.
     
    +- Added Ubuntu 22.04 and AlmaLinux 8 CI workflows so future
    +  backports to the two mainstream LTS families build and test on
    +  the same CI surface as trunk.
    +
    +- testsuite/protected-regular.test now runs unprivileged via
    +  `unshare` with user-namespace UID mapping, falling back to skip
    +  if `unshare`/`uidmap` is not available; previously it required
    +  real root.
    +
     - Added `symlink-dirlink-basis` to the Cygwin CI's expected-skipped
       list.
     
    @@ -5035,7 +5169,7 @@ to develop and test fixes.
     
     | RELEASE DATE | VER.   | DATE OF COMMIT\* | PROTOCOL    |
     |--------------|--------|------------------|-------------|
    -| ?? ??? 2026  | 3.4.3  |                  | 32          |
    +| 20 May 2026  | 3.4.3  |                  | 32          |
     | 28 Apr 2026  | 3.4.2  |                  | 32          |
     | 16 Jan 2025  | 3.4.1  |                  | 32          |
     | 15 Jan 2025  | 3.4.0  | 15 Jan 2025      | 32          |
    
c44c90e9460c

token: harden compressed-token decoding against integer overflow

https://github.com/rsyncproject/rsyncAndrew TridgellApr 29, 2026Fixed in 3.4.3via llm-release-walk
2 files changed · +67 61
  • receiver.c+10 1 modified
    @@ -318,14 +318,23 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
     		}
     	}
     
    -	while ((i = recv_token(f_in, &data)) != 0) {
    +	while (1) {
    +		data = NULL;
    +		i = recv_token(f_in, &data);
    +		if (i == 0)
    +			break;
    +
     		if (INFO_GTE(PROGRESS, 1))
     			show_progress(offset, total_size);
     
     		if (allowed_lull)
     			maybe_send_keepalive(time(NULL), MSK_ALLOW_FLUSH | MSK_ACTIVE_RECEIVER);
     
     		if (i > 0) {
    +			if (!data) {
    +				rprintf(FERROR, "Invalid literal token with no data [%s]\n", who_am_i());
    +				exit_cleanup(RERR_PROTOCOL);
    +			}
     			if (DEBUG_GTE(DELTASUM, 3)) {
     				rprintf(FINFO,"data recv %d at %s\n",
     					i, big_num(offset));
    
  • token.c+57 60 modified
    @@ -292,6 +292,14 @@ static int32 simple_recv_token(int f, char **data)
     		int32 i = read_int(f);
     		if (i <= 0)
     			return i;
    +		/* simple_send_token caps each literal chunk at CHUNK_SIZE;
    +		 * reject anything larger so a hostile peer cannot drive the
    +		 * read_buf below past our static CHUNK_SIZE buffer. */
    +		if (i > CHUNK_SIZE) {
    +			rprintf(FERROR, "invalid uncompressed token length %ld [%s]\n",
    +				(long)i, who_am_i());
    +			exit_cleanup(RERR_PROTOCOL);
    +		}
     		residue = i;
     	}
     
    @@ -494,9 +502,52 @@ static char *cbuf;
     static char *dbuf;
     
     /* for decoding runs of tokens */
    +#define MAX_TOKEN_INDEX ((int32)0x7ffffffe)
    +
     static int32 rx_token;
     static int32 rx_run;
     
    +static NORETURN void invalid_compressed_token(void)
    +{
    +	rprintf(FERROR, "invalid token number in compressed stream\n");
    +	exit_cleanup(RERR_PROTOCOL);
    +}
    +
    +static int32 recv_compressed_token_num(int f, int32 flag)
    +{
    +	if (flag & TOKEN_REL) {
    +		int32 incr = flag & 0x3f;
    +		if (rx_token > MAX_TOKEN_INDEX - incr)
    +			invalid_compressed_token();
    +		rx_token += incr;
    +		flag >>= 6;
    +	} else {
    +		rx_token = read_int(f);
    +		if (rx_token < 0 || rx_token > MAX_TOKEN_INDEX)
    +			invalid_compressed_token();
    +	}
    +
    +	if (flag & 1) {
    +		rx_run = read_byte(f);
    +		rx_run += read_byte(f) << 8;
    +		if (rx_run <= 0 || rx_token > MAX_TOKEN_INDEX - rx_run)
    +			invalid_compressed_token();
    +		recv_state = r_running;
    +	}
    +
    +	return -1 - rx_token;
    +}
    +
    +static int32 recv_compressed_token_run(void)
    +{
    +	if (rx_run <= 0 || rx_token >= MAX_TOKEN_INDEX)
    +		invalid_compressed_token();
    +	++rx_token;
    +	if (--rx_run == 0)
    +		recv_state = r_idle;
    +	return -1 - rx_token;
    +}
    +
     /* Receive a deflated token and inflate it */
     static int32 recv_deflated_token(int f, char **data)
     {
    @@ -587,22 +638,7 @@ static int32 recv_deflated_token(int f, char **data)
     			}
     
     			/* here we have a token of some kind */
    -			if (flag & TOKEN_REL) {
    -				rx_token += flag & 0x3f;
    -				flag >>= 6;
    -			} else {
    -				rx_token = read_int(f);
    -				if (rx_token < 0) {
    -					rprintf(FERROR, "invalid token number in compressed stream\n");
    -					exit_cleanup(RERR_PROTOCOL);
    -				}
    -			}
    -			if (flag & 1) {
    -				rx_run = read_byte(f);
    -				rx_run += read_byte(f) << 8;
    -				recv_state = r_running;
    -			}
    -			return -1 - rx_token;
    +			return recv_compressed_token_num(f, flag);
     
     		case r_inflating:
     			rx_strm.next_out = (Bytef *)dbuf;
    @@ -622,10 +658,7 @@ static int32 recv_deflated_token(int f, char **data)
     			break;
     
     		case r_running:
    -			++rx_token;
    -			if (--rx_run == 0)
    -				recv_state = r_idle;
    -			return -1 - rx_token;
    +			return recv_compressed_token_run();
     		}
     	}
     }
    @@ -836,22 +869,7 @@ static int32 recv_zstd_token(int f, char **data)
     				return 0;
     			}
     			/* here we have a token of some kind */
    -			if (flag & TOKEN_REL) {
    -				rx_token += flag & 0x3f;
    -				flag >>= 6;
    -			} else {
    -				rx_token = read_int(f);
    -				if (rx_token < 0) {
    -					rprintf(FERROR, "invalid token number in compressed stream\n");
    -					exit_cleanup(RERR_PROTOCOL);
    -				}
    -			}
    -			if (flag & 1) {
    -				rx_run = read_byte(f);
    -				rx_run += read_byte(f) << 8;
    -				recv_state = r_running;
    -			}
    -			return -1 - rx_token;
    +			return recv_compressed_token_num(f, flag);
     
     		case r_inflated: /* zstd doesn't get into this state */
     			break;
    @@ -882,10 +900,7 @@ static int32 recv_zstd_token(int f, char **data)
     			break;
     
     		case r_running:
    -			++rx_token;
    -			if (--rx_run == 0)
    -				recv_state = r_idle;
    -			return -1 - rx_token;
    +			return recv_compressed_token_run();
     		}
     	}
     }
    @@ -1005,22 +1020,7 @@ static int32 recv_compressed_token(int f, char **data)
     			}
     
     			/* here we have a token of some kind */
    -			if (flag & TOKEN_REL) {
    -				rx_token += flag & 0x3f;
    -				flag >>= 6;
    -			} else {
    -				rx_token = read_int(f);
    -				if (rx_token < 0) {
    -					rprintf(FERROR, "invalid token number in compressed stream\n");
    -					exit_cleanup(RERR_PROTOCOL);
    -				}
    -			}
    -			if (flag & 1) {
    -				rx_run = read_byte(f);
    -				rx_run += read_byte(f) << 8;
    -				recv_state = r_running;
    -			}
    -			return -1 - rx_token;
    +			return recv_compressed_token_num(f, flag);
     
     		case r_inflating:
     			avail_out = LZ4_decompress_safe(next_in, dbuf, avail_in, size);
    @@ -1036,10 +1036,7 @@ static int32 recv_compressed_token(int f, char **data)
     			break;
     
     		case r_running:
    -			++rx_token;
    -			if (--rx_run == 0)
    -				recv_state = r_idle;
    -			return -1 - rx_token;
    +			return recv_compressed_token_run();
     		}
     	}
     }
    
32c222a31e7c
https://github.com/rsyncproject/rsyncFixed in 3.4.3via llm-release-walk
650643109e6e

defence-in-depth: receiver block-index bounds + read_delay_line null check

https://github.com/rsyncproject/rsyncAndrew TridgellDec 31, 2025Fixed in 3.4.3via llm-release-walk
2 files changed · +15 4
  • generator.c+10 4 modified
    @@ -229,11 +229,13 @@ static int read_delay_line(char *buf, int *flags_p)
     		*flags_p = 0;
     
     	if (sscanf(bp, "%x ", &mode) != 1) {
    -	  invalid_data:
    -		rprintf(FERROR, "ERROR: invalid data in delete-delay file.\n");
    -		return -1;
    +		goto invalid_data;
    +	}
    +	past_space = strchr(bp, ' ');
    +	if (!past_space) {
    +		goto invalid_data;
     	}
    -	past_space = strchr(bp, ' ') + 1;
    +	past_space++;
     	len = j - read_pos - (past_space - bp) + 1; /* count the '\0' */
     	read_pos = j + 1;
     
    @@ -247,6 +249,10 @@ static int read_delay_line(char *buf, int *flags_p)
     	memcpy(buf, past_space, len);
     
     	return mode;
    +
    +invalid_data:
    +	rprintf(FERROR, "ERROR: invalid data in delete-delay file.\n");
    +	return -1;
     }
     
     static void do_delayed_deletions(char *delbuf)
    
  • receiver.c+5 0 modified
    @@ -352,6 +352,11 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
     		}
     
     		i = -(i+1);
    +		if (i < 0 || i >= sum.count) {
    +			rprintf(FERROR, "Invalid block index %d (count=%ld) [%s]\n",
    +				i, (long)sum.count, who_am_i());
    +			exit_cleanup(RERR_PROTOCOL);
    +		}
     		offset2 = i * (OFF_T)sum.blength;
     		len = sum.blength;
     		if (i == (int)sum.count-1 && sum.remainder != 0)
    
8112445318a3

defence-in-depth: bound wire-supplied counts and lengths

https://github.com/rsyncproject/rsyncAndrew TridgellDec 31, 2025Fixed in 3.4.3via llm-release-walk
6 files changed · +103 13
  • acls.c+1 1 modified
    @@ -697,7 +697,7 @@ static uint32 recv_acl_access(int f, uchar *name_follows_ptr)
     static uchar recv_ida_entries(int f, ida_entries *ent)
     {
     	uchar computed_mask_bits = 0;
    -	int i, count = read_varint(f);
    +	int i, count = read_varint_bounded(f, 0, MAX_WIRE_ACL_COUNT, "ACL count");
     
     	ent->idas = count ? new_array(id_access, count) : NULL;
     	ent->count = count;
    
  • flist.c+14 3 modified
    @@ -840,9 +840,9 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
     	}
     	if (xflags & XMIT_MOD_NSEC)
     #ifndef CAN_SET_NSEC
    -		(void)read_varint(f);
    +		(void)read_varint_bounded(f, 0, MAX_WIRE_NSEC, "modtime_nsec");
     #else
    -		modtime_nsec = read_varint(f);
    +		modtime_nsec = read_varint_bounded(f, 0, MAX_WIRE_NSEC, "modtime_nsec");
     	else
     		modtime_nsec = 0;
     #endif
    @@ -861,8 +861,19 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
     #endif
     	}
     #endif
    -	if (!(xflags & XMIT_SAME_MODE))
    +	if (!(xflags & XMIT_SAME_MODE)) {
     		mode = from_wire_mode(read_int(f));
    +		/* Reject modes whose type bits are not one of the standard
    +		 * file types; otherwise garbage mode values propagate through
    +		 * the file-type checks below unpredictably. */
    +		if (!S_ISREG(mode) && !S_ISDIR(mode) && !S_ISLNK(mode)
    +		 && !S_ISCHR(mode) && !S_ISBLK(mode)
    +		 && !S_ISFIFO(mode) && !S_ISSOCK(mode)) {
    +			rprintf(FERROR, "invalid file mode 0%o for %s [%s]\n",
    +				(unsigned)mode, lastname, who_am_i());
    +			exit_cleanup(RERR_PROTOCOL);
    +		}
    +	}
     	if (atimes_ndx && !S_ISDIR(mode) && !(xflags & XMIT_SAME_ATIME)) {
     		atime = read_varlong(f, 4);
     #if SIZEOF_TIME_T < SIZEOF_INT64
    
  • io.c+54 0 modified
    @@ -1868,6 +1868,45 @@ int64 read_varlong(int f, uchar min_bytes)
     	return u.x;
     }
     
    +/* Read an int32 and verify lo <= v <= hi. On out-of-range, abort with a
    + * protocol error naming "what". The bound is co-located with the read so it
    + * cannot be forgotten by a downstream user. */
    +int32 read_int_bounded(int f, int32 lo, int32 hi, const char *what)
    +{
    +	int32 v = read_int(f);
    +	if (v < lo || v > hi) {
    +		rprintf(FERROR, "wire value %s out of range: %ld not in [%ld,%ld] [%s]\n",
    +			what, (long)v, (long)lo, (long)hi, who_am_i());
    +		exit_cleanup(RERR_PROTOCOL);
    +	}
    +	return v;
    +}
    +
    +/* As read_int_bounded but for varint-encoded values. */
    +int32 read_varint_bounded(int f, int32 lo, int32 hi, const char *what)
    +{
    +	int32 v = read_varint(f);
    +	if (v < lo || v > hi) {
    +		rprintf(FERROR, "wire value %s out of range: %ld not in [%ld,%ld] [%s]\n",
    +			what, (long)v, (long)lo, (long)hi, who_am_i());
    +		exit_cleanup(RERR_PROTOCOL);
    +	}
    +	return v;
    +}
    +
    +/* Read a varint that will be used as a size_t. Rejects negative values
    + * (which would wrap to ~SIZE_MAX) and values exceeding the supplied max. */
    +size_t read_varint_size(int f, size_t max, const char *what)
    +{
    +	int32 v = read_varint(f);
    +	if (v < 0 || (size_t)v > max) {
    +		rprintf(FERROR, "wire size %s out of range: %ld > %lu [%s]\n",
    +			what, (long)v, (unsigned long)max, who_am_i());
    +		exit_cleanup(RERR_PROTOCOL);
    +	}
    +	return (size_t)v;
    +}
    +
     int64 read_longint(int f)
     {
     #if SIZEOF_INT64 >= 8
    @@ -1974,6 +2013,21 @@ void read_sum_head(int f, struct sum_struct *sum)
     			(long)sum->count, who_am_i());
     		exit_cleanup(RERR_PROTOCOL);
     	}
    +	/* Guard against integer overflow in downstream allocations sized by
    +	 * count*element_size. my_alloc uses divide-not-multiply so it is
    +	 * already wraparound-safe, but checking here gives a clearer error
    +	 * and also covers the (size_t)count * xfer_sum_len arithmetic that
    +	 * is performed *before* reaching my_alloc. */
    +	if (xfer_sum_len > 0 && (size_t)sum->count > SIZE_MAX / (size_t)xfer_sum_len) {
    +		rprintf(FERROR, "Invalid checksum count %ld (too large) [%s]\n",
    +			(long)sum->count, who_am_i());
    +		exit_cleanup(RERR_PROTOCOL);
    +	}
    +	if ((size_t)sum->count > SIZE_MAX / sizeof(struct sum_buf)) {
    +		rprintf(FERROR, "Invalid checksum count %ld (sum_buf overflow) [%s]\n",
    +			(long)sum->count, who_am_i());
    +		exit_cleanup(RERR_PROTOCOL);
    +	}
     	sum->blength = read_int(f);
     	if (sum->blength < 0 || sum->blength > max_blength) {
     		rprintf(FERROR, "Invalid block length %ld [%s]\n",
    
  • main.c+5 5 modified
    @@ -239,11 +239,11 @@ void write_del_stats(int f)
     
     void read_del_stats(int f)
     {
    -	stats.deleted_files = read_varint(f);
    -	stats.deleted_files += stats.deleted_dirs = read_varint(f);
    -	stats.deleted_files += stats.deleted_symlinks = read_varint(f);
    -	stats.deleted_files += stats.deleted_devices = read_varint(f);
    -	stats.deleted_files += stats.deleted_specials = read_varint(f);
    +	stats.deleted_files = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_files");
    +	stats.deleted_files += stats.deleted_dirs = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_dirs");
    +	stats.deleted_files += stats.deleted_symlinks = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_symlinks");
    +	stats.deleted_files += stats.deleted_devices = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_devices");
    +	stats.deleted_files += stats.deleted_specials = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_specials");
     }
     
     static void become_copy_as_user()
    
  • rsync.h+17 0 modified
    @@ -163,6 +163,23 @@
     /* For compatibility with older rsyncs */
     #define OLD_MAX_BLOCK_SIZE ((int32)1 << 29)
     
    +/* Policy ceilings on attacker-controlled wire values. Picked well above any
    + * legitimate filesystem / protocol traffic but well below sizes that could
    + * cause integer overflow or DoS-grade allocations. See input_checking.txt.
    + *
    + * Note on MAX_WIRE_XATTR_DATALEN: xattr datum size is bounded only by the
    + * wire-format maximum (signed int32 varint, ~2GB). macOS resource forks
    + * are transferred as the com.apple.ResourceFork xattr and can legitimately
    + * be many GB; --max-alloc (default 1GB, configurable) is the real
    + * allocation cap. read_varint_size() still rejects negative values so a
    + * hostile peer cannot wrap to ~SIZE_MAX. */
    +#define MAX_WIRE_XATTR_COUNT   65536
    +#define MAX_WIRE_XATTR_NAMELEN 4096
    +#define MAX_WIRE_XATTR_DATALEN ((int32)0x7fffffff)
    +#define MAX_WIRE_ACL_COUNT     65536
    +#define MAX_WIRE_NSEC          999999999
    +#define MAX_WIRE_DEL_STAT      ((int32)1 << 30)
    +
     #define ROUND_UP_1024(siz) ((siz) & (1024-1) ? ((siz) | (1024-1)) + 1 : (siz))
     
     #define IOERR_GENERAL	(1<<0) /* For backward compatibility, this must == 1 */
    
  • xattrs.c+12 4 modified
    @@ -697,6 +697,13 @@ int recv_xattr_request(struct file_struct *file, int f_in)
     	rxa = lst->items;
     	num = 0;
     	while ((rel_pos = read_varint(f_in)) != 0) {
    +		/* Detect signed overflow before the accumulating add. A hostile
    +		 * peer could otherwise wrap 'num' to land on an arbitrary value. */
    +		if ((rel_pos > 0 && num > INT_MAX - rel_pos)
    +		 || (rel_pos < 0 && num < INT_MIN - rel_pos)) {
    +			rprintf(FERROR, "xattr rel_pos accumulation overflow [%s]\n", who_am_i());
    +			exit_cleanup(RERR_PROTOCOL);
    +		}
     		num += rel_pos;
     		if (am_sender) {
     			/* The sender-related num values are only in order on the sender.
    @@ -742,7 +749,7 @@ int recv_xattr_request(struct file_struct *file, int f_in)
     		}
     
     		old_datum = rxa->datum;
    -		rxa->datum_len = read_varint(f_in);
    +		rxa->datum_len = read_varint_size(f_in, MAX_WIRE_XATTR_DATALEN, "xattr datum_len");
     
     		if (SIZE_MAX - rxa->name_len < rxa->datum_len)
     			overflow_exit("recv_xattr_request");
    @@ -783,16 +790,17 @@ void receive_xattr(int f, struct file_struct *file)
     		return;
     	}
     
    -	if ((count = read_varint(f)) != 0) {
    +	count = read_varint_bounded(f, 0, MAX_WIRE_XATTR_COUNT, "xattr count");
    +	if (count != 0) {
     		(void)EXPAND_ITEM_LIST(&temp_xattr, rsync_xa, count);
     		temp_xattr.count = 0;
     	}
     
     	for (num = 1; num <= count; num++) {
     		char *ptr, *name;
     		rsync_xa *rxa;
    -		size_t name_len = read_varint(f);
    -		size_t datum_len = read_varint(f);
    +		size_t name_len = read_varint_size(f, MAX_WIRE_XATTR_NAMELEN, "xattr name_len");
    +		size_t datum_len = read_varint_size(f, MAX_WIRE_XATTR_DATALEN, "xattr datum_len");
     		size_t dget_len = datum_len > MAX_FULL_DATUM ? 1 + (size_t)xattr_sum_len : datum_len;
     		size_t extra_len = MIGHT_NEED_RPRE ? RPRE_LEN : 0;
     		if (SIZE_MAX - dget_len < extra_len || SIZE_MAX - dget_len - extra_len < name_len)
    
559ca5a3c2f6
https://github.com/rsyncproject/rsyncFixed in 3.4.3via llm-release-walk

Vulnerability mechanics

Root cause

"A 32-bit signed counter (rx_token) in the compressed-token decoder is incremented without overflow checking, allowing a malicious sender to wrap the counter past INT32_MAX and cause out-of-bounds reads."

Attack vector

An authenticated rsync daemon peer with compression enabled (the default for protocol >= 30) sends a crafted compressed-token stream that walks the receiver's 32-bit signed rx_token counter past INT32_MAX [CWE-190]. The overflow causes the receiver to compute negative token indices that, when negated and used as block indices, read data from outside the intended buffer bounds [CWE-125]. The attacker must have valid authentication to the daemon and compression must be negotiated; the workaround is to set `refuse options = compress` in rsyncd.conf. No special client options are required on the victim side.

Affected code

The vulnerability is in token.c, where three compressed-token decoders (recv_deflated_token, recv_zstd_token, recv_compressed_token) all accumulated the global 32-bit signed counter rx_token without overflow checking. The same file's simple_recv_token() also lacked a bounds check on wire-supplied literal lengths. receiver.c's receive_data() lacked a block-index bounds check against sum.count and a NULL check on the data pointer returned by recv_token().

What the fix does

The patch caps rx_token at MAX_TOKEN_INDEX (0x7ffffffe) and folds the token-advance logic into two shared helper functions — recv_compressed_token_num() and recv_compressed_token_run() — that all three decoders (zlib, zstd, lz4) call [patch_id=801399]. These helpers check for overflow before incrementing rx_token and reject negative or out-of-range token values with a protocol error. Additionally, the simple_recv_token() function now rejects literal-block lengths larger than CHUNK_SIZE, and receive_data() in receiver.c adds a NULL check on the data pointer and a block-index bounds check against sum.count [patch_id=801396]. Defence-in-depth patches also add bounded read primitives (read_int_bounded, read_varint_bounded, read_varint_size) applied to checksum counts, xattrs, ACLs, file modes, and delete-stat counters [patch_id=801397].

Preconditions

  • authAttacker must have valid authentication to the rsync daemon
  • configCompression must be enabled (default for protocol >= 30 when both peers advertise it)
  • networkNetwork access to the rsync daemon port
  • inputAttacker sends a crafted compressed-token stream as the sender in a daemon connection

Generated on May 20, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.