Describe the bug
Auth/RPZ zone load blocks query serving
Symptom
When a slaved auth-zone or RPZ zone is (re)loaded via AXFR, unbound stops answering client queries for the duration of the apply. The stall scales linearly with zone size; multi-hundred-thousand-entry RPZ blocklists cause multi-second outages on every refresh. IXFR is less affected (delta-sized work); AXFR and HTTP-fetched zones are the pathological cases.
Root cause
Three independent design facts in services/authzone.c combine to freeze the entire worker fleet, not just the worker doing the transfer.
To reproduce
Steps to reproduce the behavior:
- in unbound, configure RPZ with a master server to fetch the RPZ zones from
- wait for zone transfers for the RPZ zones to occur, specifically for full refetching with AXFR
- from a DNS client, try asking unbound DNS queries over a non-UDP port (TCP/DoT/DoH/ProxyV2) and notice
Expected behavior
Unbound shouldn't throw away DNS queries on non-UDP ports then loading RPZ zones
System:
- Unbound version: 1.25.1
- OS: Ubuntu
unbound -V output:
Version 1.25.1
Configure line: --build=x86_64-linux-gnu --prefix=/usr --includedir=${prefix}/include --mandir=${prefix}/share/man --infodir=${prefix}/share/info --sysconfdir=/etc --localstatedir=/var --disable-option-checking --disable-silent-rules --libdir=${prefix}/lib/x86_64-linux-gnu --libexecdir=${prefix}/lib/x86_64-linux-gnu --disable-maintainer-mode --disable-dependency-tracking --disable-rpath --with-pidfile=/run/unbound.pid --with-rootkey-file=/var/lib/unbound/root.key --with-libevent --with-libhiredis --with-libnghttp2 --with-pythonmodule --enable-cachedb --enable-subnet --enable-dnstap --enable-systemd --with-chroot-dir= --with-dnstap-socket-path=/run/dnstap.sock --libdir=/usr/lib
Linked libs: libevent 2.1.12-stable (it uses epoll), OpenSSL 3.0.2 15 Mar 2022
Linked modules: dns64 python cachedb subnetcache respip validator iterator
Additional information
Claude code analysis says:
- All auth/RPZ transfers run on worker 0
daemon/worker.c gates auth-zone task pickup behind worker->thread_num == 0:
if(worker->env.auth_zones
#ifndef THREADS_DISABLED
&& worker->thread_num == 0
#endif
)
auth_xfer_pickup_initial(worker->env.auth_zones, &worker->env);
Probe, transfer, and apply for every zone run on worker 0's event loop. While apply_axfr is iterating chunks and inserting RRs, worker 0 services no sockets — every client landing on worker 0 is blind.
z->lock write-lock is held for the entire destructive rebuild
xfr_process_chunk_list acquires z->lock as a wrlock and holds it until the very end, around:
traverse_postorder(&z->data, auth_data_del, NULL) — frees the old tree
rpz_clear(z->rpz) — destroys and recreates all five RPZ sub-trees
- per-RR
az_insert_rr_decompress + rpz_insert_rr for every record
auth_zone_verify_zonemd — RRSIG/digest verification across the zone
rpz_finish_config — rebuilds parent pointers on three address-trees
There is no atomic-swap pattern. The old tree is freed before the new one is built, so the wrlock must be held end-to-end.
- RPZ policy lookup multiplies the wrlock into a fleet-wide stall
Every worker consults RPZ policy on every query through three call sites in services/rpz.c (rpz_apply_maybe_clientip_trigger, rpz_callback_from_iterator_module, rpz_callback_from_iterator_cname). All three follow the same pattern:
lock_rw_rdlock(&az->rpz_lock);
for(a = az->rpz_first; a; a = a->rpz_az_next) {
lock_rw_rdlock(&a->lock); // blocks while worker 0 holds wrlock
...
}
While worker 0 holds z->lock write on any one RPZ zone, every worker doing an iterator/CNAME callback blocks on a->lock read for that zone. With multiple configured RPZ zones the effect compounds — a single zone's apply freezes the RPZ subsystem across the whole worker fleet, not just worker 0.
Describe the bug
Auth/RPZ zone load blocks query serving
Symptom
When a slaved auth-zone or RPZ zone is (re)loaded via AXFR, unbound stops answering client queries for the duration of the apply. The stall scales linearly with zone size; multi-hundred-thousand-entry RPZ blocklists cause multi-second outages on every refresh. IXFR is less affected (delta-sized work); AXFR and HTTP-fetched zones are the pathological cases.
Root cause
Three independent design facts in
services/authzone.ccombine to freeze the entire worker fleet, not just the worker doing the transfer.To reproduce
Steps to reproduce the behavior:
Expected behavior
Unbound shouldn't throw away DNS queries on non-UDP ports then loading RPZ zones
System:
unbound -Voutput:Additional information
Claude code analysis says:
daemon/worker.cgates auth-zone task pickup behindworker->thread_num == 0:Probe, transfer, and apply for every zone run on worker 0's event loop. While
apply_axfris iterating chunks and inserting RRs, worker 0 services no sockets — every client landing on worker 0 is blind.z->lockwrite-lock is held for the entire destructive rebuildxfr_process_chunk_listacquiresz->lockas awrlockand holds it until the very end, around:traverse_postorder(&z->data, auth_data_del, NULL)— frees the old treerpz_clear(z->rpz)— destroys and recreates all five RPZ sub-treesaz_insert_rr_decompress+rpz_insert_rrfor every recordauth_zone_verify_zonemd— RRSIG/digest verification across the zonerpz_finish_config— rebuilds parent pointers on three address-treesThere is no atomic-swap pattern. The old tree is freed before the new one is built, so the
wrlockmust be held end-to-end.Every worker consults RPZ policy on every query through three call sites in
services/rpz.c(rpz_apply_maybe_clientip_trigger, rpz_callback_from_iterator_module, rpz_callback_from_iterator_cname). All three follow the same pattern:While worker 0 holds
z->lockwrite on any one RPZ zone, every worker doing an iterator/CNAME callback blocks ona->lockread for that zone. With multiple configured RPZ zones the effect compounds — a single zone's apply freezes the RPZ subsystem across the whole worker fleet, not just worker 0.