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:
- El relé de salida atacante encola en el cliente una tanda de celdas
RELAY_DATAfuera de orden tras unaCONFLUX_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
- 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).
- Con
gdbse leen en el cliente, solo como medida, los valores deconflux_get_total_bytes_allocation()yMaxMemInQueues.
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.