Commit eb615b4525 for strongswan.org

commit eb615b452539461d57503f50041f01773c1ccf86
Author: Tobias Brunner <tobias@strongswan.org>
Date:   Fri Jun 5 08:15:46 2026 +0200

    ikev2: Prevent use-after-free during collision after passive multi-KE rekeying failed

    During a multi-KE rekey collision, where the initial response to the
    active rekeying is delayed (or withheld), the active task already keeps
    track of the passive task to eventually resolve the collision (it can
    only do so once all nonces are known).

    If the passive task then fails, e.g. due to a missing or invalid KE
    payload, and completes with SUCCESS, `collide()` previously recognized
    that the passive task is not yet complete returned FALSE, which caused
    the task manager to destroy the task.  However, the reference in the
    active task would remain.  So once the active rekeying progresses and
    the collision is resolved, that dangling pointer would get dereferenced
    for an indirect method call.  This happens via the `get_lower_nonce`
    function pointer of the `child_create_t` instance in the private task
    struct.  So besides having to be authenticated, an attacker has to get
    two indirections right to exploit this flaw for a potential RCE.
    Otherwise, the effects are a crash or basically undefined behavior
    triggered by the method call.

    By passing whether the passive task is done (and would get destroyed),
    the active tasks can properly clear the held reference.

    Fixes: d2b2e1b3fae8 ("ikev2: Make CHILD_SAs properly trackable during rekey collisions")
    Fixes: ca3e6d2d144e ("ike-rekey: Support IKE_SA rekeying with multiple key exchanges")
    Fixes: CVE-2026-78133

diff --git a/src/libcharon/sa/ikev2/task_manager_v2.c b/src/libcharon/sa/ikev2/task_manager_v2.c
index 0f3b937fdd..5a19ce85b3 100644
--- a/src/libcharon/sa/ikev2/task_manager_v2.c
+++ b/src/libcharon/sa/ikev2/task_manager_v2.c
@@ -927,7 +927,8 @@ static status_t process_response(private_task_manager_t *this,
  * Handle exchange collisions, returns TRUE if the given passive task was
  * adopted by the active task and the task manager lost control over it.
  */
-static bool handle_collisions(private_task_manager_t *this, task_t *task)
+static bool handle_collisions(private_task_manager_t *this, task_t *task,
+							  bool done)
 {
 	enumerator_t *enumerator;
 	task_t *active;
@@ -951,7 +952,7 @@ static bool handle_collisions(private_task_manager_t *this, task_t *task)
 					if (type == TASK_IKE_REKEY || type == TASK_IKE_DELETE)
 					{
 						ike_rekey_t *rekey = (ike_rekey_t*)active;
-						adopted = rekey->collide(rekey, task);
+						adopted = rekey->collide(rekey, task, done);
 						break;
 					}
 					continue;
@@ -959,7 +960,7 @@ static bool handle_collisions(private_task_manager_t *this, task_t *task)
 					if (type == TASK_CHILD_REKEY)
 					{
 						child_rekey_t *rekey = (child_rekey_t*)active;
-						adopted = rekey->collide(rekey, task);
+						adopted = rekey->collide(rekey, task, done);
 						break;
 					}
 					continue;
@@ -1011,14 +1012,14 @@ static status_t build_response(private_task_manager_t *this, message_t *request)
 			case SUCCESS:
 				/* task completed, remove it */
 				array_remove_at(this->passive_tasks, enumerator);
-				if (!handle_collisions(this, task))
+				if (!handle_collisions(this, task, TRUE))
 				{
 					task->destroy(task);
 				}
 				break;
 			case NEED_MORE:
 				/* processed, but task needs another exchange */
-				if (handle_collisions(this, task))
+				if (handle_collisions(this, task, FALSE))
 				{
 					array_remove_at(this->passive_tasks, enumerator);
 				}
@@ -1029,7 +1030,7 @@ static status_t build_response(private_task_manager_t *this, message_t *request)
 				/* FALL */
 			case DESTROY_ME:
 				/* destroy IKE_SA, but SEND response first */
-				if (handle_collisions(this, task))
+				if (handle_collisions(this, task, FALSE))
 				{
 					array_remove_at(this->passive_tasks, enumerator);
 				}
diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.c b/src/libcharon/sa/ikev2/tasks/child_rekey.c
index 5292d75a2b..fb743dc024 100644
--- a/src/libcharon/sa/ikev2/tasks/child_rekey.c
+++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c
@@ -1128,8 +1128,22 @@ METHOD(child_rekey_t, handle_delete, child_rekey_collision_t,
 	return CHILD_REKEY_COLLISION_NONE;
 }

+/**
+ * Clear the colliding passive task if it did not complete successfully.
+ */
+static void clear_collision(private_child_rekey_t *this, task_t *other)
+{
+	if (this->collision == other)
+	{
+		DBG1(DBG_IKE, "colliding passive rekeying for CHILD_SA %s{%u} "
+			 "failed", this->child_sa->get_name(this->child_sa),
+			 this->child_sa->get_unique_id(this->child_sa));
+		this->collision = NULL;
+	}
+}
+
 METHOD(child_rekey_t, collide, bool,
-	private_child_rekey_t *this, task_t *other)
+	private_child_rekey_t *this, task_t *other, bool done)
 {
 	private_child_rekey_t *rekey = (private_child_rekey_t*)other;
 	child_sa_t *other_child;
@@ -1143,16 +1157,25 @@ METHOD(child_rekey_t, collide, bool,
 	other_child = rekey->child_create->get_child(rekey->child_create);
 	if (!other_child)
 	{
-		/* ignore passive tasks that did not successfully create a CHILD_SA */
+		/* ignore passive tasks that did not successfully create a CHILD_SA,
+		 * if we are already tracking it in the multi-KE case, clear it */
+		clear_collision(this, other);
 		return FALSE;
 	}
 	if (other_child->get_state(other_child) != CHILD_INSTALLED)
 	{
-		DBG1(DBG_IKE, "colliding passive rekeying for CHILD_SA %s{%u} is not "
-			 "yet complete", this->child_sa->get_name(this->child_sa),
-			 this->child_sa->get_unique_id(this->child_sa));
-		/* we do reference the task to check its state later */
-		this->collision = other;
+		if (done)
+		{	/* passive task failed, clear it if necessary */
+			clear_collision(this, other);
+		}
+		else
+		{
+			DBG1(DBG_IKE, "colliding passive rekeying for CHILD_SA %s{%u} is "
+				 "not yet complete", this->child_sa->get_name(this->child_sa),
+				 this->child_sa->get_unique_id(this->child_sa));
+			/* we do reference the task to check its state later */
+			this->collision = other;
+		}
 		return FALSE;
 	}
 	if (this->collision && this->collision != other)
diff --git a/src/libcharon/sa/ikev2/tasks/child_rekey.h b/src/libcharon/sa/ikev2/tasks/child_rekey.h
index a8daed7436..fef0bba8d0 100644
--- a/src/libcharon/sa/ikev2/tasks/child_rekey.h
+++ b/src/libcharon/sa/ikev2/tasks/child_rekey.h
@@ -79,10 +79,11 @@ struct child_rekey_t {
 	 * are going on and notifies the active task by passing the passive.
 	 *
 	 * @param other		passive task
+	 * @param done		passive task is done and gets destroyed if not adopted
 	 * @return			whether the task was adopted and should be removed from
 	 *					the task manager's control
 	 */
-	bool (*collide)(child_rekey_t* this, task_t *other);
+	bool (*collide)(child_rekey_t* this, task_t *other, bool done);
 };

 /**
diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.c b/src/libcharon/sa/ikev2/tasks/ike_rekey.c
index 6877c92bda..0818a1f757 100644
--- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c
+++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c
@@ -742,8 +742,23 @@ METHOD(ike_rekey_t, did_collide, bool,
 	return this->collision != NULL;
 }

+/**
+ * Clear the colliding passive task if it did not complete successfully.
+ */
+static bool clear_collision(private_ike_rekey_t *this,
+							private_ike_rekey_t *other)
+{
+	if (this->collision == other)
+	{
+		DBG1(DBG_IKE, "colliding passive rekeying failed, ignore");
+		this->collision = NULL;
+		return TRUE;
+	}
+	return FALSE;
+}
+
 METHOD(ike_rekey_t, collide, bool,
-	private_ike_rekey_t* this, task_t *other)
+	private_ike_rekey_t* this, task_t *other, bool done)
 {
 	DBG1(DBG_IKE, "detected %N collision with %N", task_type_names,
 		 TASK_IKE_REKEY, task_type_names, other->get_type(other));
@@ -759,23 +774,30 @@ METHOD(ike_rekey_t, collide, bool,

 			if (!rekey->ike_init)
 			{
-				DBG1(DBG_IKE, "colliding exchange did not result in an IKE_SA, "
-					 "ignore");
-				if (this->collision == rekey)
+				if (!clear_collision(this, rekey))
 				{
-					this->collision = NULL;
+					DBG1(DBG_IKE, "colliding exchange did not result in an "
+						 "IKE_SA, ignore");
 				}
 				break;
 			}
-			/* we keep track of the passive exchange in any case, if not
-			 * complete yet, this method might be called again later */
-			this->collision = rekey;
+			/* we keep track of the passive exchange, if not complete yet, this
+			 * method might be called again later */
 			if (rekey->flags & IKE_REKEY_DONE)
 			{
+				this->collision = rekey;
 				this->flags |= IKE_REKEY_ADOPTED_PASSIVE;
 				return TRUE;
 			}
-			DBG1(DBG_IKE, "colliding passive exchange is not yet complete");
+			else if (done)
+			{	/* passive task failed, clear it if necessary */
+				clear_collision(this, rekey);
+			}
+			else
+			{
+				DBG1(DBG_IKE, "colliding passive exchange is not yet complete");
+				this->collision = rekey;
+			}
 			break;
 		}
 		default:
diff --git a/src/libcharon/sa/ikev2/tasks/ike_rekey.h b/src/libcharon/sa/ikev2/tasks/ike_rekey.h
index 5fab3491c1..e68fa622d8 100644
--- a/src/libcharon/sa/ikev2/tasks/ike_rekey.h
+++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.h
@@ -54,10 +54,11 @@ struct ike_rekey_t {
 	 * are going on and notifies the active task by passing the passive.
 	 *
 	 * @param other		passive task
+	 * @param done		passive task is done and gets destroyed if not adopted
 	 * @return			whether the task was adopted and should be removed from
 	 *					the task manager's control
 	 */
-	bool (*collide)(ike_rekey_t* this, task_t *other);
+	bool (*collide)(ike_rekey_t* this, task_t *other, bool done);
 };

 /**
diff --git a/src/libcharon/tests/suites/test_child_rekey.c b/src/libcharon/tests/suites/test_child_rekey.c
index 1c81e75e2b..4ef081a249 100644
--- a/src/libcharon/tests/suites/test_child_rekey.c
+++ b/src/libcharon/tests/suites/test_child_rekey.c
@@ -2546,6 +2546,141 @@ START_TEST(test_collision_delayed_response_multi_ke)
 }
 END_TEST

+/**
+ * Remove the KE payload from the IKE_FOLLOWUP_KE request
+ */
+static bool remove_ke(listener_t *listener, ike_sa_t *ike_sa,
+					  message_t *message, bool incoming, bool plain)
+{
+	if (plain && incoming &&
+		message->get_exchange_type(message) == IKE_FOLLOWUP_KE &&
+		message->get_request(message))
+	{
+		enumerator_t *enumerator = message->create_payload_enumerator(message);
+		payload_t *pld;
+
+		while (enumerator->enumerate(enumerator, &pld))
+		{
+			if (pld->get_type(pld) == PLV2_KEY_EXCHANGE)
+			{
+				message->remove_payload_at(message, enumerator);
+				pld->destroy(pld);
+				break;
+			}
+		}
+		enumerator->destroy(enumerator);
+		free(listener);
+		return FALSE;
+	}
+	return TRUE;
+}
+
+#define remove_ke_from_ike_followup_ke() ({ \
+	listener_t *_ke_listener; \
+	INIT(_ke_listener, \
+		.message = remove_ke, \
+	); \
+	exchange_test_helper->add_listener(exchange_test_helper, _ke_listener); \
+})
+
+/**
+ * This simulates an incorrect behavior by the peer.  It triggers a collision by
+ * not responding to the initial CREATE_CHILD_SA and then sends an invalid
+ * IKE_FOLLOWUP_KE (in this case the KE payload is missing).  The initiator
+ * has to correctly track and then untrack the passive rekey task.
+ *
+ *           Peer A                   Peer B
+ *            rekey ----\       /---- rekey
+ *                       \-----/----> detect collision and withhold response
+ * detect collision <---------/
+ *                  ---------------->
+ *   handle failure <---------------- send invalid additional KE
+ *     handle rekey <---------------- send withheld response
+ */
+START_TEST(test_collision_delayed_response_multi_ke_failure)
+{
+	ike_sa_t *a, *b;
+	message_t *msg;
+
+	assert_track_sas_start();
+
+	exchange_test_helper->establish_sa(exchange_test_helper,
+									   &a, &b, &multi_ke_conf);
+
+	/* make sure the responder wins the collision so it continues */
+	exchange_test_helper->nonce_first_byte = 0x00;
+	initiate_rekey(a, 1);
+	assert_ipsec_sas_installed(a, 1, 2);
+	exchange_test_helper->nonce_first_byte = 0xff;
+	initiate_rekey(b, 2);
+	assert_ipsec_sas_installed(b, 1, 2);
+
+	/* these should not get called as no SA goes down or gets rekeyed */
+	assert_hook_not_called(child_updown);
+	assert_hook_not_called(child_rekey);
+
+	/* CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } --> */
+	exchange_test_helper->nonce_first_byte = 0xff;
+	exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+	assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED);
+	assert_ipsec_sas_installed(b, 1, 2);
+
+	/* <-- CREATE_CHILD_SA { N(REKEY_SA), SA, Ni, [KEi,] TSi, TSr } */
+	exchange_test_helper->nonce_first_byte = 0xff;
+	exchange_test_helper->process_message(exchange_test_helper, a, NULL);
+	assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED);
+	assert_ipsec_sas_installed(a, 1, 2);
+
+	/* the responder is not responding */
+	msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender);
+
+	/* CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } --> */
+	exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+	assert_num_tasks(b, 0, TASK_QUEUE_PASSIVE);
+	assert_num_tasks(b, 1, TASK_QUEUE_ACTIVE);
+	assert_child_sa_state(b, 2, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED);
+	assert_ipsec_sas_installed(b, 1, 2);
+
+	/* remove the KE payload in the IKE_FOLLOWUP_KE request */
+	remove_ke_from_ike_followup_ke();
+
+	/* <-- IKE_FOLLOWUP_KE { N(ADD_KE) } */
+	assert_no_payload(IN, PLV2_KEY_EXCHANGE);
+	assert_single_notify(OUT, INVALID_SYNTAX);
+	exchange_test_helper->process_message(exchange_test_helper, a, NULL);
+	assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED);
+	assert_ipsec_sas_installed(a, 1, 2);
+	assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE);
+
+	/* <-- CREATE_CHILD_SA { SA, Nr, [KEr,] TSi, TSr } (delayed) */
+	exchange_test_helper->process_message(exchange_test_helper, a, msg);
+	assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE);
+	assert_num_tasks(a, 1, TASK_QUEUE_ACTIVE);
+	assert_child_sa_state(a, 1, CHILD_REKEYING, CHILD_OUTBOUND_INSTALLED);
+	assert_ipsec_sas_installed(a, 1, 2);
+
+	/* drop the STATE_NOT_FOUND error message from the initiator */
+	msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender);
+	msg->destroy(msg);
+
+	/* since we explicitly forced the responder to win, it already removed
+	 * the passive task it won't accept the request */
+
+	/* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */
+	assert_payload(IN, PLV2_KEY_EXCHANGE);
+	assert_notify(IN, ADDITIONAL_KEY_EXCHANGE);
+	exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+
+	/* child_rekey/child_updown */
+	assert_hook();
+	assert_hook();
+	assert_track_sas(2, 2);
+
+	call_ikesa(a, destroy);
+	call_ikesa(b, destroy);
+}
+END_TEST
+
 /**
  * In this scenario one of the peers does not notice that there is a
  * rekey collision:
@@ -4436,6 +4571,7 @@ Suite *child_rekey_suite_create()
 	tcase_add_loop_test(tc, test_collision_delayed_response, 0, 4);
 	tcase_add_loop_test(tc, test_collision_delayed_response_delete, 0, 4);
 	tcase_add_loop_test(tc, test_collision_delayed_response_multi_ke, 0, 4);
+	tcase_add_test(tc, test_collision_delayed_response_multi_ke_failure);
 	tcase_add_loop_test(tc, test_collision_delayed_request, 0, 6);
 	tcase_add_loop_test(tc, test_collision_delayed_request_more, 0, 3);
 	tcase_add_loop_test(tc, test_collision_delayed_request_more_delete, 0, 3);
diff --git a/src/libcharon/tests/suites/test_ike_rekey.c b/src/libcharon/tests/suites/test_ike_rekey.c
index c6691acf44..2c4102201d 100644
--- a/src/libcharon/tests/suites/test_ike_rekey.c
+++ b/src/libcharon/tests/suites/test_ike_rekey.c
@@ -1784,6 +1784,152 @@ START_TEST(test_collision_delayed_response_multi_ke)
 }
 END_TEST

+/**
+ * Remove the ADDITIONAL_KEY_EXCHANGE notify payload from the IKE_FOLLOWUP_KE
+ * request
+ */
+static bool remove_notify(listener_t *listener, ike_sa_t *ike_sa,
+						  message_t *message, bool incoming, bool plain)
+{
+	if (plain && incoming &&
+		message->get_exchange_type(message) == IKE_FOLLOWUP_KE &&
+		message->get_request(message))
+	{
+		enumerator_t *enumerator = message->create_payload_enumerator(message);
+		payload_t *pld;
+
+		while (enumerator->enumerate(enumerator, &pld))
+		{	/* we only expect one notify, so just remove the first */
+			if (pld->get_type(pld) == PLV2_NOTIFY)
+			{
+				message->remove_payload_at(message, enumerator);
+				pld->destroy(pld);
+				break;
+			}
+		}
+		enumerator->destroy(enumerator);
+		free(listener);
+		return FALSE;
+	}
+	return TRUE;
+}
+
+#define remove_notify_from_ike_followup_ke() ({ \
+	listener_t *_ke_listener; \
+	INIT(_ke_listener, \
+		.message = remove_notify, \
+	); \
+	exchange_test_helper->add_listener(exchange_test_helper, _ke_listener); \
+})
+
+/**
+ * This simulates an incorrect behavior by the peer.  It triggers a collision by
+ * not responding to the initial CREATE_CHILD_SA and then sends an invalid
+ * IKE_FOLLOWUP_KE (in this case by removing the ADDITIONAL_KEY_EXCHANGE
+ * notify).  The initiator has to correctly track and then untrack the passive
+ * rekey task.
+ *
+ *           Peer A                   Peer B
+ *            rekey ----\       /---- rekey
+ *                       \-----/----> detect collision and withhold response
+ * detect collision <---------/
+ *                  ---------------->
+ *   handle failure <---------------- send invalid additional KE
+ *     handle rekey <---------------- send withheld response
+ */
+START_TEST(test_collision_delayed_response_multi_ke_failure)
+{
+	ike_sa_t *a, *b;
+	message_t *msg;
+
+	assert_track_sas_start();
+
+	exchange_test_helper->establish_sa(exchange_test_helper,
+									   &a, &b, &multi_ke_conf);
+
+	/* these should not get called as no SA goes down or gets rekeyed */
+	assert_hook_not_called(ike_updown);
+	assert_hook_not_called(ike_rekey);
+	assert_hook_not_called(child_updown);
+
+	/* make sure the responder wins the collision so it continues */
+	exchange_test_helper->nonce_first_byte = 0x00;
+	initiate_rekey(a);
+	exchange_test_helper->nonce_first_byte = 0xff;
+	initiate_rekey(b);
+
+	/* CREATE_CHILD_SA { SA, Ni, KEi } --> */
+	exchange_test_helper->nonce_first_byte = 0xff;
+	exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+	assert_ike_sa_state(b, IKE_REKEYING);
+	assert_child_sa_count(b, 1);
+	assert_ike_sa_count(0);
+
+	/* <-- CREATE_CHILD_SA { SA, Ni, KEi } */
+	exchange_test_helper->nonce_first_byte = 0xff;
+	exchange_test_helper->process_message(exchange_test_helper, a, NULL);
+	assert_ike_sa_state(a, IKE_REKEYING);
+	assert_child_sa_count(a, 1);
+	assert_ike_sa_count(0);
+
+	/* the responder is not responding */
+	msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender);
+
+	/* simplify next steps by checking in original IKE_SAs */
+	charon->ike_sa_manager->checkin(charon->ike_sa_manager, a);
+	charon->ike_sa_manager->checkin(charon->ike_sa_manager, b);
+	assert_ike_sa_count(2);
+
+	/* CREATE_CHILD_SA { SA, Nr, KEr, N(ADD_KE) } --> */
+	assert_notify(IN, ADDITIONAL_KEY_EXCHANGE);
+	exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+	assert_num_tasks(b, 0, TASK_QUEUE_PASSIVE);
+	assert_num_tasks(b, 1, TASK_QUEUE_ACTIVE);
+	assert_ike_sa_state(b, IKE_REKEYING);
+	assert_ike_sa_count(2);
+
+	/* remove the ADD_KE notify from the IKE_FOLLOWUP_KE request */
+	remove_notify_from_ike_followup_ke();
+
+	/* <-- IKE_FOLLOWUP_KE { KEi } */
+	assert_payload(IN, PLV2_KEY_EXCHANGE);
+	assert_no_notify(IN, ADDITIONAL_KEY_EXCHANGE);
+	assert_single_notify(OUT, STATE_NOT_FOUND);
+	exchange_test_helper->process_message(exchange_test_helper, a, NULL);
+	assert_ike_sa_state(a, IKE_REKEYING);
+	assert_child_sa_count(a, 1);
+	assert_ike_sa_count(2);
+
+	/* <-- CREATE_CHILD_SA { SA, Nr, KEr } (delayed) */
+	exchange_test_helper->process_message(exchange_test_helper, a, msg);
+	assert_num_tasks(a, 0, TASK_QUEUE_PASSIVE);
+	assert_num_tasks(a, 1, TASK_QUEUE_ACTIVE);
+	assert_ike_sa_state(a, IKE_REKEYING);
+	assert_child_sa_count(a, 1);
+	assert_ike_sa_count(2);
+
+	/* drop the STATE_NOT_FOUND error message from the initiator */
+	msg = exchange_test_helper->sender->dequeue(exchange_test_helper->sender);
+	msg->destroy(msg);
+
+	/* since we explicitly forced the responder to win, it already removed
+	 * the passive task it won't accept the request */
+
+	/* IKE_FOLLOWUP_KE { KEi, N(ADD_KE) } --> */
+	assert_payload(IN, PLV2_KEY_EXCHANGE);
+	assert_notify(IN, ADDITIONAL_KEY_EXCHANGE);
+	exchange_test_helper->process_message(exchange_test_helper, b, NULL);
+
+	/* ike_updown/rekey/child_updown */
+	assert_hook();
+	assert_hook();
+	assert_hook();
+	assert_track_sas(2, 2);
+
+	charon->ike_sa_manager->flush(charon->ike_sa_manager);
+}
+END_TEST
+
 /**
  * In this scenario one of the peers does not notice that there is a rekey
  * collision because the other request is dropped:
@@ -2590,6 +2736,7 @@ Suite *ike_rekey_suite_create()
 	tcase_add_loop_test(tc, test_collision_ke_invalid_delayed_retry, 0, 3);
 	tcase_add_loop_test(tc, test_collision_delayed_response, 0, 4);
 	tcase_add_loop_test(tc, test_collision_delayed_response_multi_ke, 0, 4);
+	tcase_add_test(tc, test_collision_delayed_response_multi_ke_failure);
 	tcase_add_loop_test(tc, test_collision_dropped_request, 0, 3);
 	tcase_add_loop_test(tc, test_collision_delayed_request, 0, 3);
 	tcase_add_loop_test(tc, test_collision_delayed_request_and_delete, 0, 3);