Reports
BAJA[DENEGACIÓN DE SERVICIO]#3701692

Tor: un relé de salida malicioso deja inflado el contador de memoria de las colas Conflux tras cerrar el circuito

Al destruir un conjunto Conflux, Tor libera los mensajes fuera de orden pero no resta su coste del contador global. Un relé de salida malicioso puede inflarlo de forma acumulativa hasta reiniciar Tor, acercándose al límite MaxMemInQueues.

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

Conflux es el mecanismo de Tor que reparte el tráfico de un mismo flujo entre varios circuitos ("patas"). Como las celdas pueden llegar desordenadas, el cliente guarda las que se adelantan en una cola fuera de orden (OOO, out-of-order) y lleva la cuenta de cuánta memoria ocupan en dos contadores: uno por conjunto Conflux (cfx->ooo_q_alloc_cost) y uno global (total_ooo_q_bytes).

El fallo está en que esa contabilidad solo cuadra en el camino normal. Al encolar un mensaje, src/core/or/conflux.c suma su coste a ambos contadores:

total_ooo_q_bytes += cost;
cfx->ooo_q_alloc_cost += cost;

Y al sacarlo de la cola de forma ordinaria, lo resta:

total_ooo_q_bytes -= cost;
cfx->ooo_q_alloc_cost -= cost;

Pero si el conjunto Conflux se destruye mientras aún quedan mensajes en la cola, conflux_free_() en src/core/or/conflux_pool.c los libera sin descontar nada:

SMARTLIST_FOREACH(cfx->ooo_q, conflux_msg_t *, cell,
                  conflux_relay_msg_free(cell));
smartlist_free(cfx->ooo_q);

Resultado: total_ooo_q_bytes sigue contando memoria que ya no existe. Ese valor obsoleto se suma después al total que vigila cell_queues_check_size() en src/core/or/relay.c:

const size_t conflux_total = conflux_get_total_bytes_allocation();
alloc += conflux_total;

Si ese total alcanza MaxMemInQueues, Tor entra en su modo de recuperación por falta de memoria y puede cerrar circuitos mediante circuits_handle_oom().

El origen del problema es un relé de salida compatible con Conflux bajo control del atacante: envía una celda CONFLUX_SWITCH real, que abre un hueco en la numeración, y a continuación celdas RELAY_DATA, que el cliente se ve obligado a guardar en la cola OOO. Si el conjunto se destruye antes de que esa cola se vacíe por la vía normal, el coste de esos mensajes queda "huérfano" en el contador. El cliente víctima no necesita ninguna modificación.

Pasos de reproducción

El investigador demuestra el fallo sobre una red Tor privada montada con Chutney. Usa dos compilaciones de Tor a partir del mismo commit (a66e072d7f3752fe98d4e59129b35b0942d4399b): una sin modificar, para el cliente víctima y el resto de la red, y otra a la que se aplica un parche (attacker-relay.diff) que solo afecta a los relés de salida atacantes, haciéndoles emitir el tráfico Conflux hostil por la vía normal de envío de celdas. Los ficheros de la prueba (run-poc.sh, attacker-relay.diff y chutney-network) iban adjuntos al reporte y no se reproducen aquí.

A grandes rasgos, el procedimiento es:

  1. El relé de salida atacante encola en el cliente una tanda de celdas RELAY_DATA fuera de orden tras una CONFLUX_SWITCH, hasta llenar su cola OOO. La prueba lo deja constar con una línea del tipo:
PoC: Conflux OOO exact-leg probe queued 124 DATA cells; stop this exit now to force client-side teardown
  1. El arnés sustituye entonces las salidas atacantes por salidas limpias, lo que obliga al cliente a destruir el conjunto Conflux de forma normal (teardown).
  1. Con gdb se leen en el cliente, solo como medida, los valores de conflux_get_total_bytes_allocation() y MaxMemInQueues.

El dato clave es que el contador de asignación Conflux (conflux_counter) no vuelve a cero tras el teardown y crece ronda tras ronda, mientras que la memoria real del proceso (client_rss_mb) apenas varía:

timestamp                  label                         mem_available_mb  client_rss_mb  client_vmsize_mb  conflux_counter  max_mem_in_queues  attacker_sends  oom_lines
2026-04-28T22:59:29+05:30  baseline                      7872.5            12.6           306.1             0                6625393048         0               0
2026-04-28T23:00:14+05:30  round-1-after-attack          7886.2            12.8           313.9             327716           6625393048         83              0
2026-04-28T23:00:24+05:30  round-1-after-clean-restart   8052.1            12.8           313.9             327716           6625393048         83              0
2026-04-28T23:01:09+05:30  round-2-after-attack          7918.1            12.8           313.9             720904           6625393048         185             0
2026-04-28T23:01:19+05:30  round-2-after-clean-restart   8079.9            12.8           313.9             720904           6625393048         185             0
2026-04-28T23:02:04+05:30  round-3-after-attack          7870.9            12.8           313.9             1441452          6625393048         278             0
2026-04-28T23:02:14+05:30  round-3-after-clean-restart   8205.4            12.8           313.9             1441452          6625393048         278             0
[result] target counter reached: 1441452 >= 1000000

Sube solo la cifra que Tor cree estar usando, no la memoria realmente ocupada. En esta ejecución no llegó a dispararse la recuperación por falta de memoria (oom_lines = 0).

Impacto

Un relé de salida malicioso con soporte de Conflux puede hacer que el contador global de memoria de las colas OOO de un cliente Tor quede inflado tras cerrar los circuitos, y ese exceso se mantiene hasta que Tor se reinicia. Como el contador se acumula entre ataques, cada vez queda menos margen antes de MaxMemInQueues.

Si se acumulara lo suficiente, Tor creería estar sin memoria y empezaría a cerrar circuitos legítimos mediante su mecanismo de recuperación OOM. El investigador aclara que su prueba no llega a provocar ese cierre con la configuración por defecto: lo demostrado es el desajuste persistente del contador. El programa lo clasificó como severidad baja y pagó recompensa, cuyo importe no es público.

Remediación

El reporte figura como resuelto. La corrección que propuso el investigador es aplicar al destruir el conjunto Conflux la misma contabilidad que al desencolar: una función que resta el coste de cada mensaje de ambos contadores antes de liberarlo, con protección por si el contador ya fuese menor que el coste:

static void
conflux_free_queued_msg_accounted(conflux_t *cfx, conflux_msg_t *msg)
{
  size_t cost = conflux_msg_alloc_cost(msg);

  if (BUG(total_ooo_q_bytes < cost))
    total_ooo_q_bytes = 0;
  else
    total_ooo_q_bytes -= cost;

  if (BUG(cfx->ooo_q_alloc_cost < cost))
    cfx->ooo_q_alloc_cost = 0;
  else
    cfx->ooo_q_alloc_cost -= cost;

  conflux_relay_msg_free(msg);
}

Y usarla desde conflux_free_():

 SMARTLIST_FOREACH(cfx->ooo_q, conflux_msg_t *, cell,
-                  conflux_relay_msg_free(cell));
+                  conflux_free_queued_msg_accounted(cfx, cell));
 smartlist_free(cfx->ooo_q);

También recomendó añadir pruebas de regresión que comprueben que, tras destruir un conjunto Conflux con la cola OOO no vacía, conflux_get_total_bytes_allocation() vuelve a cero. En el reporte no consta cuál fue el parche final aplicado por Tor.