From 901041dddc9a343ed51f8e2cd3992aed3ae0180c Mon Sep 17 00:00:00 2001
From: Andrew Tridgell <andrew@tridgell.net>
Date: Wed, 29 Apr 2026 11:10:59 +1000
Subject: [PATCH 44/60] token: harden compressed-token decoding against integer
 overflow

The receiver's three compressed-token decoders --
recv_deflated_token (zlib), recv_zstd_token, and
recv_compressed_token (lz4) -- accumulated rx_token (a 32-bit
signed counter) without overflow checking. A malicious sender
could craft a compressed-token stream that walked rx_token past
INT32_MAX, with careful manipulation leaking process memory
contents to the wire (environment variables, passwords, heap
pointers, library pointers -- significantly weakening ASLR
and facilitating further exploitation).

Cap rx_token at MAX_TOKEN_INDEX = 0x7ffffffe. Fold the
bookkeeping into recv_compressed_token_num() and
recv_compressed_token_run() shared by all three decoders. Reject
negative or out-of-range token values explicitly. Also cap the
simple_recv_token literal-block length at the source: any
wire-supplied length > CHUNK_SIZE is ill-formed (the matching
simple_send_token never writes a chunk larger than CHUNK_SIZE),
so reject before looping on attacker-controlled bytes.

Reach: an authenticated daemon connection with compression
enabled (the default for protocols >= 30 when both peers
advertise it). Disabling compression on the daemon
("refuse options = compress" in rsyncd.conf) is the available
workaround.

Reporter: Omar Elsayed (seks99x).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 receiver.c |  11 ++++-
 token.c    | 117 ++++++++++++++++++++++++++---------------------------
 2 files changed, 67 insertions(+), 61 deletions(-)

diff --git a/receiver.c b/receiver.c
index 8f5b51dd..63e5cedb 100644
--- a/receiver.c
+++ b/receiver.c
@@ -318,7 +318,12 @@ 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);
 
@@ -326,6 +331,10 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
 			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));
diff --git a/token.c b/token.c
index b7a02ea1..02dabd8d 100644
--- a/token.c
+++ b/token.c
@@ -291,6 +291,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;
 	}
 
@@ -493,9 +501,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)
 {
@@ -586,22 +637,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;
@@ -621,10 +657,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();
 		}
 	}
 }
@@ -833,22 +866,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;
@@ -879,10 +897,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();
 		}
 	}
 }
@@ -1002,22 +1017,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);
@@ -1033,10 +1033,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();
 		}
 	}
 }
-- 
2.51.0

