dns(macOS): replace getaddrinfo_async_start with DNSServiceGetAddrInfo over a shared connection
Problem
On macOS, dns.lookup() and the internal connect-path resolver use libinfo's getaddrinfo_async_start. Despite the name, libinfo implements this by dispatching a blocking si_addrinfo onto the libdispatch overcommit root queue (si_module.c:1173):
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, DISPATCH_QUEUE_OVERCOMMIT), ^{ si_async_launchpad(req); });
So each distinct in-flight lookup parks one worker thread in-process. Measured on Darwin 25.5 arm64 with distinct unresolvable hostnames (to defeat same-host coalescing), threads via ps -M:
N=50 baseline threads=9 -> PEAK threads=64
N=200 baseline threads=9 -> PEAK threads=214
N=500 baseline threads=9 -> PEAK threads=513 (+26 MB RSS)
N=1000 baseline threads=9 -> PEAK threads=525
Linear until libdispatch's own ~512-thread ceiling; threads do not retire promptly after completion.
Why this is the same resolver
libinfo's search_addrinfo only consults modules that implement sim_addrinfo; ds_module (Open Directory / LDAP) sets it to NULL with the literal comment /* host lookups not supported */ (ds_module.c:2241-2275). Only mdns_module (→ DNSServiceQueryRecord → mDNSResponder) and file_module (→ /etc/hosts) do. mDNSResponder itself watches /etc/hosts. So getaddrinfo() on macOS = mDNSResponder + /etc/hosts, and the thread wrapper is the only thing this change removes.
Fix
Talk to mDNSResponder directly via DNSServiceGetAddrInfo over a single DNSServiceCreateConnection + kDNSServiceFlagsShareConnection primary: one Unix-domain socket to the daemon, one kqueue knote, zero extra threads regardless of in-flight count. Both dns.lookup() (the GetAddrInfoRequest path) and the internal connect-path resolver (internal::Request) now go through the shared connection.
Completion on a shared connection can't use kDNSServiceFlagsMoreComing (it's collective across all sub-refs). Instead this tracks per-family pending bits: dnssd_clientstub always passes a family-tagged sockaddr in the callback, including for NoSuchRecord and Timeout (dnssd_clientstub.c handle_addrinfo_response), so a query is ready once every requested family has reported in. kDNSServiceFlagsReturnIntermediates + kDNSServiceFlagsTimeout ensure each family eventually does.
If mDNSResponder is unreachable (DNSServiceCreateConnection fails, e.g. in a sandbox), both paths fall back to the existing blocking getaddrinfo on the work pool.
Deleted: the lib_info dlopen/dlsym module, getaddrinfo_send_reply (the mach-port reply bridge in io_darwin.cpp), the per-request machport FilePolls.
Test
Covered by the existing DNS suites, which run through this backend on macOS: test/js/node/dns/node-dns.test.js (119), test/js/bun/dns/dns-interleave.test.ts, test/js/node/net/node-net.test.ts (67), test/js/node/dgram/.
Thread behaviour was measured by hand rather than asserted in CI: 200 concurrent dns.lookup() of distinct .invalid hostnames, sampling ps -M <pid> while in flight — peak ≈ baseline + 200 before, ≈ baseline after. An earlier dns-sd-threads.test.ts asserting this was dropped at the maintainer's request.
Notes
protocolis passed asIPv4|IPv6explicitly (not0), because the completion check needs to know which families were requested. This drops the daemon's "skip family with no routable address" heuristic (theAI_ADDRCONFIGequivalent). The existingBUN_FEATURE_FLAG_DISABLE_IPV4/IPV6env vars still constrain it, and happy-eyeballs handles unreachable addresses at connect time.BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO=1still forces the work-pool fallback (name kept for compat).
no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/dns/dns-sd-threads.test.ts