Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
0

Configure Feed

Select the types of activity you want to include in your feed.

ircd.rs / PLAN-P6-progress.md
46 kB

P6 — Config & DNS: per-cluster summary#

One-line top-level summary per P6 cluster (detailed entries live in docs/progress-log/p6.md).

  • P6a DONE: s_conf.c TKLINE cluster (m_tkline/m_untkline/prep_kline/do_kline/wdhms2sec/tkline_expire + the tkconf list) — the oper temporary-kill family ported to ircd-common/s_conf.rs via an s_conf_link.o partial port (-DPORT_TKLINE_P6; the rest of s_conf.c's config grammar stays C). Handler cluster, not server-reachable (msgtab col 2/3; do_kline emits only local SCH_NOTICE&NOTICES notices + reaps local clients). Gates: L1 (s_conf_tkline_diff: wdhms2sec corpus + tkconf add/dedup/expire lifecycle with inverses, highest_fd pinned -1) + L2 (golden_s_conf_tkline: oper TKLINE notice/UNTKLINE/bad-format, the two-client reap → 465+disconnect, non-oper 481 inverse). No S2S (no IsServer/remote-field/propagation path).
  • P6b DONE: res.c m_dns (the resolver/cache debug command — the last command handler still in C) — ported to ircd-common/res.rs via a res_link.o partial port (-DPORT_DNS_M_DNS, de-static'ing cachetop/cainfo/reinfo); the resolver proper stays C. Every msgtab handler is now Rust. Handler cluster, msgtab col 2 (STAT_OPER) only + the is_allowed(ACL_DNS) O-line d gate. L2 (golden_s_dns, dns_oper.conf): oper DNS stats + DNS l empty cache dump, non-oper 481 inverse. No S2S.
  • P6c DONE: s_conf.c flags cluster (the six pure config-flag converters {iline,pline,oline}_flags_parse/_to_string) — same s_conf_link.o partial port (-DPORT_SCONF_FLAGS_P6); first bite into the config grammar. Utility/callee (none in msgtab). L1 (s_conf_flags_diff: all six vs cref_) + L2 (golden_s_conf_flags, flags_oper.conf). No S2S.
  • P6d DONE: s_conf.c match_ipmask (the CIDR/#IP-bitmask matcher — a pure leaf predicate over cptr->ip/username, used by Rust channel ban-matching + the TKLINE/conf-lookup paths) — same s_conf_link.o partial port (-DPORT_SCONF_IPMASK_P6). Utility/callee. L1 (s_conf_match_ipmask_diff: v4-mapped /32//24//0, j-boundary, user@host/cidr, badmask, native v6, each with its inverse) + L2 (golden_s_conf_match_ipmask: channel CIDR +b). No S2S.
  • P6e DONE: s_conf.c find_admin/find_me (two leaf read-only walks of the global conf list for the CONF_ADMIN/CONF_ME entry) — same s_conf_link.o partial port (-DPORT_SCONF_FINDADMIN_P6). Utility/callee. L1 (s_conf_find_admin_me_diff: head/non-head/absent/combined-bits incl. inverses, both globals pointed at one list) + L2 (golden_s_conf_find_admin: the ADMIN command via Rust m_admin). No S2S.
  • P6f DONE: s_conf.c config-file parser (openconf + initconf — the whole ircd.conf grammar) — same s_conf_link.o partial port (-DPORT_INITCONF_P6, un-static'ing lookup_confhost); the line lexer is a new nom-based module (config_parse.rs — faithful process_escapessplit_fields port of the escape/comment pass + getfield), the semantic switch builds byte-identical aConfItem lists/classes + side effects. Not in msgtab. L1 (config_parse_diff: nom split_fields vs cref_getfield; s_conf_initconf_diff: full conf-list build vs cref_initconf over A/I/i/O/o/Y/C/c/N/D/H/L/V/Q/B/K/k) + L2 (golden_s_conf_initconf: STATS y/i/o over the parsed config). No S2S.
  • P6g DONE: s_conf.c find_Oline (the read-only conf-list scan m_oper uses to resolve OPER <name> <pass>: best CONF_OPS entry by name mycmp + host match/match_ipmask, honouring clients < MaxLinks(Class) with the over-limit tmp2 fallback) — same s_conf_link.o partial port (-DPORT_SCONF_FINDOLINE_P6). Utility/callee (m_oper calls it). L1 (s_conf_find_oline_diff: name/host-glob/CIDR-in-out/over-limit-fallback/wrong-status/empty incl. inverses) + L2 (golden_s_conf_find_oline, find_oline.conf: a 10.0.0.0/8 O-line that misses the 127.0.0.1 client → 491, vs a *@* O-line → 381 — the host/CIDR-miss branch the existing OPER golden can't reach). No S2S (no IsServer/remote-field path).
  • P6h DONE: s_conf.c find_conf* family (the seven read-only conf lookups find_conf_exact/find_conf_name/find_conf/find_conf_host/find_conf_host_sid/find_conf_ip/find_conf_entry) — same s_conf_link.o partial port (-DPORT_SCONF_FINDCONF_P6). Utility/callee (none in msgtab; the still-C s_serv.c/s_bsd.c/s_misc.c server-link + registration paths call them). L1 (s_conf_find_conf_diff: all seven vs cref_ over positive + inverse — name/host/status/port/passwd-asymmetry misses, the CONF_OPERATOR over-limit skip, the server-mask mycmp-exact-vs-match-glob split, the NUL HOSTLEN early-outs, the find_conf_ip in-place @-split restore; the differential caught an inverted !match(passwd,sid) sid gate in the first draft) + S2S (golden_s2s_s_conf_find_conf: a peer link drives the C/N-line find_conf accept + find_conf_host/find_conf_host_sid leaf/hub checks → byte-identical burst + post-link routing; the 37-test golden_s2s_* suite + registration goldens are the broader regression). No client-only L2 numeric.
  • P6i DONE: s_conf.c K-line lookup family (find_kill/find_two_masks/find_conf_flags/find_denied) — the read-only K-line / version-mask / deny lookups named in the PLAN P6 row, ported to ircd-common/s_conf.rs via the s_conf_link.o partial port (-DPORT_SCONF_FINDKILL_P6, three guard regions; find_bounce stays C). Config-resolved: TKLINE on (find_kill's tkconf→kconf two-pass loop compiles), KLINE off, TIMEDKLINES off (the reply/check_time_interval block not compiled; timedklines arg inert). Utility/callees (none in msgtab); the still-C s_serv.c check_version, Rust s_user.c register_user, and ircd.c resolve to the Rust defs at link. L1 (s_conf_find_kill_diff: all four vs cref_ over positive + inverse — find_kill tkconf/kconf hit, expired-TK fallthrough, OTHERKILL→ident vs KILL→username check, port gate, CIDR in/out, '='-numeric-only skip, resolved-hostname match, IsKlineExempt short-circuit, no-user guard; find_two_masks/find_conf_flags status/host/name/no-slash/prefix/strpbrk inverses; find_denied host-scan/reversed/digit-passwd/no-D-line) + L2 (golden_s_conf_find_kill, kline.conf: a 127.0.0.1 client hits K|127.0.0.1|…|*|0| mid-registration → 465 + K-lined: Closing Link) + S2S (golden_s2s_s_conf_find_two_masks, vline_reject.conf = s2s.conf + V|*||peer.test|: m_server_estab check_version → find_two_masks → "Bad version" exit). The no-match version-check path is exercised by every other golden_s2s_* test; find_denied is reachable only from try_connections server auto-connect (not in the golden harness) → L1 only.
  • P6j DONE: s_conf.c find_bounce (the RPL_BOUNCE sender — the read-only conf-list scan that emits 010 :Please use this Server/Port instead to a rejected connection) — ported to ircd-common/s_conf.rs via the s_conf_link.o partial port (-DPORT_SCONF_FINDBOUNCE_P6, one guard region; the mutating attach/detach/rehash family + lookup_confhost/ipv6_convert stay C). Three dispatch modes by fd: fd>=0 early-reject (cptr NULL → sprintf+raw sendto), fd==-1 class-number host match, fd==-2 host-match-only. Config: CONF_BOUNCE=262144, RPL_BOUNCE=10. Utility/callee (not in msgtab); callers s_bsd.c (all-conns-in-use), C count_cnlines, and Rust m_nick/m_user (server-only P-line) resolve to the Rust def at link. L1 (s_conf_find_bounce_diff: 9 tests diffing sent bytes — sendQ via sendto_one for fd<0, socketpair recv for fd>=0 — over host-glob hit/miss, CIDR in/out, class-number host on fd==-1 vs the fd==-2 skip, non-bounce-skip + first-match-wins, the cptr-NULL guard, empty list, and the fd>=0 empty-host write vs non-empty-host no-op) + L2 (golden_s_conf_find_bounce, bounce.conf: a server-only P-line P||||16667|0|S| + B|127.0.0.1||bounce.elsewhere|7000| → client NICK on the server-only port → 010 bounce + Server only port exit, byte-identical). No S2S (no IsServer/remote-field path; the fd>=0 raw-sendto branch needs MAXCLIENTS conns → L1 only).
  • P6k DONE: s_conf.c attach/detach mutating cluster (attach_conf/detach_conf/det_confs_butmask/attach_confs/attach_confs_host + the private is_attached/add_cidr_limit/remove_cidr_limit) — the connected component of conf-attachment mutators (grow/shrink cptr->confs + the global conf list, mutate class refcounts aconf->clients/ConfLinks and the per-class CIDR-limit patricia trees), ported to ircd-common/s_conf.rs via the s_conf_link.o partial port (-DPORT_SCONF_ATTACH_P6, three guard regions). Config-resolved: ENABLE_CIDR_LIMITS on (add/remove_cidr_limit compile; pnode->data void* counter), YLINE_LIMITS_IPHASH on (per-host limit loop walks the IP hash), YLINE_LIMITS_OLD_BEHAVIOUR off (max-links gate ConfLinks >= ConfMaxLinks). Utility/callees (none in msgtab); callers — C attach_Iline/count_cnlines/s_bsd.c/s_serv.c/s_service.c + Rust register_user/m_oper/m_umode — resolve to the Rust defs at link. Gates: L1 (s_conf_attach_diff: two fully-isolated worlds via an Api of fn-pointers; round-trip/inverse invariants — attach→detach unlink, is_attached idempotency, -3 Y-line-max + -4 CIDR gates, CIDR freed-slot reuse, free-class on maxLinks==-1, illegal-conf removal from global list, det_confs_butmask, attach_confs/host glob-vs-mycmp; zero diff) + L2 (golden_s_conf_attach, ylinemax.conf: 2 clients in a maxlinks-1 class → bob rejected via attach_conf -3 → "Too many connections", byte-identical; success+detach paths covered pervasively by the registration + golden_s2s_* suite). No S2S (no IsServer/remote-field path; attach_confs C/N + det_confs_butmask SQUIT covered by the 37-test golden_s2s_* establishment/teardown suite). What stays C in s_conf.c is now only rehash, attach_Iline/count_cnlines (callers of this core), and the initconf-private lookup_confhost/ipv6_convert.
  • P6l DONE: s_conf.c attach_Iline + count_cnlines (the two callers of the P6k attach/detach core) — the local-client I-line lookup used during registration (attach_Iline: scan conf for the best CONF_CLIENT line matching the client's port/name/host/password, set the +r/kline-exempt conf flags onto cptr->user->flags, copy the resolved host into sockhost, then attach_conf; emits 464 ERR_PASSWDMISMATCH/find_bounce on rejection) and the read-only single-N-line counter used during inbound server-link establishment (count_cnlines), ported to ircd-common/s_conf.rs via the s_conf_link.o partial port (-DPORT_SCONF_ILINE_P6, one guard region s_conf.c:483–664). Config-resolved: UNIXPORT off (no unix-socket host branch), XLINE off (no XlineExempt clear), NO_DNS_LOOKUP on (hp usually NULL). Utility/callees (none in msgtab); callers — C check_client/check_server (s_bsd.c) — resolve to the Rust defs at link. What stays C in s_conf.c is now only rehash + the initconf-private lookup_confhost/ipv6_convert. Gates: L1 (s_conf_iline_diff: two fully-isolated worlds via an Api of fn-pointers — happy glob/CIDR/empty-mask attach + the no-I-line -2 inverse, port gate, password fall-through vs -8+464, +r/kline-exempt flag-set, hp!=NULL name-match + get_sockhost; count_cnlines single-N/zero/two/non-server-skip/empty; zero diff over 8 tests) + L2 (golden_s_conf_iline, iline_pass.conf: a password I-line with no F flag → a client sending no PASS464 + "Bad password" Closing Link, byte-identical). No S2S (neither has an IsServer branch / remote-field formatting; count_cnlines's server-link path is covered by the 37-test golden_s2s_* link suite).
  • P6m DONE: s_conf.c finalize — rehash + ipv6_convert + lookup_confhost + the conf/kconf/networkname data globals (the last three functions + the last three data globals s_conf.c owned) — ported to ircd-common/s_conf.rs, so s_conf.c is now FULLY Rust → s_conf.o dropped outright (added to PORTED; the s_conf_link.o partial-compile + the "s_conf.o" => "s_conf_link.o" link map removed; the cref oracle keeps the unguarded s_conf.o). rehash = the faithful config reload (free conf/kconf, mark classes MaxLinks=-1, initconf(0), close/reopen listeners, re-read MOTD, the sig 1/'a'/'d'/'t' branches; ULTRIX/USE_SYSLOG off, TKLINE on); ipv6_convert = the pure [user@]addr[/cidr] canonicaliser (in-place @// poke + restore); lookup_confhost = numeric host via inetpton (the NO_DNS path) / alpha host via the still-C gethost_byname, badlookup AND16==255 reset. The conf/kconf/networkname globals flip from bindgen externs to #[no_mangle] pub static mut defs (list.rs/s_serv.rs importers resolve to them at link, same as tkconf/classes). build.rs expands IRCDMOTD_PATH via make → the IRCD_MOTD_PATH rustc-env → ircd_sys::MOTD_PATH, so the Rust rehash's read_motd reads the identical file reference-C does. Utility/callees (none in msgtab); rehash reached from Rust m_rehash (oper REHASH) + still-C ircd.c SIGHUP. Gates: L1 (s_conf_rehash_diff: ipv6_convert vs cref_ over bare-v6/user@/cidr/v4-mapped/passthrough + the input-restore invariant) + L2 (golden_s_serv_maint REHASH 382 success/481 reject now drives the Rust rehash; golden_s_conf_initconf boot exercises lookup_confhost/ipv6_convert). No S2S (no IsServer/remote-field path). lookup_confhost is static in the unguarded oracle → no cref_ symbol → covered by the boot golden, not L1.
  • P6n DONE: res.c's pure wire-format leaf res_comp.c (the BIND DNS name compression/expansion ircd_dn_expand/ircd_dn_comp/__ircd_dn_skipname + the big-endian integer marshalers ircd_getshort/ircd_getlong/ircd__putshort/ircd__putlong) — ported outright to ircd-common/res_comp.rsres_comp.o dropped (added to PORTED; no _link.o). Self-contained leaf: nm --undefined-only shows zero ircd-internal deps (only libc memcpy/strchr/errno); the 7 BIND static helpers (ns_name_*/special/printable/mklower/dn_find) ported as private Rust fns. Faithful raw-pointer port; errno set (EMSGSIZE/ENOENT) on every error path as the C does. Utility/leaf (none in msgtab); callers — still-C res.c/res_mkquery.c — resolve to the Rust defs at link. Gate: L1 only (res_comp_diff: get/put match + put→get round-trip; dn_compdn_expand pack/expand round-trip; the compression-pointer reuse diffed as buffer-relative dnptrs offsets + emitted wire bytes; a hand-built compressed-pointer message; dn_skipname plain/compressed/truncated; the dstsiz-too-small, malformed-pointer, overlength -1 inverses with errno compared). No L2/S2S — pure wire-format leaf with no client/server-reachable command path under the locked config; end-to-end DNS coverage arrives when the resolver proper is ported under a fixture nameserver.
  • P6o DONE: res.c's DNS query-packet builder res_mkquery.c (ircd_res_mkquery — forms QUERY/NS_NOTIFY_OP/IQUERY messages into a caller buffer, returns the packet length or -1) — ported outright to ircd-common/res_mkquery.rsres_mkquery.o dropped (added to PORTED; no _link.o). Deps resolve at link: ircd_dn_comp/ircd__putshort/ircd__putlong are the P6n Rust defs, the resolver-state global ircd_res + ircd_res_init stay C (res_init.o). Layout hazard neutralized: the 12-byte DNS header is written through the bindgen HEADER bitfield type (size 12, set_id/set_opcode/set_rd/set_rcode/set_qdcount/set_ancount/set_arcount byte-identical to the gcc little-endian nameser_def.h layout) instead of hand-packing bitfields; h_errno set via __h_errno_location exactly as the C. Config-resolved: DEBUG off → the RES_DEBUG printf block not compiled. Utility/leaf (none in msgtab); callers — still-C res.c (query_name/do_query_*) — resolve to the Rust def at link. Gate: L1 only (res_mkquery_diff: Rust vs cref_ byte-identical packets over QUERY±recurse, NS_NOTIFY_OP with/without the additional-record branch, IQUERY answer section with/without rdata, plus the -1 inverses — bad op, NULL buf, buflen<HFIXEDSZ, and the question-section buflen/dn_comp overflow — and the shared ++ircd_res.id side effect diffed identically; both ircd_res/cref_ircd_res pinned with RES_INIT to skip the nondeterministic res_init/res_randomid path, a process-global RES_LOCK serializing the parallel #[test] threads). No L2/S2S — pure wire-format leaf with no client/server-reachable command path under NO_DNS_LOOKUP; end-to-end DNS coverage arrives when the resolver proper (res.c+res_init.c) is ported under a fixture nameserver.
  • P6p DONE: res.c's resolver-state bootstrap res_init.c (ircd_res_init — reads /etc/resolv.conf + LOCALDOMAIN/RES_OPTIONS env into the global ircd_res; the static res_setoptions it drives; the random message-id ircd_res_randomid) — ported outright to ircd-common/res_init.rsres_init.o dropped (added to PORTED; no _link.o). The ircd_res global moves from C to a Rust #[no_mangle] static mut (MaybeUninit::zeroed); res_mkquery.rs/res.c/s_bsd.c resolve their ircd_res/ircd_res_init references to the Rust defs at link. Self-contained leaf — the only non-libc dep is inetpton (P1 support.rs). Config-resolved: DEBUG/NEXT/RESOLVSORT/RFC1535/HAVE_GETIPNODEBYNAME/USELOOPBACK/__BIND_RES_TEXT all OFF → only the simple path compiles (no NetInfo, no sortlist, no debug printfs, default-dnsrch derivation active, inet6RES_USE_INET6; AFINET=AF_INET6 so nameservers parse as IPv6). Utility/leaf (none in msgtab). Gate: L1 only (res_init_diff: the ircd_res struct produced by ircd_res_init diffed vs cref_ field-by-field — scalars, ndots/nsort bitfields, the 3 nsaddr_list sockaddr_in6 byte-for-byte, defdname bytes, dnsrch[] as offsets into defdname — over the default-dnsrch/LOCALDOMAIN-search-tokenizer/RES_OPTIONS ndots:+inet6+RES_MAXNDOTS-clamp paths, both globals zeroed + .id pinned nonzero to skip the nondeterministic res_randomid, env-set/init/restore under a process-global RES_LOCK; res_randomid is nondeterministic → only its & 0xffff mask smoke-tested). Boot/registration + golden_s_dns goldens confirm the Rust ircd_res_init/ircd_res boot byte-identical. No L2/S2S (pure leaf, no client/server command path under NO_DNS_LOOKUP; end-to-end DNS coverage arrives when the resolver proper res.c is ported under a fixture nameserver). res.c is the only DNS file still in C.
  • P6q DONE: res.c cache-hash leaves hash_number/hash_name (the two pure DNS-cache bucket hashes — first bite of the res.c cache cluster, ported bottom-up from the resolver's dependency floor) — ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_CACHE_P6q, dropping their bodies + declaring them extern so the still-C cache walkers find_cache_name/add_to_cache/rem_cache/find_cache_number call the Rust defs). Both are upstream file-static, so a new three-way RES_CACHE_LINKAGE macro de-statics them to GLOBAL in the cref oracle's res.o (-DRES_CACHE_EXPOSE, behavior-neutral; res.o is consumed only by libcref.a) so cref_hash_* exists for the L1 differential — neutralizing the l1-cref-static-symbol-limit. Faithful: hash_number is the unrolled hashv=ip[0]; for i in 1..16 { hashv=2*hashv+ip[i] } over a wrapping u32 (% 1009=ARES_CACSIZE only at the end; reads exactly 16 bytes = sizeof in6_addr, AFINET=AF_INET6); hash_name sums bytes to the first ./NUL with signed-char promotion (i8 as i32 as u32). Utility/leaf (not in msgtab). Gate: L1 only (res_hash_diff: Rust vs cref_ over 16-byte v6 addrs incl. all-0xff u32-overflow/v4-mapped/high-bit, names incl. empty/dot-first/embedded-dot/8-bit/long; range-checked [0,1009)). No L2/S2S (no client/server command path; resolver inert under NO_DNS_LOOKUP, cache never populates at runtime — end-to-end DNS coverage arrives when the resolver proper is ported under a fixture nameserver). golden_s_dns + boot goldens confirm the still-C cache walkers call the Rust hashes without regression.
  • P6r DONE: res.c cache removal/expiry core rem_list/rem_cache/expire_cache/flush_cache (the second bite of the res.c cache cluster, building on the P6q hashes) — the connected component that deletes cache entries, ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_CACHE_REM_P6r, generalizing the P6q three-way RES_CACHE_LINKAGE to rem_cache/rem_list). rem_list unlinks one entry from cachetop only (leaves the hash buckets + incache untouched — the faithful quirk); rem_cache does the full delete (NULL any local[]->hostp at this he, unlink from cachetop + both hashtable buckets via the Rust hash_name/hash_number, free name/aliases/addrs/entry, incache-- + ca_dels++); expire_cache reaps the past-due + returns the next poll time; flush_cache reaps everything. The cache data globals hashtable/incache join cachetop/cainfo on RES_DNS_STATIC, now also exposed under -DRES_CACHE_EXPOSE so the cref oracle gets cref_cachetop/cref_hashtable/cref_incache/cref_cainfo for the L1 isolated worlds. Utility/leaf (not in msgtab). Gate: L1 only (res_cache_rem_diff: two fully-isolated Worlds of fn-pointers + global base pointers, entries hand-built à la add_to_cache; flush/expire/rem_cache-hostp/rem_list each with its inverse — flush empties + re-inserts clean, expire keeps survivors in order + returns min-future/now+AR_TTL, rem_cache clears a per-world dangling hostp, rem_list returns the successor + leaks incache by design; zero diff). No L2/S2S (resolver inert under NO_DNS_LOOKUP; golden_s_dns + registration goldens confirm the still-C cache walkers call the Rust removal core without regression). Still C in res.c: the resolver proper + the cache build/insert/lookup half (add_to_cache/update_list/find_cache_*/make_cache).
  • P6s DONE: res.c cache lookup/reorder half update_list/find_cache_name/find_cache_number (the third bite of the res.c port — the half of the hostent cache that looks up and reorders/merges entries, built on the P6q hashes + P6r removal core) — the connected component ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_CACHE_LOOKUP_P6s, extending the three-way RES_CACHE_LINKAGE to the three upstream file-statics so the still-C gethost_byname_type/gethost_byaddr/proc_answer/make_cache callers resolve to the Rust defs at link, and the cref oracle de-statics them to cref_* for L1). update_list reorders a hit to the cachetop head and (if rptr != NULL) merges new aliases (T_PTR) / addresses (T_A/T_AAAA) from the ResRQ reply into the cached hostent — the gnarly double-MyRealloc of the contiguous IP block + the pointer array, then the FLG_PTR_* flag update by addrcount; find_cache_name/find_cache_number do the hashed-bucket lookup + the full-list fallback (the fallback's fixed-s=alias[0] quirk + the find_cache_number rem_list degenerate-entry reap preserved), update_list on hit, ca_na_hits/ca_nu_hits counters. Faithful raw-pointer port preserving the C post-increment index walks (s = h_aliases[i++]) and the for-loop continue→increment semantics. Utility/leaf (not in msgtab). Gate: L1 only (res_cache_lookup_diff: two isolated Worlds of fn-pointers + global base pointers — cachetop/hashtable/cainfo; find_cache_name hashed hit+reorder+counter / name-miss / flags-gate-miss / alias full-list fallback; find_cache_number hashed hit / absent-addr miss / FLG_PTR_VALID gate / multi-addr fallback; update_list rptr=NULL reorder-only + T_PTR alias-merge & T_A addr-merge each with the already-present no-growth inverse; zero diff over 6 tests). No L2/S2S (resolver inert under NO_DNS_LOOKUP; golden_s_dns + golden_registration confirm the still-C build/insert half calls the Rust lookup/reorder core without regression). Still C in res.c: the resolver proper + the cache build/insert half (add_to_cache/make_cache).
  • P6t DONE: res.c cache build/insert half add_to_cache/make_cache (the fourth and final bite of the res.c cache cluster — the two functions that create and link in hostent-cache entries, built on the P6q hashes + P6r removal core + P6s lookup/reorder) → res.c's hostent cache is now FULLY Rust; only the resolver proper remains C. Ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_CACHE_BUILD_P6t, extending the three-way RES_CACHE_LINKAGE to both upstream file-statics so the still-C proc_answer caller of make_cache resolves to the Rust def at link, and the cref oracle de-statics them to cref_* for L1). add_to_cache = list-head push + both hash-bucket links (hash_name/hash_number) + ++incache > MAXCACHED LRU tail-evict via rem_cache + ca_adds++; make_cache = dedup-or-build (T_PTR→find_cache_number per reply addr / T_A·T_AAAA→find_cache_name by name — a hit returns the already-reordered+merged entry; a miss MyMallocs a fresh entry, builds the contiguous IP block + pointer array, steals the ResRQ aliases + h_name (source nulled), clamps ttl<600→600 + re_shortttl++, expireat = timeofday + ttl, sets the FLG_A_VALID/FLG_AAAA_VALID flag, then add_to_cache). Config: DEBUG off (no Debug blocks), AFINET=AF_INET6 (16-byte IN_ADDR), MAXCACHED 512. Utility/leaf (not in msgtab). Gate: L1 only (res_cache_build_diff: two fully-isolated Worlds of fn-pointers + base pointers incl. incache/reinfo/timeofday — build-by-name A/AAAA with the steal inverse, short-ttl clamp + the ≥600 inverse, dedup-by-name + dedup-by-addr-PTR no-add, multi-addr contiguous-block build, add_to_cache LRU boundary at MAXCACHED (evict via ca_dels delta) + the under-cap inverse, both-bucket linkage via the two finders; timeofday pinned so expireat is deterministic; zero diff over 8 tests). No L2/S2S (resolver inert under NO_DNS_LOOKUP, cache never populates at runtime; golden_s_dns + golden_registration confirm the still-C resolver calls the Rust build/insert core without regression). Still C in res.c: only the resolver proper (request queue + answer parser + socket I/O).
  • P6u DONE: res.c request-queue core add_request/rem_request/make_request/find_id (the first bite of the res.c resolver proper — the queue-management primitives the rest of the resolver builds on, ported bottom-up from the outstanding-DNS-request floor after the P6q–P6t cache cluster) — ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_REQ_P6u: a parallel three-way RES_REQ_LINKAGE de-statics the four fns — extern in res_link.o so the still-C walkers del_queries/timeout_query_list/get_res resolve to the Rust defs, GLOBAL in the cref oracle for cref_* L1, static otherwise — and the first/last queue heads flip from static to RES_DNS_STATIC so the Rust core + the still-C walkers share one storage, same as the cache globals). add_request tail-appends + reinfo.re_requests++ (NULL → -1 no-op); rem_request unlinks (repointing last on tail removal) + frees he.h_name/he.h_aliases[]/name/the node; make_request MyMalloc+bzero + seeds sentat=timeofday/retries=3/resend=1/srch=-1/timeout=4/he.h_addrtype=AFINET + copies the Link into cinfo + add_request; find_id linear-scans first. Config: AFINET=AF_INET6, DEBUG off (Debug() traces gone), MyFree→libc::free. Utility/callees (not in msgtab). Gate: L1 only (res_request_diff: two isolated Worlds of fn-pointers + first/last/reinfo base pointers — make_request seeds+appends (NULL-lp zeroes cinfo), add_request order+counter + the NULL no-op, find_id hit/miss + misses-after-rem, rem_request head/middle/tail unlink+last-repoint+free, drained queue first==last==NULL+reusable; timeofday pinned, a process-global LOCK serializes the parallel threads; zero diff over 8 tests). No L2/S2S (resolver inert under NO_DNS_LOOKUP, the queue never populates at runtime; golden_s_dns + golden_registration confirm the still-C walkers call the Rust queue core without regression). Still C in res.c: del_queries/timeout_query_list + the resolver socket/answer half (send_res_msg/do_query_*/query_name/resend_query/proc_answer/get_res/gethost_*/init_resolver/cres_mem).
  • P6v DONE: res.c queue walkers del_queries/timeout_query_list (the second bite of the res.c resolver proper — the reaper/timeout layer that walks the outstanding-DNS request queue, built on the P6u queue core) — ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_WALK_P6v). del_queries(cp) reaps every node whose cinfo.value.cp matches; timeout_query_list(now) walks first, reaping past-due requests whose --retries is exhausted (reinfo.re_timeouts++ + the per-cinfo.flags notify — ASYNC_CLIENT sendto_iauth+ClearDNS, ASYNC_CONNECT sendto_flag SCH_ERROR — then rem_request) or backing off the rest (sentat=now, timeout doubled, resend_query, tout=now+timeout), returning (next>now)?next:now+AR_TTL. A new RES_RESEND_LINKAGE seam de-statics the still-C resend_query (extern in res_link.o so the Rust timeout_query_list resolves to it; the cref oracle keeps it static — cref_timeout_query_list resolves to the objcopy-renamed local, self-contained since resend_query has no out-of-TU caller). Config-resolved: DEBUG off (Debug/myctime traces gone), USE_IAUTH on; retries is unsigned char on this platform → faithful wrapping_sub. Utility/callees (not in msgtab); still-C callers s_bsd.c/ircd.c + Rust list.rs resolve to the Rust defs at link. Gate: L1 only (res_walk_diff: two fully-isolated Worlds of fn-pointers + first/last/reinfo base pointers — del_queries head/middle/tail match + absent-cp no-op + all-match drain; timeout future-survive (min tout) / exhausted ASYNC_CONF·ASYNC_CLIENT (ClearDNS clears the DNS bit only)·ASYNC_CONNECT / retry back-off (resend=0 → resend_query no-op) / a mixed survivor+reaped+retried walk — return value, queue shape, re_timeouts all diffed; timeofday pinned, adfd=-1 + BSS-zero svchans neutralize the variadic notifies, process LOCK serializes; zero diff over 9 tests). No L2/S2S (resolver inert under NO_DNS_LOOKUP, the queue never populates at runtime; golden_s_dns + golden_registration confirm the still-C resolver calls the Rust walkers without regression). Still C in res.c: only the answer parser + socket I/O half (send_res_msg/do_query_*/query_name/resend_query/proc_answer/get_res/gethost_*/init_resolver/cres_mem).
  • P6w DONE: res.c bad_hostname (the pure RESTRICT_HOSTNAMES character validator used by the still-C answer parser proc_answer) — the next bottom-up leaf of the resolver proper, ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_HOSTNAME_P6w; a new three-way RES_HOSTNAME_LINKAGE seam: extern in res_link.o so still-C proc_answer resolves to the Rust def, GLOBAL in the cref oracle under -DRES_CACHE_EXPOSE for cref_bad_hostname, static otherwise). Utility/callee (not in msgtab — called only from proc_answer). Faithful to the compiled body (HOSTNAMES_UNDERSCORE off): the for-increment-on-continue semantics + the isalnum/s[1] short-circuit ordering. Gate: L1 (res_bad_hostname_diff: a hostname corpus × every prefix length 0..=len+1, asserting identical return; valid names, hyphen/dot edge cases incl. the len==1 trailing-hyphen guard, illegal chars space/_///*/:/control/high-bit, and the len boundary). No L2/S2S path (unreachable without the still-C answer parser).
  • P6x DONE: res.c cres_mem (the DNS-cache memory-usage reporter, called from s_debug.c's count_memory via STATS z) — the next bottom-up leaf of the resolver proper, ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_CRES_MEM_P6x). Unlike the file-static cache/queue leaves, cres_mem is exported (res_ext.h) → the cref oracle already carries cref_cres_mem, so no RES_*_LINKAGE/*_EXPOSE seam was needed — the guard just drops the C body and the Rust def resolves it for count_memory at link. Utility/callee (not in msgtab). Faithful to the nm += i - 1 unsigned modular arithmetic (0 aliases → i==0 wraps −1, cancelled by the following += sizeof(char*); reproduced with wrapping_add) and the %d-on-u_long format quirk. Gates: L1 (res_cres_mem_diff: two-world return-value + RPL_STATSDEBUG sendQ byte diff over empty/0-alias-wrap/multi-alias/null-name/multi-entry caches — the per-entry summation the empty boot cache never exercises) + L2 (golden_debug::stats_memory_match_reference, STATS z, stays green). No S2S (oper STATS only, no IsServer/remote-field path).
  • P6y DONE: res.c send_res_msg (the resolver UDP datagram-send leaf — the bottom of the send tree do_query_*/resend_queryquery_namesend_res_msg) — ported to ircd-common/res.rs via res_link.o (-DPORT_RES_SEND_P6y; a new three-way RES_SEND_LINKAGE seam de-statics it: extern in res_link.o so the still-C query_name resolves the Rust def at link, GLOBAL in the cref oracle for cref_send_res_msg). Reads the Rust ircd_res (P6p) + the s_bsd.c resfd global; fans msg[0..len] to max=MIN(nscount,rcount) nameservers (clamped to 1 on RES_PRIMARY, floored to 1), bumping reinfo.re_sent only on a full-length sendto. Utility/callee (not in msgtab; only query_name calls it). Gate: L1 (res_send_msg_diff: two isolated Worlds each with own loopback ::1 UDP recv+send sockets; single-send / NULL-msg / rcount=0 floor / RES_PRIMARY override / two-nameserver / bad-resfd cases diffed on {ret, datagram bytes, re_sent delta, family}). No L2 (DNS path disabled project-wide via NO_DNS_LOOKUP; boot goldens stay byte-identical), no S2S.
  • P6z DONE: res.c query_name (the DNS query builder/sender — the leaf above the P6y send_res_msg: do_query_name/do_query_number/resend_query/get_res call it) — ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_QUERY_P6z; a new three-way RES_QUERY_LINKAGE seam de-statics it: extern in res_link.o so the still-C callers resolve the Rust def at link, GLOBAL in the cref oracle for cref_query_name). Builds the query into a MAXPACKET buffer via the Rust ircd_res_mkquery (P6o), picks a DNS id that doesn't collide with the outstanding-request queue (do { nstmp = ntohs(hptr->id)+k + (tv.tv_usec & 0xffff); … } while (find_id(...)) — LRAND48 off → gettimeofday-seeded), records id/sends++/sent on the ResRQ, and fans the packet via send_res_msg (P6y); r<=0h_errno=NO_RECOVERY return r, send -1h_errno=TRY_AGAIN (with sends already bumped). Utility/callee (not in msgtab). Gate: L1 (res_query_name_diff: two isolated Worlds each with own loopback ::1 UDP recv+send sockets + ircd_res/reinfo/resfd/first base pointers; a strong interposed gettimeofday pins the id randomization so packets are byte-identical between worlds + the find_id collision loop is deterministic — A-record/PTR queries, a forced one-collision id advance vs the no-collision inverse, mkquery-failure NO_RECOVERY no-send, send-failure TRY_AGAIN with sends still bumped; diffed on {ret, datagram bytes, rptr.{id,sends,sent}, re_sent delta, h_errno}). No L2 (DNS path disabled project-wide via NO_DNS_LOOKUP; boot goldens golden_registration/golden_s_dns stay byte-identical — the still-C resolver calls the Rust query_name), no S2S. Still C in res.c: the answer parser + socket I/O (do_query_name/do_query_number/resend_query/proc_answer/get_res/gethost_*/init_resolver).
  • P6aa DONE: res.c do_query_name/do_query_number (the query-dispatch layer) — the next bottom-up cluster of the resolver proper, above the Rust query_name (P6z) and below the still-C gethost_*/get_res/resend_query callers. do_query_name builds the forward-lookup hostname (truncate to HOSTLEN, append ircd_res.defdname on the dotless+RES_DEFNAMES resend path), do_query_number builds the reverse-lookup name (IPv4-mapped → d.d.d.d.in-addr.arpa., else the 32-nibble …ip6.arpa. form); both allocate the ResRQ via make_request (P6u) when none was passed, then call the Rust query_name (P6z). Ported to ircd-common/res.rs via res_link.o (-DPORT_RES_DOQUERY_P6aa) + a new three-way RES_DOQUERY_LINKAGE (extern in res_link.o / GLOBAL in the cref oracle / static otherwise). Utility/callee leaf (not in msgtab), inert under NO_DNS_LOOKUPL1 is the gate (res_do_query_diff, 7 tests, zero diff over a loopback UDP harness with gettimeofday interposed; fresh-vs-resend, domain-append vs already-dotted, overlong-defdname -1, all three reverse-name arms, resend no-alloc). No L2/S2S (no IsServer/remote-field path; golden_s_dns+golden_registration stay green).
  • P6bb DONE: res.c gethost_byname_type/gethost_byname/gethost_byaddr (the public host-lookup entry points, the layer above the cache lookup + query dispatch) — ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_GETHOST_P6bb). No linkage seam — they are exported res_ext.h symbols, so the cref_* oracle already exists (like P6x cres_mem). Each bumps a lookup counter (re_na_look/re_nu_look), consults the Rust cache (find_cache_name/find_cache_number), and on a miss fires the Rust do_query_name/do_query_number when lp != NULL (else returns NULL). gethost_byaddr's only caller (s_bsd.c) is gated behind NO_DNS_LOOKUP's #else → compiled out of the locked binary (ported + L1-verified regardless). Utility/leaf — none in msgtabL1 only. L1 (res_gethost_diff, 8 cases): two isolated cache+query worlds (live vs cref) over the cache globals + resolver-query globals, each with a loopback ::1 UDP recv socket + pinned gettimeofday; branch matrix + inverses (bad-type early-out, cache HIT vs MISS, lp==NULL short-circuit vs lp!=NULL AAAA/PTR query issue, byname→T_AAAA delegation, re_na_look vs re_nu_look split). No S2S (resolver is inert under NO_DNS_LOOKUP; no IsServer/remote-field path).
  • P6cc DONE: res.c resend_query (the DNS request-retry dispatcher) — ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_RESEND_P6cc); the next bottom-up unit above the P6aa query-dispatch layer (do_query_name/do_query_number) and below the still-C get_res. Upstream file-static → the P6v RES_RESEND_LINKAGE seam promoted to the standard three-way shape (extern in res_link.o, GLOBAL in the cref oracle → cref_resend_query for L1, else static). A no-op when resend==0, else bumps reinfo.re_resends and re-dispatches by record type (T_PTRdo_query_number, T_A/T_AAAAdo_query_name, default→no-op). Utility/callee (file-static, not in msgtab; callers = Rust timeout_query_list + still-C get_res). L1 gate (res_resend_query_diff: two isolated cache+query worlds over a loopback UDP harness with gettimeofday pinned; matrix = resend==0 no-op / T_A dispatch / T_PTR dispatch / default-arm counter-only). No L2 (resolver is dead code under NO_DNS_LOOKUP). No S2S. Still C in res.c: only proc_answer/get_res/init_resolver.
  • P6dd DONE: res.c proc_answer (the DNS answer-section parser — fills rptr->he from a nameserver datagram) — ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_PROC_ANSWER_P6dd; a new three-way RES_PROC_ANSWER_LINKAGE de-statics the upstream file-static fn for the Rust def + the cref oracle cref_proc_answer). Walks the answer section (skip questions via __ircd_dn_skipname+QFIXEDSZ; per answer expand name + read type/class/ttl/dlen, optional RES_DEFNAMES defdname append, wrong-type skip, A/AAAA→v4-mapped addr slot, PTR→validate+find_cache_name flag fixup, CNAME→alias) using the already-Rust ircd_dn_expand/ircd_getshort/ircd_getlong (P6n), bad_hostname (P6w), find_cache_name (P6s), MyMalloc (P2) + the still-C variadic sendto_flag; the res.c-private scratch hostbuf moved into Rust with it. Utility/callee (file-static, sole caller the still-C get_res); L1 is the gate, no L2 path (resolver dead-code under NO_DNS_LOOKUP), no S2S. L1 (res_proc_answer_diff: 12 cases over two isolated worlds parsing constructed DNS reply buffers — single A/AAAA, multi-A dedup, good vs bad-hostname PTR, CNAME-then-A, wrong-type skip, bad-IP-len -2, RES_DEFNAMES dotless-append vs dotted-untouched, two-question skip, empty answers). Only get_res/init_resolver (socket I/O) remain C in res.c.
  • P6ee DONE: res.c init_resolver (the bit-flag resolver bootstrap — RES_INITLIST/RES_CALLINIT/RES_INITSOCK/RES_INITCACH + bare op==0) — ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_INIT_P6ee; exported res_ext.h symbol → cref_init_resolver already exists, no linkage seam). Under the locked config LRAND48/DEBUG are off so the srand48 line + RES_INITDEBG block drop out. Still-C I/O callers (s_bsd.c rehash / ircd.c boot) resolve to the Rust def at link. Utility/callee (no msgtab entry; no client path → L1 is the gate). L1 (res_init_resolver_diff: isolated-worlds vs cref_ per op flag — INITLIST zeroes reinfo+nulls queue, INITCACH zeroes cainfo+hashtable, CALLINIT diffs ircd_res masking the random .id incl. the nscount==0 ::1-fallback, INITSOCK asserts return==resfd>=0 + AF_INET6 socket, op==0 returns resfd). No S2S. Only get_res (recvfrom answer driver) remains C in res.c.
  • P6ff DONE: res.c get_res — the LAST C function in res.c (the recvfrom DNS answer driver) — ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_GET_RES_P6ff; exported res_ext.h symbol → cref_get_res already exists, no linkage seam). Reads one nameserver datagram off resfd, ntohs the header, find_ids the request, anti‑spoofs the source against ircd_res.nsaddr_list, branches on rcode (NOERROR/NXDOMAIN/SERVFAIL→TRY_AGAIN, NOERROR+ancount0→NO_DATA, else NO_RECOVERY), parses via the Rust proc_answer, then make_caches an A/AAAA result / PTR‑relookups via gethost_byname_type / retries via resend_query — all callees already Rust. getres_err factored to an early‑return helper. Utility/callee (no msgtab entry; sole caller is the still‑C s_bsd.c io‑loop, inert under NO_DNS_LOOKUP → L1 is the gate). L1 (res_get_res_diff: two isolated loopback‑UDP worlds — resfd recv socket + an ::1 ns socket that sends the crafted reply and captures follow‑ups — with a fixed gettimeofday; diffed {ret, hostent, reinfo deltas, incache, ca_adds, request‑gone, h_errno, follow‑up bytes} across A‑success/unknown‑id/short‑packet/NXDOMAIN/SERVFAIL/REFUSED/NO_DATA/anti‑spoof/PTR‑relookup). res.c now has NO C functions leftres_link.o defines only the resolver data globals; the final res.o drop is the last P6 res step. No S2S.
  • P6gg DONE: res.c data globals → Rust + the final res.o drop (reinfo/cachetop/cainfo/hashtable/incache/first/last — the resolver's private data globals, the last C remnant after P6ff left res.c with no functions) — moved into ircd-common/res.rs as #[no_mangle] pub static mut definitions (null_mut()/0/MaybeUninit::zeroed() reproduce C BSS), so res.o is added to PORTED and dropped outright (no more res_link.o; the long -DPORT_RES_* second-compile is gone). res.c is now FULLY Rust. The cref oracle keeps the full unguarded res.o (-DRES_CACHE_EXPOSE) so the 19 res_*_diff L1 tests still resolve cref_*. Data-global move (no new function) → no new L1 test; gate is the existing res L1 suite (all 19, 130 assertions) still zero-diff with the live-world globals now resolving to the Rust storage, + clean build (0 warnings) + golden_registration/golden_s_dns byte-identical. No L2 resolver path (dead-code under NO_DNS_LOOKUP), no S2S.
  • P6hh DONE: chkconf.c → standalone chkconf-rs binary (the IRCnet config-file checker; the last C in P6) — faithful byte-for-byte port of every path compiled under the locked config (M4_PREPROC/CONFIG_DIRECTIVE_INCLUDE/DEBUGMODE/XLINE OFF): main+arg-parse, new_class/get_class, dgets, getfield (incl. the escape-loop quirk), initconf, validate, confchar, checkSID, config_error (CHKCONF non-CDI branch), mycmp. Standalone crate (no ircd-sys dep, like iauth-rs); the single-file config collapses the mywc/files/findConfLineNumber machinery to the constant per-line location ("ircd.conf", line nr). config_read.c needs no separate Rust lib (ircd uses config_parse.rs from P6f; chkconf-rs reimplements config_error). Gate: L1 black-box differential (tests/differential.rs) — builds the reference C chkconf oracle (make -C cbuild chkconf) and diffs stdout+stderr+exit byte-identical over 13 fixtures × {none,-d,-d3,-d9,-s} + -h (argv0-masked). Two C UB paths (non-NUL-terminated dgets buffer: ≥511-char line echo under -d, and a config with no trailing newline) are documented and excluded. No L2/S2S (standalone CLI, not a msgtab handler).