From f2b51e7c90ca8a0f0b10213ad581d950662b739b Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 5 Jun 2026 08:15:46 +0200 Subject: [PATCH] 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. Note that this patch includes another fix for a state change during Child SA rekeying that's included in 6.1.0 (4611f41b1e14 ("child-rekey: Only reset state of SAs not actively rekeyed if passive rekeying fails")). 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 --- src/libcharon/sa/ikev2/task_manager_v2.c | 13 +- src/libcharon/sa/ikev2/tasks/child_rekey.c | 47 ++++-- src/libcharon/sa/ikev2/tasks/child_rekey.h | 3 +- src/libcharon/sa/ikev2/tasks/ike_rekey.c | 40 +++-- src/libcharon/sa/ikev2/tasks/ike_rekey.h | 3 +- src/libcharon/tests/suites/test_child_rekey.c | 136 ++++++++++++++++ src/libcharon/tests/suites/test_ike_rekey.c | 147 ++++++++++++++++++ 7 files changed, 361 insertions(+), 28 deletions(-) diff --git a/src/libcharon/sa/ikev2/task_manager_v2.c b/src/libcharon/sa/ikev2/task_manager_v2.c index 0f3b937fdd53..5a19ce85b331 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 fb3ba2aafaf7..e984668c766e 100644 --- a/src/libcharon/sa/ikev2/tasks/child_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/child_rekey.c @@ -403,7 +403,7 @@ METHOD(task_t, build_r, status_t, child_sa_t *child_sa, *old_replacement; child_sa_state_t state = CHILD_INSTALLED; uint32_t reqid; - bool followup_sent = FALSE; + bool active, followup_sent = FALSE; if (!this->child_sa) { @@ -423,7 +423,8 @@ METHOD(task_t, build_r, status_t, message->add_notify(message, TRUE, TEMPORARY_FAILURE, chunk_empty); return SUCCESS; } - if (actively_rekeying(this, &followup_sent) && followup_sent) + active = actively_rekeying(this, &followup_sent); + if (active && followup_sent) { DBG1(DBG_IKE, "peer initiated rekeying, but we did too and already " "sent IKE_FOLLOWUP_KE"); @@ -483,8 +484,9 @@ METHOD(task_t, build_r, status_t, /* like installing the outbound SA, we only trigger the child-rekey * event once the old SA is deleted */ } - else if (this->child_sa->get_state(this->child_sa) == CHILD_REKEYING) - { /* rekeying failed, reuse old child */ + else if (!active && + this->child_sa->get_state(this->child_sa) == CHILD_REKEYING) + { /* rekeying failed, reuse old child, unless we are actively rekeying */ this->child_sa->set_state(this->child_sa, state); } return SUCCESS; @@ -1127,8 +1129,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; @@ -1142,16 +1158,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 a8daed743687..fef0bba8d071 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 c7e8ffbc8f02..f275d2da8f3f 100644 --- a/src/libcharon/sa/ikev2/tasks/ike_rekey.c +++ b/src/libcharon/sa/ikev2/tasks/ike_rekey.c @@ -743,8 +743,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)); @@ -760,23 +775,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 5fab3491c1e2..e68fa622d8af 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 1c81e75e2bcd..4ef081a2495e 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 c6691acf44aa..2c4102201d43 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); -- 2.43.0