Reports
MEDIA[OTROS]#3709605

Tor: un RELAY_END vacío reordenado por Conflux provoca una lectura fuera de límites en el heap

Tor leía el byte de motivo de un RELAY_END antes de comprobar su longitud. Si la celda llegaba vacía y pasaba por la cola de desorden de Conflux, la lectura caía fuera del bloque reservado en el heap y podía tumbar el proceso.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

El fallo es una lectura fuera de límites en el heap (CWE-125) en el código de Tor que gestiona las celdas RELAY_END que llegan a un stream de cliente (AP) que todavía no está abierto.

El protocolo de Tor admite RELAY_END sin cuerpo: si la longitud es 0, el motivo de cierre se da por END_STREAM_REASON_MISC. El problema está en el orden de las operaciones de connection_ap_process_end_not_open(): primero lee el primer byte del cuerpo y solo después mira si la longitud era cero.

src/core/or/relay.c:846:

int reason = get_uint8(msg->body);

Y la comprobación de longitud llega más tarde:

if (msg->length == 0) {
  reason = END_STREAM_REASON_MISC;
}

En condiciones normales esto no se nota, porque el mensaje apunta al búfer de la celda completa y ese byte existe aunque no tenga sentido. La cosa cambia con Conflux. Cuando una celda llega fuera de orden, Conflux la guarda en una cola haciendo una copia con relay_msg_copy(), que reserva exactamente la memoria que necesita el mensaje:

src/core/or/conflux.c:923:

c_msg->msg = relay_msg_copy(msg);

src/core/or/relay_msg.c:71:

void *alloc = tor_malloc_zero(sizeof(relay_msg_t) + msg->length);

Con msg->length == 0, el msg->body de la copia apunta justo al final de la reserva, así que la llamada get_uint8(msg->body) lee un byte que ya no pertenece a ese bloque.

Pasos de reproducción

El investigador lo demostró con un test de regresión añadido a src/test/test_relaycell.c (test relaycell/end_cell_conflux_copy_oob, con la función test_relaycell_end_cell_conflux_copy_oob() y el auxiliar copy_msg_through_conflux_ooo()), que adjuntó como parche al reporte. El test hace lo siguiente:

  1. Construye un mensaje RELAY_END de longitud cero dirigido a un stream AP que aún no está abierto.
  2. Lo pasa por la cola de mensajes fuera de orden real de Tor, conflux_process_relay_msg().
  3. Saca de la cola el relay_msg_t copiado en el heap por relay_msg_copy().
  4. Simula la entrega ordenada de Conflux llamando a connection_edge_process_relay_cell().
  5. AddressSanitizer detecta el desbordamiento en connection_ap_process_end_not_open().

Para compilar y lanzarlo con ASan desde un checkout limpio de Tor con el parche aplicado:

./autogen.sh
CC=clang \
CFLAGS='-O0 -g -fsanitize=address -fno-omit-frame-pointer' \
LDFLAGS='-fsanitize=address' \
./configure --disable-asciidoc --disable-manpage --disable-html-manual
make micro-revision.i
make -j"$(nproc)" src/test/test
ASAN_OPTIONS='abort_on_error=1:halt_on_error=1:detect_leaks=0:symbolize=1' \
  ./src/test/test relaycell/end_cell_conflux_copy_oob

En su entorno, además, tuvo que indicar a configure dónde estaba OpenSSL de Homebrew:

PKG_CONFIG_PATH=/home/linuxbrew/.linuxbrew/lib/pkgconfig \
CPPFLAGS='-I/home/linuxbrew/.linuxbrew/include' \
LDFLAGS='-fsanitize=address -L/home/linuxbrew/.linuxbrew/lib -Wl,-rpath,/home/linuxbrew/.linuxbrew/lib' \
./configure --with-openssl-dir=/home/linuxbrew/.linuxbrew \
  --disable-asciidoc --disable-manpage --disable-html-manual

La salida de ASan confirma la lectura de un byte justo después de un bloque de 16 bytes reservado por relay_msg_copy():

ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 1
    #0 get_uint8 src/lib/arch/bytes.h:25
    #1 connection_ap_process_end_not_open src/core/or/relay.c:846
    #2 connection_edge_process_relay_cell_not_open src/core/or/relay.c:1402
    #3 connection_edge_process_ordered_relay_cell src/core/or/relay.c:2168
    #4 connection_edge_process_relay_cell src/core/or/relay.c:2099
    #5 test_relaycell_end_cell_conflux_copy_oob src/test/test_relaycell.c:1136

allocated by thread T0 here:
    #3 relay_msg_copy src/core/or/relay_msg.c:71
    #4 conflux_process_relay_msg src/core/or/conflux.c:923
    #5 copy_msg_through_conflux_ooo src/test/test_relaycell.c:214

0 bytes after 16-byte region

Impacto

Quien actúe como relay o exit en un circuito Conflux y consiga que un RELAY_END vacío quede encolado fuera de orden puede provocar de forma remota esta lectura fuera de límites en el cliente Tor. El escenario más claro es un stream AP que todavía espera conexión (por ejemplo en AP_CONN_STATE_CONNECT_WAIT) y recibe ese RELAY_END vacío desde el lado de la salida.

  • En compilaciones con ASan o endurecidas, el proceso de Tor aborta de forma fiable: denegación de servicio.
  • En compilaciones normales es igualmente un comportamiento inseguro de memoria que lee un byte adyacente del heap. No se demostró fuga de información, y el valor leído se sustituye por END_STREAM_REASON_MISC antes de usarse.

Explotarlo no es trivial: hace falta un par Conflux enlazado, un hueco en la secuencia que fuerce el encolado fuera de orden y que el stream esté en un estado no abierto. De ahí la puntuación propuesta por el investigador:

CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

El programa lo clasificó con severidad media y pagó recompensa (el importe no es público). Se envió el 2 de mayo de 2026 y se divulgó el 10 de septiembre de 2026.

Remediación

El reporte figura como resuelto, aunque no detalla el parche que aplicó finalmente Tor ni la versión en la que se corrigió. La corrección que propuso el investigador consiste en leer el byte de motivo solo si el mensaje tiene cuerpo:

int reason = msg->length > 0 ? get_uint8(msg->body)
                             : END_STREAM_REASON_MISC;