From 9d38601ecd36262be986847a05dd084996f2ebfe Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:29:52 -0700 Subject: [PATCH 01/43] F-4562 - https://fenrir.wolfssl.com/finding/4562 - Fix byte/bit unit mismatch in bitmask address match scoring --- src/routes.c | 4 +- tests/unittests.c | 114 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/src/routes.c b/src/routes.c index c9a13ac..9ee866d 100644 --- a/src/routes.c +++ b/src/routes.c @@ -304,12 +304,12 @@ static inline int addr_bitmask_match_size( longer_addr = b; mask_len = WOLFSENTRY_BITS_TO_BYTES(b_len >> 1); shorter_addr = a; - padding_offset = mask_len - a_len; + padding_offset = mask_len - WOLFSENTRY_BITS_TO_BYTES(a_len); } else { longer_addr = a; mask_len = WOLFSENTRY_BITS_TO_BYTES(a_len >> 1); shorter_addr = b; - padding_offset = mask_len - b_len; + padding_offset = mask_len - WOLFSENTRY_BITS_TO_BYTES(b_len); } for (ret = 0, i = 0; i < mask_len; ++i) { diff --git a/tests/unittests.c b/tests/unittests.c index 7a75686..dd61f80 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3469,6 +3469,115 @@ static int test_dynamic_rules(void) { WOLFSENTRY_RETURN_OK; } +#ifdef WOLFSENTRY_ADDR_BITMASK_MATCHING +static wolfsentry_errcode_t test_regression_bitmask_match_specificity(void) { + struct wolfsentry_context *wolfsentry; + wolfsentry_action_res_t action_results; + wolfsentry_route_flags_t inexact_matches; + wolfsentry_ent_id_t route_id, broad_id, narrow_id; + struct wolfsentry_route_exports broad_route, narrow_route; + byte broad_addr[8], narrow_addr[8]; + struct { + struct wolfsentry_sockaddr sa; + byte addr_buf[4]; + } remote, local; + static const wolfsentry_route_flags_t common_flags = + WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN + | WOLFSENTRY_ROUTE_FLAG_REMOTE_ADDR_BITMASK + | WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_ADDR_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_PORT_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_SA_REMOTE_PORT_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_LOCAL_INTERFACE_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_REMOTE_INTERFACE_WILDCARD; +#ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS + static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT }; +#else + static const struct wolfsentry_eventconfig config = { PRIVATE_DATA_SIZE, PRIVATE_DATA_ALIGNMENT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +#endif + + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_init_ex( + wolfsentry_build_settings, + WOLFSENTRY_CONTEXT_ARGS_OUT_EX(WOLFSENTRY_TEST_HPI), + &config, + &wolfsentry, + WOLFSENTRY_INIT_FLAG_NONE)); + + memset(&broad_route, 0, sizeof broad_route); + memset(&narrow_route, 0, sizeof narrow_route); + memset(&remote, 0, sizeof remote); + memset(&local, 0, sizeof local); + + /* broad bitmask route 10.0.0.0/255.0.0.0, greenlisted (accept). */ + memcpy(broad_addr, "\x0A\x00\x00\x00\xFF\x00\x00\x00", 8); /* NOLINT(bugprone-not-null-terminated-result) */ + broad_route.sa_family = AF_INET; + broad_route.sa_proto = IPPROTO_TCP; + broad_route.remote_address = broad_addr; + broad_route.remote.addr_len = 64; + broad_route.flags = common_flags | WOLFSENTRY_ROUTE_FLAG_GREENLISTED; + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_insert_by_exports( + WOLFSENTRY_CONTEXT_ARGS_OUT, + NULL /* caller_arg */, + &broad_route, + &broad_id, &action_results)); + + /* narrow bitmask route 10.1.2.0/255.255.255.0, penaltyboxed (reject). */ + memcpy(narrow_addr, "\x0A\x01\x02\x00\xFF\xFF\xFF\x00", 8); /* NOLINT(bugprone-not-null-terminated-result) */ + narrow_route.sa_family = AF_INET; + narrow_route.sa_proto = IPPROTO_TCP; + narrow_route.remote_address = narrow_addr; + narrow_route.remote.addr_len = 64; + narrow_route.flags = common_flags | WOLFSENTRY_ROUTE_FLAG_PENALTYBOXED; + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_insert_by_exports( + WOLFSENTRY_CONTEXT_ARGS_OUT, + NULL /* caller_arg */, + &narrow_route, + &narrow_id, &action_results)); + + (void)broad_id; + + /* 10.1.2.3 matches both; the more specific /24 route must win, so the + * verdict is reject, not accept. + */ + remote.sa.sa_family = AF_INET; + remote.sa.sa_proto = IPPROTO_TCP; + remote.sa.addr_len = 32; + memcpy(remote.sa.addr, "\x0A\x01\x02\x03", 4); /* NOLINT(bugprone-not-null-terminated-result) */ + local.sa.sa_family = AF_INET; + local.sa.sa_proto = IPPROTO_TCP; + local.sa.addr_len = 0; + + WOLFSENTRY_CLEAR_ALL_BITS(action_results); + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_event_dispatch( + WOLFSENTRY_CONTEXT_ARGS_OUT, + &remote.sa, &local.sa, + WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN, + NULL /* event_label */, 0 /* event_label_len */, + NULL /* caller_arg */, + &route_id, &inexact_matches, &action_results)); + + WOLFSENTRY_EXIT_ON_FALSE(route_id == narrow_id); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_ACCEPT)); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + WOLFSENTRY_RETURN_OK; +} +#endif /* WOLFSENTRY_ADDR_BITMASK_MATCHING */ + +static int test_dynamic_rules_regressions(void) { +#ifdef WOLFSENTRY_ADDR_BITMASK_MATCHING + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_bitmask_match_specificity(), OK)); +#endif + WOLFSENTRY_RETURN_OK; +} + #endif /* TEST_DYNAMIC_RULES */ #ifdef TEST_USER_VALUES @@ -5749,6 +5858,11 @@ int main(int argc, char* argv[]) { (void)fprintf(stderr, "test_dynamic_rules failed, " WOLFSENTRY_ERROR_FMT "\n", WOLFSENTRY_ERROR_FMT_ARGS(ret)); err = 1; } + ret = test_dynamic_rules_regressions(); + if (! WOLFSENTRY_ERROR_CODE_IS(ret, OK)) { + (void)fprintf(stderr, "test_dynamic_rules_regressions failed, " WOLFSENTRY_ERROR_FMT "\n", WOLFSENTRY_ERROR_FMT_ARGS(ret)); + err = 1; + } #endif #ifdef TEST_USER_VALUES From 836f601cccb1bbe7dd6b4678273e415ebb141bb2 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:30:26 -0700 Subject: [PATCH 02/43] F-4705 - https://fenrir.wolfssl.com/finding/4705 - Compute bitmask compare padding offset in bytes not bits --- src/routes.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/routes.c b/src/routes.c index 9ee866d..e12d318 100644 --- a/src/routes.c +++ b/src/routes.c @@ -248,17 +248,16 @@ static inline int cmp_addrs_bitmaskful( if (left_addr_len < right_addr_len) { maskful_addr = right_addr; - mask_len = right_addr_len >> 1; + mask_len = WOLFSENTRY_BITS_TO_BYTES(right_addr_len >> 1); target_addr = left_addr; - padding_offset = mask_len - left_addr_len; + padding_offset = mask_len - WOLFSENTRY_BITS_TO_BYTES(left_addr_len); } else { maskful_addr = left_addr; - mask_len = left_addr_len >> 1; + mask_len = WOLFSENTRY_BITS_TO_BYTES(left_addr_len >> 1); target_addr = right_addr; - padding_offset = mask_len - right_addr_len; + padding_offset = mask_len - WOLFSENTRY_BITS_TO_BYTES(right_addr_len); } - mask_len = WOLFSENTRY_BITS_TO_BYTES(mask_len); mask = maskful_addr + mask_len; for (i = 0; i < mask_len; ++i) { From 2dfcdbf9a8a9379add0ac4f66700ccfd4e48e4fe Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:33:19 -0700 Subject: [PATCH 03/43] F-8764 - https://fenrir.wolfssl.com/finding/8764 - Preserve connected routes within the max purgeable idle time --- src/routes.c | 31 +++++++++---- tests/unittests.c | 113 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 9 deletions(-) diff --git a/src/routes.c b/src/routes.c index e12d318..9520d5e 100644 --- a/src/routes.c +++ b/src/routes.c @@ -3074,6 +3074,7 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( wolfsentry_errcode_t ret; int n = 0; wolfsentry_action_res_t fallback_action_results = 0; + struct wolfsentry_list_ent_header *cursor; #ifdef WOLFSENTRY_THREADSAFE int have_mutex = 0; int got_lock = 0; @@ -3120,17 +3121,29 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( table = wolfsentry->routes; while (table->purge_list.tail) { - struct wolfsentry_route *route = WOLFSENTRY_ROUTE_PURGE_HEADER_TO_TABLE_ENT_HEADER(table->purge_list.tail); - if (mode != 3) { - if (route->meta.purge_after > now) - break; - if ((route->meta.connection_count > 0) && - (! (route->flags & WOLFSENTRY_ROUTE_FLAG_PENDING_DELETE)) && - ((table->max_purgeable_idle_time == 0) || (now - route->meta.last_hit_time > table->max_purgeable_idle_time))) - { - break; + struct wolfsentry_route *route = NULL; + int found = 0; + /* re-scan from the most stale entry each pass, because a delete runs + * application callbacks that can mutate the purge list; skip routes + * that still have a live connection within the idle limit so one + * protected route does not strand the expired routes behind it. */ + for (cursor = table->purge_list.tail; cursor != NULL; cursor = cursor->prev) { + route = WOLFSENTRY_ROUTE_PURGE_HEADER_TO_TABLE_ENT_HEADER(cursor); + if (mode != 3) { + if (route->meta.purge_after > now) + break; + if ((route->meta.connection_count > 0) && + (! (route->flags & WOLFSENTRY_ROUTE_FLAG_PENDING_DELETE)) && + ((table->max_purgeable_idle_time == 0) || (now - route->meta.last_hit_time <= table->max_purgeable_idle_time))) + { + continue; + } } + found = 1; + break; } + if (! found) + break; #ifdef WOLFSENTRY_THREADSAFE if (! have_mutex) { if (mode == 2) diff --git a/tests/unittests.c b/tests/unittests.c index dd61f80..d0c7279 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3571,10 +3571,123 @@ static wolfsentry_errcode_t test_regression_bitmask_match_specificity(void) { } #endif /* WOLFSENTRY_ADDR_BITMASK_MATCHING */ +static wolfsentry_errcode_t test_regression_stale_purge_connected_idle(void) { + struct wolfsentry_context *wolfsentry; + struct wolfsentry_route_table *table; + struct wolfsentry_route *route; + wolfsentry_action_res_t action_results; + wolfsentry_route_flags_t inexact_matches, route_flags; + wolfsentry_ent_id_t id, id_b, route_id; + struct { + struct wolfsentry_sockaddr sa; + byte addr_buf[4]; + } remote, remote_b, local; +#ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS + static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT, .max_connection_count = 10 }; +#else + static const struct wolfsentry_eventconfig config = { PRIVATE_DATA_SIZE, PRIVATE_DATA_ALIGNMENT, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +#endif + + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_init_ex( + wolfsentry_build_settings, + WOLFSENTRY_CONTEXT_ARGS_OUT_EX(WOLFSENTRY_TEST_HPI), + &config, + &wolfsentry, + WOLFSENTRY_INIT_FLAG_NONE)); + + memset(&remote, 0, sizeof remote); + memset(&local, 0, sizeof local); + remote.sa.sa_family = AF_INET; + remote.sa.sa_proto = IPPROTO_TCP; + remote.sa.addr_len = 32; + memcpy(remote.sa.addr, "\x0A\x09\x08\x07", 4); /* NOLINT(bugprone-not-null-terminated-result) */ + local.sa.sa_family = AF_INET; + local.sa.sa_proto = IPPROTO_TCP; + local.sa.addr_len = 0; + + WOLFSENTRY_CLEAR_ALL_BITS(route_flags); + WOLFSENTRY_SET_BITS(route_flags, WOLFSENTRY_ROUTE_FLAG_TCPLIKE_PORT_NUMBERS + | WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN + | WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_ADDR_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_PORT_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_SA_REMOTE_PORT_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_LOCAL_INTERFACE_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_REMOTE_INTERFACE_WILDCARD); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_insert( + WOLFSENTRY_CONTEXT_ARGS_OUT, + NULL /* caller_arg */, + &remote.sa, &local.sa, route_flags, + NULL /* event_label */, 0 /* event_label_len */, + &id, &action_results)); + + /* a second route with no connection, less stale than the first; it sits + * ahead of the protected route in the purge list and must still be purged. */ + memcpy(&remote_b, &remote, sizeof remote_b); + memcpy(remote_b.sa.addr, "\x0A\x09\x08\x06", 4); /* NOLINT(bugprone-not-null-terminated-result) */ + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_insert( + WOLFSENTRY_CONTEXT_ARGS_OUT, + NULL /* caller_arg */, + &remote_b.sa, &local.sa, route_flags, + NULL /* event_label */, 0 /* event_label_len */, + &id_b, &action_results)); + + /* idle-time limit far larger than this test's wall-clock runtime, so a + * connected route is within the limit and must be preserved by a purge. */ + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_get_main_table(WOLFSENTRY_CONTEXT_ARGS_OUT, &table)); + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_table_max_purgeable_idle_time_set( + WOLFSENTRY_CONTEXT_ARGS_OUT, table, (wolfsentry_time_t)1 << 30)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + + /* open a connection on the route (connection_count -> 1). */ + WOLFSENTRY_CLEAR_ALL_BITS(action_results); + WOLFSENTRY_SET_BITS(action_results, WOLFSENTRY_ACTION_RES_CONNECT); + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_event_dispatch_with_inited_result( + WOLFSENTRY_CONTEXT_ARGS_OUT, + &remote.sa, &local.sa, route_flags, + NULL /* event_label */, 0 /* event_label_len */, + NULL /* caller_arg */, + &route_id, &inexact_matches, &action_results)); + WOLFSENTRY_EXIT_ON_FALSE(route_id == id); + + /* mark the protected route as the most stale entry (purge_after 1), and the + * unconnected route as less stale (purge_after 2); both are in the past. */ + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_table_ent_get_by_id(WOLFSENTRY_CONTEXT_ARGS_OUT, id, (struct wolfsentry_table_ent_header **)&route)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_purge_time_set(WOLFSENTRY_CONTEXT_ARGS_OUT, route, 1)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_table_ent_get_by_id(WOLFSENTRY_CONTEXT_ARGS_OUT, id_b, (struct wolfsentry_table_ent_header **)&route)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_purge_time_set(WOLFSENTRY_CONTEXT_ARGS_OUT, route, 2)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + + (void)wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_shared(WOLFSENTRY_CONTEXT_ARGS_OUT)); + /* the route's live connection is within the idle limit, so it must survive. */ + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_table_ent_get_by_id(WOLFSENTRY_CONTEXT_ARGS_OUT, id, (struct wolfsentry_table_ent_header **)&route)); + /* the unconnected route behind it must not be stranded by the purge. */ + WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE( + ITEM_NOT_FOUND, + wolfsentry_table_ent_get_by_id(WOLFSENTRY_CONTEXT_ARGS_OUT, id_b, (struct wolfsentry_table_ent_header **)&route)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + WOLFSENTRY_RETURN_OK; +} + static int test_dynamic_rules_regressions(void) { #ifdef WOLFSENTRY_ADDR_BITMASK_MATCHING WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_bitmask_match_specificity(), OK)); #endif + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_stale_purge_connected_idle(), OK)); WOLFSENTRY_RETURN_OK; } From cdc247b4638f6dd4aee8bce69b85a917439e92cf Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:35:47 -0700 Subject: [PATCH 04/43] F-13881 - https://fenrir.wolfssl.com/finding/13881 - Surface dispatch resource failures instead of masking them as fallthrough --- src/routes.c | 12 +++++++++ tests/unittests.c | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/routes.c b/src/routes.c index 9520d5e..8a74b11 100644 --- a/src/routes.c +++ b/src/routes.c @@ -1178,6 +1178,7 @@ WOLFSENTRY_LOCAL wolfsentry_errcode_t wolfsentry_route_clone( thread, #endif config->config.route_private_data_alignment, *new_route); + *new_route = NULL; WOLFSENTRY_ERROR_RERETURN(ret); } WOLFSENTRY_REFCOUNT_INCREMENT((*new_route)->parent_event->header.refcount, ret); @@ -2694,6 +2695,7 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_1( struct wolfsentry_route *rule_route = NULL; struct wolfsentry_event *trigger_event = NULL; wolfsentry_errcode_t ret; + int apply_default_policy = 0; if ((remote == NULL) || (local == NULL) || @@ -2766,6 +2768,7 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_1( else { /* nothing to do. */ /* carry through ret from final wolfsentry_route_lookup_0(), and impose the default policy. */ + apply_default_policy = 1; goto just_free_resources; } @@ -2805,6 +2808,15 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_1( WOLFSENTRY_WARN_ON_FAILURE(wolfsentry_event_drop_reference(WOLFSENTRY_CONTEXT_ARGS_OUT, trigger_event, NULL /* action_results */)); if (rule_route == NULL) { + if (! apply_default_policy) { + /* a resource failure (not a clean no-match) prevented rule + * evaluation; fail closed with an explicit reject rather than + * silently applying the default policy as a successful fallthrough, + * so an integrator that gates on _RES_REJECT drops the packet. + */ + *action_results |= WOLFSENTRY_ACTION_RES_ERROR | WOLFSENTRY_ACTION_RES_REJECT; + WOLFSENTRY_ERROR_UNLOCK_AND_RERETURN(ret); + } if (inexact_matches) *inexact_matches = WOLFSENTRY_ROUTE_WILDCARD_FLAGS | WOLFSENTRY_ROUTE_FLAG_PARENT_EVENT_WILDCARD; *action_results |= route_table->default_policy | WOLFSENTRY_ACTION_RES_FALLTHROUGH; diff --git a/tests/unittests.c b/tests/unittests.c index d0c7279..94c8c18 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3683,11 +3683,73 @@ static wolfsentry_errcode_t test_regression_stale_purge_connected_idle(void) { WOLFSENTRY_RETURN_OK; } +static wolfsentry_errcode_t test_regression_dispatch_error_not_masked(void) { + struct wolfsentry_context *wolfsentry; + wolfsentry_action_res_t action_results; + wolfsentry_route_flags_t inexact_matches, flags; + wolfsentry_ent_id_t id; + wolfsentry_errcode_t ret; + struct { + struct wolfsentry_sockaddr sa; + byte addr_buf[4]; + } remote, local; +#ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS + static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT }; +#else + static const struct wolfsentry_eventconfig config = { PRIVATE_DATA_SIZE, PRIVATE_DATA_ALIGNMENT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +#endif + + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_init_ex( + wolfsentry_build_settings, + WOLFSENTRY_CONTEXT_ARGS_OUT_EX(WOLFSENTRY_TEST_HPI), + &config, + &wolfsentry, + WOLFSENTRY_INIT_FLAG_NONE)); + + memset(&remote, 0, sizeof remote); + memset(&local, 0, sizeof local); + remote.sa.sa_family = AF_INET; + remote.sa.addr_len = 0; + local.sa.sa_family = AF_INET; + local.sa.addr_len = 0; + + /* a family wildcard without the remote/local/proto wildcards it requires is + * rejected while the ephemeral target route is built, before any rule is + * found; the dispatcher must surface that error, not report a successful + * default-policy fallthrough. + */ + WOLFSENTRY_CLEAR_ALL_BITS(flags); + WOLFSENTRY_SET_BITS(flags, WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN + | WOLFSENTRY_ROUTE_FLAG_SA_FAMILY_WILDCARD); + + WOLFSENTRY_CLEAR_ALL_BITS(action_results); + ret = wolfsentry_route_event_dispatch( + WOLFSENTRY_CONTEXT_ARGS_OUT, + &remote.sa, &local.sa, flags, + NULL /* event_label */, 0 /* event_label_len */, + NULL /* caller_arg */, + &id, &inexact_matches, &action_results); + + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_IS_FAILURE(ret)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_ERROR)); + /* the default policy is unset (NONE) here, so the failure must fail closed + * with an explicit reject rather than leaving the packet to be allowed. */ + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + WOLFSENTRY_RETURN_OK; +} + static int test_dynamic_rules_regressions(void) { #ifdef WOLFSENTRY_ADDR_BITMASK_MATCHING WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_bitmask_match_specificity(), OK)); #endif WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_stale_purge_connected_idle(), OK)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_dispatch_error_not_masked(), OK)); WOLFSENTRY_RETURN_OK; } From 632037d7f20e413766375dad7de3c25019a8ae38 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:37:38 -0700 Subject: [PATCH 05/43] F-12514 - https://fenrir.wolfssl.com/finding/12514 - Keep the purge list consistent when copying route metadata --- src/routes.c | 8 +++++ tests/unittests.c | 85 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/src/routes.c b/src/routes.c index 8a74b11..f5760a0 100644 --- a/src/routes.c +++ b/src/routes.c @@ -4914,8 +4914,16 @@ WOLFSENTRY_LOCAL wolfsentry_errcode_t wolfsentry_route_copy_metadata( /* pointers are advanced inside the loop. */) { if (from_i->header.id == to_i->header.id) { + /* meta carries purge_after, which determines purge_list membership + * and ordering; refresh the list around the copy so it stays + * consistent with the new value. + */ + if (to_i->meta.purge_after) + wolfsentry_list_ent_delete(&to_table->purge_list, &to_i->purge_links); to_i->flags = from_i->flags; to_i->meta = from_i->meta; + if (to_i->meta.purge_after) + wolfsentry_route_purge_list_insert(to_table, to_i); } else { int cmpret = wolfsentry_route_key_cmp_1(from_i, to_i, 0 /* match_wildcards_p */, NULL /* inexact_matches */); if (cmpret < 0) { diff --git a/tests/unittests.c b/tests/unittests.c index 94c8c18..75ae557 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3744,12 +3744,97 @@ static wolfsentry_errcode_t test_regression_dispatch_error_not_masked(void) { WOLFSENTRY_RETURN_OK; } +static wolfsentry_errcode_t test_regression_copy_metadata_purge_list(void) { + struct wolfsentry_context *wolfsentry; + struct wolfsentry_context *ctx_clone = NULL; + struct wolfsentry_route *route; + wolfsentry_action_res_t action_results; + wolfsentry_route_flags_t route_flags; + wolfsentry_ent_id_t id; + struct { + struct wolfsentry_sockaddr sa; + byte addr_buf[4]; + } remote, local; +#ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS + static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT }; +#else + static const struct wolfsentry_eventconfig config = { PRIVATE_DATA_SIZE, PRIVATE_DATA_ALIGNMENT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +#endif + + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_init_ex( + wolfsentry_build_settings, + WOLFSENTRY_CONTEXT_ARGS_OUT_EX(WOLFSENTRY_TEST_HPI), + &config, + &wolfsentry, + WOLFSENTRY_INIT_FLAG_NONE)); + + memset(&remote, 0, sizeof remote); + memset(&local, 0, sizeof local); + remote.sa.sa_family = AF_INET; + remote.sa.sa_proto = IPPROTO_TCP; + remote.sa.addr_len = 32; + memcpy(remote.sa.addr, "\x0A\x05\x06\x07", 4); /* NOLINT(bugprone-not-null-terminated-result) */ + local.sa.sa_family = AF_INET; + local.sa.sa_proto = IPPROTO_TCP; + local.sa.addr_len = 0; + + WOLFSENTRY_CLEAR_ALL_BITS(route_flags); + WOLFSENTRY_SET_BITS(route_flags, WOLFSENTRY_ROUTE_FLAG_TCPLIKE_PORT_NUMBERS + | WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN + | WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_ADDR_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_PORT_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_SA_REMOTE_PORT_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_LOCAL_INTERFACE_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_REMOTE_INTERFACE_WILDCARD); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_insert( + WOLFSENTRY_CONTEXT_ARGS_OUT, + NULL /* caller_arg */, + &remote.sa, &local.sa, route_flags, + NULL /* event_label */, 0 /* event_label_len */, + &id, &action_results)); + + /* clone before the route has a purge time, so the clone's copy of the route + * is not in the clone's purge list. */ + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_clone(WOLFSENTRY_CONTEXT_ARGS_OUT, &ctx_clone, WOLFSENTRY_CLONE_FLAG_NONE)); + + /* give the live route a (stale) purge time, so copy_metadata will propagate + * a nonzero purge_after into the clone's not-yet-listed route. */ + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_table_ent_get_by_id(WOLFSENTRY_CONTEXT_ARGS_OUT, id, (struct wolfsentry_table_ent_header **)&route)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_purge_time_set(WOLFSENTRY_CONTEXT_ARGS_OUT, route, 1)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + + /* swap the clone in, copying the live metadata across. */ + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_exchange(WOLFSENTRY_CONTEXT_ARGS_OUT, ctx_clone)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_free(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&ctx_clone))); + + /* the swapped-in route now has a stale purge_after, so it must be reachable + * from the purge list and get purged. */ + (void)wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_shared(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE( + ITEM_NOT_FOUND, + wolfsentry_table_ent_get_by_id(WOLFSENTRY_CONTEXT_ARGS_OUT, id, (struct wolfsentry_table_ent_header **)&route)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + WOLFSENTRY_RETURN_OK; +} + static int test_dynamic_rules_regressions(void) { #ifdef WOLFSENTRY_ADDR_BITMASK_MATCHING WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_bitmask_match_specificity(), OK)); #endif WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_stale_purge_connected_idle(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_dispatch_error_not_masked(), OK)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_copy_metadata_purge_list(), OK)); WOLFSENTRY_RETURN_OK; } From d516ac0b74cc8eddff3d3cb800cae2a2632f2428 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:40:02 -0700 Subject: [PATCH 06/43] F-8751 - https://fenrir.wolfssl.com/finding/8751 - Refresh config and route flags after switching to the fallthrough route --- src/routes.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/routes.c b/src/routes.c index f5760a0..9332daa 100644 --- a/src/routes.c +++ b/src/routes.c @@ -2474,6 +2474,11 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_0( wolfsentry_route_increment_hitcount(WOLFSENTRY_CONTEXT_ARGS_OUT, rule_route, action_results); } parent_event = route_table->default_event; + /* the rule route and its event changed, so refresh the cached config + * and flags to match the fallthrough route rather than reusing the + * superseded route's state. */ + config = (parent_event && parent_event->config) ? parent_event->config : &wolfsentry->config; + current_rule_route_flags = WOLFSENTRY_ATOMIC_LOAD(rule_route->flags); } if (parent_event && (wolfsentry_list_ent_get_len(&parent_event->match_action_list.header) > 0)) { From 06466c0da0a8ce16290366743be5a5259b3a6661 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:40:56 -0700 Subject: [PATCH 07/43] F-13287 - https://fenrir.wolfssl.com/finding/13287 - Decrement connection count on disconnect regardless of the configured limit --- src/routes.c | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/routes.c b/src/routes.c index 9332daa..740e385 100644 --- a/src/routes.c +++ b/src/routes.c @@ -2510,29 +2510,36 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_0( (void)ret; } - if ((! (current_rule_route_flags & WOLFSENTRY_ROUTE_FLAG_DONT_COUNT_CURRENT_CONNECTIONS)) && - (config->config.max_connection_count > 0)) - { + if (! (current_rule_route_flags & WOLFSENTRY_ROUTE_FLAG_DONT_COUNT_CURRENT_CONNECTIONS)) { if (*action_results & WOLFSENTRY_ACTION_RES_CONNECT) { - if (rule_route->meta.connection_count >= config->config.max_connection_count) { - *action_results |= WOLFSENTRY_ACTION_RES_REJECT; - ret = WOLFSENTRY_ERROR_ENCODE(OK); - goto done; - } - if (WOLFSENTRY_ATOMIC_INCREMENT_BY_ONE(rule_route->meta.connection_count) > config->config.max_connection_count) { - WOLFSENTRY_ATOMIC_DECREMENT_BY_ONE(rule_route->meta.connection_count); - *action_results |= WOLFSENTRY_ACTION_RES_REJECT; - ret = WOLFSENTRY_ERROR_ENCODE(OK); - goto done; + /* the limit is only enforced when configured; a zero limit + * means unlimited, so no increment or rejection applies. */ + if (config->config.max_connection_count > 0) { + if (rule_route->meta.connection_count >= config->config.max_connection_count) { + *action_results |= WOLFSENTRY_ACTION_RES_REJECT; + ret = WOLFSENTRY_ERROR_ENCODE(OK); + goto done; + } + if (WOLFSENTRY_ATOMIC_INCREMENT_BY_ONE(rule_route->meta.connection_count) > config->config.max_connection_count) { + WOLFSENTRY_ATOMIC_DECREMENT_BY_ONE(rule_route->meta.connection_count); + *action_results |= WOLFSENTRY_ACTION_RES_REJECT; + ret = WOLFSENTRY_ERROR_ENCODE(OK); + goto done; + } } } else if (*action_results & WOLFSENTRY_ACTION_RES_DISCONNECT) { uint16_t new_connection_count; - /* important to decrement safely, because untimely route - * deletions can lead to a decrement being assigned to a - * different route than the one originally incremented. + /* decrement regardless of the limit, so counts seeded at insert + * time still drain when the limit is zero (unlimited); decrement + * safely because untimely route deletions can assign a decrement + * to a different route than the one originally incremented. */ WOLFSENTRY_ATOMIC_DECREMENT_UNSIGNED_SAFELY(rule_route->meta.connection_count, 1, new_connection_count); - if (new_connection_count == MAX_UINT_OF(rule_route->meta.connection_count)) + /* only an already-zero count under an enforced limit is + * anomalous; with no limit, unconnected routes legitimately see + * disconnects, so do not warn on them. */ + if ((new_connection_count == MAX_UINT_OF(rule_route->meta.connection_count)) && + (config->config.max_connection_count > 0)) WOLFSENTRY_WARN("_RES_DISCONNECT for route #" WOLFSENTRY_ENT_ID_FMT ", whose connection_count is already zero.", rule_route->header.id); } } From 0f61445fee8a7dcc5f5d7fb8bde70e3c071f3519 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:41:24 -0700 Subject: [PATCH 08/43] F-15468 - https://fenrir.wolfssl.com/finding/15468 - Always reposition a refreshed route in the purge list --- src/routes.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/routes.c b/src/routes.c index 740e385..e6cbdb5 100644 --- a/src/routes.c +++ b/src/routes.c @@ -2431,10 +2431,11 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_0( #endif { rule_route->meta.purge_after = new_purge_after; /* coherent assignment */ - if (route_table->purge_list.tail != &rule_route->purge_links) { - wolfsentry_list_ent_delete(&route_table->purge_list, &rule_route->purge_links); - wolfsentry_route_purge_list_insert(route_table, rule_route); - } + /* the refreshed route may now be the newest rather than the + * stalest, so always reposition it instead of leaving a + * just-refreshed tail stranded at the stale end. */ + wolfsentry_list_ent_delete(&route_table->purge_list, &rule_route->purge_links); + wolfsentry_route_purge_list_insert(route_table, rule_route); } } } From 7075b983f6acf3cbf93e74628e2abee14ffda440 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:42:27 -0700 Subject: [PATCH 09/43] F-2442 - https://fenrir.wolfssl.com/finding/2442 - Set the error result bit when an action chain dispatch fails --- src/routes.c | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/routes.c b/src/routes.c index e6cbdb5..43d095a 100644 --- a/src/routes.c +++ b/src/routes.c @@ -2380,6 +2380,7 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_0( struct wolfsentry_eventconfig_internal *config; wolfsentry_route_flags_t current_rule_route_flags; wolfsentry_errcode_t ret; + wolfsentry_errcode_t action_ret; wolfsentry_time_t now; int penalty_triggered = 0; wolfsentry_hitcount_t derog_snap; @@ -2448,7 +2449,7 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_0( * trigger_event->action_list must call wolfsentry_route_set_wildcard(), * in addition to setting _ACTION_RES_INSERT. */ - WOLFSENTRY_WARN_ON_FAILURE(wolfsentry_action_list_dispatch( + action_ret = wolfsentry_action_list_dispatch( WOLFSENTRY_CONTEXT_ARGS_OUT, caller_arg, trigger_event, @@ -2457,7 +2458,10 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_0( target_route, route_table, rule_route, - action_results)); + action_results); + WOLFSENTRY_WARN_ON_FAILURE(action_ret); + if (action_ret < 0) + *action_results |= WOLFSENTRY_ACTION_RES_ERROR; WOLFSENTRY_CLEAR_BITS(*action_results, WOLFSENTRY_ACTION_RES_STOP); current_rule_route_flags = WOLFSENTRY_ATOMIC_LOAD(rule_route->flags); } @@ -2483,7 +2487,7 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_0( } if (parent_event && (wolfsentry_list_ent_get_len(&parent_event->match_action_list.header) > 0)) { - WOLFSENTRY_WARN_ON_FAILURE(wolfsentry_action_list_dispatch( + action_ret = wolfsentry_action_list_dispatch( WOLFSENTRY_CONTEXT_ARGS_OUT, caller_arg, parent_event, @@ -2492,7 +2496,10 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_0( target_route, route_table, rule_route, - action_results)); + action_results); + WOLFSENTRY_WARN_ON_FAILURE(action_ret); + if (action_ret < 0) + *action_results |= WOLFSENTRY_ACTION_RES_ERROR; WOLFSENTRY_CLEAR_BITS(*action_results, WOLFSENTRY_ACTION_RES_STOP); current_rule_route_flags = WOLFSENTRY_ATOMIC_LOAD(rule_route->flags); } @@ -2651,7 +2658,7 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_0( parent_event && (wolfsentry_list_ent_get_len(&parent_event->update_action_list.header) > 0)) { WOLFSENTRY_CLEAR_BITS(*action_results, WOLFSENTRY_ACTION_RES_STOP); - WOLFSENTRY_WARN_ON_FAILURE(wolfsentry_action_list_dispatch( + action_ret = wolfsentry_action_list_dispatch( WOLFSENTRY_CONTEXT_ARGS_OUT, caller_arg, parent_event, @@ -2660,13 +2667,16 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_0( target_route, route_table, rule_route, - action_results)); + action_results); + WOLFSENTRY_WARN_ON_FAILURE(action_ret); + if (action_ret < 0) + *action_results |= WOLFSENTRY_ACTION_RES_ERROR; /* no need to refresh current_rule_route_flags */ } if (parent_event && (wolfsentry_list_ent_get_len(&parent_event->decision_action_list.header) > 0)) { WOLFSENTRY_CLEAR_BITS(*action_results, WOLFSENTRY_ACTION_RES_STOP); - WOLFSENTRY_WARN_ON_FAILURE(wolfsentry_action_list_dispatch( + action_ret = wolfsentry_action_list_dispatch( WOLFSENTRY_CONTEXT_ARGS_OUT, caller_arg, parent_event, @@ -2675,7 +2685,10 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_0( target_route, route_table, rule_route, - action_results)); + action_results); + WOLFSENTRY_WARN_ON_FAILURE(action_ret); + if (action_ret < 0) + *action_results |= WOLFSENTRY_ACTION_RES_ERROR; /* no need to refresh current_rule_route_flags */ } From a77765c20e88369e23f08d04ad06beff381dc2ef Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:43:35 -0700 Subject: [PATCH 10/43] F-2437 - https://fenrir.wolfssl.com/finding/2437 - Warn once when unmatched traffic falls through with an unset default policy --- src/routes.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/routes.c b/src/routes.c index 43d095a..75a4d16 100644 --- a/src/routes.c +++ b/src/routes.c @@ -2722,6 +2722,7 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_1( struct wolfsentry_event *trigger_event = NULL; wolfsentry_errcode_t ret; int apply_default_policy = 0; + static int warned_unset_default_policy = 0; if ((remote == NULL) || (local == NULL) || @@ -2845,6 +2846,18 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_1( } if (inexact_matches) *inexact_matches = WOLFSENTRY_ROUTE_WILDCARD_FLAGS | WOLFSENTRY_ROUTE_FLAG_PARENT_EVENT_WILDCARD; + if (route_table->default_policy == WOLFSENTRY_ACTION_RES_NONE) { + /* claim the one-shot atomically so only a single caller warns. */ + int warn_expected = 0; +#ifdef WOLFSENTRY_THREADSAFE + if (WOLFSENTRY_ATOMIC_CMPXCHG(&warned_unset_default_policy, &warn_expected, 1, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) +#else + warn_expected = warned_unset_default_policy; + warned_unset_default_policy = 1; + if (! warn_expected) +#endif + WOLFSENTRY_WARN("%s", "default route policy is unset (NONE); unmatched traffic falls through with no accept or reject verdict.\n"); + } *action_results |= route_table->default_policy | WOLFSENTRY_ACTION_RES_FALLTHROUGH; WOLFSENTRY_SUCCESS_UNLOCK_AND_RETURN(USED_FALLBACK); } From 45dbe2e0a3f1e1cfdd73537c95496a4dd07d0852 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:45:05 -0700 Subject: [PATCH 11/43] F-3441 - https://fenrir.wolfssl.com/finding/3441 - Reject internal-only result bits in caller-initialized dispatch input --- src/routes.c | 2 ++ tests/unittests.c | 65 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/routes.c b/src/routes.c index 75a4d16..3852b6f 100644 --- a/src/routes.c +++ b/src/routes.c @@ -2907,6 +2907,8 @@ static wolfsentry_errcode_t check_user_inited_result(wolfsentry_action_res_t act if (WOLFSENTRY_MASKIN_BITS(action_results, WOLFSENTRY_ACTION_RES_ACCEPT | WOLFSENTRY_ACTION_RES_REJECT | + WOLFSENTRY_ACTION_RES_FALLTHROUGH | + WOLFSENTRY_ACTION_RES_INSERTED | WOLFSENTRY_ACTION_RES_STOP | WOLFSENTRY_ACTION_RES_DEALLOCATED | WOLFSENTRY_ACTION_RES_ERROR)) diff --git a/tests/unittests.c b/tests/unittests.c index 75ae557..4599170 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3828,6 +3828,70 @@ static wolfsentry_errcode_t test_regression_copy_metadata_purge_list(void) { WOLFSENTRY_RETURN_OK; } +static wolfsentry_errcode_t test_regression_route_api_invariants(void) { + struct wolfsentry_context *wolfsentry; + wolfsentry_action_res_t action_results; + wolfsentry_route_flags_t inexact_matches; + wolfsentry_ent_id_t id; + struct { + struct wolfsentry_sockaddr sa; + byte addr_buf[4]; + } remote, local; +#ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS + static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT }; +#else + static const struct wolfsentry_eventconfig config = { PRIVATE_DATA_SIZE, PRIVATE_DATA_ALIGNMENT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +#endif + + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_init_ex( + wolfsentry_build_settings, + WOLFSENTRY_CONTEXT_ARGS_OUT_EX(WOLFSENTRY_TEST_HPI), + &config, + &wolfsentry, + WOLFSENTRY_INIT_FLAG_NONE)); + + memset(&remote, 0, sizeof remote); + memset(&local, 0, sizeof local); + remote.sa.sa_family = AF_INET; + remote.sa.sa_proto = IPPROTO_TCP; + remote.sa.addr_len = 32; + memcpy(remote.sa.addr, "\x0A\x02\x03\x04", 4); /* NOLINT(bugprone-not-null-terminated-result) */ + local.sa.sa_family = AF_INET; + local.sa.sa_proto = IPPROTO_TCP; + local.sa.addr_len = 0; + + /* internal-only result bits must not be accepted as caller-initialized + * input to the dispatch-with-inited-result entry points. */ + action_results = WOLFSENTRY_ACTION_RES_FALLTHROUGH; + WOLFSENTRY_EXIT_ON_FALSE( + WOLFSENTRY_ERROR_CODE_IS( + wolfsentry_route_event_dispatch_with_inited_result( + WOLFSENTRY_CONTEXT_ARGS_OUT, + &remote.sa, &local.sa, WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN, + NULL /* event_label */, 0 /* event_label_len */, + NULL /* caller_arg */, + &id, &inexact_matches, &action_results), + INVALID_ARG)); + + action_results = WOLFSENTRY_ACTION_RES_INSERTED; + WOLFSENTRY_EXIT_ON_FALSE( + WOLFSENTRY_ERROR_CODE_IS( + wolfsentry_route_event_dispatch_with_inited_result( + WOLFSENTRY_CONTEXT_ARGS_OUT, + &remote.sa, &local.sa, WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN, + NULL /* event_label */, 0 /* event_label_len */, + NULL /* caller_arg */, + &id, &inexact_matches, &action_results), + INVALID_ARG)); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + WOLFSENTRY_RETURN_OK; +} + static int test_dynamic_rules_regressions(void) { #ifdef WOLFSENTRY_ADDR_BITMASK_MATCHING WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_bitmask_match_specificity(), OK)); @@ -3835,6 +3899,7 @@ static int test_dynamic_rules_regressions(void) { WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_stale_purge_connected_idle(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_dispatch_error_not_masked(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_copy_metadata_purge_list(), OK)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_route_api_invariants(), OK)); WOLFSENTRY_RETURN_OK; } From 9eaf7da9d5fa905b189a67371943200d22e34589 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:46:01 -0700 Subject: [PATCH 12/43] F-6510 - https://fenrir.wolfssl.com/finding/6510 - Reject a default policy that sets both accept and reject --- src/routes.c | 2 ++ tests/unittests.c | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/routes.c b/src/routes.c index 3852b6f..14c885c 100644 --- a/src/routes.c +++ b/src/routes.c @@ -1996,6 +1996,8 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_table_default_policy_set( if ((default_policy != WOLFSENTRY_ACTION_RES_NONE) && (! WOLFSENTRY_MASKIN_BITS(default_policy, WOLFSENTRY_ACTION_RES_ACCEPT | WOLFSENTRY_ACTION_RES_REJECT))) WOLFSENTRY_ERROR_RETURN(INVALID_ARG); + if (WOLFSENTRY_CHECK_BITS(default_policy, WOLFSENTRY_ACTION_RES_ACCEPT | WOLFSENTRY_ACTION_RES_REJECT)) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); WOLFSENTRY_MUTEX_OR_RETURN(); table->default_policy = default_policy; if (table == wolfsentry->routes) diff --git a/tests/unittests.c b/tests/unittests.c index 4599170..fa5f837 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3887,6 +3887,14 @@ static wolfsentry_errcode_t test_regression_route_api_invariants(void) { &id, &inexact_matches, &action_results), INVALID_ARG)); + /* a default policy may be accept or reject, never both at once. */ + WOLFSENTRY_EXIT_ON_FALSE( + WOLFSENTRY_ERROR_CODE_IS( + wolfsentry_route_default_policy_set( + WOLFSENTRY_CONTEXT_ARGS_OUT, + WOLFSENTRY_ACTION_RES_ACCEPT | WOLFSENTRY_ACTION_RES_REJECT), + INVALID_ARG)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 738f85fd275c0c1c6152c5b92cc5473c18b4b80e Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:47:25 -0700 Subject: [PATCH 13/43] F-1216 - https://fenrir.wolfssl.com/finding/1216 - Report the written label length from the default event getter --- src/routes.c | 1 + tests/unittests.c | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/routes.c b/src/routes.c index 14c885c..2a1f385 100644 --- a/src/routes.c +++ b/src/routes.c @@ -3652,6 +3652,7 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_table_get_default_event( if (table->default_event->label_len >= *event_label_len) WOLFSENTRY_ERROR_UNLOCK_AND_RETURN(BUFFER_TOO_SMALL); memcpy(event_label, table->default_event->label, (size_t)(table->default_event->label_len + 1)); + *event_label_len = table->default_event->label_len; WOLFSENTRY_UNLOCK_AND_RETURN_OK; } diff --git a/tests/unittests.c b/tests/unittests.c index fa5f837..629097b 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3830,9 +3830,12 @@ static wolfsentry_errcode_t test_regression_copy_metadata_purge_list(void) { static wolfsentry_errcode_t test_regression_route_api_invariants(void) { struct wolfsentry_context *wolfsentry; + struct wolfsentry_route_table *table; wolfsentry_action_res_t action_results; wolfsentry_route_flags_t inexact_matches; wolfsentry_ent_id_t id; + char default_event_label[WOLFSENTRY_MAX_LABEL_BYTES + 1]; + int default_event_label_len; struct { struct wolfsentry_sockaddr sa; byte addr_buf[4]; @@ -3895,6 +3898,27 @@ static wolfsentry_errcode_t test_regression_route_api_invariants(void) { WOLFSENTRY_ACTION_RES_ACCEPT | WOLFSENTRY_ACTION_RES_REJECT), INVALID_ARG)); + /* reading back the default event must report the written label length, not + * leave the caller's in/out length untouched. */ + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_event_insert( + WOLFSENTRY_CONTEXT_ARGS_OUT, + "default_evt", -1 /* label_len */, 10 /* priority */, + NULL /* config */, WOLFSENTRY_EVENT_FLAG_NONE, &id)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_get_main_table(WOLFSENTRY_CONTEXT_ARGS_OUT, &table)); + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_table_set_default_event( + WOLFSENTRY_CONTEXT_ARGS_OUT, table, "default_evt", -1)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + + default_event_label_len = (int)sizeof default_event_label; + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_table_get_default_event( + WOLFSENTRY_CONTEXT_ARGS_OUT, table, default_event_label, &default_event_label_len)); + WOLFSENTRY_EXIT_ON_FALSE(default_event_label_len == (int)strlen("default_evt")); + WOLFSENTRY_EXIT_ON_FALSE(strcmp(default_event_label, "default_evt") == 0); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 8b8eebc7ec3ba91e86a0d0ee476b2258af5dfd04 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:49:01 -0700 Subject: [PATCH 14/43] F-10502 - https://fenrir.wolfssl.com/finding/10502 - Treat a zero count delta as a no-op and negate safely in count adjustment --- src/routes.c | 14 ++++++++++---- tests/unittests.c | 32 +++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/routes.c b/src/routes.c index 2a1f385..5dc7643 100644 --- a/src/routes.c +++ b/src/routes.c @@ -3462,10 +3462,13 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_increment_derogatory_count( WOLFSENTRY_ATOMIC_INCREMENT_UNSIGNED_SAFELY(route->meta.derogatory_count, (unsigned)count_to_add, new_derogatory_count); if (new_derogatory_count == 0) WOLFSENTRY_ERROR_RETURN(OVERFLOW_AVERTED); - } else { - WOLFSENTRY_ATOMIC_DECREMENT_UNSIGNED_SAFELY(route->meta.derogatory_count, (unsigned)(-count_to_add), new_derogatory_count); + } else if (count_to_add < 0) { + /* negate in the unsigned domain so INT_MIN does not overflow. */ + WOLFSENTRY_ATOMIC_DECREMENT_UNSIGNED_SAFELY(route->meta.derogatory_count, 0U - (unsigned)count_to_add, new_derogatory_count); if (new_derogatory_count == MAX_UINT_OF(route->meta.derogatory_count)) WOLFSENTRY_ERROR_RETURN(OVERFLOW_AVERTED); + } else { + new_derogatory_count = WOLFSENTRY_ATOMIC_LOAD(route->meta.derogatory_count); } if (new_derogatory_count_ptr) @@ -3488,10 +3491,13 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_increment_commendable_count WOLFSENTRY_ATOMIC_INCREMENT_UNSIGNED_SAFELY(route->meta.commendable_count, (unsigned)count_to_add, new_commendable_count); if (new_commendable_count == 0) WOLFSENTRY_ERROR_RETURN(OVERFLOW_AVERTED); - } else { - WOLFSENTRY_ATOMIC_DECREMENT_UNSIGNED_SAFELY(route->meta.commendable_count, (unsigned)(-count_to_add), new_commendable_count); + } else if (count_to_add < 0) { + /* negate in the unsigned domain so INT_MIN does not overflow. */ + WOLFSENTRY_ATOMIC_DECREMENT_UNSIGNED_SAFELY(route->meta.commendable_count, 0U - (unsigned)count_to_add, new_commendable_count); if (new_commendable_count == MAX_UINT_OF(route->meta.commendable_count)) WOLFSENTRY_ERROR_RETURN(OVERFLOW_AVERTED); + } else { + new_commendable_count = WOLFSENTRY_ATOMIC_LOAD(route->meta.commendable_count); } if (new_commendable_count_ptr) diff --git a/tests/unittests.c b/tests/unittests.c index 629097b..860c91c 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3831,11 +3831,13 @@ static wolfsentry_errcode_t test_regression_copy_metadata_purge_list(void) { static wolfsentry_errcode_t test_regression_route_api_invariants(void) { struct wolfsentry_context *wolfsentry; struct wolfsentry_route_table *table; + struct wolfsentry_route *route; wolfsentry_action_res_t action_results; - wolfsentry_route_flags_t inexact_matches; + wolfsentry_route_flags_t inexact_matches, route_flags; wolfsentry_ent_id_t id; char default_event_label[WOLFSENTRY_MAX_LABEL_BYTES + 1]; int default_event_label_len; + int new_count; struct { struct wolfsentry_sockaddr sa; byte addr_buf[4]; @@ -3919,6 +3921,34 @@ static wolfsentry_errcode_t test_regression_route_api_invariants(void) { WOLFSENTRY_EXIT_ON_FALSE(default_event_label_len == (int)strlen("default_evt")); WOLFSENTRY_EXIT_ON_FALSE(strcmp(default_event_label, "default_evt") == 0); + /* incrementing a derogatory count by zero is a no-op that must succeed even + * when the count is saturated, not be mistaken for an averted overflow. */ + WOLFSENTRY_CLEAR_ALL_BITS(route_flags); + WOLFSENTRY_SET_BITS(route_flags, WOLFSENTRY_ROUTE_FLAG_TCPLIKE_PORT_NUMBERS + | WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN + | WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_ADDR_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_PORT_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_SA_REMOTE_PORT_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_LOCAL_INTERFACE_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_REMOTE_INTERFACE_WILDCARD); + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_insert( + WOLFSENTRY_CONTEXT_ARGS_OUT, + NULL /* caller_arg */, + &remote.sa, &local.sa, route_flags, + NULL /* event_label */, 0 /* event_label_len */, + &id, &action_results)); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_table_ent_get_by_id(WOLFSENTRY_CONTEXT_ARGS_OUT, id, (struct wolfsentry_table_ent_header **)&route)); + new_count = -1; + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_increment_derogatory_count(WOLFSENTRY_CONTEXT_ARGS_OUT, route, 65535, &new_count)); + WOLFSENTRY_EXIT_ON_FALSE(new_count == 65535); + new_count = -1; + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_increment_derogatory_count(WOLFSENTRY_CONTEXT_ARGS_OUT, route, 0, &new_count)); + WOLFSENTRY_EXIT_ON_FALSE(new_count == 65535); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 2ba552c18bdb18514fc3cf8e218061e79b523115 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:50:45 -0700 Subject: [PATCH 15/43] F-11618 - https://fenrir.wolfssl.com/finding/11618 - Reject setting a purge time on a route with no parent table --- src/routes.c | 2 ++ tests/unittests.c | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/routes.c b/src/routes.c index 5dc7643..1dca38a 100644 --- a/src/routes.c +++ b/src/routes.c @@ -3111,6 +3111,8 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_purge_time_set( wolfsentry_time_t purge_after) { WOLFSENTRY_HAVE_MUTEX_OR_RETURN(); + if (route->header.parent_table == NULL) + WOLFSENTRY_ERROR_RETURN(NOT_PERMITTED); if (route->meta.purge_after) wolfsentry_list_ent_delete(&((struct wolfsentry_route_table *)route->header.parent_table)->purge_list, &route->purge_links); route->meta.purge_after = purge_after; diff --git a/tests/unittests.c b/tests/unittests.c index 860c91c..743765e 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3832,6 +3832,7 @@ static wolfsentry_errcode_t test_regression_route_api_invariants(void) { struct wolfsentry_context *wolfsentry; struct wolfsentry_route_table *table; struct wolfsentry_route *route; + const struct wolfsentry_route *fallthrough_route; wolfsentry_action_res_t action_results; wolfsentry_route_flags_t inexact_matches, route_flags; wolfsentry_ent_id_t id; @@ -3949,6 +3950,17 @@ static wolfsentry_errcode_t test_regression_route_api_invariants(void) { WOLFSENTRY_EXIT_ON_FALSE(new_count == 65535); WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + /* the fallthrough route belongs to no table, so setting a purge time on it + * must be refused rather than dereferencing a null parent table. */ + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_table_fallthrough_route_get(WOLFSENTRY_CONTEXT_ARGS_OUT, table, &fallthrough_route)); + WOLFSENTRY_EXIT_ON_FALSE( + WOLFSENTRY_ERROR_CODE_IS( + wolfsentry_route_purge_time_set(WOLFSENTRY_CONTEXT_ARGS_OUT, (struct wolfsentry_route *)fallthrough_route, 1), + NOT_PERMITTED)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_drop_reference(WOLFSENTRY_CONTEXT_ARGS_OUT, (struct wolfsentry_route *)fallthrough_route, NULL /* action_results */)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 75d3c170c51b98ea4cbcee716254150f46f256d0 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:52:19 -0700 Subject: [PATCH 16/43] F-2860 - https://fenrir.wolfssl.com/finding/2860 - Render the final partial byte of link and CAN addresses --- src/routes.c | 9 +++++---- tests/unittests.c | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/routes.c b/src/routes.c index 1dca38a..09e583d 100644 --- a/src/routes.c +++ b/src/routes.c @@ -4002,9 +4002,10 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_format_address( if (sa_family == WOLFSENTRY_AF_LINK || sa_family == WOLFSENTRY_AF_LINK64) { unsigned int i; - if ((addr_bits >> 3) * 3 > (size_t)*buflen) + unsigned int addr_bytes = (unsigned int)WOLFSENTRY_BITS_TO_BYTES(addr_bits); + if (addr_bytes * 3 > (size_t)*buflen) WOLFSENTRY_ERROR_RETURN(BUFFER_TOO_SMALL); - for (i=0; i < (addr_bits >> 3); ++i) { + for (i=0; i < addr_bytes; ++i) { if (i > 0) *buf++ = ':'; *buf++ = hexdigit_ntoa(addr[i] >> 4); @@ -4031,7 +4032,7 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_format_address( *buf++ = '0'; *buf++ = 'x'; for (i=0; i < 4; ++i) { - if (i < (addr_bits >> 3)) { + if (i < (unsigned int)WOLFSENTRY_BITS_TO_BYTES(addr_bits)) { *buf++ = hexdigit_ntoa(addr[i] >> 4); *buf++ = hexdigit_ntoa(addr[i]); } else { @@ -4531,7 +4532,7 @@ static wolfsentry_errcode_t wolfsentry_route_render_address(WOLFSENTRY_CONTEXT_A if (sa_family == WOLFSENTRY_AF_LINK || sa_family == WOLFSENTRY_AF_LINK64) { unsigned int i; - for (i=0; i < (addr_bits >> 3); ++i) { + for (i=0; i < (unsigned int)WOLFSENTRY_BITS_TO_BYTES(addr_bits); ++i) { if (fprintf(f, "%s%02x", i ? ":" : "", (unsigned int)addr[i]) < 0) WOLFSENTRY_ERROR_RETURN(IO_FAILED); } diff --git a/tests/unittests.c b/tests/unittests.c index 743765e..33d1650 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3839,6 +3839,9 @@ static wolfsentry_errcode_t test_regression_route_api_invariants(void) { char default_event_label[WOLFSENTRY_MAX_LABEL_BYTES + 1]; int default_event_label_len; int new_count; + char addr_fmt_buf[32]; + int addr_fmt_len; + byte link_addr[2]; struct { struct wolfsentry_sockaddr sa; byte addr_buf[4]; @@ -3961,6 +3964,18 @@ static wolfsentry_errcode_t test_regression_route_api_invariants(void) { WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_drop_reference(WOLFSENTRY_CONTEXT_ARGS_OUT, (struct wolfsentry_route *)fallthrough_route, NULL /* action_results */)); WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + /* a link address whose length is not a whole number of bytes must render + * its final partial byte, not silently drop it. */ + link_addr[0] = 0xAB; + link_addr[1] = 0xCD; + addr_fmt_len = (int)sizeof addr_fmt_buf; + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_format_address( + WOLFSENTRY_CONTEXT_ARGS_OUT, WOLFSENTRY_AF_LINK, link_addr, 12 /* addr_bits */, + addr_fmt_buf, &addr_fmt_len)); + WOLFSENTRY_EXIT_ON_FALSE(addr_fmt_len == 5); + WOLFSENTRY_EXIT_ON_FALSE(addr_fmt_buf[2] == ':'); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 3e9f67c9897ab7a46244bed5489385d22d7526df Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:56:32 -0700 Subject: [PATCH 17/43] F-5564 - https://fenrir.wolfssl.com/finding/5564 - Null check result and count outputs before dereferencing them in route APIs --- src/routes.c | 43 +++++++++++++++++++++++++++++++++++++++---- tests/unittests.c | 12 ++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/routes.c b/src/routes.c index 09e583d..3681459 100644 --- a/src/routes.c +++ b/src/routes.c @@ -1472,6 +1472,9 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_insert_into_table( wolfsentry_errcode_t ret; struct wolfsentry_event *event = NULL; + if (action_results == NULL) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); + WOLFSENTRY_MUTEX_OR_RETURN(); if (event_label) { @@ -1496,6 +1499,9 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_insert_by_exports_into_tabl wolfsentry_errcode_t ret; struct wolfsentry_event *event = NULL; + if (action_results == NULL) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); + WOLFSENTRY_MUTEX_OR_RETURN(); if (route_exports->parent_event_label) { @@ -1566,6 +1572,9 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_insert_into_table_and_check wolfsentry_errcode_t ret; struct wolfsentry_event *event = NULL; + if (action_results == NULL) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); + WOLFSENTRY_MUTEX_OR_RETURN(); if (event_label) { @@ -2282,6 +2291,9 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_delete_from_table( wolfsentry_errcode_t ret; struct wolfsentry_event *event = NULL; + if ((action_results == NULL) || (n_deleted == NULL)) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); + WOLFSENTRY_MUTEX_OR_RETURN(); if (event_label) { @@ -2334,6 +2346,9 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_delete_by_id( struct wolfsentry_event *trigger_event = NULL; struct wolfsentry_route *route; + if (action_results == NULL) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); + WOLFSENTRY_MUTEX_OR_RETURN(); if (trigger_label) { @@ -2884,6 +2899,8 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_event_dispatch_with_table( wolfsentry_action_res_t *action_results ) { + if (action_results == NULL) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); WOLFSENTRY_CLEAR_ALL_BITS(*action_results); WOLFSENTRY_ERROR_RERETURN(wolfsentry_route_event_dispatch_1(WOLFSENTRY_CONTEXT_ARGS_OUT, route_table, remote, local, flags, event_label, event_label_len, caller_arg, id, inexact_matches, action_results)); } @@ -2901,6 +2918,8 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_event_dispatch( wolfsentry_action_res_t *action_results ) { + if (action_results == NULL) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); WOLFSENTRY_CLEAR_ALL_BITS(*action_results); WOLFSENTRY_ERROR_RERETURN(wolfsentry_route_event_dispatch_1(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, remote, local, flags, event_label, event_label_len, caller_arg, id, inexact_matches, action_results)); } @@ -2933,7 +2952,10 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_event_dispatch_with_table_w wolfsentry_action_res_t *action_results ) { - wolfsentry_errcode_t ret = check_user_inited_result(*action_results); + wolfsentry_errcode_t ret; + if (action_results == NULL) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); + ret = check_user_inited_result(*action_results); WOLFSENTRY_RERETURN_IF_ERROR(ret); WOLFSENTRY_ERROR_RERETURN(wolfsentry_route_event_dispatch_1(WOLFSENTRY_CONTEXT_ARGS_OUT, route_table, remote, local, flags, event_label, event_label_len, caller_arg, id, inexact_matches, action_results)); } @@ -2951,7 +2973,10 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_event_dispatch_with_inited_ wolfsentry_action_res_t *action_results ) { - wolfsentry_errcode_t ret = check_user_inited_result(*action_results); + wolfsentry_errcode_t ret; + if (action_results == NULL) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); + ret = check_user_inited_result(*action_results); WOLFSENTRY_RERETURN_IF_ERROR(ret); WOLFSENTRY_ERROR_RERETURN(wolfsentry_route_event_dispatch_1(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, remote, local, flags, event_label, event_label_len, caller_arg, id, inexact_matches, action_results)); } @@ -3007,6 +3032,8 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_event_dispatch_by_id( wolfsentry_action_res_t *action_results ) { + if (action_results == NULL) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); WOLFSENTRY_CLEAR_ALL_BITS(*action_results); WOLFSENTRY_ERROR_RERETURN(wolfsentry_route_event_dispatch_by_id_1(WOLFSENTRY_CONTEXT_ARGS_OUT, id, event_label, event_label_len, caller_arg, action_results)); } @@ -3020,7 +3047,10 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_event_dispatch_by_id_with_i wolfsentry_action_res_t *action_results ) { - wolfsentry_errcode_t ret = check_user_inited_result(*action_results); + wolfsentry_errcode_t ret; + if (action_results == NULL) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); + ret = check_user_inited_result(*action_results); WOLFSENTRY_RERETURN_IF_ERROR(ret); WOLFSENTRY_ERROR_RERETURN(wolfsentry_route_event_dispatch_by_id_1(WOLFSENTRY_CONTEXT_ARGS_OUT, id, event_label, event_label_len, caller_arg, action_results)); } @@ -3073,6 +3103,8 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_event_dispatch_by_route( wolfsentry_action_res_t *action_results ) { + if (action_results == NULL) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); WOLFSENTRY_CLEAR_ALL_BITS(*action_results); WOLFSENTRY_ERROR_RERETURN(wolfsentry_route_event_dispatch_by_route_1(WOLFSENTRY_CONTEXT_ARGS_OUT, route, event_label, event_label_len, caller_arg, action_results)); } @@ -3086,7 +3118,10 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_event_dispatch_by_route_wit wolfsentry_action_res_t *action_results ) { - wolfsentry_errcode_t ret = check_user_inited_result(*action_results); + wolfsentry_errcode_t ret; + if (action_results == NULL) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); + ret = check_user_inited_result(*action_results); WOLFSENTRY_RERETURN_IF_ERROR(ret); WOLFSENTRY_ERROR_RERETURN(wolfsentry_route_event_dispatch_by_route_1(WOLFSENTRY_CONTEXT_ARGS_OUT, route, event_label, event_label_len, caller_arg, action_results)); } diff --git a/tests/unittests.c b/tests/unittests.c index 33d1650..29efd5a 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3976,6 +3976,18 @@ static wolfsentry_errcode_t test_regression_route_api_invariants(void) { WOLFSENTRY_EXIT_ON_FALSE(addr_fmt_len == 5); WOLFSENTRY_EXIT_ON_FALSE(addr_fmt_buf[2] == ':'); + /* a null action_results must be rejected up front, not dereferenced while + * clearing it. */ + WOLFSENTRY_EXIT_ON_FALSE( + WOLFSENTRY_ERROR_CODE_IS( + wolfsentry_route_event_dispatch( + WOLFSENTRY_CONTEXT_ARGS_OUT, + &remote.sa, &local.sa, WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN, + NULL /* event_label */, 0 /* event_label_len */, + NULL /* caller_arg */, + &id, &inexact_matches, NULL /* action_results */), + INVALID_ARG)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From fbcbeec72a2803f634b52541a147926220934848 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:57:08 -0700 Subject: [PATCH 18/43] F-10494 - https://fenrir.wolfssl.com/finding/10494 - Match route flag names by exact length to avoid reading past the name --- src/routes.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/routes.c b/src/routes.c index 3681459..a17a213 100644 --- a/src/routes.c +++ b/src/routes.c @@ -4123,8 +4123,10 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_flag_assoc_by_name(const ch if (len < 0) len = (int)strlen(name); for (i = wolfsentry_route_flag_names; i < end_ptr_of_array(wolfsentry_route_flag_names); ++i) { - if ((strncmp(i->name, name, (size_t)len) == 0) && - (i->name[len] == 0)) + /* compare lengths first so a shared NUL does not lead to reading + * i->name[len] past the end of the name literal. */ + if ((strlen(i->name) == (size_t)len) && + (strncmp(i->name, name, (size_t)len) == 0)) { *flag = i->flag; WOLFSENTRY_RETURN_OK; From d4799e62fd5b35cb51922df11e0cd1091c25a9bd Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:57:35 -0700 Subject: [PATCH 19/43] F-11605 - https://fenrir.wolfssl.com/finding/11605 - Free and null a new route when taking its parent event reference fails --- src/routes.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/routes.c b/src/routes.c index a17a213..d8439cc 100644 --- a/src/routes.c +++ b/src/routes.c @@ -1003,6 +1003,10 @@ static wolfsentry_errcode_t wolfsentry_route_new( } else { if (parent_event != NULL) { WOLFSENTRY_REFCOUNT_INCREMENT(parent_event->header.refcount, ret); + if (ret < 0) { + wolfsentry_route_free_1(WOLFSENTRY_CONTEXT_ARGS_OUT, config->config.route_private_data_alignment, *new); + *new = NULL; + } } } @@ -1062,6 +1066,10 @@ static wolfsentry_errcode_t wolfsentry_route_new_by_exports( } else { if (parent_event != NULL) { WOLFSENTRY_REFCOUNT_INCREMENT(parent_event->header.refcount, ret); + if (ret < 0) { + wolfsentry_route_free_1(WOLFSENTRY_CONTEXT_ARGS_OUT, config->config.route_private_data_alignment, *new); + *new = NULL; + } } } From 47ac8129151f8b8c6a75459cea90198247318d57 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 10:57:58 -0700 Subject: [PATCH 20/43] F-10493 - https://fenrir.wolfssl.com/finding/10493 - Read the cloned route address from the source address offset --- src/routes.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes.c b/src/routes.c index d8439cc..c896362 100644 --- a/src/routes.c +++ b/src/routes.c @@ -1172,8 +1172,9 @@ WOLFSENTRY_LOCAL wolfsentry_errcode_t wolfsentry_route_clone( /* now zero the private data section. */ memset((*new_route)->data, 0, config->config.route_private_data_size); - /* finally, copy the address section, carefully. */ - memcpy((byte *)(*new_route)->data + (*new_route)->data_addr_offset, src_route->data, src_route->data_addr_size); + /* finally, copy the address section, carefully, reading it from the + * source's own address offset rather than the start of its data. */ + memcpy((byte *)(*new_route)->data + (*new_route)->data_addr_offset, (const byte *)src_route->data + src_route->data_addr_offset, src_route->data_addr_size); } WOLFSENTRY_TABLE_ENT_HEADER_RESET(**new_ent); From 30e1b350d051a0ba3b59ce0f13ebdb967e93376a Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 11:00:03 -0700 Subject: [PATCH 21/43] F-15469 - https://fenrir.wolfssl.com/finding/15469 - Compact the address buffer when wildcarding the remote address --- src/routes.c | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/routes.c b/src/routes.c index c896362..f722af2 100644 --- a/src/routes.c +++ b/src/routes.c @@ -3602,11 +3602,24 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_set_wildcard( } if (wildcards_to_set & WOLFSENTRY_ROUTE_FLAG_SA_REMOTE_ADDR_WILDCARD) { route->flags |= WOLFSENTRY_ROUTE_FLAG_SA_REMOTE_ADDR_WILDCARD; - route->remote.addr_len = 0; + if (route->remote.addr_len > 0) { + /* the local address is stored immediately after the remote address, + * so dropping the remote address requires sliding the local bytes + * down to keep WOLFSENTRY_ROUTE_LOCAL_ADDR() valid. */ + if (route->local.addr_len > 0) + memmove(WOLFSENTRY_ROUTE_REMOTE_ADDR(route), + WOLFSENTRY_ROUTE_LOCAL_ADDR(route), + WOLFSENTRY_ROUTE_LOCAL_ADDR_BYTES(route)); + route->remote.addr_len = 0; + route->data_addr_size = (uint16_t)WOLFSENTRY_ROUTE_LOCAL_ADDR_BYTES(route); + } } if (wildcards_to_set & WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_ADDR_WILDCARD) { route->flags |= WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_ADDR_WILDCARD; - route->local.addr_len = 0; + if (route->local.addr_len > 0) { + route->local.addr_len = 0; + route->data_addr_size = (uint16_t)WOLFSENTRY_ROUTE_REMOTE_ADDR_BYTES(route); + } } if (wildcards_to_set & WOLFSENTRY_ROUTE_FLAG_SA_PROTO_WILDCARD) { route->flags |= WOLFSENTRY_ROUTE_FLAG_SA_PROTO_WILDCARD; From 69944c0a7f82c130f0530e525ec0dede467bb389 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 11:00:28 -0700 Subject: [PATCH 22/43] F-8766 - https://fenrir.wolfssl.com/finding/8766 - Copy the max purgeable idle time when cloning a route table header --- src/routes.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes.c b/src/routes.c index f722af2..41b97e4 100644 --- a/src/routes.c +++ b/src/routes.c @@ -4949,6 +4949,8 @@ WOLFSENTRY_LOCAL wolfsentry_errcode_t wolfsentry_route_table_clone_header( ((struct wolfsentry_route_table *)dest_table)->max_purgeable_routes = ((struct wolfsentry_route_table *)src_table)->max_purgeable_routes; + ((struct wolfsentry_route_table *)dest_table)->max_purgeable_idle_time = + ((struct wolfsentry_route_table *)src_table)->max_purgeable_idle_time; ((struct wolfsentry_route_table *)dest_table)->default_policy = ((struct wolfsentry_route_table *)src_table)->default_policy; From 94bc17257b0f0f05bd90da201eb786694b2db600 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 11:00:50 -0700 Subject: [PATCH 23/43] F-12516 - https://fenrir.wolfssl.com/finding/12516 - Reference the fallthrough route before publishing it into the cloned table --- src/routes.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/routes.c b/src/routes.c index 41b97e4..338cae8 100644 --- a/src/routes.c +++ b/src/routes.c @@ -4975,14 +4975,17 @@ WOLFSENTRY_LOCAL wolfsentry_errcode_t wolfsentry_route_table_clone_header( } if (((struct wolfsentry_route_table *)src_table)->fallthrough_route != NULL) { + /* take the source reference before releasing the destination's own + * route, so a failed increment leaves the destination route intact + * rather than a freed, still-published pointer. */ + WOLFSENTRY_REFCOUNT_INCREMENT(((struct wolfsentry_route_table *)src_table)->fallthrough_route->header.refcount, ret); + WOLFSENTRY_RERETURN_IF_ERROR(ret); if (((struct wolfsentry_route_table *)dest_table)->fallthrough_route != NULL) { WOLFSENTRY_WARN_ON_FAILURE(wolfsentry_route_drop_reference_1( WOLFSENTRY_CONTEXT_ARGS_OUT_EX(dest_context), ((struct wolfsentry_route_table *)dest_table)->fallthrough_route, NULL /* action_results */)); } ((struct wolfsentry_route_table *)dest_table)->fallthrough_route = ((struct wolfsentry_route_table *)src_table)->fallthrough_route; - WOLFSENTRY_REFCOUNT_INCREMENT(((struct wolfsentry_route_table *)dest_table)->fallthrough_route->header.refcount, ret); - WOLFSENTRY_RERETURN_IF_ERROR(ret); } if (WOLFSENTRY_CHECK_BITS(flags, WOLFSENTRY_CLONE_FLAG_NO_ROUTES)) From 1d577b27e8afa7ec760b3811d694ce7cc9cc512c Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 11:01:11 -0700 Subject: [PATCH 24/43] F-10489 - https://fenrir.wolfssl.com/finding/10489 - Carry the route hit count across a metadata copy --- src/routes.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes.c b/src/routes.c index 338cae8..3febb21 100644 --- a/src/routes.c +++ b/src/routes.c @@ -5039,6 +5039,7 @@ WOLFSENTRY_LOCAL wolfsentry_errcode_t wolfsentry_route_copy_metadata( wolfsentry_list_ent_delete(&to_table->purge_list, &to_i->purge_links); to_i->flags = from_i->flags; to_i->meta = from_i->meta; + to_i->header.hitcount = from_i->header.hitcount; if (to_i->meta.purge_after) wolfsentry_route_purge_list_insert(to_table, to_i); } else { From 5ef63434ea42bf6ea116ad2334233bd887fd6dde Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 13:38:42 -0700 Subject: [PATCH 25/43] F-3667 - https://fenrir.wolfssl.com/finding/3667 - Accept a reset default policy and carry the port reset into fallthrough verdicts --- src/routes.c | 4 ++++ tests/unittests.c | 25 +++++++++++++++++++++++++ wolfsentry/wolfsentry.h | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/routes.c b/src/routes.c index 3febb21..66d8b44 100644 --- a/src/routes.c +++ b/src/routes.c @@ -2016,6 +2016,10 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_table_default_policy_set( WOLFSENTRY_ERROR_RETURN(INVALID_ARG); if (WOLFSENTRY_CHECK_BITS(default_policy, WOLFSENTRY_ACTION_RES_ACCEPT | WOLFSENTRY_ACTION_RES_REJECT)) WOLFSENTRY_ERROR_RETURN(INVALID_ARG); + /* a port reset only qualifies a reject verdict. */ + if (WOLFSENTRY_MASKIN_BITS(default_policy, WOLFSENTRY_ACTION_RES_PORT_RESET) && + (! WOLFSENTRY_MASKIN_BITS(default_policy, WOLFSENTRY_ACTION_RES_REJECT))) + WOLFSENTRY_ERROR_RETURN(INVALID_ARG); WOLFSENTRY_MUTEX_OR_RETURN(); table->default_policy = default_policy; if (table == wolfsentry->routes) diff --git a/tests/unittests.c b/tests/unittests.c index 29efd5a..f74e7fa 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3988,6 +3988,31 @@ static wolfsentry_errcode_t test_regression_route_api_invariants(void) { &id, &inexact_matches, NULL /* action_results */), INVALID_ARG)); + /* a "reset" default policy (reject plus port reset) is accepted and carried + * into unmatched verdicts; a port reset cannot qualify an accept. */ + WOLFSENTRY_EXIT_ON_FALSE( + WOLFSENTRY_ERROR_CODE_IS( + wolfsentry_route_default_policy_set( + WOLFSENTRY_CONTEXT_ARGS_OUT, + WOLFSENTRY_ACTION_RES_ACCEPT | WOLFSENTRY_ACTION_RES_PORT_RESET), + INVALID_ARG)); + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_default_policy_set( + WOLFSENTRY_CONTEXT_ARGS_OUT, + WOLFSENTRY_ACTION_RES_REJECT | WOLFSENTRY_ACTION_RES_PORT_RESET)); + memcpy(remote.sa.addr, "\x0A\x63\x63\x63", 4); /* NOLINT(bugprone-not-null-terminated-result) */ + WOLFSENTRY_CLEAR_ALL_BITS(action_results); + WOLFSENTRY_EXIT_ON_FALSE( + WOLFSENTRY_SUCCESS_CODE_IS( + wolfsentry_route_event_dispatch( + WOLFSENTRY_CONTEXT_ARGS_OUT, + &remote.sa, &local.sa, WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN, + NULL /* event_label */, 0 /* event_label_len */, + NULL /* caller_arg */, + &id, &inexact_matches, &action_results), + USED_FALLBACK)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT | WOLFSENTRY_ACTION_RES_PORT_RESET)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; diff --git a/wolfsentry/wolfsentry.h b/wolfsentry/wolfsentry.h index 225980b..640ebdf 100644 --- a/wolfsentry/wolfsentry.h +++ b/wolfsentry/wolfsentry.h @@ -1003,7 +1003,7 @@ typedef wolfsentry_errcode_t (*wolfsentry_action_callback_t)( * @{ */ -#define WOLFSENTRY_ROUTE_DEFAULT_POLICY_MASK (WOLFSENTRY_ACTION_RES_ACCEPT | WOLFSENTRY_ACTION_RES_REJECT | WOLFSENTRY_ACTION_RES_STOP | WOLFSENTRY_ACTION_RES_ERROR) +#define WOLFSENTRY_ROUTE_DEFAULT_POLICY_MASK (WOLFSENTRY_ACTION_RES_ACCEPT | WOLFSENTRY_ACTION_RES_REJECT | WOLFSENTRY_ACTION_RES_STOP | WOLFSENTRY_ACTION_RES_ERROR | WOLFSENTRY_ACTION_RES_PORT_RESET) /*!< \brief Bit mask spanning the bits allowed by wolfsentry_route_table_default_policy_set() */ /*! \brief bit field specifying attributes of a route/rule */ From f60740acbbb996f5aba39f02534cf6532709bf41 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 13:41:25 -0700 Subject: [PATCH 26/43] F-2841 - https://fenrir.wolfssl.com/finding/2841 - Test that dispatch rejects caller-initialized verdict and status bits --- tests/unittests.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/unittests.c b/tests/unittests.c index f74e7fa..c8e2b59 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3842,6 +3842,14 @@ static wolfsentry_errcode_t test_regression_route_api_invariants(void) { char addr_fmt_buf[32]; int addr_fmt_len; byte link_addr[2]; + size_t i; + static const wolfsentry_action_res_t forbidden_inited[] = { + WOLFSENTRY_ACTION_RES_ACCEPT, + WOLFSENTRY_ACTION_RES_REJECT, + WOLFSENTRY_ACTION_RES_STOP, + WOLFSENTRY_ACTION_RES_DEALLOCATED, + WOLFSENTRY_ACTION_RES_ERROR + }; struct { struct wolfsentry_sockaddr sa; byte addr_buf[4]; @@ -4013,6 +4021,20 @@ static wolfsentry_errcode_t test_regression_route_api_invariants(void) { USED_FALLBACK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT | WOLFSENTRY_ACTION_RES_PORT_RESET)); + /* a caller may not pre-decide the verdict or pre-set internal status bits. */ + for (i = 0; i < sizeof forbidden_inited / sizeof forbidden_inited[0]; ++i) { + action_results = forbidden_inited[i]; + WOLFSENTRY_EXIT_ON_FALSE( + WOLFSENTRY_ERROR_CODE_IS( + wolfsentry_route_event_dispatch_with_inited_result( + WOLFSENTRY_CONTEXT_ARGS_OUT, + &remote.sa, &local.sa, WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN, + NULL /* event_label */, 0 /* event_label_len */, + NULL /* caller_arg */, + &id, &inexact_matches, &action_results), + INVALID_ARG)); + } + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 765b15d61102a8112880315c9a6769db742c8a64 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 13:42:36 -0700 Subject: [PATCH 27/43] F-2465 - https://fenrir.wolfssl.com/finding/2465 - Test the exact stale purge time and idle limit boundaries --- tests/unittests.c | 179 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/tests/unittests.c b/tests/unittests.c index c8e2b59..5b2fddd 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4040,6 +4040,184 @@ static wolfsentry_errcode_t test_regression_route_api_invariants(void) { WOLFSENTRY_RETURN_OK; } +/* insert an inbound TCP/IPv4 route with wildcarded local side and ports; a NULL + * remote_addr leaves the remote address empty (pair with the address wildcard). */ +static wolfsentry_errcode_t regression_insert( + WOLFSENTRY_CONTEXT_ARGS_IN, + const char *remote_addr, + unsigned int remote_prefix_bits, + const char *event_label, + wolfsentry_route_flags_t extra_flags, + wolfsentry_ent_id_t *id) +{ + wolfsentry_action_res_t action_results; + struct { + struct wolfsentry_sockaddr sa; + byte addr_buf[4]; + } remote, local; + + memset(&remote, 0, sizeof remote); + memset(&local, 0, sizeof local); + remote.sa.sa_family = local.sa.sa_family = AF_INET; + remote.sa.sa_proto = local.sa.sa_proto = IPPROTO_TCP; + if (remote_addr != NULL) { + remote.sa.addr_len = (wolfsentry_addr_bits_t)remote_prefix_bits; + memcpy(remote.sa.addr, remote_addr, sizeof remote.addr_buf); + } + + return wolfsentry_route_insert( + WOLFSENTRY_CONTEXT_ARGS_OUT, + NULL /* caller_arg */, + &remote.sa, &local.sa, + extra_flags + | WOLFSENTRY_ROUTE_FLAG_TCPLIKE_PORT_NUMBERS + | WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN + | WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_ADDR_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_PORT_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_SA_REMOTE_PORT_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_LOCAL_INTERFACE_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_REMOTE_INTERFACE_WILDCARD, + event_label, event_label ? -1 : 0, + id, &action_results); +} + +/* dispatch an inbound TCP/IPv4 packet from a 4-byte remote address, with the + * caller-initialized result bits in inited_results. */ +static wolfsentry_errcode_t regression_dispatch( + WOLFSENTRY_CONTEXT_ARGS_IN, + const char *remote_addr, + wolfsentry_action_res_t inited_results, + wolfsentry_ent_id_t *id, + wolfsentry_action_res_t *action_results) +{ + wolfsentry_route_flags_t inexact_matches; + struct { + struct wolfsentry_sockaddr sa; + byte addr_buf[4]; + } remote, local; + + memset(&remote, 0, sizeof remote); + memset(&local, 0, sizeof local); + remote.sa.sa_family = local.sa.sa_family = AF_INET; + remote.sa.sa_proto = local.sa.sa_proto = IPPROTO_TCP; + remote.sa.addr_len = 32; + memcpy(remote.sa.addr, remote_addr, sizeof remote.addr_buf); + *action_results = inited_results; + + return wolfsentry_route_event_dispatch_with_inited_result( + WOLFSENTRY_CONTEXT_ARGS_OUT, + &remote.sa, &local.sa, WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN, + NULL /* event_label */, 0 /* event_label_len */, + NULL /* caller_arg */, + id, &inexact_matches, action_results); +} + +static wolfsentry_time_t regression_fake_now; + +static wolfsentry_errcode_t regression_fake_get_time(void *context, wolfsentry_time_t *now) { + (void)context; + *now = regression_fake_now; + WOLFSENTRY_RETURN_OK; +} + +static wolfsentry_errcode_t regression_route_meta( + WOLFSENTRY_CONTEXT_ARGS_IN, + wolfsentry_ent_id_t id, + struct wolfsentry_route_metadata_exports *meta) +{ + struct wolfsentry_route *route; + wolfsentry_errcode_t ret, unlock_ret; + + ret = wolfsentry_context_lock_shared(WOLFSENTRY_CONTEXT_ARGS_OUT); + if (ret < 0) + return ret; + ret = wolfsentry_table_ent_get_by_id(WOLFSENTRY_CONTEXT_ARGS_OUT, id, (struct wolfsentry_table_ent_header **)&route); + if (ret >= 0) + ret = wolfsentry_route_get_metadata(route, meta); + unlock_ret = wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT); + if (ret >= 0) + ret = unlock_ret; + return ret; +} + +static wolfsentry_errcode_t regression_set_purge_time( + WOLFSENTRY_CONTEXT_ARGS_IN, + wolfsentry_ent_id_t id, + wolfsentry_time_t purge_after) +{ + struct wolfsentry_route *route; + wolfsentry_errcode_t ret, unlock_ret; + + ret = wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT); + if (ret < 0) + return ret; + ret = wolfsentry_table_ent_get_by_id(WOLFSENTRY_CONTEXT_ARGS_OUT, id, (struct wolfsentry_table_ent_header **)&route); + if (ret >= 0) + ret = wolfsentry_route_purge_time_set(WOLFSENTRY_CONTEXT_ARGS_OUT, route, purge_after); + unlock_ret = wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT); + if (ret >= 0) + ret = unlock_ret; + return ret; +} + +static wolfsentry_errcode_t test_regression_route_timing(void) { + struct wolfsentry_context *wolfsentry; + struct wolfsentry_route_table *table; + struct wolfsentry_route_metadata_exports meta; + wolfsentry_action_res_t action_results; + wolfsentry_ent_id_t id, matched_id; +#ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS + static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT, .max_connection_count = 10 }; +#else + static const struct wolfsentry_eventconfig config = { PRIVATE_DATA_SIZE, PRIVATE_DATA_ALIGNMENT, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +#endif + + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_init_ex( + wolfsentry_build_settings, + WOLFSENTRY_CONTEXT_ARGS_OUT_EX(WOLFSENTRY_TEST_HPI), + &config, + &wolfsentry, + WOLFSENTRY_INIT_FLAG_NONE)); + + /* drive the context clock by hand so time boundaries are exact. */ + regression_fake_now = 1000000; + wolfsentry_get_timecbs(wolfsentry)->get_time = regression_fake_get_time; + + /* a route expiring one tick from now survives a purge, and is purged once + * the clock reaches its purge time. */ + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x01", 32, NULL, WOLFSENTRY_ROUTE_FLAG_NONE, &id)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_set_purge_time(WOLFSENTRY_CONTEXT_ARGS_OUT, id, regression_fake_now + 1)); + WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE(ALREADY, wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + ++regression_fake_now; + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL)); + WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE(ITEM_NOT_FOUND, regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + + /* an expired route with a live connection idle for exactly the limit is + * kept; one tick longer and it is purged. */ + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_get_main_table(WOLFSENTRY_CONTEXT_ARGS_OUT, &table)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_table_max_purgeable_idle_time_set(WOLFSENTRY_CONTEXT_ARGS_OUT, table, 100)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x02", 32, NULL, WOLFSENTRY_ROUTE_FLAG_NONE, &id)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x02", WOLFSENTRY_ACTION_RES_CONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_set_purge_time(WOLFSENTRY_CONTEXT_ARGS_OUT, id, regression_fake_now)); + regression_fake_now += 100; + WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE(ALREADY, wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + WOLFSENTRY_EXIT_ON_FALSE(meta.connection_count == 1); + ++regression_fake_now; + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL)); + WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE(ITEM_NOT_FOUND, regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + WOLFSENTRY_RETURN_OK; +} + static int test_dynamic_rules_regressions(void) { #ifdef WOLFSENTRY_ADDR_BITMASK_MATCHING WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_bitmask_match_specificity(), OK)); @@ -4048,6 +4226,7 @@ static int test_dynamic_rules_regressions(void) { WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_dispatch_error_not_masked(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_copy_metadata_purge_list(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_route_api_invariants(), OK)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_route_timing(), OK)); WOLFSENTRY_RETURN_OK; } From 13d9d8d4456601952838813401de9213edcc0c95 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 13:43:09 -0700 Subject: [PATCH 28/43] F-2459 - https://fenrir.wolfssl.com/finding/2459 - Test that routes exempt from connection counting are never limited --- tests/unittests.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/unittests.c b/tests/unittests.c index 5b2fddd..489c87b 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4218,6 +4218,49 @@ static wolfsentry_errcode_t test_regression_route_timing(void) { WOLFSENTRY_RETURN_OK; } +static wolfsentry_errcode_t test_regression_connection_limits(void) { + struct wolfsentry_context *wolfsentry; + struct wolfsentry_route_metadata_exports meta; + struct wolfsentry_eventconfig limit_config; + wolfsentry_action_res_t action_results; + wolfsentry_ent_id_t id, matched_id; + unsigned int i; +#ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS + static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT }; +#else + static const struct wolfsentry_eventconfig config = { PRIVATE_DATA_SIZE, PRIVATE_DATA_ALIGNMENT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +#endif + + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_init_ex( + wolfsentry_build_settings, + WOLFSENTRY_CONTEXT_ARGS_OUT_EX(WOLFSENTRY_TEST_HPI), + &config, + &wolfsentry, + WOLFSENTRY_INIT_FLAG_NONE)); + + memset(&limit_config, 0, sizeof limit_config); + limit_config.route_private_data_size = PRIVATE_DATA_SIZE; + limit_config.route_private_data_alignment = PRIVATE_DATA_ALIGNMENT; + limit_config.max_connection_count = 2; + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_event_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "limit2", -1, 10, &limit_config, WOLFSENTRY_EVENT_FLAG_NONE, &matched_id)); + + /* a route that opts out of connection counting is never limited. */ + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x08\x01", 32, "limit2", WOLFSENTRY_ROUTE_FLAG_DONT_COUNT_CURRENT_CONNECTIONS, &id)); + for (i = 0; i < 3; ++i) { + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x08\x01", WOLFSENTRY_ACTION_RES_CONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + } + WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + WOLFSENTRY_EXIT_ON_FALSE(meta.connection_count == 0); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + WOLFSENTRY_RETURN_OK; +} + static int test_dynamic_rules_regressions(void) { #ifdef WOLFSENTRY_ADDR_BITMASK_MATCHING WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_bitmask_match_specificity(), OK)); @@ -4227,6 +4270,7 @@ static int test_dynamic_rules_regressions(void) { WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_copy_metadata_purge_list(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_route_api_invariants(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_route_timing(), OK)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_connection_limits(), OK)); WOLFSENTRY_RETURN_OK; } From d6d60012327c09c35f32126b52b86d82bd3b70ec Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 13:43:32 -0700 Subject: [PATCH 29/43] F-6258 - https://fenrir.wolfssl.com/finding/6258 - Test connection limit enforcement and slot reuse at a positive limit --- tests/unittests.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/unittests.c b/tests/unittests.c index 489c87b..7423ca4 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4256,6 +4256,23 @@ static wolfsentry_errcode_t test_regression_connection_limits(void) { WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); WOLFSENTRY_EXIT_ON_FALSE(meta.connection_count == 0); + /* a counted route admits exactly the limit, rejects the next connection + * without counting it, and reopens a slot after a disconnect. */ + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x08\x02", 32, "limit2", WOLFSENTRY_ROUTE_FLAG_NONE, &id)); + for (i = 0; i < 2; ++i) { + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x08\x02", WOLFSENTRY_ACTION_RES_CONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + } + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x08\x02", WOLFSENTRY_ACTION_RES_CONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + WOLFSENTRY_EXIT_ON_FALSE(meta.connection_count == 2); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x08\x02", WOLFSENTRY_ACTION_RES_DISCONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x08\x02", WOLFSENTRY_ACTION_RES_CONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + WOLFSENTRY_EXIT_ON_FALSE(meta.connection_count == 2); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From e61719c0420ecbebc7a831cba539d9cc12d612c7 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 13:44:11 -0700 Subject: [PATCH 30/43] F-5565 - https://fenrir.wolfssl.com/finding/5565 - Test that the most specific of equal-priority overlapping routes wins --- tests/unittests.c | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/unittests.c b/tests/unittests.c index 7423ca4..ebf22f9 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4278,6 +4278,42 @@ static wolfsentry_errcode_t test_regression_connection_limits(void) { WOLFSENTRY_RETURN_OK; } +static wolfsentry_errcode_t test_regression_prefix_specificity(void) { + struct wolfsentry_context *wolfsentry; + wolfsentry_action_res_t action_results; + wolfsentry_ent_id_t broad_id, narrow_id, matched_id; +#ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS + static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT }; +#else + static const struct wolfsentry_eventconfig config = { PRIVATE_DATA_SIZE, PRIVATE_DATA_ALIGNMENT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +#endif + + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_init_ex( + wolfsentry_build_settings, + WOLFSENTRY_CONTEXT_ARGS_OUT_EX(WOLFSENTRY_TEST_HPI), + &config, + &wolfsentry, + WOLFSENTRY_INIT_FLAG_NONE)); + + /* equal-priority overlapping prefixes: the longer match must win inside + * the /25, and the /24 must still match outside it. */ + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\xC0\xA8\x01\x00", 24, NULL, WOLFSENTRY_ROUTE_FLAG_GREENLISTED, &broad_id)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\xC0\xA8\x01\x80", 25, NULL, WOLFSENTRY_ROUTE_FLAG_PENALTYBOXED, &narrow_id)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\xC0\xA8\x01\xC8", WOLFSENTRY_ACTION_RES_NONE, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FALSE(matched_id == narrow_id); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\xC0\xA8\x01\x0A", WOLFSENTRY_ACTION_RES_NONE, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FALSE(matched_id == broad_id); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_ACCEPT)); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + WOLFSENTRY_RETURN_OK; +} + static int test_dynamic_rules_regressions(void) { #ifdef WOLFSENTRY_ADDR_BITMASK_MATCHING WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_bitmask_match_specificity(), OK)); @@ -4288,6 +4324,7 @@ static int test_dynamic_rules_regressions(void) { WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_route_api_invariants(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_route_timing(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_connection_limits(), OK)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_prefix_specificity(), OK)); WOLFSENTRY_RETURN_OK; } From 3c42f5361fb9c11f9b916ad11ea69542f636351e Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 13:45:01 -0700 Subject: [PATCH 31/43] F-2456 - https://fenrir.wolfssl.com/finding/2456 - Test the exact penalty box duration boundary --- tests/unittests.c | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/unittests.c b/tests/unittests.c index ebf22f9..60f60bc 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4160,6 +4160,24 @@ static wolfsentry_errcode_t regression_set_purge_time( return ret; } +static wolfsentry_errcode_t regression_insert_pbox_event( + WOLFSENTRY_CONTEXT_ARGS_IN, + const char *label, + wolfsentry_hitcount_t derog_threshold, + wolfsentry_time_t penaltybox_duration, + wolfsentry_eventconfig_flags_t flags) +{ + struct wolfsentry_eventconfig event_config; + + memset(&event_config, 0, sizeof event_config); + event_config.route_private_data_size = PRIVATE_DATA_SIZE; + event_config.route_private_data_alignment = PRIVATE_DATA_ALIGNMENT; + event_config.derogatory_threshold_for_penaltybox = derog_threshold; + event_config.penaltybox_duration = penaltybox_duration; + event_config.flags = flags; + return wolfsentry_event_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, label, -1, 10, &event_config, WOLFSENTRY_EVENT_FLAG_NONE, NULL /* id */); +} + static wolfsentry_errcode_t test_regression_route_timing(void) { struct wolfsentry_context *wolfsentry; struct wolfsentry_route_table *table; @@ -4213,6 +4231,21 @@ static wolfsentry_errcode_t test_regression_route_timing(void) { WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL)); WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE(ITEM_NOT_FOUND, regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + /* a route penalty-boxed by its second derogatory event stays boxed at + * exactly the box duration, and is released one tick later. */ + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert_pbox_event(WOLFSENTRY_CONTEXT_ARGS_OUT, "pbox", 2, 50, WOLFSENTRY_EVENTCONFIG_FLAG_NONE)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x03", 32, "pbox", WOLFSENTRY_ROUTE_FLAG_NONE, &id)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x03", WOLFSENTRY_ACTION_RES_DEROGATORY, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x03", WOLFSENTRY_ACTION_RES_DEROGATORY, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + regression_fake_now += 50; + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x03", WOLFSENTRY_ACTION_RES_NONE, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + ++regression_fake_now; + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x03", WOLFSENTRY_ACTION_RES_NONE, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 5f7714104c5c074466472beb6d3915769593742f Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 13:45:36 -0700 Subject: [PATCH 32/43] F-2461 - https://fenrir.wolfssl.com/finding/2461 - Test that a zero derogatory threshold disables penalty boxing --- tests/unittests.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unittests.c b/tests/unittests.c index 60f60bc..c7ba834 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4184,6 +4184,7 @@ static wolfsentry_errcode_t test_regression_route_timing(void) { struct wolfsentry_route_metadata_exports meta; wolfsentry_action_res_t action_results; wolfsentry_ent_id_t id, matched_id; + unsigned int i; #ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT, .max_connection_count = 10 }; #else @@ -4246,6 +4247,15 @@ static wolfsentry_errcode_t test_regression_route_timing(void) { WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x03", WOLFSENTRY_ACTION_RES_NONE, &matched_id, &action_results)); WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + /* a zero derogatory threshold disables penalty boxing entirely. */ + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert_pbox_event(WOLFSENTRY_CONTEXT_ARGS_OUT, "nothresh", 0, 50, WOLFSENTRY_EVENTCONFIG_FLAG_NONE)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x04", 32, "nothresh", WOLFSENTRY_ROUTE_FLAG_GREENLISTED, &id)); + for (i = 0; i < 3; ++i) { + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x04", WOLFSENTRY_ACTION_RES_DEROGATORY, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_ACCEPT)); + WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + } + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 3c66f924bfa497c11d5f1e61853174f98d690c0d Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 13:46:08 -0700 Subject: [PATCH 33/43] F-2464 - https://fenrir.wolfssl.com/finding/2464 - Test permanent penalty boxing and boxes with no recorded time --- tests/unittests.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unittests.c b/tests/unittests.c index c7ba834..7a7faaf 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4256,6 +4256,20 @@ static wolfsentry_errcode_t test_regression_route_timing(void) { WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); } + /* a zero box duration is permanent, and a route boxed at insert (no box + * time recorded) is never released by the timeout. */ + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert_pbox_event(WOLFSENTRY_CONTEXT_ARGS_OUT, "pboxperm", 2, 0, WOLFSENTRY_EVENTCONFIG_FLAG_NONE)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x05", 32, "pboxperm", WOLFSENTRY_ROUTE_FLAG_NONE, &id)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x06", 32, "pbox", WOLFSENTRY_ROUTE_FLAG_PENALTYBOXED, &id)); + for (i = 0; i < 2; ++i) + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x05", WOLFSENTRY_ACTION_RES_DEROGATORY, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + regression_fake_now += 1000000; + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x05", WOLFSENTRY_ACTION_RES_NONE, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x06", WOLFSENTRY_ACTION_RES_NONE, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 7932ad9bd558ec76006b8c948066331364c41e79 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 22 Oct 2026 13:46:40 -0700 Subject: [PATCH 34/43] F-3663 - https://fenrir.wolfssl.com/finding/3663 - Test the penalty box trigger when commendable events are ignored --- tests/unittests.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unittests.c b/tests/unittests.c index 7a7faaf..fc51fb0 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4270,6 +4270,17 @@ static wolfsentry_errcode_t test_regression_route_timing(void) { WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x06", WOLFSENTRY_ACTION_RES_NONE, &matched_id, &action_results)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + /* with commendable events ignored, reaching the derogatory threshold boxes + * the route even though commendable events would otherwise offset it. */ + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert_pbox_event(WOLFSENTRY_CONTEXT_ARGS_OUT, "ignorecommend", 2, 0, WOLFSENTRY_EVENTCONFIG_FLAG_DEROGATORY_THRESHOLD_IGNORE_COMMENDABLE)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x07", 32, "ignorecommend", WOLFSENTRY_ROUTE_FLAG_NONE, &id)); + for (i = 0; i < 2; ++i) + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x07", WOLFSENTRY_ACTION_RES_COMMENDABLE, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x07", WOLFSENTRY_ACTION_RES_DEROGATORY, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x05\x05\x07", WOLFSENTRY_ACTION_RES_DEROGATORY, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 4a047a2b75c8fca2ebe93fb6ea1679e7a1c36dee Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 09:51:25 -0700 Subject: [PATCH 35/43] F-13287 - https://fenrir.wolfssl.com/finding/13287 - Count every connection so disconnects stay balanced across limit changes --- src/routes.c | 40 +++++++++++++++++++++----------------- tests/unittests.c | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/routes.c b/src/routes.c index 66d8b44..e023b1a 100644 --- a/src/routes.c +++ b/src/routes.c @@ -2550,28 +2550,32 @@ static wolfsentry_errcode_t wolfsentry_route_event_dispatch_0( if (! (current_rule_route_flags & WOLFSENTRY_ROUTE_FLAG_DONT_COUNT_CURRENT_CONNECTIONS)) { if (*action_results & WOLFSENTRY_ACTION_RES_CONNECT) { - /* the limit is only enforced when configured; a zero limit - * means unlimited, so no increment or rejection applies. */ - if (config->config.max_connection_count > 0) { - if (rule_route->meta.connection_count >= config->config.max_connection_count) { - *action_results |= WOLFSENTRY_ACTION_RES_REJECT; - ret = WOLFSENTRY_ERROR_ENCODE(OK); - goto done; - } - if (WOLFSENTRY_ATOMIC_INCREMENT_BY_ONE(rule_route->meta.connection_count) > config->config.max_connection_count) { + uint16_t new_connection_count; + /* count every connection so disconnects stay balanced when the + * limit changes; a zero limit only means nothing is rejected. */ + if ((config->config.max_connection_count > 0) && + (rule_route->meta.connection_count >= config->config.max_connection_count)) + { + *action_results |= WOLFSENTRY_ACTION_RES_REJECT; + ret = WOLFSENTRY_ERROR_ENCODE(OK); + goto done; + } + WOLFSENTRY_ATOMIC_INCREMENT_UNSIGNED_SAFELY_BY_ONE(rule_route->meta.connection_count, new_connection_count); + /* with no limit a saturated counter simply stops counting; under a + * limit it cannot track another connection, so refuse it. */ + if ((config->config.max_connection_count > 0) && + ((new_connection_count == 0) || (new_connection_count > config->config.max_connection_count))) + { + if (new_connection_count != 0) WOLFSENTRY_ATOMIC_DECREMENT_BY_ONE(rule_route->meta.connection_count); - *action_results |= WOLFSENTRY_ACTION_RES_REJECT; - ret = WOLFSENTRY_ERROR_ENCODE(OK); - goto done; - } + *action_results |= WOLFSENTRY_ACTION_RES_REJECT; + ret = WOLFSENTRY_ERROR_ENCODE(OK); + goto done; } } else if (*action_results & WOLFSENTRY_ACTION_RES_DISCONNECT) { uint16_t new_connection_count; - /* decrement regardless of the limit, so counts seeded at insert - * time still drain when the limit is zero (unlimited); decrement - * safely because untimely route deletions can assign a decrement - * to a different route than the one originally incremented. - */ + /* decrement safely: an untimely route deletion can assign a + * decrement to a different route than the one incremented. */ WOLFSENTRY_ATOMIC_DECREMENT_UNSIGNED_SAFELY(rule_route->meta.connection_count, 1, new_connection_count); /* only an already-zero count under an enforced limit is * anomalous; with no limit, unconnected routes legitimately see diff --git a/tests/unittests.c b/tests/unittests.c index fc51fb0..dfbd475 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4346,6 +4346,54 @@ static wolfsentry_errcode_t test_regression_connection_limits(void) { WOLFSENTRY_RETURN_OK; } +static wolfsentry_errcode_t test_regression_connection_count_zero_limit(void) { + struct wolfsentry_context *wolfsentry; + struct wolfsentry_route_metadata_exports meta; + struct wolfsentry_eventconfig limit_config; + wolfsentry_action_res_t action_results; + wolfsentry_ent_id_t id, matched_id; +#ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS + static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT }; +#else + static const struct wolfsentry_eventconfig config = { PRIVATE_DATA_SIZE, PRIVATE_DATA_ALIGNMENT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +#endif + + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_init_ex( + wolfsentry_build_settings, + WOLFSENTRY_CONTEXT_ARGS_OUT_EX(WOLFSENTRY_TEST_HPI), + &config, + &wolfsentry, + WOLFSENTRY_INIT_FLAG_NONE)); + + memset(&limit_config, 0, sizeof limit_config); + limit_config.route_private_data_size = PRIVATE_DATA_SIZE; + limit_config.route_private_data_alignment = PRIVATE_DATA_ALIGNMENT; + limit_config.max_connection_count = 2; + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_event_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "zerolimit", -1, 10, &limit_config, WOLFSENTRY_EVENT_FLAG_NONE, &matched_id)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x09\x01", 32, "zerolimit", WOLFSENTRY_ROUTE_FLAG_NONE, &id)); + + /* a connection opened under a limit stays counted after the limit is + * removed, and connections opened afterward are counted too. */ + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x09\x01", WOLFSENTRY_ACTION_RES_CONNECT, &matched_id, &action_results)); + limit_config.max_connection_count = 0; + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_event_update_config(WOLFSENTRY_CONTEXT_ARGS_OUT, "zerolimit", -1, &limit_config)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x09\x01", WOLFSENTRY_ACTION_RES_CONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x09\x01", WOLFSENTRY_ACTION_RES_DISCONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + WOLFSENTRY_EXIT_ON_FALSE(meta.connection_count == 1); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x09\x01", WOLFSENTRY_ACTION_RES_DISCONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + WOLFSENTRY_EXIT_ON_FALSE(meta.connection_count == 0); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + WOLFSENTRY_RETURN_OK; +} + static wolfsentry_errcode_t test_regression_prefix_specificity(void) { struct wolfsentry_context *wolfsentry; wolfsentry_action_res_t action_results; @@ -4392,6 +4440,7 @@ static int test_dynamic_rules_regressions(void) { WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_route_api_invariants(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_route_timing(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_connection_limits(), OK)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_connection_count_zero_limit(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_prefix_specificity(), OK)); WOLFSENTRY_RETURN_OK; } From f5ee3df4dd9c98790e405f4587527bbe9b61c6da Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 10:58:08 -0700 Subject: [PATCH 36/43] F-15468 - https://fenrir.wolfssl.com/finding/15468 - Clear a route's purge links before reinserting it into the purge list --- src/routes.c | 3 +++ tests/unittests.c | 50 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/routes.c b/src/routes.c index e023b1a..1cac77e 100644 --- a/src/routes.c +++ b/src/routes.c @@ -3148,6 +3148,9 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_route_event_dispatch_by_route_wit WOLFSENTRY_LOCAL_VOID wolfsentry_route_purge_list_insert(struct wolfsentry_route_table *route_table, struct wolfsentry_route *route_to_insert) { struct wolfsentry_list_ent_header *point_ent; + /* deletion leaves an entry's own links stale, and a head insert keeps prev */ + route_to_insert->purge_links.prev = NULL; + route_to_insert->purge_links.next = NULL; for (wolfsentry_list_ent_get_first(&route_table->purge_list, &point_ent); point_ent; point_ent = point_ent->next) { const struct wolfsentry_route *point_route = WOLFSENTRY_ROUTE_PURGE_HEADER_TO_TABLE_ENT_HEADER(point_ent); if (point_route->meta.purge_after < route_to_insert->meta.purge_after) diff --git a/tests/unittests.c b/tests/unittests.c index dfbd475..7e8ba88 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4394,6 +4394,55 @@ static wolfsentry_errcode_t test_regression_connection_count_zero_limit(void) { WOLFSENTRY_RETURN_OK; } +static wolfsentry_errcode_t test_regression_purge_list_reinsert(void) { + struct wolfsentry_context *wolfsentry; + struct wolfsentry_route_table *table; + struct wolfsentry_route_metadata_exports meta; + wolfsentry_action_res_t action_results; + wolfsentry_ent_id_t ids[3], matched_id; + static const char * const addrs[3] = { "\x0A\x08\x0A\x01", "\x0A\x08\x0A\x02", "\x0A\x08\x0A\x03" }; + int i; +#ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS + static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT }; +#else + static const struct wolfsentry_eventconfig config = { PRIVATE_DATA_SIZE, PRIVATE_DATA_ALIGNMENT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +#endif + + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_init_ex( + wolfsentry_build_settings, + WOLFSENTRY_CONTEXT_ARGS_OUT_EX(WOLFSENTRY_TEST_HPI), + &config, + &wolfsentry, + WOLFSENTRY_INIT_FLAG_NONE)); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_get_main_table(WOLFSENTRY_CONTEXT_ARGS_OUT, &table)); + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_table_max_purgeable_idle_time_set( + WOLFSENTRY_CONTEXT_ARGS_OUT, table, (wolfsentry_time_t)1 << 30)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + + /* three expired routes, each protected by a live connection */ + for (i = 0; i < 3; ++i) { + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, addrs[i], 32, NULL, WOLFSENTRY_ROUTE_FLAG_NONE, &ids[i])); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, addrs[i], WOLFSENTRY_ACTION_RES_CONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_set_purge_time(WOLFSENTRY_CONTEXT_ARGS_OUT, ids[i], (wolfsentry_time_t)(10 * (i + 1)))); + } + /* moving the stalest entry to the head must not leave a stale back link, + * so a scan that visits every protected entry still terminates. */ + WOLFSENTRY_EXIT_ON_FAILURE(regression_set_purge_time(WOLFSENTRY_CONTEXT_ARGS_OUT, ids[0], 40)); + WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE(ALREADY, wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL)); + for (i = 0; i < 3; ++i) + WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, ids[i], &meta)); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + WOLFSENTRY_RETURN_OK; +} + static wolfsentry_errcode_t test_regression_prefix_specificity(void) { struct wolfsentry_context *wolfsentry; wolfsentry_action_res_t action_results; @@ -4441,6 +4490,7 @@ static int test_dynamic_rules_regressions(void) { WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_route_timing(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_connection_limits(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_connection_count_zero_limit(), OK)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_purge_list_reinsert(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_prefix_specificity(), OK)); WOLFSENTRY_RETURN_OK; } From 19f150fdb5636e1c4f09929ef87c09401e10b196 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 10:58:59 -0700 Subject: [PATCH 37/43] F-8764 - https://fenrir.wolfssl.com/finding/8764 - Bound the per-dispatch stale purge scan on the packet path --- src/routes.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/routes.c b/src/routes.c index 1cac77e..69341b6 100644 --- a/src/routes.c +++ b/src/routes.c @@ -3235,11 +3235,15 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( while (table->purge_list.tail) { struct wolfsentry_route *route = NULL; int found = 0; + int examined = 0; /* re-scan from the most stale entry each pass, because a delete runs * application callbacks that can mutate the purge list; skip routes * that still have a live connection within the idle limit so one * protected route does not strand the expired routes behind it. */ for (cursor = table->purge_list.tail; cursor != NULL; cursor = cursor->prev) { + /* the per-dispatch purge runs on the packet path, so bound its scan */ + if ((mode == 2) && (++examined > 8)) + break; route = WOLFSENTRY_ROUTE_PURGE_HEADER_TO_TABLE_ENT_HEADER(cursor); if (mode != 3) { if (route->meta.purge_after > now) From 8b5812b262a0015344bfc8252ced52c2e53f4ff4 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 11:23:29 -0700 Subject: [PATCH 38/43] F-8764 - https://fenrir.wolfssl.com/finding/8764 - Requeue connected routes so the bounded per-dispatch purge reaches the routes behind them --- src/routes.c | 27 ++++++++++++++++++++-- tests/unittests.c | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/routes.c b/src/routes.c index 69341b6..737df44 100644 --- a/src/routes.c +++ b/src/routes.c @@ -3187,6 +3187,8 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( int n = 0; wolfsentry_action_res_t fallback_action_results = 0; struct wolfsentry_list_ent_header *cursor; + int examined = 0; + wolfsentry_time_t requeue_delay = 0; #ifdef WOLFSENTRY_THREADSAFE int have_mutex = 0; int got_lock = 0; @@ -3204,6 +3206,11 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( WOLFSENTRY_ERROR_RETURN(ALREADY); } + if (mode == 2) { + if ((ret = WOLFSENTRY_FROM_EPOCH_TIME(WOLFSENTRY_ROUTE_PURGE_MARGIN_SECONDS, 0 /* epoch_nsecs */, &requeue_delay)) < 0) + WOLFSENTRY_ERROR_RERETURN(ret); + } + if (action_results == NULL) action_results = &fallback_action_results; @@ -3235,7 +3242,7 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( while (table->purge_list.tail) { struct wolfsentry_route *route = NULL; int found = 0; - int examined = 0; + int requeue = 0; /* re-scan from the most stale entry each pass, because a delete runs * application callbacks that can mutate the purge list; skip routes * that still have a live connection within the idle limit so one @@ -3252,7 +3259,10 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( (! (route->flags & WOLFSENTRY_ROUTE_FLAG_PENDING_DELETE)) && ((table->max_purgeable_idle_time == 0) || (now - route->meta.last_hit_time <= table->max_purgeable_idle_time))) { - continue; + if (mode != 2) + continue; + /* requeue it so later bounded scans reach the routes behind it */ + requeue = 1; } } found = 1; @@ -3274,6 +3284,19 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( have_mutex = 1; } #endif + if (requeue) { + wolfsentry_list_ent_delete(&table->purge_list, &route->purge_links); + /* not before the margin, unless the idle limit expires sooner */ + route->meta.purge_after = now + requeue_delay; + if ((table->max_purgeable_idle_time > 0) && + (now - route->meta.last_hit_time >= 0) && + (table->max_purgeable_idle_time - (now - route->meta.last_hit_time) < requeue_delay)) + { + route->meta.purge_after = route->meta.last_hit_time + table->max_purgeable_idle_time + 1; + } + wolfsentry_route_purge_list_insert(table, route); + continue; + } ret = wolfsentry_route_delete_0(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL /* caller_arg */, table, NULL /* trigger_event */, route, action_results, 0 /* defer_p */); if (ret < 0) { #ifdef WOLFSENTRY_THREADSAFE diff --git a/tests/unittests.c b/tests/unittests.c index 7e8ba88..93608a8 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4443,6 +4443,64 @@ static wolfsentry_errcode_t test_regression_purge_list_reinsert(void) { WOLFSENTRY_RETURN_OK; } +static wolfsentry_errcode_t test_regression_bounded_purge_progress(void) { + struct wolfsentry_context *wolfsentry; + struct wolfsentry_route_table *table; + struct wolfsentry_route_metadata_exports meta; + wolfsentry_action_res_t action_results; + wolfsentry_ent_id_t ids[9], matched_id; + byte addr[4] = { 0x0A, 0x08, 0x0B, 0x00 }; + int i; +#ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS + static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT }; +#else + static const struct wolfsentry_eventconfig config = { PRIVATE_DATA_SIZE, PRIVATE_DATA_ALIGNMENT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +#endif + + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_init_ex( + wolfsentry_build_settings, + WOLFSENTRY_CONTEXT_ARGS_OUT_EX(WOLFSENTRY_TEST_HPI), + &config, + &wolfsentry, + WOLFSENTRY_INIT_FLAG_NONE)); + + regression_fake_now = 1000000; + wolfsentry_get_timecbs(wolfsentry)->get_time = regression_fake_get_time; + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_get_main_table(WOLFSENTRY_CONTEXT_ARGS_OUT, &table)); + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_table_max_purgeable_idle_time_set( + WOLFSENTRY_CONTEXT_ARGS_OUT, table, (wolfsentry_time_t)1 << 30)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + + /* eight expired connected routes at the stale end, and one expired + * unconnected route queued behind them. */ + for (i = 0; i < 9; ++i) { + addr[3] = (byte)(i + 1); + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, (const char *)addr, 32, NULL, WOLFSENTRY_ROUTE_FLAG_NONE, &ids[i])); + if (i < 8) + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, (const char *)addr, WOLFSENTRY_ACTION_RES_CONNECT, &matched_id, &action_results)); + } + for (i = 0; i < 9; ++i) + WOLFSENTRY_EXIT_ON_FAILURE(regression_set_purge_time(WOLFSENTRY_CONTEXT_ARGS_OUT, ids[i], (wolfsentry_time_t)(10 * (i + 1)))); + + for (i = 0; i < 3; ++i) + (void)wolfsentry_route_stale_purge_one_opportunistically(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL); + WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE(ITEM_NOT_FOUND, regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, ids[8], &meta)); + for (i = 0; i < 8; ++i) { + WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, ids[i], &meta)); + WOLFSENTRY_EXIT_ON_FALSE(meta.connection_count == 1); + } + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + WOLFSENTRY_RETURN_OK; +} + static wolfsentry_errcode_t test_regression_prefix_specificity(void) { struct wolfsentry_context *wolfsentry; wolfsentry_action_res_t action_results; @@ -4491,6 +4549,7 @@ static int test_dynamic_rules_regressions(void) { WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_connection_limits(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_connection_count_zero_limit(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_purge_list_reinsert(), OK)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_bounded_purge_progress(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_prefix_specificity(), OK)); WOLFSENTRY_RETURN_OK; } From 7f83766b8c85c463545b75170cb49658ca46d422 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 12:01:45 -0700 Subject: [PATCH 39/43] F-4562 - https://fenrir.wolfssl.com/finding/4562 - Score bitmask route specificity by the bits its mask constrains --- src/routes.c | 21 +++++++-------------- tests/unittests.c | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/routes.c b/src/routes.c index 737df44..9b66ef1 100644 --- a/src/routes.c +++ b/src/routes.c @@ -295,28 +295,21 @@ static inline int addr_bitmask_match_size( const byte *b, unsigned int b_len) { - const byte *longer_addr, *shorter_addr; - unsigned int mask_len, i, padding_offset; + const byte *mask; + unsigned int mask_len, i; int ret = 0; + /* specificity is the number of bits the longer, maskful address's mask constrains */ if (a_len < b_len) { - longer_addr = b; mask_len = WOLFSENTRY_BITS_TO_BYTES(b_len >> 1); - shorter_addr = a; - padding_offset = mask_len - WOLFSENTRY_BITS_TO_BYTES(a_len); + mask = b + mask_len; } else { - longer_addr = a; mask_len = WOLFSENTRY_BITS_TO_BYTES(a_len >> 1); - shorter_addr = b; - padding_offset = mask_len - WOLFSENTRY_BITS_TO_BYTES(b_len); + mask = a + mask_len; } - for (ret = 0, i = 0; i < mask_len; ++i) { - if (i < padding_offset) - ret += popcount32(~(uint32_t)(longer_addr[i])) - 24; - else - ret += popcount32(~(uint32_t)(longer_addr[i] ^ shorter_addr[i - padding_offset])) - 24; - } + for (i = 0; i < mask_len; ++i) + ret += popcount32((uint32_t)mask[i]); return ret; } diff --git a/tests/unittests.c b/tests/unittests.c index 93608a8..5c9e594 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3565,6 +3565,40 @@ static wolfsentry_errcode_t test_regression_bitmask_match_specificity(void) { WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_ACCEPT)); + /* non-nested masks: 192.0.0.1 inexactly matches a two-bit mask whose + * address is closer to it and a three-bit mask whose address is not; the + * three-bit mask constrains more bits, so it must win. + */ + memcpy(broad_addr, "\xC0\x00\x00\x00\xC0\x00\x00\x00", 8); /* NOLINT(bugprone-not-null-terminated-result) */ + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_insert_by_exports( + WOLFSENTRY_CONTEXT_ARGS_OUT, + NULL /* caller_arg */, + &broad_route, + &broad_id, &action_results)); + memcpy(narrow_addr, "\x00\x00\x00\x00\x38\x00\x00\x00", 8); /* NOLINT(bugprone-not-null-terminated-result) */ + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_insert_by_exports( + WOLFSENTRY_CONTEXT_ARGS_OUT, + NULL /* caller_arg */, + &narrow_route, + &narrow_id, &action_results)); + + memcpy(remote.sa.addr, "\xC0\x00\x00\x01", 4); /* NOLINT(bugprone-not-null-terminated-result) */ + WOLFSENTRY_CLEAR_ALL_BITS(action_results); + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_route_event_dispatch( + WOLFSENTRY_CONTEXT_ARGS_OUT, + &remote.sa, &local.sa, + WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN, + NULL /* event_label */, 0 /* event_label_len */, + NULL /* caller_arg */, + &route_id, &inexact_matches, &action_results)); + + WOLFSENTRY_EXIT_ON_FALSE(route_id == narrow_id); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_ACCEPT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 5b36f213083bea2c91965a4bef23f4f1b76dea23 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 12:01:45 -0700 Subject: [PATCH 40/43] F-8764 - https://fenrir.wolfssl.com/finding/8764 - Compute the purge requeue deadline through the time callbacks --- src/routes.c | 17 +++++++++-------- tests/unittests.c | 9 +++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/routes.c b/src/routes.c index 9b66ef1..c5d9d16 100644 --- a/src/routes.c +++ b/src/routes.c @@ -3200,7 +3200,7 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( } if (mode == 2) { - if ((ret = WOLFSENTRY_FROM_EPOCH_TIME(WOLFSENTRY_ROUTE_PURGE_MARGIN_SECONDS, 0 /* epoch_nsecs */, &requeue_delay)) < 0) + if ((ret = WOLFSENTRY_INTERVAL_FROM_SECONDS(WOLFSENTRY_ROUTE_PURGE_MARGIN_SECONDS, 0 /* howlong_nsecs */, &requeue_delay)) < 0) WOLFSENTRY_ERROR_RERETURN(ret); } @@ -3250,7 +3250,7 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( break; if ((route->meta.connection_count > 0) && (! (route->flags & WOLFSENTRY_ROUTE_FLAG_PENDING_DELETE)) && - ((table->max_purgeable_idle_time == 0) || (now - route->meta.last_hit_time <= table->max_purgeable_idle_time))) + ((table->max_purgeable_idle_time == 0) || (WOLFSENTRY_DIFF_TIME(now, route->meta.last_hit_time) <= table->max_purgeable_idle_time))) { if (mode != 2) continue; @@ -3278,15 +3278,16 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( } #endif if (requeue) { - wolfsentry_list_ent_delete(&table->purge_list, &route->purge_links); + wolfsentry_time_t idle = WOLFSENTRY_DIFF_TIME(now, route->meta.last_hit_time); + wolfsentry_time_t delay = requeue_delay; /* not before the margin, unless the idle limit expires sooner */ - route->meta.purge_after = now + requeue_delay; - if ((table->max_purgeable_idle_time > 0) && - (now - route->meta.last_hit_time >= 0) && - (table->max_purgeable_idle_time - (now - route->meta.last_hit_time) < requeue_delay)) + if ((table->max_purgeable_idle_time > 0) && (idle >= 0) && + (table->max_purgeable_idle_time - idle < requeue_delay)) { - route->meta.purge_after = route->meta.last_hit_time + table->max_purgeable_idle_time + 1; + delay = table->max_purgeable_idle_time - idle + 1; } + wolfsentry_list_ent_delete(&table->purge_list, &route->purge_links); + route->meta.purge_after = WOLFSENTRY_ADD_TIME(now, delay); wolfsentry_route_purge_list_insert(table, route); continue; } diff --git a/tests/unittests.c b/tests/unittests.c index 5c9e594..9d0dd89 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4477,6 +4477,12 @@ static wolfsentry_errcode_t test_regression_purge_list_reinsert(void) { WOLFSENTRY_RETURN_OK; } +/* intervals in milliseconds, unlike the microsecond absolute clock. */ +static wolfsentry_errcode_t regression_ms_interval_from_seconds(time_t howlong_secs, long howlong_nsecs, wolfsentry_time_t *howlong) { + *howlong = ((wolfsentry_time_t)howlong_secs * 1000) + (howlong_nsecs / 1000000); + WOLFSENTRY_RETURN_OK; +} + static wolfsentry_errcode_t test_regression_bounded_purge_progress(void) { struct wolfsentry_context *wolfsentry; struct wolfsentry_route_table *table; @@ -4503,6 +4509,7 @@ static wolfsentry_errcode_t test_regression_bounded_purge_progress(void) { regression_fake_now = 1000000; wolfsentry_get_timecbs(wolfsentry)->get_time = regression_fake_get_time; + wolfsentry_get_timecbs(wolfsentry)->interval_from_seconds = regression_ms_interval_from_seconds; WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT)); WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_get_main_table(WOLFSENTRY_CONTEXT_ARGS_OUT, &table)); @@ -4525,9 +4532,11 @@ static wolfsentry_errcode_t test_regression_bounded_purge_progress(void) { for (i = 0; i < 3; ++i) (void)wolfsentry_route_stale_purge_one_opportunistically(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL); WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE(ITEM_NOT_FOUND, regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, ids[8], &meta)); + /* the connected routes are requeued one purge margin out, as an interval */ for (i = 0; i < 8; ++i) { WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, ids[i], &meta)); WOLFSENTRY_EXIT_ON_FALSE(meta.connection_count == 1); + WOLFSENTRY_EXIT_ON_FALSE(meta.purge_after == regression_fake_now + (WOLFSENTRY_ROUTE_PURGE_MARGIN_SECONDS * 1000)); } WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); From bbce3238ddebf06573a70ebf6cbdfad1410c3fd4 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 12:06:43 -0700 Subject: [PATCH 41/43] F-4100 - https://fenrir.wolfssl.com/finding/4100 - Reject max_connection_count exceeding the per-route counter width --- src/wolfsentry_util.c | 2 ++ tests/unittests.c | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/wolfsentry_util.c b/src/wolfsentry_util.c index fc590a0..4fb1668 100644 --- a/src/wolfsentry_util.c +++ b/src/wolfsentry_util.c @@ -3605,6 +3605,8 @@ WOLFSENTRY_API wolfsentry_errcode_t wolfsentry_eventconfig_check( WOLFSENTRY_ERROR_RETURN(INVALID_ARG); if (config->derogatory_threshold_for_penaltybox > MAX_UINT_OF(instance_of_field(struct wolfsentry_route, meta.derogatory_count))) WOLFSENTRY_ERROR_RETURN(INVALID_ARG); + if (config->max_connection_count > MAX_UINT_OF(instance_of_field(struct wolfsentry_route, meta.connection_count))) + WOLFSENTRY_ERROR_RETURN(NUMERIC_ARG_TOO_BIG); ret = wolfsentry_route_check_flags_sensical(config->route_flags_to_add_on_insert); WOLFSENTRY_RERETURN_IF_ERROR(ret); diff --git a/tests/unittests.c b/tests/unittests.c index 9d0dd89..da206e9 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4349,6 +4349,23 @@ static wolfsentry_errcode_t test_regression_connection_limits(void) { limit_config.max_connection_count = 2; WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_event_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "limit2", -1, 10, &limit_config, WOLFSENTRY_EVENT_FLAG_NONE, &matched_id)); + /* a limit the 16-bit per-route counter cannot enforce is refused rather + * than silently capped, and the counter maximum is enforced exactly. */ + limit_config.max_connection_count = 0x10000U; + WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE(NUMERIC_ARG_TOO_BIG, wolfsentry_event_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "limitmax", -1, 10, &limit_config, WOLFSENTRY_EVENT_FLAG_NONE, &id)); + limit_config.max_connection_count = 0xffffU; + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_event_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "limitmax", -1, 10, &limit_config, WOLFSENTRY_EVENT_FLAG_NONE, &id)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x08\x03", 32, "limitmax", WOLFSENTRY_ROUTE_FLAG_NONE, &id)); + for (i = 0; i < 0xffffU; ++i) { + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x08\x03", WOLFSENTRY_ACTION_RES_CONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_TRUE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + } + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x08\x03", WOLFSENTRY_ACTION_RES_CONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_CHECK_BITS(action_results, WOLFSENTRY_ACTION_RES_REJECT)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + WOLFSENTRY_EXIT_ON_FALSE(meta.connection_count == 0xffffU); + limit_config.max_connection_count = 2; + /* a route that opts out of connection counting is never limited. */ WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x08\x01", 32, "limit2", WOLFSENTRY_ROUTE_FLAG_DONT_COUNT_CURRENT_CONNECTIONS, &id)); for (i = 0; i < 3; ++i) { From dc7da594c095a02f8af4b5cfa85cef367284677f Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 12:40:38 -0700 Subject: [PATCH 42/43] F-8764 - https://fenrir.wolfssl.com/finding/8764 - Measure purge idle time from insertion for unhit routes and treat a negative idle limit as unlimited --- src/routes.c | 6 ++-- tests/unittests.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/routes.c b/src/routes.c index c5d9d16..50d47d9 100644 --- a/src/routes.c +++ b/src/routes.c @@ -3236,6 +3236,7 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( struct wolfsentry_route *route = NULL; int found = 0; int requeue = 0; + wolfsentry_time_t idle = 0; /* re-scan from the most stale entry each pass, because a delete runs * application callbacks that can mutate the purge list; skip routes * that still have a live connection within the idle limit so one @@ -3248,9 +3249,11 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( if (mode != 3) { if (route->meta.purge_after > now) break; + /* a route never hit since insertion has been idle since its insertion */ + idle = WOLFSENTRY_DIFF_TIME(now, (route->meta.last_hit_time > route->meta.insert_time) ? route->meta.last_hit_time : route->meta.insert_time); if ((route->meta.connection_count > 0) && (! (route->flags & WOLFSENTRY_ROUTE_FLAG_PENDING_DELETE)) && - ((table->max_purgeable_idle_time == 0) || (WOLFSENTRY_DIFF_TIME(now, route->meta.last_hit_time) <= table->max_purgeable_idle_time))) + ((table->max_purgeable_idle_time <= 0) || (idle <= table->max_purgeable_idle_time))) { if (mode != 2) continue; @@ -3278,7 +3281,6 @@ static wolfsentry_errcode_t wolfsentry_route_stale_purge_1( } #endif if (requeue) { - wolfsentry_time_t idle = WOLFSENTRY_DIFF_TIME(now, route->meta.last_hit_time); wolfsentry_time_t delay = requeue_delay; /* not before the margin, unless the idle limit expires sooner */ if ((table->max_purgeable_idle_time > 0) && (idle >= 0) && diff --git a/tests/unittests.c b/tests/unittests.c index da206e9..d12be38 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -4561,6 +4561,77 @@ static wolfsentry_errcode_t test_regression_bounded_purge_progress(void) { WOLFSENTRY_RETURN_OK; } +static wolfsentry_errcode_t test_regression_purge_idle_edges(void) { + struct wolfsentry_context *wolfsentry; + struct wolfsentry_route_table *table; + struct wolfsentry_route_metadata_exports meta; + struct wolfsentry_route_exports exports; + wolfsentry_action_res_t action_results; + wolfsentry_ent_id_t id, matched_id; + byte addr[4] = { 0x0A, 0x08, 0x0C, 0x01 }; +#ifdef WOLFSENTRY_HAVE_DESIGNATED_INITIALIZERS + static const struct wolfsentry_eventconfig config = { .route_private_data_size = PRIVATE_DATA_SIZE, .route_private_data_alignment = PRIVATE_DATA_ALIGNMENT }; +#else + static const struct wolfsentry_eventconfig config = { PRIVATE_DATA_SIZE, PRIVATE_DATA_ALIGNMENT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +#endif + + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + WOLFSENTRY_EXIT_ON_FAILURE( + wolfsentry_init_ex( + wolfsentry_build_settings, + WOLFSENTRY_CONTEXT_ARGS_OUT_EX(WOLFSENTRY_TEST_HPI), + &config, + &wolfsentry, + WOLFSENTRY_INIT_FLAG_NONE)); + + regression_fake_now = 1000000; + wolfsentry_get_timecbs(wolfsentry)->get_time = regression_fake_get_time; + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_get_main_table(WOLFSENTRY_CONTEXT_ARGS_OUT, &table)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_table_max_purgeable_idle_time_set(WOLFSENTRY_CONTEXT_ARGS_OUT, table, 100)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + + /* an imported route carrying a live connection but never hit is idle + * since its insertion, not since the epoch. */ + memset(&exports, 0, sizeof exports); + exports.sa_family = AF_INET; + exports.sa_proto = IPPROTO_TCP; + exports.remote_address = addr; + exports.remote.addr_len = 32; + exports.flags = WOLFSENTRY_ROUTE_FLAG_DIRECTION_IN + | WOLFSENTRY_ROUTE_FLAG_TCPLIKE_PORT_NUMBERS + | WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_ADDR_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_SA_LOCAL_PORT_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_SA_REMOTE_PORT_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_LOCAL_INTERFACE_WILDCARD + | WOLFSENTRY_ROUTE_FLAG_REMOTE_INTERFACE_WILDCARD; + exports.meta.connection_count = 1; + exports.meta.purge_after = regression_fake_now + 1; + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_insert_by_exports(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL /* caller_arg */, &exports, &id, &action_results)); + regression_fake_now += 2; + WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE(ALREADY, wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_delete_by_id(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL /* caller_arg */, id, NULL /* trigger_label */, 0, &action_results)); + + /* a negative idle limit, like zero, never purges a connected route. */ + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_mutex(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_table_max_purgeable_idle_time_set(WOLFSENTRY_CONTEXT_ARGS_OUT, table, -1)); + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_insert(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x0C\x02", 32, NULL, WOLFSENTRY_ROUTE_FLAG_NONE, &id)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_dispatch(WOLFSENTRY_CONTEXT_ARGS_OUT, "\x0A\x08\x0C\x02", WOLFSENTRY_ACTION_RES_CONNECT, &matched_id, &action_results)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_set_purge_time(WOLFSENTRY_CONTEXT_ARGS_OUT, id, regression_fake_now)); + ++regression_fake_now; + WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE(ALREADY, wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL)); + WOLFSENTRY_EXIT_ON_FAILURE(regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, id, &meta)); + WOLFSENTRY_EXIT_ON_FALSE(meta.connection_count == 1); + + WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_shutdown(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&wolfsentry))); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + WOLFSENTRY_RETURN_OK; +} + static wolfsentry_errcode_t test_regression_prefix_specificity(void) { struct wolfsentry_context *wolfsentry; wolfsentry_action_res_t action_results; @@ -4610,6 +4681,7 @@ static int test_dynamic_rules_regressions(void) { WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_connection_count_zero_limit(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_purge_list_reinsert(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_bounded_purge_progress(), OK)); + WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_purge_idle_edges(), OK)); WOLFSENTRY_EXIT_ON_FALSE(WOLFSENTRY_ERROR_CODE_IS(test_regression_prefix_specificity(), OK)); WOLFSENTRY_RETURN_OK; } From f03bbe81531f73ae96157cd64e7f398d336ac0d5 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 13:38:35 -0700 Subject: [PATCH 43/43] Check stale purge results in the purge regression tests --- tests/unittests.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/unittests.c b/tests/unittests.c index d12be38..bc39dbc 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -3605,6 +3605,11 @@ static wolfsentry_errcode_t test_regression_bitmask_match_specificity(void) { } #endif /* WOLFSENTRY_ADDR_BITMASK_MATCHING */ +/* a stale purge either purged something or had nothing due */ +static int regression_purge_ok(wolfsentry_errcode_t ret) { + return WOLFSENTRY_IS_SUCCESS(ret) || WOLFSENTRY_ERROR_CODE_IS(ret, ALREADY); +} + static wolfsentry_errcode_t test_regression_stale_purge_connected_idle(void) { struct wolfsentry_context *wolfsentry; struct wolfsentry_route_table *table; @@ -3701,7 +3706,7 @@ static wolfsentry_errcode_t test_regression_stale_purge_connected_idle(void) { WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_route_purge_time_set(WOLFSENTRY_CONTEXT_ARGS_OUT, route, 2)); WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_unlock(WOLFSENTRY_CONTEXT_ARGS_OUT)); - (void)wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL); + WOLFSENTRY_EXIT_ON_FALSE(regression_purge_ok(wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL))); WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_shared(WOLFSENTRY_CONTEXT_ARGS_OUT)); /* the route's live connection is within the idle limit, so it must survive. */ @@ -3849,7 +3854,7 @@ static wolfsentry_errcode_t test_regression_copy_metadata_purge_list(void) { /* the swapped-in route now has a stale purge_after, so it must be reachable * from the purge list and get purged. */ - (void)wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL); + WOLFSENTRY_EXIT_ON_FALSE(regression_purge_ok(wolfsentry_route_stale_purge(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL))); WOLFSENTRY_EXIT_ON_FAILURE(wolfsentry_context_lock_shared(WOLFSENTRY_CONTEXT_ARGS_OUT)); WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE( @@ -4547,7 +4552,7 @@ static wolfsentry_errcode_t test_regression_bounded_purge_progress(void) { WOLFSENTRY_EXIT_ON_FAILURE(regression_set_purge_time(WOLFSENTRY_CONTEXT_ARGS_OUT, ids[i], (wolfsentry_time_t)(10 * (i + 1)))); for (i = 0; i < 3; ++i) - (void)wolfsentry_route_stale_purge_one_opportunistically(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL); + WOLFSENTRY_EXIT_ON_FALSE(regression_purge_ok(wolfsentry_route_stale_purge_one_opportunistically(WOLFSENTRY_CONTEXT_ARGS_OUT, NULL, NULL))); WOLFSENTRY_EXIT_UNLESS_EXPECTED_FAILURE(ITEM_NOT_FOUND, regression_route_meta(WOLFSENTRY_CONTEXT_ARGS_OUT, ids[8], &meta)); /* the connected routes are requeued one purge margin out, as an interval */ for (i = 0; i < 8; ++i) {