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>
This commit is contained in:
Andrew Tridgell
2026-04-29 11:10:59 +10:00
parent fc592a8e25
commit c44c90e946
2 changed files with 67 additions and 61 deletions

View 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));

117
token.c
View File

@@ -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();
}
}
}