strongSwan is a widely used open-source IPsec implementation: it terminates IKEv2 VPN tunnels for gateways, routers, and cloud workloads. As part of our research into the network daemons that carry authenticated traffic, Cipher found a use-after-free in its IKEv2 rekeying. When two peers rekey the same security association at the same time, strongSwan’s collision handling caches a pointer to a task it does not own. An authenticated peer can drive that task to be freed while the pointer is still held, then trigger a later dereference of the freed memory through an indirect function call.
strongSwan’s advisory assigns no severity score. We assess it as High, CVSS 7.5 (AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H), reflecting the authenticated-peer requirement and the extra heap-state control an attacker needs for anything past a crash. We reproduced the fault three ways, up to and including control of the program counter under a controlled heap, and we explain below exactly where we stop and what a remote attacker would still have to achieve. The vendor’s advisory allows that remote code execution “might be possible” for an authenticated peer; we rate it High rather than Critical because the final step, a wire-driven heap groom, is characterized but not demonstrated.
IMPORTANT
Who’s exposed. You’re affected if you run strongSwan 6.0.0 or later and your configuration accepts multiple key exchanges, the multi-KE / post-quantum extension that negotiates an additional key-exchange group on top of the first. The attacker has to be an authenticated IKE peer with an established IKE_SA. Given that, the attack needs no host access and no configuration on the victim beyond the multi-KE proposal both sides already agreed to. If your servers do not accept multiple key exchanges, this code path is not reached and you are not vulnerable. Fixed in strongSwan 6.1.0; a patch for older releases is published alongside the advisory.
The bug, in child_rekey.c
An IKE or Child SA rekeying can collide: both peers decide to rekey the same SA at once. IKEv2 resolves the collision by comparing the nonces of the SAs involved, so the active rekeying task, the one that initiated, keeps a reference to the passive task, the one that responded, to reach its nonces later. That reference is a raw, non-owning pointer:
// src/libcharon/sa/ikev2/tasks/child_rekey.c
task_t *collision; /* weak reference to the colliding task */
Ownership of that pointer is conditional, and the condition is the whole bug. In collide(), when the colliding passive task is a multi-KE rekey whose CHILD_SA is not yet installed, the active task stores the reference but explicitly does not take ownership:
// child_rekey.c, collide()
if (other_child->get_state(other_child) != CHILD_INSTALLED) {
this->collision = other; // weak ref; PASSIVE_INSTALLED not set, ownership not transferred
return FALSE; // task manager keeps the passive task in its own queue
}
/* ...only the adopt path below transfers ownership... */
this->flags |= CHILD_REKEY_PASSIVE_INSTALLED;
this->collision = other;
On the adopt path, the active task sets CHILD_REKEY_PASSIVE_INSTALLED and owns the passive task; it is the one that will later free it. On the path above, it holds the same pointer but the task manager still owns the object. The collision pointer is cleared only in handle_collision() and migrate(), and it is destroyed only when PASSIVE_INSTALLED is set. Nothing clears it if the task manager frees the passive task on its own. There is no back-link from the object being freed to the active task still holding it.
How the pointer is orphaned
That “nothing clears it” is reachable, and a malicious peer controls the trigger. During a multi-KE rekey the passive task is not finished after the first CREATE_CHILD_SA; it still has to process further key-exchange payloads in IKE_FOLLOWUP_KE exchanges. A peer can make one of those fail: it emits a wrong, but still supported, additional-KE method in its IKE_FOLLOWUP_KE. The packet is encrypted under the peer’s own negotiated keys, so the victim accepts it; only the inner method is rejected after decryption, setting NO_PROPOSAL_CHOSEN. The passive task completes as a terminal failure, and the task manager tears it down:
// src/libcharon/sa/ikev2/task_manager_v2.c, build_response()
array_remove_at(this->passive_tasks, enumerator); // passive task removed
if (!handle_collisions(this, task)) // re-runs collide(): still not installed -> FALSE
task->destroy(task); // passive task freed; active task's collision now dangles
Because collide() still returns FALSE, the active task never adopts the object, so the task manager frees it. The active task’s collision field is now a pointer into freed memory, and nothing has told it.
The dereference comes later, when the delayed CREATE_CHILD_SA response from the other peer finally arrives at the active task. That runs process_i(), which calls handle_collision(), which calls lost_collision(), and there the freed pointer is read and immediately used as the base of an indirect virtual call:
// child_rekey.c, lost_collision()
other->child_create->get_lower_nonce(other->child_create);
other is the freed task. Reading its child_create member reads freed memory, and the value read is then called through as a function-pointer base. The attacker’s two controllable moves, a wrong key-exchange method in its own follow-up and reordering the delivery of its own packets, are both within the normal capability of a network peer.
What we observed
We reproduced this on strongSwan 6.0.6 (commit 4d709df8), using the project’s own exchange_tests harness: two real charon IKE stacks, real authenticated encryption, real task-queue processing. Our only additions were a one-shot, environment-gated fault injector modeling the single field a malicious peer controls in its follow-up, and a new test case that drives the simultaneous rekey and the delayed response. Neither touches the victim’s child_rekey.c or task_manager_v2.c; those run unmodified. With the injector compiled in but disabled, the full suite passes clean, including its existing dozen collision cases. The fault appears only under the adversarial wrong-method-plus-delay input.
Tier 1, the clean use-after-free. With no heap manipulation at all, AddressSanitizer aborts on the read:
==ERROR: AddressSanitizer: heap-use-after-free
READ of size 8 ... thread T0
#0 lost_collision sa/ikev2/tasks/child_rekey.c:563
#1 handle_collision sa/ikev2/tasks/child_rekey.c:581
#2 process_i sa/ikev2/tasks/child_rekey.c:956
#3 process_response sa/ikev2/task_manager_v2.c:862
located 120 bytes inside of 168-byte region, freed by thread T0 here:
#0 free
#1 destroy sa/ikev2/tasks/child_rekey.c:1213
#2 build_response sa/ikev2/task_manager_v2.c:1016
The read lands at offset 120 of the freed 168-byte task object, its child_create member, and that value is used as the base of the indirect call.
Tier 2, the dereferenced value is attacker-influenced. With ASAN’s quarantine disabled so the freed slot can be reclaimed, refilling it with a 0x42 pattern makes the call read through 0x4242424242424242, a value that came from the reused allocation rather than the original object.
Tier 3, control of the program counter. Reclaiming the freed slot with a crafted dispatch table whose entries point at a sentinel address makes the indirect call transfer control there. Under a debugger the program counter lands on the sentinel: the value read from the freed object is called as a function pointer.
Where we stop, and why this is High and not Critical. Tiers 2 and 3 reclaim the freed 168-byte slot through in-process allocations with the sanitizer’s quarantine turned off. That faithfully models a freed slot being reused by attacker-influenced heap data, and it proves the dereference is a real control-flow sink rather than a benign crash. It does not prove that the same reclaim is achievable from the wire alone. A turnkey remote exploit would additionally need a heap groom driven entirely by protocol messages, landing attacker-chosen bytes in that specific slot in the window between the free and the dereference. We characterized that requirement but did not demonstrate it, so we rate the finding High. The honest ceiling we can stand behind is a remotely triggerable use-after-free with a demonstrated path to control-flow hijack under a controlled heap.
Impact
An authenticated peer, on a connection that negotiated multiple key exchanges, can reliably free a live task object and then have strongSwan dereference it through an indirect call. On an uninstrumented build, the resulting use-after-free can cause a crash or other undefined behavior, a remotely triggerable denial of service against every tunnel the daemon terminates. Above that sits the control-flow sink we demonstrated under a controlled heap, and above that a remote code execution the vendor allows “might be possible” and that we did not carry to a wire-only proof. The precondition that bounds all of this is the credential: the attacker is already an accepted IKE peer. That narrows the set of attackers to those who can complete IKE authentication, which is exactly why we score it PR:L and AC:H rather than treating it as unauthenticated.
Remediation
If you maintain the code. The root cause is a non-owning pointer retained across a lifetime the holder does not control. When collide() declines to adopt the passive task, it should not keep a raw pointer the task manager can free without notice. Any of three fixes closes it: re-derive the colliding passive task at each use site by enumerating the task manager’s passive queue instead of caching a pointer; add a task-manager hook that clears an active rekey task’s collision when a passive rekey task is destroyed; or reference-count the task object so the free cannot happen underneath a live holder. Note that the post-6.0.6 commits 158b4c4a and 84a5208d hardened neighboring aliases in the same collision machinery but leave this exact dangling-pointer case, so a stack built from those commits is still affected.
If you operate it. Upgrade to strongSwan 6.1.0, which fixes this, or apply the patch published alongside the advisory to an older release. If you cannot upgrade immediately, the reachable precondition is a configuration that accepts multiple key exchanges: deployments that do not negotiate an additional-KE group never enter the vulnerable path. Restricting which peers can authenticate at all also shrinks the attacker set, since the attack requires a valid credential, but the upgrade is the fix.
Disclosure
- Software: strongSwan, all releases since 6.0.0 (
src/libcharon/sa/ikev2/tasks/child_rekey.c, the IKEv2 rekey-collision handling). Reproduced on 6.0.6 at commit4d709df8. - Class: CWE-416, use-after-free.
- Severity: High, CVSS 7.5 (our assessment; the vendor published no score).
- CVE: CVE-2026-78133.
- Advisory: strongSwan security advisory, September 7, 2026.
- Fix: strongSwan 6.1.0, with a standalone patch published for older releases. Reported to the strongSwan security team.
All testing was against strongSwan builds we ran ourselves, using the project’s own test harness; the triggering behavior is that of a single malicious authenticated peer.
The reference that outlived its task
The pointer here was never wrong when it was written. On the common collision path the active task adopts the passive one and owns its lifetime, and on that path the cached pointer is exactly right. The bug lives on the sibling path where ownership is declined but the pointer is kept anyway, and on that path a third party, the task manager, is free to destroy the object with no way to tell the holder. A borrowed pointer is only as safe as the guarantee that every path which frees the target also clears the borrow. When you find one object caching a raw pointer to something another object owns and frees, the method is to enumerate every site that can free the target and check that each one reaches back and clears the copy. The free path that doesn’t is the bug.
This work is part of our ongoing vulnerability research into the network stacks and daemons that carry authenticated traffic.