Commit 1dfc4121be for bind
commit 1dfc4121be40834aa3fd9f88441b9f55062379e1
Author: OndÅ™ej Surý <ondrej@isc.org>
Date: Fri Sep 4 11:22:05 2026 +0200
Test recovery from route socket overflow
The netmgr test hands a route socket the ENOBUFS that libuv would
deliver and checks that the error reaches the read callback with an
empty region, that the read can be re-armed from there, and that the
receive buffer is released. The interface manager test mocks the
route socket reads and the rescan timer so it can walk the recovery
by hand: an overflow arms the rescan, messages read while recovering
keep it armed, and the first idle tick ends it.
The interface manager now owns a timer bound to the main loop, so the
ns test harness creates it from a loop job instead of from the cmocka
setup, which runs before the loop exists.
diff --git a/tests/isc/udp_test.c b/tests/isc/udp_test.c
index 430074cfd3..0beb67e44a 100644
--- a/tests/isc/udp_test.c
+++ b/tests/isc/udp_test.c
@@ -73,6 +73,55 @@ udp_connect_nomemory_cb(isc_nmhandle_t *handle, isc_result_t eresult,
isc_loopmgr_shutdown();
}
+#ifdef USE_ROUTE_SOCKET
+static unsigned int route_overflow_reads;
+
+static void
+route_overflow_read_cb(isc_nmhandle_t *handle, isc_result_t eresult,
+ isc_region_t *region, void *cbarg) {
+ UNUSED(cbarg);
+
+ assert_int_equal(eresult, ISC_R_NORESOURCES);
+ assert_non_null(region);
+ assert_null(region->base);
+ assert_int_equal(region->length, 0);
+ route_overflow_reads++;
+ isc_nm_read(handle, route_overflow_read_cb, NULL);
+}
+
+static void
+route_overflow_connect_cb(isc_nmhandle_t *handle, isc_result_t eresult,
+ void *cbarg) {
+ isc_nmsocket_t *sock = NULL;
+ uv_buf_t buf = { 0 };
+
+ UNUSED(cbarg);
+
+ assert_int_equal(eresult, ISC_R_SUCCESS);
+
+ sock = handle->sock;
+ assert_true(sock->route_sock);
+
+ isc_nm_read(handle, route_overflow_read_cb, NULL);
+
+ /*
+ * Deliver ENOBUFS the way libuv does: the receive buffer has
+ * already been allocated when the read fails.
+ */
+ isc__nm_alloc_cb(&sock->uv_handle.handle, 0, &buf);
+ isc__nm_udp_read_cb(&sock->uv_handle.udp, UV_ENOBUFS, &buf, NULL, 0);
+
+ /* The error was passed on, the read re-armed, the buffer released */
+ assert_int_equal(route_overflow_reads, 1);
+ assert_true(sock->reading);
+ assert_false(isc__nmsocket_closing(sock));
+ assert_false(sock->worker->recvbuf_inuse);
+
+ isc_nmhandle_close(handle);
+ isc_loopmgr_shutdown();
+}
+#endif /* USE_ROUTE_SOCKET */
+
/* UDP */
ISC_LOOP_TEST_IMPL(mock_listenudp_uv_udp_open) {
@@ -213,6 +262,18 @@ ISC_LOOP_TEST_IMPL(udp_listener_child_ref) {
isc_loopmgr_shutdown();
}
+#ifdef USE_ROUTE_SOCKET
+ISC_LOOP_TEST_IMPL(udp_route_overflow) {
+ isc_result_t result;
+
+ route_overflow_reads = 0;
+ result = isc_nm_routeconnect(route_overflow_connect_cb, NULL);
+ assert_int_equal(result, ISC_R_SUCCESS);
+}
+#else
+ISC_RUN_TEST_IMPL(udp_route_overflow) { skip(); }
+#endif /* USE_ROUTE_SOCKET */
+
ISC_LOOP_TEST_IMPL(udp_noop) { udp_noop(arg); }
ISC_LOOP_TEST_IMPL(udp_noresponse) { udp_noresponse(arg); }
@@ -252,6 +313,7 @@ ISC_TEST_ENTRY_CUSTOM(mock_udpconnect_uv_recv_buffer_size, setup_udp_test,
ISC_TEST_ENTRY_CUSTOM(mock_udpconnect_uv_send_buffer_size, setup_udp_test,
teardown_udp_test)
ISC_TEST_ENTRY_CUSTOM(udp_listener_child_ref, setup_udp_test, teardown_udp_test)
+ISC_TEST_ENTRY_CUSTOM(udp_route_overflow, setup_udp_test, teardown_udp_test)
ISC_TEST_ENTRY_CUSTOM(udp_noop, udp_noop_setup, udp_noop_teardown)
ISC_TEST_ENTRY_CUSTOM(udp_noresponse, udp_noresponse_setup,
diff --git a/tests/libtest/ns.c b/tests/libtest/ns.c
index fb149c19a0..5f0d4714b6 100644
--- a/tests/libtest/ns.c
+++ b/tests/libtest/ns.c
@@ -81,9 +81,16 @@ matchview(isc_netaddr_t *srcaddr, isc_netaddr_t *destaddr,
return ISC_R_NOTIMPLEMENTED;
}
+/*
+ * The interface manager owns a timer bound to the main loop, so it has to
+ * be created (and, by the tests, shut down) from that loop.
+ */
static void
-scan_interfaces(void *arg) {
- UNUSED(arg);
+setup_interfacemgr(void *arg ISC_ATTR_UNUSED) {
+ isc_result_t result = ns_interfacemgr_create(
+ isc_g_mctx, sctx, dispatchmgr, NULL, &interfacemgr);
+ RUNTIME_CHECK(result == ISC_R_SUCCESS);
+
ns_interfacemgr_scan(interfacemgr, true, false);
}
@@ -106,12 +113,8 @@ setup_server(void **state) {
ns_server_create(isc_g_mctx, matchview, &sctx);
CHECK(dns_dispatchmgr_create(isc_g_mctx, &dispatchmgr));
- CHECK(ns_interfacemgr_create(isc_g_mctx, sctx, dispatchmgr, NULL,
- &interfacemgr));
- isc_loop_setup(isc_loop_main(), setup_zone_services, NULL);
- isc_loop_setup(isc_loop_main(), scan_interfaces, NULL);
- isc_loop_teardown(isc_loop_main(), shutdown_zone_services, NULL);
+ isc_loop_setup(isc_loop_main(), setup_interfacemgr, NULL);
return 0;
diff --git a/tests/ns/interfacemgr_test.c b/tests/ns/interfacemgr_test.c
new file mode 100644
index 0000000000..9a51e2006b
--- /dev/null
+++ b/tests/ns/interfacemgr_test.c
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+#include <inttypes.h>
+#include <sched.h> /* IWYU pragma: keep */
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#ifdef HAVE_NET_ROUTE_H
+#include <net/route.h>
+#endif
+
+#if defined(HAVE_LINUX_NETLINK_H) && defined(HAVE_LINUX_RTNETLINK_H)
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#endif
+
+#define UNIT_TESTING
+#include <cmocka.h>
+
+#include <isc/lib.h>
+#include <isc/loop.h>
+#include <isc/netmgr.h>
+#include <isc/time.h>
+#include <isc/timer.h>
+#include <isc/util.h>
+
+#include <dns/lib.h>
+
+#include <ns/interfacemgr.h>
+#include <ns/server.h>
+
+#include <tests/ns.h>
+
+/*
+ * The route socket is mocked: the test hands route_recv() whatever result
+ * it likes and watches how the single-shot read gets re-armed.
+ */
+static ns_client_t route_handle;
+static isc_nm_recv_cb_t route_recv_cb;
+static void *route_recv_cbarg;
+static unsigned int read_calls;
+static unsigned int close_calls;
+
+/*
+ * Timers are mocked as well. Besides the interface manager's rescan timer
+ * there are the zone manager's rate limiter timers, which never get started
+ * here. The test fires the rescan timer's callback by hand and checks
+ * exactly when it gets (re-)armed.
+ */
+typedef struct mock_timer {
+ isc_job_cb cb;
+ void *cbarg;
+ bool running;
+ isc_interval_t interval;
+} mock_timer_t;
+
+static mock_timer_t timers[8];
+static mock_timer_t *rescan;
+
+static mock_timer_t *
+timer_get(isc_timer_t *timer) {
+ mock_timer_t *t = NULL;
+
+ for (size_t i = 0; i < ARRAY_SIZE(timers); i++) {
+ if (timer == (isc_timer_t *)(void *)&timers[i]) {
+ t = &timers[i];
+ }
+ }
+ assert_non_null(t);
+ assert_non_null(t->cb);
+
+ return t;
+}
+
+isc_result_t
+isc_nm_routeconnect(isc_nm_cb_t cb, void *cbarg) {
+ cb((isc_nmhandle_t *)&route_handle, ISC_R_SUCCESS, cbarg);
+ return ISC_R_SUCCESS;
+}
+
+void
+isc_nm_read(isc_nmhandle_t *handle, isc_nm_recv_cb_t cb, void *cbarg) {
+ assert_ptr_equal(handle, &route_handle);
+ /* A client read is single-shot; arming it twice is a bug. */
+ assert_null(route_recv_cb);
+
+ route_recv_cb = cb;
+ route_recv_cbarg = cbarg;
+ read_calls++;
+}
+
+/*
+ * Deliver a read result the way the network manager would: the read is
+ * consumed, and the callback has to arm a new one.
+ */
+static void
+deliver(isc_result_t result, isc_region_t *region) {
+ isc_nm_recv_cb_t cb = route_recv_cb;
+
+ assert_non_null(cb);
+ route_recv_cb = NULL;
+
+ cb((isc_nmhandle_t *)&route_handle, result, region, route_recv_cbarg);
+}
+
+void
+isc_nm_cancelread(isc_nmhandle_t *handle) {
+ isc_region_t region = { 0 };
+
+ assert_ptr_equal(handle, &route_handle);
+
+ deliver(ISC_R_CANCELED, ®ion);
+}
+
+void
+isc_nmhandle_close(isc_nmhandle_t *handle) {
+ assert_ptr_equal(handle, &route_handle);
+ close_calls++;
+}
+
+void
+isc_timer_create(isc_loop_t *loop, isc_job_cb cb, void *cbarg,
+ isc_timer_t **timerp) {
+ mock_timer_t *t = NULL;
+
+ assert_ptr_equal(loop, isc_loop_main());
+ assert_non_null(timerp);
+ assert_null(*timerp);
+
+ for (size_t i = 0; i < ARRAY_SIZE(timers) && t == NULL; i++) {
+ if (timers[i].cb == NULL) {
+ t = &timers[i];
+ }
+ }
+ assert_non_null(t);
+
+ *t = (mock_timer_t){ .cb = cb, .cbarg = cbarg };
+ *timerp = (isc_timer_t *)(void *)t;
+}
+
+void
+isc_timer_start(isc_timer_t *timer, isc_timertype_t type,
+ const isc_interval_t *interval) {
+ mock_timer_t *t = timer_get(timer);
+
+ assert_int_equal(type, isc_timertype_once);
+
+ t->running = true;
+ t->interval = *interval;
+}
+
+void
+isc_timer_stop(isc_timer_t *timer) {
+ timer_get(timer)->running = false;
+}
+
+void
+isc_timer_destroy(isc_timer_t **timerp) {
+ mock_timer_t *t = NULL;
+
+ assert_non_null(timerp);
+ t = timer_get(*timerp);
+
+ *t = (mock_timer_t){ 0 };
+ *timerp = NULL;
+}
+
+/*
+ * The interface manager's rescan timer is the one it created with itself as
+ * the callback argument.
+ */
+static mock_timer_t *
+rescan_timer(void) {
+ mock_timer_t *t = NULL;
+
+ assert_non_null(interfacemgr);
+ for (size_t i = 0; i < ARRAY_SIZE(timers); i++) {
+ if (timers[i].cb != NULL && timers[i].cbarg == interfacemgr) {
+ t = &timers[i];
+ }
+ }
+ assert_non_null(t);
+
+ return t;
+}
+
+/*
+ * Run the pending rescan. Every rescan on the route socket path is
+ * scheduled with a zero delay, i.e. for the next loop iteration.
+ */
+static void
+fire_timer(void) {
+ assert_true(rescan->running);
+ assert_int_equal(isc_interval_tonanosecs(&rescan->interval), 0);
+
+ rescan->running = false;
+ rescan->cb(rescan->cbarg);
+}
+
+/*
+ * A route message that never needs a rescan by itself: a link state change.
+ */
+static isc_region_t
+link_message(void) {
+#if defined(HAVE_LINUX_NETLINK_H) && defined(HAVE_LINUX_RTNETLINK_H)
+ static struct nlmsghdr msg = {
+ .nlmsg_len = sizeof(struct nlmsghdr),
+ .nlmsg_type = RTM_NEWLINK,
+ };
+#elif defined(RTM_VERSION)
+ static struct rt_msghdr msg = {
+ .rtm_msglen = sizeof(struct rt_msghdr),
+ .rtm_version = RTM_VERSION,
+ .rtm_type = RTM_IFINFO,
+ };
+#else
+ static unsigned char msg = 0;
+#endif
+
+ return (isc_region_t){ .base = (unsigned char *)&msg,
+ .length = sizeof(msg) };
+}
+
+static void
+check_shutdown(void *arg ISC_ATTR_UNUSED) {
+ /*
+ * Shutting down cancelled the read, which released the route
+ * handle, and destroyed the rescan timer.
+ */
+ assert_int_equal(read_calls, 5);
+ assert_int_equal(close_calls, 1);
+ assert_null(route_recv_cb);
+ assert_null(rescan->cb);
+ assert_false(rescan->running);
+ assert_int_equal(atomic_load(&client_refs[0]), 1);
+}
+
+ISC_LOOP_TEST_IMPL(route_overflow) {
+ isc_region_t empty = { 0 };
+ isc_region_t link = link_message();
+
+ atomic_store(&client_addrs[0], (uintptr_t)&route_handle);
+ atomic_store(&client_refs[0], 1);
+ sctx->interface_auto = true;
+
+ /* The rescan timer was created along with the interface manager */
+ rescan = rescan_timer();
+ assert_false(rescan->running);
+
+ ns_interfacemgr_routeconnect(interfacemgr);
+ assert_int_equal(read_calls, 1);
+ assert_int_equal(close_calls, 0);
+
+ /* A message that needs no rescan only re-arms the read */
+ deliver(ISC_R_SUCCESS, &link);
+ assert_int_equal(read_calls, 2);
+ assert_false(rescan->running);
+
+ /* An overflow re-arms the read and schedules a rescan */
+ deliver(ISC_R_NORESOURCES, &empty);
+ assert_int_equal(read_calls, 3);
+ assert_int_equal(close_calls, 0);
+ assert_true(rescan->running);
+
+ /* The rescan stays armed while the backlog is being drained */
+ fire_timer();
+ assert_true(rescan->running);
+
+ /* While recovering, every message counts, even one needing no rescan */
+ deliver(ISC_R_SUCCESS, &link);
+ assert_int_equal(read_calls, 4);
+ fire_timer();
+ assert_true(rescan->running);
+
+ /* Nothing was read since the last rescan: the backlog is drained */
+ fire_timer();
+ assert_false(rescan->running);
+
+ /* Back to normal: the same message is ignored again */
+ deliver(ISC_R_SUCCESS, &link);
+ assert_int_equal(read_calls, 5);
+ assert_false(rescan->running);
+
+ isc_loop_teardown(isc_loop_main(), shutdown_interfacemgr, NULL);
+ isc_loop_teardown(isc_loop_main(), check_shutdown, NULL);
+ isc_loopmgr_shutdown();
+}
+
+ISC_TEST_LIST_START
+ISC_TEST_ENTRY_CUSTOM(route_overflow, setup_server, teardown_server)
+ISC_TEST_LIST_END
+
+ISC_TEST_MAIN
diff --git a/tests/ns/meson.build b/tests/ns/meson.build
index 452c7ea05f..f364371f72 100644
--- a/tests/ns/meson.build
+++ b/tests/ns/meson.build
@@ -10,6 +10,7 @@
# information regarding copyright ownership.
ns_tests = [
+ 'interfacemgr',
'notify',
'plugin',
'query',