Vulnerability Research
strongSwan CVE-2026-47895 — Double-Free in Empty Identity Cloning
CVE-2026-47895 lets an unauthenticated IKE peer use the two-byte EAP identity @# to create a zero-length but freeable allocation; strongSwan's old identity clone gave the same address to two owners, so failed-authentication teardown double-frees it, reliably threatening gateway availability while the project assesses potential remote code execution.

In this article
1 An identity with only two bytes
The shortest public path into this defect starts with an ordinary EAP Response whose identity body is just @#. There is no oversized field, nested certificate, or contradictory packet length. strongSwan reads the text as a raw hexadecimal ID_KEY_ID; the hexadecimal portion after the hash sign happens to be empty. At the value layer, the identity contains no bytes. At the allocation layer, parsing can still create an object that owns memory.
The two characters then cross three abstraction boundaries. An EAP method extracts a username, the general identity constructor turns those bytes into an identification_t, and the authenticator clones that identity into an auth_cfg_t. Each stage looks uneventful when read alone. The conflict appears later, when two legitimate owners follow separate teardown paths and discover that their objects still name one allocation.
1.1 @# reaches the EAP authenticator before credentials are proved
EAP-Identity exists to provide an identity hint for the method that follows. RFC 3748 permits an Identity Response to carry a username, a network access identifier, or another implementation-defined string. Receipt of that response does not prove a password, SIM secret, certificate, or any other authenticating factor. On an IKEv2 responder reachable from an untrusted network, the identity bytes must therefore be treated as unauthenticated remote input.
The server-side process() function in eap_identity.c obtains the EAP method packet, verifies its basic shape, and skips the five-byte Code, Identifier, Length, and Type header. It clones the remaining bytes into method state whenever their length is nonzero. An empty EAP body does not set an identity. The two-byte body @# does, so it survives this first gate unchanged.
At this point the method owns a conventional two-byte buffer. The method's get_msk() output slot is repurposed to return identity data to the authenticator, a convention that is safe because the caller knows the negotiated method is Identity. No zero-size allocation has yet been made, and this first byte-buffer clone is not the ownership bug described by the CVE.
After the Identity method reports success, server_process_eap() in eap_authenticator.c retrieves that data and calls identification_create_from_data(). The term “success” here means the Identity exchange completed. The responder has not accepted the user. A real authentication method still has to be selected and completed, and policy still has to match the claimed identity.
The data constructor lives in src/libstrongswan/utils/identification.c. It creates a temporary NUL-terminated character array and delegates to the string constructor. The fixed 6.0.7 source uses a buffer sized from data.len + 1 and a precision-limited format operation. That copy is bounded; its security consequence is semantic. Bytes that arrived through EAP now receive the same identity syntax used by administrator-written configuration strings.
The string parser recognizes explicit type prefixes and several short forms beginning with @. If the second character is a hash sign, it selects ID_KEY_ID, advances past @#, and sends the suffix to chunk_from_hex(). For the minimal input, that suffix has length zero. Typed forms ending in :# can reach the same helper with the same empty hexadecimal body.
This syntax has a legitimate purpose. Binary key identifiers are not always convenient or safe to type directly, so hexadecimal notation gives operators an exact representation. The remote path is created by constructor reuse: an EAP username is parsed as if it were a configuration identity. EAP itself does not assign special meaning to the two characters.
The attacker does not need a valid account. Once the identity exchange completes, the authenticator must preserve the identity and apply it to the active authentication configuration. That transition invokes clone() before a password, inner method, or other credential has established trust. Failed authentication later supplies the teardown event that exposes the alias.
Placed back into a wire-format EAP packet, the minimal method data has Code Response, an Identifier copied from the server request, Length seven, Type Identity, and the final octets 40 23. The exploit input is not literally a two-byte UDP datagram. It travels inside a syntactically valid IKEv2 EAP payload; the attacker-controlled identity body itself is two bytes.
That distinction matters during testing. A responder must issue an Identity Request, and the peer must return a valid method packet. Sending an arbitrary IKE notify containing @# will not exercise this path. Conversely, restrictions in a graphical VPN client are irrelevant because a custom EAP peer can construct the legal packet directly.
The data constructor treats its input as text. An embedded NUL would terminate parsing before later bytes acquire identity syntax, while @# contains no NUL and reaches the parser intact. A packet capture or reproducer should therefore preserve exact octets; a formatted username log may truncate or escape the value.
1.2 Zero length can mean no bytes and still mean one owner
strongSwan uses chunk_t throughout the codebase. The structure contains a pointer and a length. The common empty value, chunk_empty, is { NULL, 0 }. It contains no data and carries no release obligation. chunk_free() may pass its pointer to free(), and the C allocation interface defines free(NULL) as a no-op.
chunk_from_hex() takes a different route for empty input. It scans the source, computes the number of output bytes, and still executes buf = malloc(len). With no hexadecimal digits, len is zero. The initialization and conversion loops process no bytes, but the helper returns chunk_create(buf, len) with whatever pointer the allocator supplied.
The C allocation contract permits malloc(0) to return NULL or a unique pointer that can later be freed. glibc deliberately returns the latter. On a typical Linux strongSwan gateway, the new identity therefore contains { unique_ptr, 0 }: no readable payload, one real allocation, and exactly one release obligation.
Both empty representations compare the same at the business layer. Their lengths are zero, there are no bytes to hash, and formatting produces no hexadecimal body. Their ownership ledgers are different. NULL may appear in any number of objects without coordination. A unique zero-size pointer belongs to one owner and can be released once.
A useful state table has four rows. NULL with length zero is an unowned empty chunk. A non-null pointer with length zero is an owned empty chunk. A non-null pointer with a positive length is an ordinary owned buffer. NULL with a positive length violates the usual chunk contract. The old clone distinguished the first three states using only len, collapsing the first two into one branch.
The helper's empty path does not dereference the input or output. Its scan, memset(), and conversion loops all operate over zero bytes. AddressSanitizer has no reason to complain during parsing. The memory error appears only after the unusual representation is copied into more than one owner and both lifetimes end.
Allocator behavior explains why a vulnerable build may appear healthy on one platform. An allocator that returns NULL for zero-size requests never creates the freeable address required for this exact alias. That difference is useful diagnostic evidence, but replacing the allocator of a VPN gateway is a broad and poorly supportable mitigation compared with applying the source repair.
The minimal object-level reproducer is consequently just the ASCII bytes @#, or hexadecimal 40 23. It follows a supported syntax branch. It does not depend on an odd hexadecimal nibble or an invalid character. A functional unit test that checks only type and length will consider the object correct and miss the resource distinction.
Nearby values clarify the boundary. An EAP body of length zero never creates the identity object. A one-byte @ typically becomes an ordinary empty representation. @# creates the allocator-dependent zero/non-null state. A nonempty raw hexadecimal identity follows the normal deep-copy branch. A durable regression corpus should retain all four neighbors.
The parser also supports a typed raw-hex form, making @# the shortest witness among multiple possible constructor states. Fixing one textual prefix would leave the general clone contract exposed to another producer. The upstream repair correctly targets ownership at the object boundary.
Local configuration can create the same representation. An administrator who loads and clones an empty raw identifier on a vulnerable build may see a local crash. The security elevation comes from the EAP constructor reuse that gives an unauthenticated peer control of the same state. Incident review should distinguish network input from configuration load before assigning an actor.
The decisive question is therefore not whether an empty identity has useful authentication semantics. It is who must release its representation. As chunk_from_hex() returns, the original identification_t is the sole owner. The vulnerable clone creates a second ownership claim without a second allocation or a shared reference count. The later double-free is already encoded in that object graph.
2 Three historical changes assembled the defect
The production fix changes one condition, but the cause spans seventeen years of source history. In the 2026 parent revision, else if (encoded.len) resembles a small allocation optimization. In the 2007 implementation where the guard first appeared, it was compatible with the way a clone was constructed. A new input representation in 2008 and a new clone skeleton in 2009 gradually removed the assumptions that had made the guard safe.
No individual change reads like an intentional double-free. One fixed a zero-byte leak, one added a useful identity syntax, and one reduced repetitive field assignment. A review confined to each diff could reasonably pass all three. The system failed because the representation and ownership invariant was never carried across their boundaries.
2.1 The 2007 leak repair and 2008 raw syntax each changed half of the precondition
Commit 418dbd624363d9712c0d883084962cf93ae01b2a, dated April 2007, is titled “cloning %any ID without zero-byte memleak.” The clone implementation at the time began by calling identification_create(). That constructor returned a fresh object whose encoded member already held chunk_empty. The function then copied the identity type, encoding, and selected method pointers explicitly.
The previous code always assigned clone->encoded = chunk_clone(this->encoded). Cloning some empty identities could ask the allocator for a zero-size block, creating a pointless resource that later had to be freed. The commit wrapped that assignment in if (this->encoded.len). An empty source left the destination's constructor-supplied { NULL, 0 } untouched.
Skipping the assignment was safe in that object model. The clone had not inherited the source pointer. The guard's complete meaning was: when there are no bytes, preserve the destination's already-safe default. That context was not written into the condition, so the line survived after the default-initialization step disappeared.
Commit 86ab5636c2c9085c70688910fc3cd1f90a38187d, dated May 2008, implemented the @#hex form of ID_KEY_ID. The older branch contained a TODO and returned NULL. The new branch advanced past the prefix, called chunk_from_hex(), stored the result, and returned the identity object.
The feature solved a real configuration problem: administrators could now express precise binary key identifiers. It also expanded the set of empty internal states. Most familiar empty identities were normalized to NULL. An empty raw-hex suffix could now retain a non-null pointer returned by malloc(0).
The 2008 clone was still constructor-based. If an original held { unique_ptr, 0 }, the length guard skipped the assignment and the newly constructed destination kept NULL. Original and clone remained independently destructible. The first condition needed by the future vulnerability existed, but the second did not.
The parser returned the object even for an empty suffix because an empty byte string is a representable ID_KEY_ID. That choice need not be reclassified as a parser failure. A general object API must safely clone every valid representation its constructors return, including values with no useful deployment meaning.
The historical diff also shows why reading a single git blame line is insufficient. Blame points the length test to a leak fix that was correct against its parent tree. The eventual security failure requires the constructor, the later syntax, the later structure copy, and the destructor to be read together.
2.2 The 2009 structure copy made the old guard preserve the source pointer
Commit 2147da40a5d79ca7921c028d38fe9503feb42bfb, from July 2009, is titled “simplified identification_t.clone() using memcpy.” It replaced construction plus multiple explicit assignments with malloc_thing(private_identification_t) followed by memcpy(clone, this, sizeof(private_identification_t)). The embedded public interface, identity type, and scalar state were copied in one operation.
The operation copied encoded.ptr as well. The existing if (this->encoded.len) remained underneath. A nonempty identity entered the branch and replaced the copied pointer with a new allocation. A conventional empty identity skipped the branch and retained NULL. An empty but non-null identity also skipped the branch and carried the original address out of the function.
The same source condition had acquired the opposite effect. In 2007, skipping an assignment preserved a fresh default. In 2009, skipping it preserved a shallow copy. A code simplification removed several lines while turning the remaining deep-copy branch into the sole ownership boundary.
This composition explains the project's affected range beginning with 4.3.3. Operators should use the vendor's stated range because release branches and backports do not map one-to-one onto mainline commit dates. The history explains causality; the advisory defines supported version scope.
The defect was difficult to encounter accidentally. Normal identities have positive-length encodings and receive a deep copy. Common empty values usually contain NULL. An operator has little reason to configure an empty binary identifier. EAP changed the economics by letting a remote peer choose the edge representation with two bytes.
Ordinary functional tests offered a weak oracle. Original and clone had different object addresses, the same type, the same length, the same hash, and the same formatted value. Destroying only one succeeded. The allocator detected an error only after a test retained both objects and ended both lifetimes.
In May 2026, a fuller identity fuzz lifecycle exposed the path. R. Elliott Childre reported it, and commit 075323d895f574424cfc4a5f491a1d388cdfda37 was merged on June 5. strongSwan 6.0.7 and the public advisory followed on June 8, accompanied by patches for historical release families.
The repair changes else if (this->encoded.len) to an unconditional else. Regex identities still take their specialized branch. Every other identity now invokes chunk_clone(), whose zero-length behavior normalizes the clone to { NULL, 0 }. The original keeps and eventually frees its allocator-supplied pointer; the clone owns no alias.
The patch preserves raw identity syntax and EAP behavior while repairing the generic clone contract. That location closes configuration, EAP, XAuth, group, and future caller paths at once. All official backports modify the object library, which strongly identifies the root-cause boundary.
This history suggests a practical maintenance rule. Whenever a private structure gains a pointer, regex cache, or container, reviewers should enumerate which members a whole-structure copy leaves behind. Whenever a constructor adds an empty representation, clone-and-destroy tests should receive it. Historical guards must be continuously revalidated against the constructor model they assume.
memcpy(), that premise no longer held.3 From packet bytes to two owners
Proving a double-free requires more than showing that a value was copied. The analysis has to identify the address, the owners that believe they may release it, and the paths by which both lifetimes end. In pinned 6.0.7 source, that chain crosses the EAP-Identity plugin, the IKEv2 EAP authenticator, the common identity library, and the authentication configuration.
The direct EAP-Identity route is the clearest primary path because both remote control and clone timing are explicit. Other methods and trust boundaries are treated separately in the next chapter. On the primary path, every step from packet view to destructor can be assigned to a concrete function and field.
3.1 server_process_eap() turns method output into a general identity
The server-side identity method in src/libcharon/plugins/eap_identity/eap_identity.c receives an EAP payload, skips the five-byte method header, and clones the remaining data into this->identity when nonempty. Its destructor owns that byte buffer. The method's get_msk() returns a view to the authenticator without transferring the allocation.
For @#, this first allocation contains two ordinary bytes. It is useful to separate it from the later zero-size allocation. A heap dump may contain the characters in method state even though the address that is double-freed contains no payload bytes and cannot be found by searching for 40 23.
server_process_eap() in src/libcharon/sa/ikev2/authenticators/eap_authenticator.c recognizes successful completion of the Identity method, retrieves its data, and calls identification_create_from_data(). The returned identification_t is a new object; it does not borrow the method's two-byte buffer.
The data constructor creates a NUL-terminated temporary string and reaches the @# branch in identification_create_from_string(). chunk_from_hex() in src/libstrongswan/utils/chunk.c calculates zero output bytes and executes malloc(0). On glibc, the resulting identity now owns a distinct non-null pointer with length zero.
private_identification_t embeds its public interface and stores the encoded chunk, identity type, and regex-related state. Consumers obtain an encoding view and respect its length. The identity destructor holds responsibility for releasing the encoded pointer. Ownership is therefore attached to the object even when there are no readable bytes.
apply_eap_identity() first stores the object in this->eap_identity. That assignment establishes the authenticator as the original owner. The function retrieves the remote auth_cfg_t from the IKE SA, evaluates any configured EAP identity rule, and decides whether the claimed identity is acceptable as context for the method that follows.
The function then adds an AUTH_RULE_EAP_IDENTITY entry whose value is eap_identity->clone(eap_identity). Keeping a clone is necessary. The authenticator may end its method lifecycle while authorization and IKE SA reporting still need the identity. A borrowed pointer would create a use-after-free; the contract calls for an independently destructible object.
The vulnerable clone_() allocates a second private_identification_t and copies the whole source structure. The old length condition is false for the empty raw identity, so the encoded pointer copied by memcpy() is never replaced. The function returns two distinct object addresses whose encoded descriptors both contain the same non-null address and zero length.
All value-level operations continue to work. The two identities report the same type, compare equal, hash the same, and display the same empty body. The bug is visible only in an ownership table: the authenticator row and the auth-config row name different objects but the same allocation.
3.2 auth_cfg_t and the EAP authenticator converge on one free() address
auth_cfg_t stores typed entries. During purge, destroy_entry_value() selects a release policy by rule type. Certificate rules release references, string rules free strings, scalar values require no object cleanup, and identity rules call the identity virtual destructor. AUTH_RULE_EAP_IDENTITY is explicitly in the identity group.
That switch is an ownership specification. The value handed to add() must be safe to destroy independently. The container does not make another hidden copy when it receives the clone. A later auth-config clone or merge may create more identity clones, but the shortest path requires only the first one.
Full destruction calls purge(), walks the entry array, destroys each owned value, removes entries, and releases the configuration. The cloned identity therefore has a stable owner and a normal teardown path. It is not a temporary pointer that disappears only under unusual error handling.
The EAP authenticator has a separate destroy() function. It releases method and payload state and invokes DESTROY_IF(this->eap_identity). That call ends the original identity's lifetime. Authentication failure and IKE SA teardown determine which owner is destroyed first, but either order eventually reaches both destructors.
The identity destructor calls chunk_free(&this->encoded). chunk_free() submits the descriptor's pointer to free() and then resets that descriptor to chunk_empty. This local reset is a sound defense against certain repeated cleanup calls on the same object.
The other owner contains another chunk_t structure. The two descriptors share an address, not storage for the descriptor itself. Resetting the first descriptor cannot update the second. When the second destructor runs, it still reads the old address and submits it to free() again.
An empty identity is unlikely to satisfy account policy, so failed authentication naturally drives the IKE SA toward cleanup. The vendor describes the double-free as occurring when the SA is destroyed after likely authentication failure. No valid user credential is required to create the identity or execute the first clone.
A mismatch in apply_eap_identity() can terminate the current exchange. Failure in a later EAP method can do the same. The exact upper call stack varies, while the lower ownership facts remain invariant: the authenticator retains the original, the auth configuration retains the clone, and both reach the same identity destructor.
The allocator's bad-free report may therefore name either high-level owner as the current free. A useful AddressSanitizer report ties together the original allocation in chunk_from_hex(), the first free through one owner, and the current free through the other. A single final libc frame is insufficient to explain where aliasing began.
If the vulnerable identity is cloned again, every zero-length clone can inherit the same pointer. The object graph may contain three or more owners even though the process aborts on the first duplicate free. One observed double-free does not prove that exactly two aliases existed.
The regex branch provides a useful control case. It duplicates the NUL-terminated encoding with strdup() and compiles independent regex state. The vulnerability is not an unconditional property of every structure copy; it is the ordinary branch's failure to overwrite one owned member for a particular representation.
At the object level, a minimal test needs no VPN: create @#, clone it, record both encoding addresses, and destroy both objects. A full IKE/EAP integration test proves the separate reachability claim. Keeping those layers distinct makes failures easier to attribute to the object library, the protocol state machine, or deployment configuration.
4 Authentication plugins determine practical reachability
Direct EAP-Identity is the shortest public route, but production deployments often hide identity exchanges inside PEAP, TTLS, SIM, AKA, XAuth, or RADIUS delegation. Searching for one eap_id option cannot settle exposure. The relevant questions are where untrusted bytes enter identification_create_from_data() and whether the resulting object is cloned before credentials fail.
The strongSwan project advises operators to begin by assuming all EAP methods are affected, then narrow the scope with implementation evidence. This rule sets patch priority while allowing each method to retain its own packet trace. A useful inventory records the concrete plugin, configuration, identity source, and clone timing for each connection.
4.1 Tunneled EAP and xauth-eap clone usernames at different stages
When a responder explicitly requests EAP-Identity, the path from the prior chapter applies directly. The method exchange completes and apply_eap_identity() clones the parsed value before a real authentication method has completed. The server chooses whether to ask; a custom peer controls the legal response body.
Many methods exchange an identity internally even if the outer EAP conversation does not request Identity through a global setting. That is why absence of one option does not prove absence of the constructor. The vendor specifically warns that implementation, configuration, protocol details, and the point of failure affect whether a clone occurs.
EAP-PEAP establishes an outer TLS tunnel and runs an inner EAP conversation. In the server implementation, an inner Identity Response is parsed into this->peer before phase two begins. The selected phase-two backend may clone that peer during construction. The outer TLS session protects confidentiality in transit; it does not prove the inner username belongs to an authenticated account.
EAP-TTLS follows a related pattern. An identity carried through tunneled AVPs or inner EAP can be turned into an identification_t and supplied to an internal method. Exact clone timing depends on the inner authentication choice. Network intrusion detection outside the tunnel generally cannot see @#, so server telemetry and object-level testing become more important.
EAP-SIM and EAP-AKA handle permanent identities, pseudonyms, and reauthentication identities. Their server code parses identity data through the common constructor, and selected pseudonym paths clone a peer object. Other failure paths return before a clone. A method-specific test should record EAP subtype, identity class, provider result, and the clone breakpoint; the method name alone cannot prove reachability.
This variation supports the vendor's conservative operational advice. Waiting for a tailored proof of concept for every method delays a shared library repair without changing the patch. Method-level analysis is still useful for emergency isolation, incident attribution, and deciding which gateways receive the first maintenance window.
IKEv1 XAuth plugins parse usernames through the same data constructor. In the ordinary task path, the identity is generally added to authentication rules only after the XAuth backend returns SUCCESS. An empty raw identity is unlikely to pass account validation, so the unauthenticated clone path is constrained.
xauth-eap is the notable exception identified by the project. It parses the XAuth username and immediately creates an internal EAP backend with that identity as the peer. The common RADIUS-backed implementation clones peer and server identities during construction, before a credential has been accepted. An IKEv1 deployment must identify the actual XAuth plugin stack before excluding remote reachability.
A vendor fork may change backend construction. A proprietary EAP adapter could borrow the identity, clone it later, or apply a separate normalization. Product documentation that says only “XAuth” or “RADIUS authentication” is not enough. Source, symbols, or a breakpoint in the shipped implementation should decide the branch.
Multi-round authentication also needs separate reasoning for each round. A certificate in an earlier round may establish some trust, while a later EAP identity remains attacker-chosen within the privileges of that certificate. Connections that permit an anonymous or broad first round can still expose the vulnerable second-round constructor. An inventory should record every prerequisite because one authentication label loses this sequence.
Runtime configuration may be loaded through VICI and generated by an orchestrator. Static files can omit an identity request that the active connection contains, or show a plugin that is not loaded in the current process. swanctl --list-conns --raw, loaded-module evidence, and effective daemon logs should be reconciled with the source of configuration.
4.2 RADIUS can remove the client path and reintroduce the state at the AAA boundary
If strongSwan does not request EAP-Identity locally and delegates the full conversation to RADIUS, client identity bytes can be forwarded as RADIUS attributes without becoming a local identification_t. The upstream advisory lists that arrangement as a mitigation for the direct peer route.
The eap-radius plugin can also interpret response attributes as authorization groups. With Class-as-group enabled, it reads a qualifying Class attribute, constructs an identity from the attribute bytes, and adds an AUTH_RULE_GROUP. An enabled Filter-Id path can perform similar parsing when tunnel conditions are met.
Those options move attacker control. An ordinary IKE peer may no longer feed the local constructor directly; a rogue or compromised RADIUS server can return @# as group data. Auth-config clone, merge, and teardown then use the same vulnerable identity clone. The trust boundary now resides in AAA infrastructure.
The Class path includes length constraints, and the Filter-Id path checks tunnel type and nonempty data. A two-byte raw-hex form fits easily within such bounds. Schema validation on the AAA side may reduce reachability, but it is an operational assumption that can change with another server, policy update, or replayed response. The gateway should still be patched.
A statement that “RADIUS delegation is safe” therefore needs two qualifiers: the gateway does not issue its own Identity request, and it does not enable attribute-to-group parsing, or the AAA server is accepted as the relevant trusted source. Missing the second condition converts a client-side finding into an unexamined infrastructure-side risk.
PPK identities also use identification_create_from_data(). The IKE_AUTH task proceeds to clone a PPK identity only after finding a post-quantum preshared key whose owner matches. Matching an empty ID_KEY_ID is unlikely in a normal key store, giving this path much lower remote reachability than direct EAP.
Attribute-certificate group identities are created after certificate verification. An actor would first need a trusted attribute certificate or influence over the issuing chain. The code still shares the vulnerable object library, but its threat model is not equivalent to an unauthenticated EAP peer.
Allocator semantics supply the final prerequisite across all of these paths. glibc returns a unique freeable pointer for zero-size allocation. An allocator that returns NULL removes this exact alias. Static-linked appliances and custom libc builds can be tested, but that evidence should affect temporary prioritization, not replace the version repair.
A practical order follows from these facts. Internet- or untrusted-network-facing gateways on affected builds with direct EAP identity handling come first. xauth-eap follows closely. RADIUS gateways with group attribute parsing require simultaneous AAA review. Ordinary XAuth, PPK, and verified attribute-certificate paths have stronger prerequisites. Every layer still needs the fixed library.
Tenant control can change actor classification. In a managed VPN service, a tenant administrator may choose an EAP method or RADIUS endpoint without controlling the underlying gateway. That role is neither a random Internet peer nor a central infrastructure administrator. Exposure records should identify who can alter each connection and attribute mapping.
Emergency configuration changes need a rollback and an impact statement. Disabling all EAP may remove remote access or MFA. Moving authentication to RADIUS changes logging, group mapping, and failure domains. A mitigation plan should name the exact path it blocks, affected users, evidence that the setting is effective, and the date a code fix will replace it.
5 The second free turns an identity edge case into a gateway failure
The source establishes a concrete security result: unauthenticated input can create two owners of one non-null address on affected configurations, and IKE SA cleanup can submit that address to free() twice. Common allocators treat the event as heap inconsistency and terminate the process. The strongSwan and Ubuntu notices also retain potential arbitrary code execution as the upper impact.
A rigorous assessment separates three layers: the memory error proved by source, the crash commonly observed at runtime, and the exploitation ceiling stated by vendors. That framing gives a VPN-boundary double-free appropriate urgency without presenting a stable cross-allocator code-execution chain that public material has not demonstrated.
5.1 A glibc zero-byte request still participates in normal heap management
glibc returns a unique freeable pointer for malloc(0). The requested payload length is zero, but the allocator still manages the returned address within alignment, metadata, cache, and arena rules. The first free() returns that allocation to allocator state. Every later operation on the address must follow an ordinary heap object's lifetime.
Modern glibc performs consistency checks for many duplicate-free patterns. If the second call arrives while the chunk remains represented in a relevant tcache or free list, the allocator commonly emits a double-free or corruption diagnostic and aborts. systemd observes a signal-terminated daemon. Whether a core is retained depends on unit policy, limits, container settings, and the host's core handler.
Other allocations may occur between the two identity destructors. EAP tasks, notification payloads, logs, and SA teardown all use memory. Build options, worker scheduling, and the plugin stack influence that activity. If the old address is reused for a new object before the second free, the stale owner may release memory that now belongs to someone else and create a more complex use-after-free condition.
The issue is a memory-safety vulnerability with consequences beyond a failed assertion. Source proves attacker control of the representation and proves the duplicate release. It does not by itself prove control over an intervening allocation, an allocator-check bypass, a chosen write, and control-flow redirection. Those properties require a pinned build, libc, ASLR setting, and repeatable heap experiments.
The upstream advisory says remote code execution might be possible. Ubuntu's notice says a remote attacker could crash strongSwan or possibly execute arbitrary code. Both sources make the crash concrete and the execution outcome conditional. Neither the public fix commit nor its regression tests include a weaponized exploit.
A crash alone can have substantial operational impact. One charon process may manage many IKE SAs and Child SAs. Exit interrupts new negotiation, reauthentication, DPD, rekey, and policy maintenance. Existing kernel XFRM state may carry traffic briefly, creating a deceptive period in which a ping works even though the control plane is gone.
As SAs expire or rekey, traffic fails. A health check that tests only a UDP socket or one data-plane packet will understate the incident. Monitoring should include daemon PID, SA freshness, rekey success, new-user authentication, and route or policy installation.
Service supervision can amplify a single trigger into a crash loop. A peer repeats the same identity when the new process begins listening, systemd restarts it again, and start-rate limiting may eventually keep the service down. An appliance watchdog may restart a broader network stack, extending the blast radius beyond IPsec.
High availability shortens recovery but changes the load pattern. Peers reconnect to a surviving node, authentication traffic spikes, RADIUS latency rises, and synchronized control-plane work can exhaust a standby that was sized for normal sharing. Maintenance and incident plans should measure the reconnect surge and its real failover cost.
Generic severity labels differ. Ubuntu marks its CVE page Medium while upstream emphasizes potential RCE. A deployment-specific priority should include reachability, EAP/XAuth configuration, gateway tenancy, SA count, redundancy, and recovery objectives. An Internet-facing EAP responder on an affected build often deserves a short remediation window regardless of the generic label.
A researcher extending the exploitation analysis would need to measure the allocator size class chosen for zero requests, which allocations occur between frees, whether the release order is stable, and where consistency checks terminate execution. Worker count, CPU affinity, concurrent IKE load, and tcache behavior must be recorded. The public record does not currently supply that experimental chain.
Hardened allocators may terminate earlier, while an implementation returning NULL removes the alias at its source. Those differences change observable platform impact while the clone contract remains broken. Cross-platform results should always name allocator and version; one non-crashing test cannot establish object safety.
5.2 Incident evidence must reconnect the allocation, both owners, and the EAP session
An AddressSanitizer report is the clearest diagnostic artifact. It can show the current invalid free, the earlier free, and the original allocation. The allocation should resolve into chunk_from_hex(); both release stacks should pass through the identity destructor, with auth-config purge and EAP-authenticator cleanup appearing above in either order.
Production packages are rarely sanitizer-built. glibc may write “double free detected,” “free(): double free,” or a more general heap-corruption message before aborting. The visible top frame can be libc. Debug symbols and the package build ID are needed to recover inlined chunk_free() and its caller accurately.
The two chunk descriptors explain why a local safety measure does not help. chunk_free() resets the descriptor it receives after releasing the pointer. That protects against a repeated cleanup of the same descriptor. The clone created a second descriptor, so its pointer does not observe the reset. Incident notes should distinguish address aliasing from descriptor aliasing.
Network evidence should be correlated with process evidence. Preserve the IKE source, SPIs, exchange, EAP Code, Identifier, Type, selected connection, and method around the crash. A full identity may be personal or credential-adjacent data. Detection of this CVE requires only length, an authorized hash, or the exact minimal pattern; ordinary logs need not retain usernames indefinitely.
@# is the smallest public source-level witness, not the only representation. Typed empty hex syntax and AAA attributes can create related states. A high-confidence rule combines an empty raw-hex identity representation, an affected plugin path, failed authentication, and an allocator abort. Alerting on two characters alone will produce misleading results.
The same syntax may appear in local configuration. A crash during config load or a management operation does not establish remote exploitation. Investigators should tag the source as IKE peer, tunneled EAP, XAuth, RADIUS response, VICI/config load, or certificate-derived group before assigning the event's trust boundary.
Testing a vulnerable production gateway with the trigger is unnecessary and risky. An isolated reproduction should pin source commit, allocator, plugins, and connection configuration; restrict traffic to an authorized test network; and prepare service containment. The verification objective is independent ownership and clean teardown, not allocator grooming.
If historical cores retain only libc frames, package symbols can recover the call chain. Journal timestamps can be joined with EAP failures, RADIUS transactions, and systemd restart events. Even without the raw identity, the sequence “authentication failure followed immediately by heap abort” on an affected build is useful triage evidence, though not definitive attribution.
Core files can contain keys, identities, certificates, and topology. They belong in a restricted evidence store and should be analyzed from controlled copies. Public reporting needs sanitized address relationships and stacks, not a raw heap dump that creates a second credential-exposure problem.
When core collection is disabled, an incident team can temporarily enable bounded coredump retention or reproduce on a staging clone of the exact build. Diagnostic changes must be reviewed for storage, performance, and privacy, then reverted. Restart and authentication logs remain valuable but do not replace remediation.
Confirm the running image after recovery. A package database may show a fixed revision while the old daemon still maps a deleted executable. A container scheduler may retain one old replica, or an HA peer may have missed the update. Process start time, mapped executable, build ID, image digest, and runtime version form the closure evidence.
External communication should state the confirmed impact, affected authentication entries, observed behavior, and fix status. Vendor-assessed potential RCE should remain visible, alongside whether the investigation found control-flow hijacking, data access, or persistence. That separation supports decisive response without turning a crash into an unproved compromise claim.
6 One branch change restores the ownership invariant
Commit 075323d895f574424cfc4a5f491a1d388cdfda37 changes production code, a directed unit test, and the identity fuzz target. The three pieces answer different questions: how a clone must treat an empty encoding, what invariant a regression should assert, and how arbitrary input reaches a realistic object lifecycle.
The repair can be stated precisely. Every non-regex identity clone must overwrite the encoded pointer inherited from the initial structure copy. The number of payload bytes determines an allocation size; it cannot decide whether ownership correction occurs.
6.1 Unconditional chunk_clone() normalizes a zero-length clone to NULL
The fixed clone_() still begins with malloc_thing() and memcpy(). Upstream did not rewrite the object constructor or abandon efficient scalar copying. Instead, the branches have an explicit obligation: regex state is rebuilt in one branch, and every other encoded chunk is rebuilt in the other.
chunk_clone captures its argument, allocates only when x.len is positive, and copies that many bytes. For a zero-length source it chooses NULL directly and returns an empty chunk. It does not call the platform's malloc(0), so the destination representation is stable across allocator implementations.
The original object is unchanged. If its parser-created encoding is { unique_ptr, 0 }, it remains the sole owner and releases that pointer once. The clone's descriptor is overwritten with { NULL, 0 }. Its destructor calls the harmless free(NULL).
Nonempty identities retain their prior behavior: an allocation of the same length receives a byte-for-byte copy. Regex identities continue to duplicate a complete NUL-terminated string and compile independent regex state. The patch alters ownership for the edge representation without changing identity matching, hashing, or wire behavior.
A condition on encoded.ptr could patch the immediate case, yet NULL states would still depend on the shallow copy happening to contain NULL. The unconditional branch is stronger. Every ordinary identity passes through one normalization point, including future empty representations.
Rejecting empty raw-hex input would remove the public witness but change a supported syntax and leave the generic clone contract weak. Another constructor could produce the same state. Repairing the object boundary closes all current callers and preserves compatibility.
Upstream publishes patches for 4.3.3–5.1.1, 5.1.2–6.0.1, and 6.0.2–6.0.6. Their context differs because method macros, regex support, and tests evolved. The central semantic change is the same. A manual backport should use the matching patch family and re-read the function if downstream changes shift hunks.
strongSwan 6.0.7 is the first fixed upstream release. Distribution packages do not need to display that bare version to be safe. Ubuntu and Debian backport the one-line production change onto maintained 5.x and earlier 6.0.x bases. Validation must compare a complete package revision or inspect the repaired branch.
The placement of the fix is itself useful root-cause evidence. If EAP alone misused an object, a repair in the authenticator might suffice. Upstream instead repaired the common clone and issued patches across all consumers. Configuration, EAP, XAuth, group, and future paths inherit the correction.
6.2 The regression asserts pointer independence, while fuzzing completes the lifecycle
test_clone_empty() begins with identification_create_from_string("@#"). It confirms that object creation succeeds, reads the original encoding, clones the identity, and reads the cloned encoding. Both lengths must be zero. The test then continues beyond value equality to inspect ownership.
The key assertion requires different pointers unless both pointers are NULL. If the allocator gives the original a unique zero-size pointer, the clone cannot retain it. If the allocator returns NULL at construction, two NULL values are safe. The test expresses the interface invariant without assuming one legal allocator policy.
The test destroys the clone first and the original second. A vulnerable glibc build fails on the second destructor. Reversing the order should also be added in downstream suites; success in both orders demonstrates independent storage and rules out an accidental cleanup sequence.
An allocator that returns NULL will let the regression pass on both vulnerable and fixed source because the hazardous address never exists. A comprehensive CI matrix should include at least one glibc job or a controlled zero-size allocation wrapper that returns a freeable block. Native allocator jobs remain necessary to match product behavior.
The old fuzz_ids.c constructed an identity from arbitrary bytes and immediately destroyed it. That covered parsing and one destructor but never called the vulnerable method. High parser coverage could therefore coexist with zero coverage of the clone contract.
The new harness invokes hash(), equals(), matches(), wildcard handling, and the part enumerator. It clones the identity and destroys the clone, formats the original through normal and tiny buffers, then destroys the original. The two-byte input now reaches the duplicate ownership lifecycle automatically on a vulnerable allocator.
Formatting and enumeration keep diverse identity types alive through multiple public operations, exercise internal views, and may expose adjacent lifetime errors. A security fuzzer for an interface-driven C object should model its protocol of operations and continue beyond the constructor.
The seed corpus should retain @#, typed empty hex, normal nonempty hex, odd nibbles, separators, and data containing NUL. Generic minimization can reduce a crash to an empty string that no longer creates an object, so the semantic two-byte seed belongs in a directed regression even after fuzz discovery.
Downstream testing should cover normal identities as well: FQDN, RFC822, IPv4, IPv6, nonempty ID_KEY_ID, regex, and wildcards. Clones must preserve type, encoding, matching, hash, and formatting while owning appropriate independent resources. That negative coverage demonstrates the security change did not alter authentication decisions.
Older branches use different test infrastructure. Taking only the production hunk and omitting an adapted pointer assertion loses the strongest protection against regression. Each maintained fork should express the same invariant in its available unit framework and run it under an allocator that makes the vulnerable state observable.
Debug builds should retain symbols and frame pointers for sanitizer analysis; release-like builds should verify the real supervisor, plugin set, and restart behavior. Compiler optimization changes stack visibility while leaving the duplicate ownership fact intact. The two build classes answer distinct questions.
Results must be tied to source and deployment artifacts. “The test passed” does not prove the production image contains that object code. CI source revision, patch manifest, package changelog, build ID, SBOM, and deployed digest should connect the unit invariant to the running gateway.
7 Remediation ends at the running gateway process
strongSwan appears as a distribution package, container layer, SD-WAN component, cloud VPN image, network appliance library, or custom static build. A fixed package in a repository closes only half of the supply chain. Old processes, stale container replicas, and a missed HA member can continue accepting IKE traffic.
Temporary measures can reduce exposure: disable EAP or XAuth, move traffic to fixed nodes, remove a local Identity request in a verified RADIUS delegation design, and disable unused Class or Filter-Id group parsing. Each measure changes authentication capability or trust, and each needs a planned return to a code fix.
7.1 Upstream releases and distribution backports require different version checks
A deployment built directly from upstream should use 6.0.7 or later. A maintained 4.x, 5.x, or earlier 6.0.x fork can apply the official patch for its range. Review the resulting clone_(): the non-regex branch must always assign clone->encoded = chunk_clone(this->encoded).
Ubuntu uses backports. As of July 16, 2026, Canonical lists 6.0.4-1ubuntu3.1 for 26.04, 6.0.1-6ubuntu4.4 for 25.10, 5.9.13-2ubuntu4.24.04.4 for 24.04, and 5.9.5-2ubuntu2.7 for 22.04 as fixed. A 5.9 version is not automatically vulnerable; the full revision matters.
The Debian tracker on the same date lists bookworm security 5.9.8-5+deb12u5, trixie security 6.0.1-6+deb13u6, and forky/sid 6.0.7-1 as fixed. Its bullseye entries remain marked vulnerable. ELTS and appliance channels may carry separate support decisions and must be checked directly.
RPM distributions, cloud images, and devices need vendor-specific evidence. If an advisory says only “security improvements,” request a source RPM, patch manifest, SBOM, or explicit CVE statement. A binary string showing 6.0.x identifies a baseline, not whether a downstream one-line backport exists.
Runtime confirmation should include PID, executable mapping, complete package revision, image digest, and process start time. After a package update, /proc/<pid>/exe may still point to a deleted inode. In a cluster, one replica can remain on an old digest after a failed drain.
Update HA peers one at a time. Patch and restart a standby, complete health and authentication tests, move new sessions, observe state synchronization and reauthentication, then patch the prior active node. If the product cannot migrate SAs without interruption, include Child SA rebuild time in the maintenance plan.
Run the normal authentication matrix after the version closes: certificates, PSK, every deployed EAP method, XAuth, RADIUS fallback, group mapping, reauthentication, MOBIKE, and any vendor extension. The ownership fix should preserve identity values, so a normal matching regression is a stop signal requiring investigation.
Run authorized failure tests as well. On an isolated endpoint, an empty identity and @# should be rejected without killing the process. Repeat the sequence under allocator diagnostics and watch RSS, SA counts, worker queues, and core storage for a leak or hang that a one-shot test would miss.
Version status changes over time. The package floors above are a dated snapshot. Compliance automation should ingest current vendor tracker data and preserve retrieval time. A hard-coded permanent rule will become stale when security updates, ESM, or ELTS revisions move.
Blue-green health checks should perform a real IKE authentication, not just confirm a UDP socket or PID. The probe needs a restricted, auditable account and must avoid credentials on command lines or in logs. This catches missing plugins, failed RADIUS connectivity, and identity matching regressions before traffic migration.
7.2 An executable runbook must deliver version, configuration, session, and evidence results
The following steps are written for direct use in a change record. Large fleets can batch nodes, but “package installed” cannot substitute for “the running process maps fixed code.”
- Build a node-level asset table. Merge CMDB records, cloud instances, Kubernetes workloads, device management, and processes listening on UDP 500 or 4500. Each row must include node identifier, HA role, environment, running PID, complete package revision, image digest, libc or allocator, reachable networks, and accountable service owner; unresolved fields keep the row open.
- Map authentication entry points. Read effective
swanctl.conf, VICI-generated state, legacyipsec.conf, and plugin settings. For every connection, record local and remote auth, EAP method, explicit Identity request, XAuth backend,xauth-eapuse, RADIUS delegation, Class or Filter-Id group parsing, and AAA endpoint. Installed plugins alone do not establish use. - Set a defensible fixed floor. Require upstream 6.0.7 or later, the current official fixed revision for a supported distribution, or an appliance advisory or source diff explicitly covering CVE-2026-47895. A vendor build with no backport evidence remains pending even if its user interface reports a recent product version.
- Patch an inactive member first. Install the fixed package or image on a standby or low-traffic node, restart
charonorcharon-systemd, and verify process start time, executable mapping, build ID, package database, and container digest. A deleted inode, old PID, or stale digest blocks traffic migration. - Run identity regression. For every deployed EAP, XAuth, and RADIUS combination, complete one successful authentication, one bad credential, one empty identity, and one
@#or equivalent typed-empty-hex cleanup. Capture client result, server log, IKE and Child SA counts, process survival, and allocator output; a crash or sustained memory increase blocks promotion. - Validate high availability. Move new connections to the fixed node and timestamp UDP takeover, IKE_SA_INIT, full authentication, Child SA installation, and restored application traffic. Exercise reauthentication, MOBIKE, and DPD for existing sessions, and confirm that a reconnect surge does not saturate the identity backend.
- Roll the remaining fleet. Drain, patch, restart, and reverify one member at a time. After each batch, establish a tunnel from an external vantage point and confirm load balancing, NAT-T, routing, and anycast or DNS send traffic to a fixed instance. Continue until the asset table contains no old running build.
- Search historical failures. Across at least the available log-retention window, find abnormal strongSwan exits, libc double-free or corruption diagnostics, cores, systemd restarts, and EAP or XAuth failures. Correlate timestamp, source, IKE SPI, connection, and RADIUS transaction; retain the first full core and matching symbols.
- Remove temporary exceptions. If the response disabled EAP, removed Identity requests, disabled group mapping, or restricted UDP sources, restore only required functions after every node is fixed and tested. Revalidate the trust boundary, and permanently remove unused high-risk plugins from builds or configuration.
- Archive an audit package. Preserve the advisory, version evidence, before-and-after node state, configuration snapshots, authorized test record, regression output, HA timeline, and owners for residual exceptions. Distinguish “entry not used,” “temporarily isolated,” and “running fixed code”; only the last state closes remediation.
When an immediate upgrade is impossible, the strongest mitigation is to prevent the affected daemon from accepting untrusted EAP or XAuth. RADIUS delegation is a useful alternative only after confirming that the gateway does not request Identity locally and does not parse Class or Filter-Id into groups, or that the AAA source is the explicitly accepted trust boundary.
Source ACLs, cookies, and connection rate limits reduce opportunity and resource pressure. They cannot repair ownership. Remote-access clients often share NAT exits, an attacker may control a device on an allowed network, and the same input can return after every automatic restart.
Collect a pre-change baseline for IKE_SA_INIT rate, EAP success and failure, RADIUS latency, active IKE and Child SAs, worker backlog, RSS, restart count, and cores. Compare the same metrics during and after rollout. They reveal both patch regression and continued hostile failure traffic.
The observation window should span a normal reauthentication cycle. A successful new connection proves initial auth only. Long-lived tunnels may hide rekey or group-refresh problems. Controlled reauthentication batches shorten discovery time without overwhelming AAA.
Define rollback before the change. Falling EAP success, lost groups, failed HA synchronization, or Child SA recovery can trigger it. Rollback must not simply return the vulnerable public node to service; use another fixed peer or a temporary authentication restriction while correcting the regression.
Close with a reverse sample. From each NAT-T address, load-balancer path, HA member, and disaster-recovery entry, build an external tunnel and verify the receiving node's PID and build ID. This final check catches old anycast paths, DNS records, and dormant peers that fleet dashboards commonly miss.
8 Following the same code smell through the repository
memcpy(clone, this, sizeof(...)) is a valuable adjacent-vulnerability fingerprint, but it is not a finding by itself. After reconstructing the CVE, we enumerated clone methods, whole-structure copies, conditional chunk clones, chunk_from_hex() consumers, and downstream identity holders in the pinned 6.0.7 tree.
The goal was an independent second chain: untrusted input creates a special representation, cloning leaves a resource alias, separate owners can execute separate destructors, and the result has a security impact. A similar spelling or a theoretical pointer without that chain remains an audit candidate, not a new vulnerability.
8.1 The other whole-object copies can account for their resources
The core candidates that directly copy an entire private object appear in identification.c, seg_contract.c, pts_comp_func_name.c, and host.c. Each requires its structure declaration, clone, and destructor to be read together. A grep result cannot tell whether a copied field owns heap storage.
pts_comp_func_name stores its public interface plus vendor ID, component name, qualifier, and related scalar fields. It has no independently owned heap pointer. Its destructor releases only the object. A structure copy duplicates values and function pointers without creating a shared lower resource.
host.c places IPv4 and IPv6 sockaddr forms in an inline union. Address bytes, port, and family live inside each object. A clone allocates a new object and copies the union; destruction frees only that new object. Address chunks returned by accessors point into their respective object storage, not one external allocation.
seg_contract.c is a stronger-looking candidate because its private structure contains linked_list_t *seg_envs. The line immediately after its structure copy assigns clone->seg_envs = linked_list_create(), overwriting the shallow pointer. Other relevant fields are scalar. The clone intentionally begins with an empty envelope list, and each destructor owns a different list.
backtrace.c uses memcpy() as well, but copies frame values into the flexible array of a newly sized object. The source is this->frames and the destination is clone->frames; it does not copy a pointer that owns an external frame array. The program-counter values are plain data, and destruction frees the containing object once.
The network packet clone offers a useful positive pattern. It checks this->data.ptr before cloning adjusted data, so a non-null zero-length buffer still passes through the normalization helper. The security-label clone explicitly deep-copies both its binary encoding and NUL-terminated string. These conditions track ownership more closely than business length.
auth_cfg_t::clone_() does not copy its private structure. It enumerates rules, clones identity and group values, increments certificate references, duplicates strings, and copies scalar values. The vulnerable identity method made this container an amplification route, but the container's own policy does not create a separate shallow alias.
We also followed callers of chunk_from_hex(). Many live in command-line tools, configuration loaders, credential utilities, TPM or PTS code, and tests. Receiving { ptr, 0 } is not itself an error. A second independently destructible ownership claim or another unsafe consumer must be shown.
The scan moved in layers: explicit clone methods and structure copies, direct member assignments and conditional chunk_clone() calls, then destructors such as free(), chunk_free(), container teardown, and reference release. Candidates advanced only if an untrusted boundary controlled the value, a special representation was stable, clone or transfer created multiple owners, and those owners reached a dangerous operation. The intersection is far more useful than a spelling match.
Deduplication matters just as much. Another EAP method reaching identification_t::clone_() adds reachability to CVE-2026-47895; an adjacent vulnerability needs its own object, allocation, repair point, and impact chain. This review did not establish such a second chain: the other whole-object copies have verifiable ownership explanations, and the examined empty-chunk consumers did not combine remote control, aliasing, and separate destruction.
strongSwan 6.0.7 release notes mention separate DHCP and EAP-AKA out-of-bounds-read risks, a PKCS#7 NULL dereference, and a SWID command-injection condition. They do not share this ownership fingerprint and require their own pinned parents, reachability proofs, and impact analyses rather than being folded into the empty-identity result.
Part of the method can be automated. Members released by a destructor seed an owned set; after a whole-object copy, every return path should overwrite, reference, or explicitly mark each owned member as borrowed. Debug builds can add allocation IDs or assert that owned chunk addresses differ after clone, allowing both to be NULL, while reference-counted objects assert the increment. Macros and unions will create false positives, but enforcing the contract near the API finds a new representation earlier than a full IKE teardown.
8.2 Conclusion: a clone promises both value and lifetime
The decisive chain is compact. An unauthenticated EAP body @# creates { unique_ptr, 0 }. The old clone copied that pointer and skipped deep cloning because length was zero. The EAP authenticator and remote authentication configuration later invoked separate destructors on the same address.
The long history matters because every local change had a purpose. A 2007 guard avoided an empty allocation, a 2008 syntax represented binary identities, and a 2009 structure copy reduced repetitive code. The condition outlived the construction model that justified it. Without object-level history, the one-line 2026 repair looks simpler than the defect actually was.
For maintainers, the direct lesson is a member-level ownership table for every clone. Each pointer, chunk, string, container, compiled expression, and reference must state whether the source owns it, how the clone obtains its own resource, and who cleans failure paths. Any pointer surviving a whole-structure copy needs an explicit sharing protocol or an overwrite.
Empty values must be tested as a set of representations. { NULL, 0 }, { unique_ptr, 0 }, an initialized empty container, an interned pointer, and a borrowed view may compare equal while requiring different destruction. Assertions should observe pointer independence, a reference count, or an ownership flag, not just content and length.
For operators, the closure criterion is 6.0.7 or a documented backport running on every HA member. Direct and tunneled EAP, xauth-eap, and optional RADIUS group parsing set remediation order. Ordinary XAuth, PPK, and verified attribute-certificate paths have higher prerequisites but still use the same affected library.
For responders, an allocator abort is the strongest starting signal. Reconnect allocation, first free, second free, and the IKE/EAP session. The characters @# are a clue, while process version, plugin, clone timing, and core evidence establish attribution.
The upstream repair is clean: retain syntax and authentication behavior, pass every non-regex identity clone through the existing helper, and normalize an empty destination to NULL. The unit test states pointer independence. The fuzzer finally exercises the object after parsing and ends both lifetimes.
The broader lesson from a two-byte identity is that empty content never proves empty ownership. An interface that promises an independently destructible clone must provide an independently provable end of life even when the value carries no bytes.
Product teams can turn that principle into an executable release condition. Any C object on a security boundary that exposes constructor, clone, and destroy should test empty representations, multiple owners, and reversed destruction order, then bind the result to the built artifact. Future vulnerability work can use the same order: prove an abnormal representation and its owners, find the untrusted route, establish observable impact, and only then scan for the same contract violation. A look-alike without an independent chain remains a documented exclusion.
Upstream, historical patch files, Ubuntu, and Debian now provide actionable repair paths. Affected gateways do not need to wait for public exploitation research: update the version, restart the actual process, run authentication regressions, and review historical crashes. The final acceptance record should identify the remote bytes that created the object, the clone statement that changed the owner graph, the two destructor paths that once converged, and the exact running build that now breaks the chain.
That record gives each later role a concrete comparison point. A responder can match a crash against the allocation and owner signature; a maintainer can test a refactor against the historical invariant; an operator can verify the deployed build where the vulnerable path once gained its second owner. The one-line fix then survives changes in source layout, packaging, and authentication architecture as a durable ownership rule rather than a vague “patched” label.
References
- strongSwan technical advisory for CVE-2026-47895
- strongSwan 6.0.7 release announcement
- Upstream fix, unit regression, and fuzz-target change
- Official CVE-2026-47895 historical patch directory
- Official patch for 4.3.3–5.1.1
- Official patch for 5.1.2–6.0.1
- Official patch for 6.0.2–6.0.6
- 2007 commit: cloning %any ID without zero-byte memleak
- 2008 commit: support for @#hex ID_KEY_ID
- 2009 commit: memcpy-based identification_t clone
- strongSwan identity parsing and raw-hex syntax documentation
- strongSwan 6.0.7 identification.c: parser, clone, and destructor
- strongSwan 6.0.7 chunk.c: chunk_from_hex()
- strongSwan 6.0.7 chunk.h: chunk_clone() and chunk_free()
- strongSwan 6.0.7 EAP-Identity method
- strongSwan 6.0.7 EAP authenticator and apply_eap_identity()
- strongSwan 6.0.7 auth_cfg identity ownership
- strongSwan 6.0.7 xauth-eap adapter
- strongSwan 6.0.7 eap-radius peer and group handling
- strongSwan 6.0.7 EAP-PEAP server identity path
- strongSwan 6.0.7 EAP-TTLS server identity path
- strongSwan 6.0.7 test_clone_empty()
- strongSwan 6.0.7 identity fuzz lifecycle
- RFC 3748: Extensible Authentication Protocol and Identity
- RFC 7296: IKEv2 exchanges and authentication
- GNU C Library zero-size malloc behavior
- Linux malloc/free manual and free(NULL) behavior
- CVE.org record for CVE-2026-47895
- Ubuntu CVE tracker and fixed package revisions
- Ubuntu USN-8407-1 impact and update notice
- Debian security tracker status
- strongSwan 6.0.7 GitHub release tag
Research record
9Evidence, objects, and sources
The material below preserves the identifiers and references used in this report.
9.1Research objects
Products, actors, techniques, affected objects, and control points discussed in the report.
Double-free in strongSwan identity object cloning
Upstream release published on June 8, 2026
Routes every non-regex encoding through chunk_clone()
Empty raw-hex ID_KEY_ID representation
Whole-structure copy failed to replace a non-null, zero-length owned pointer
9.2Event chronology
- Length guard added to empty encoding clones
Commit 418dbd624 avoided an unnecessary zero-byte allocation in the constructor-based clone.
- Raw @# identity syntax added
Commit 86ab5636c made an empty hexadecimal suffix reachable through chunk_from_hex().
- clone() changed to whole-structure memcpy
Commit 2147da40a retained the old length guard after copying the encoded pointer first.
- Ownership repair merged
Commit 075323d895f5 fixed the branch and added a unit test plus a fuller fuzz lifecycle.
- Advisory and 6.0.7 released
strongSwan documented remote entry points, mitigations, and patches for maintained historical lines.
- SOSEC extended source review completed
The review traced EAP, XAuth, RADIUS, and repository-wide adjacent clone patterns.
9.3Sources and material
- strongSwan technical advisory for CVE-2026-47895https://www.strongswan.org/blog/2026/06/08/strongswan-vulnerability-%28cve-2026-47895%29.html
- strongSwan 6.0.7 release announcementhttps://www.strongswan.org/blog/2026/06/08/strongswan-6.0.7-released.html
- Upstream fix, unit regression, and fuzz-target changehttps://github.com/strongswan/strongswan/commit/075323d895f574424cfc4a5f491a1d388cdfda37
- Official CVE-2026-47895 historical patch directoryhttps://download.strongswan.org/security/CVE-2026-47895/
- Official patch for 4.3.3–5.1.1https://download.strongswan.org/security/CVE-2026-47895/strongswan-4.3.3-5.1.1_empty_id_clone.patch
- Official patch for 5.1.2–6.0.1https://download.strongswan.org/security/CVE-2026-47895/strongswan-5.1.2-6.0.1_empty_id_clone.patch
- Official patch for 6.0.2–6.0.6https://download.strongswan.org/security/CVE-2026-47895/strongswan-6.0.2-6.0.6_empty_id_clone.patch
- 2007 commit: cloning %any ID without zero-byte memleakhttps://github.com/strongswan/strongswan/commit/418dbd624363d9712c0d883084962cf93ae01b2a
- 2008 commit: support for @#hex ID_KEY_IDhttps://github.com/strongswan/strongswan/commit/86ab5636c2c9085c70688910fc3cd1f90a38187d
- 2009 commit: memcpy-based identification_t clonehttps://github.com/strongswan/strongswan/commit/2147da40a5d79ca7921c028d38fe9503feb42bfb
- strongSwan identity parsing and raw-hex syntax documentationhttps://docs.strongswan.org/docs/latest/config/identityParsing.html
- strongSwan 6.0.7 identification.c: parser, clone, and destructorhttps://github.com/strongswan/strongswan/blob/6.0.7/src/libstrongswan/utils/identification.c
- strongSwan 6.0.7 chunk.c: chunk_from_hex()https://github.com/strongswan/strongswan/blob/6.0.7/src/libstrongswan/utils/chunk.c
- strongSwan 6.0.7 chunk.h: chunk_clone() and chunk_free()https://github.com/strongswan/strongswan/blob/6.0.7/src/libstrongswan/utils/chunk.h
- strongSwan 6.0.7 EAP-Identity methodhttps://github.com/strongswan/strongswan/blob/6.0.7/src/libcharon/plugins/eap_identity/eap_identity.c
- strongSwan 6.0.7 EAP authenticator and apply_eap_identity()https://github.com/strongswan/strongswan/blob/6.0.7/src/libcharon/sa/ikev2/authenticators/eap_authenticator.c
- strongSwan 6.0.7 auth_cfg identity ownershiphttps://github.com/strongswan/strongswan/blob/6.0.7/src/libstrongswan/credentials/auth_cfg.c
- strongSwan 6.0.7 xauth-eap adapterhttps://github.com/strongswan/strongswan/blob/6.0.7/src/libcharon/plugins/xauth_eap/xauth_eap.c
- strongSwan 6.0.7 eap-radius peer and group handlinghttps://github.com/strongswan/strongswan/blob/6.0.7/src/libcharon/plugins/eap_radius/eap_radius.c
- strongSwan 6.0.7 EAP-PEAP server identity pathhttps://github.com/strongswan/strongswan/blob/6.0.7/src/libcharon/plugins/eap_peap/eap_peap_server.c
- strongSwan 6.0.7 EAP-TTLS server identity pathhttps://github.com/strongswan/strongswan/blob/6.0.7/src/libcharon/plugins/eap_ttls/eap_ttls_server.c
- strongSwan 6.0.7 test_clone_empty()https://github.com/strongswan/strongswan/blob/6.0.7/src/libstrongswan/tests/suites/test_identification.c
- strongSwan 6.0.7 identity fuzz lifecyclehttps://github.com/strongswan/strongswan/blob/6.0.7/fuzz/fuzz_ids.c
- RFC 3748: Extensible Authentication Protocol and Identityhttps://datatracker.ietf.org/doc/html/rfc3748
- RFC 7296: IKEv2 exchanges and authenticationhttps://datatracker.ietf.org/doc/html/rfc7296
- GNU C Library zero-size malloc behaviorhttps://sourceware.org/glibc/manual/2.42/html_node/Malloc-Examples.html
- Linux malloc/free manual and free(NULL) behaviorhttps://man7.org/linux/man-pages/man3/free.3.html
- CVE.org record for CVE-2026-47895https://www.cve.org/CVERecord?id=CVE-2026-47895
- Ubuntu CVE tracker and fixed package revisionshttps://ubuntu.com/security/CVE-2026-47895
- Ubuntu USN-8407-1 impact and update noticehttps://ubuntu.com/security/notices/USN-8407-1
- Debian security tracker statushttps://security-tracker.debian.org/tracker/CVE-2026-47895
- strongSwan 6.0.7 GitHub release taghttps://github.com/strongswan/strongswan/releases/tag/6.0.7