Skip to content

Unbound with RPZ enabled, stops answering client queries then loading new RPZ zones after AXFR #1455

Description

@pettai

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:

  1. in unbound, configure RPZ with a master server to fetch the RPZ zones from
  2. wait for zone transfers for the RPZ zones to occur, specifically for full refetching with AXFR
  3. 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:

  1. 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.

  1. 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.

  1. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions