commit ff736bc10baadea6c66cc4761ecb7de54571b4b8
parent dcecd3c0e451813776a15505a9d71ce99b449081
Author: finwo <finwo@pm.me>
Date: Wed, 30 Sep 2026 15:16:08 +0200
Wake the poll loop on SIGCHLD so a dead dhcp client is reported at once
Diffstat:
3 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/src/dhcp/client.c b/src/dhcp/client.c
@@ -73,6 +73,28 @@ static char *find_in_path(const char *bin) {
}
// ---------------------------------------------------------------------------
+
+// poll(2) is never restarted, so this drops nl_run out of its wait and the
+// reap at the top of that loop runs immediately. SA_RESTART keeps the
+// blocking waitpid in plugin.c from being interrupted.
+static void sigchld_noop(int sig) { (void)sig; }
+
+static void arm_sigchld(void) {
+ static int armed = 0;
+ if (armed) return;
+ armed = 1;
+
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = sigchld_noop;
+ sa.sa_flags = SA_RESTART;
+ sigemptyset(&sa.sa_mask);
+ if (sigaction(SIGCHLD, &sa, NULL) != 0) {
+ log_warn("dhcp: sigaction SIGCHLD: %s", strerror(errno));
+ }
+}
+
+// ---------------------------------------------------------------------------
// process table
static struct dhcp_proc *proc_find(const char *ifname) {
@@ -107,6 +129,7 @@ int dhcp_start(const char *ifname) {
return -1;
}
const char *const *tpl = (const char *const *)cfg->dhcp_client;
+ arm_sigchld();
char **argv = calloc(argc + 1, sizeof(char *));
if (!argv) {
diff --git a/src/netlink/netlink.c b/src/netlink/netlink.c
@@ -587,6 +587,8 @@ int nl_run(int fd, int resync_interval_s) {
clock_gettime(CLOCK_MONOTONIC, &last_resync);
while (!nl_stop) {
+ dhcp_reap();
+
// Netlink occupies slot 0; the IPC server contributes its listeners and
// every live connection after it.
struct pollfd pfds[1 + IPC_MAX_POLLFDS];
@@ -622,7 +624,6 @@ int nl_run(int fd, int resync_interval_s) {
log_error("netlink: poll failed: %s", strerror(errno));
return -1;
}
- dhcp_reap();
if (!pr) continue;
if (nfds > 1) ipc_dispatch(&pfds[1], nfds - 1);
diff --git a/tests/unit/test_dhcp.sh b/tests/unit/test_dhcp.sh
@@ -30,8 +30,17 @@ iface swp-static inet static
address 10.9.9.9/24
EOF
+# An empty netns, because reaping is driven by SIGCHLD waking poll(2): on a
+# host with ambient netlink traffic the loop spins anyway and a broken reap
+# still passes. 3s for the same reason -- the wake is causal, not timed.
+if unshare --user --map-root-user --net true 2>/dev/null; then
+ in_ns() { unshare --user --map-root-user --net "$@"; }
+else
+ in_ns() { "$@"; }
+fi
+
run_linkd() {
- timeout 15 env PATH="${T}/emptybin" DHCP_LOG="${T}/dhcp.log" \
+ in_ns timeout 3 env PATH="${T}/emptybin" DHCP_LOG="${T}/dhcp.log" \
"${BIN}" --config "$1" --resync-interval 0 \
>"${T}/linkd.log" 2>&1 || true
cat "${T}/linkd.log"