From: Andrew Tridgell Date: Wed, 29 Apr 2026 01:10:59 +0000 (+1000) Subject: token: harden compressed-token decoding against integer overflow X-Git-Tag: v3.4~13 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=c44c90e9;p=thirdparty%2Frsync.git 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) --- diff --git a/receiver.c b/receiver.c index 8cf8366b..a487ad5a 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 773f14a2..62ffae15 100644 --- 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(); } } }