heap-use-after-free with a conflux recovery leg
I'll let the LLM explain this one since I think it does a good job: """ A conflux_t ("cfx") is shared by all legs (circuits) of a conflux set. When the *last* leg of a linked set is closed, linked_circuit_closed() removes that leg but deliberately KEEPS circ->conflux pointing at the cfx (so the cfx can be freed later when the circuit itself is freed). The cfx is removed from the linked pool but NOT freed. Meanwhile, conflux supports "recovery legs": a client may launch a new leg for a nonce that already has a set (conflux_launch_leg -> unlinked_get_or_create finds the existing cfx and shares it, flagged is_for_linked_set). When that recovery leg finishes linking (conflux_process_linked -> try_finalize_set -> cfx_add_leg), it REVIVES the same cfx: it re-adds it to the linked pool and points its own circ->conflux at it. The result is TWO distinct circuits whose circ->conflux both point at the same cfx: * the original last leg (already marked-for-close, conflux kept), and * the freshly-linked recovery leg. When both circuits are later freed (conflux_circuit_about_to_free -> linked_circuit_free), the first one whose free sees cfx->legs empty calls conflux_free(cfx) (conflux_pool.c:1703). The second circuit still points at the now-freed cfx and dereferences it at conflux_pool.c:1650 (tor_assert(circ->conflux->legs)) -> heap-use-after-free (and, absent the asserts, a second conflux_free -> double free). """ The attack scenario is an exit relay causing this UAF on a client: if it "gets a client to (a) link a leg, (b) launch a recovery leg for the same nonce, (c) close the linked leg, (d) link the recovery leg, then (e) close it, all before the circuits are reaped, triggers the UAF on the next post-loop circuit cleanup." Saying the same thing in slightly different words: "launch+open+LINK a first client leg; launch+open a recovery leg for the same nonce; close the first (last) leg (linked_circuit_closed keeps its conflux pointer); deliver CONFLUX_LINKED to the recovery leg (try_finalize_set revives the cfx); then close and free both circuits, at which point the UAF fires." It looks related to #41262, but the LLM is convinced that this new bug exists after that fix. Cc'ing @mikeperry and @dgoulet as the conflux people.
issue