← board

dns_libc — a getaddrinfo backend, for the policy dns_resolved cannot see

Why it is still wanted now that dns_resolved exists

The two backends' gaps are roughly disjoint, which is the whole reason the decision kept both:

needs misses
dns_resolved systemd + resolved running + its Varlink socket musl distros, minimal containers, systemd hosts with resolved disabled
dns_libc glibc at runtime static/libc-free targets

Specifically dns_libc reaches nsswitch policy — custom NSS modules, mDNS (.local), and VPN split-DNS arrangements that route through NSS rather than through resolved. dns_resolved cannot see any of that, and dns_wire sees none of it by construction.

Shape

dlopen("libc.so.6") + getaddrinfo through the existing dynlib machinery ([[feature-real-dynlib-loader]]), accepting the glibc-runtime dependency and version-sensitive struct binding as the known cost. Note PalBackendDlOpen is real only under -dPXX_DYNLIB_LIBC today (the syscall-only core avoids linking libc), so this backend inherits that define or must state its own.

struct addrinfo and struct sockaddr_in/_in6 layouts are the risk: they are ABI, not API, and getting a field offset wrong yields a plausible wrong address rather than a failure. Bind them against a gcc-built oracle that prints offsetof for every field used, the way the M_* constants were pinned in test/cmath_constants.c.

Selection

Already wired: -dPXX_DNS_LIBC is accepted by dns.pas today and fails the build with an error naming this ticket, so nobody silently gets dns_wire when they asked for libc. Implementing this means replacing that {$error} with the uses entry and the two dispatch calls — the mutual-exclusion guard against -dPXX_DNS_RESOLVED is already there and already tested.

Landed 2026-08-02 (commit 55b0a5b1e)

lib/rtl/dns_libc.pasdlopen("libc.so.6") + getaddrinfo/freeaddrinfo through the existing dynlib machinery, wired into dns.pas behind -dPXX_DNS_LIBC.

The ABI was pinned first, before any code

This ticket flagged struct addrinfo as the risk, so the layout was taken from a gcc offsetof probe and checked against pxx's record layout before the backend was written:

field gcc x86-64 pxx x86-64
ai_flags / family / socktype / protocol 0 / 4 / 8 / 12 same
ai_addrlen 16 16
ai_addr 24 24
ai_canonname 32 32
ai_next 40 40
sizeof 48 48

ai_addr precedes ai_canonname — glibc's order, and the reverse of how POSIX reference pages commonly list them. That pair is the easiest thing here to get backwards, and doing so yields a plausible wrong pointer rather than a failure, so the test asserts the ordering explicitly on top of the offsets.

The layout probe was run on all four targets: x86-64 and aarch64 give 48/24/32/40, i386 and arm32 give 32/20/24/28, which is the natural-alignment layout a 32-bit glibc produces.

Selection and its guard

-dPXX_DNS_LIBC joins the existing convention. Two new compile-time refusals, both verified to fail with exit 1 and produce no binary:

Fallback contract matches dns_resolved's: a real DNS verdict including NXDOMAIN is returned as-is, and only DNS_ERR_LIBC_UNAVAIL — no loader, no glibc, getaddrinfo unresolved — falls through to wire. getaddrinfo's own codes are mapped rather than passed through: EAI_NONAME/EAI_NODATA become rcode 3, EAI_AGAIN/EAI_FAIL become 2, and anything else is not a DNS verdict so it stays a backend error and falls back, rather than reporting a lookup result we did not actually get.

hints.ai_socktype is set to SOCK_STREAM deliberately: without it getaddrinfo returns every address once per socket type, which would have looked like duplicate answers.

Verified

check result
vs getent ahostsv4 (the nsswitch oracle) identical addresses
vs dns_wire and dns_resolved through the facade identical
NXDOMAIN rcode 3, zero addresses
ABI offsets vs gcc all 10 assertions, x86-64 and i386
both defines / missing loader define compile error, exit 1, no binary
default build (no define) undisturbed on all four targets

Cross-target, stated precisely. The layout is verified on all four targets by the static probe. The live getaddrinfo call runs on x86-64 and i386 only: this backend necessarily produces a dynamically-linked binary, and this box has no aarch64/arm32 sysroot, so qemu cannot find those targets' ld-linux. That is an environment limit, not a code result, and it is item (b) of [[feature-real-dynlib-loader]] — the same missing cross-runner story. Not claimed as passing.

Test: test/lib_dns_libc.pas, in lib-test twice (default and with the define), localhost only so it needs no network, skipping its libc half where glibc or the loader is absent.

Gate

Agreement with dns_resolved and dns_wire on names all three can answer, plus at least one name only NSS can (an /etc/nsswitch.conf module or a .local mDNS name) to prove it is actually going through NSS. localhost-only tests like test/lib_dns_resolved.pas keep it network-free; the backend must skip itself where glibc is absent, as the resolved backend skips where its socket is. Cross-target i386/aarch64/arm32, since the struct layouts differ by word size — that check is what caught nothing here only because dns_resolved sends JSON; dns_libc binds real C structs and is far more exposed.

Log