]> git.ipfire.org Git - thirdparty/rsync.git/commitdiff
token: harden compressed-token decoding against integer overflow
authorAndrew Tridgell <andrew@tridgell.net>
Wed, 29 Apr 2026 01:10:59 +0000 (11:10 +1000)
committerAndrew Tridgell <andrew@tridgell.net>
Thu, 7 May 2026 21:49:13 +0000 (07:49 +1000)
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
token.c

index 8cf8366bf6c579a974aefa532d0eba4c839a7a9b..a487ad5a583b4f69371b2e3c4a344e2faa084252 100644 (file)
@@ -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 773f14a2ce2810ca815cb094b982280468c79dff..62ffae151b37c46c26c96e7ca555636d95760ff1 100644 (file)
--- a/token.c
+++ b/token.c
@@ -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();
                }
        }
 }