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.

P6 — Config & DNS — progress log#

Per-cluster entries for P6 (s_conf.c+config_read.c, chkconf.c, the resolver res*.c). The command-handler clusters inside these P6 TUs follow the same command-cluster-port skill seam used in P5 (the msgtab fn-pointer table + a <tu>_link.o partial port).

  • 2026-06-04 — P6a (s_conf.c TKLINE cluster) merged. The first P6 sub-phase. Ported the TKLINE command cluster — m_tkline/m_untkline (the oper handlers), prep_kline (param validation), do_kline (add/update a t/kline + reap matching local clients), wdhms2sec (the 0w1d2h3m4s duration parser), tkline_expire (the periodic ring-down called from the ircd.c event loop), and the tkconf list global — to ircd-common/s_conf.rs.

    • Cluster choice / why now. P5's command-handler clusters are complete (only s_auth.c remains in P5, no m_* handlers, deferred to P7/P-IAUTH). The only m_* handlers still in C are the P6 TUs: m_dns (res.c, couples to the open resolver-swap decision) and the TKLINE family. TKLINE is the cleanest remaining real command cluster — no socket I/O, no unmade design decision, a self-contained connected component over tkconf.
    • Guard / partial port. config_read.c is #include-d into s_conf.c (one big P6 TU) → s_conf.o can't be dropped outright. #if defined(TKLINE) && !defined(PORT_TKLINE_P6) guards wrap the seven symbol definitions; an s_conf_link.o second-compile (-DPORT_TKLINE_P6, carrying the same machine-path defines as the native s_conf.o recipe — IRCDMOTD_PATH/IRCDM4_PATH/ IRCDCONF_PATH/KLINE_PATH/IRCDCONF_DIR/M4_PATH) is substituted into the link set ("s_conf.o" => "s_conf_link.o" in link_objs()). The cref oracle keeps the full unguarded s_conf.o. Gotcha: tkconf is referenced by surviving C (find_kill, s_conf.c:2163) and its extern decl in s_conf_ext.h is gated #ifndef S_CONF_C (suppressed inside the owning TU) — so the guard does not delete the definition; it swaps it for an extern aConfItem *tkconf; so the C side still compiles while Rust owns the definition. (The function prototypes use EXTERN→empty, so they stay visible and need no such treatment.)
    • Config-resolved body. KLINE offm_kline + the #ifdef KLINE kline-config-file-write block in prep_kline are not compiled/ported; do_kline/ prep_kline only ever run with tkline=1, but the tkline?tkconf:kconf ternary keeps kconf referenced → it stays a C extern. TKLINE on; TKLINE_MAXTIME = 99999999 (the prep_kline clamp applies); SCH_TKILL = SCH_NOTICE (the notices go to &NOTICES, not a dedicated channel). NO_DNS_LOOKUP on (clients register numeric-host → do_kline's resolved/unresolved branch is faithful either way). Callees: make_conf/free_conf are the Rust ports in list.rs, find_class in class.rs, match/exit_client/is_allowed/m_nopriv are Rust; kconf/match_ipmask/ipv6_convert/get_client_name/nexttkexpire stay C externs (via bindings). Faithful gotchas: wdhms2sec's tmp persists across loop iterations (set in the digit branch, consumed in the unit branch) and the int multi*tmp uses wrapping_mul (C signed overflow vs Rust debug panic); (%u) formats a time_t exactly as the C does (low 32 bits).
    • Classification. Handler cluster — msgtab TKLINE/UNTKLINE = {m_nop, m_nopriv, m_tkline, m_tkline, m_unreg}: reachable by an oper (col 2) or service (col 3); a server prefix hits m_nop (col 0). No IsServer(cptr) branch, no remote-user field formatting, no sendto_serv propagation. → L1 + L2, NO S2S (no server-reachable / remote-field path at all).
    • L1 (ircd-testkit/tests/s_conf_tkline_diff.rs): wdhms2sec corpus (unit math, bare-digit TKLINE_MULTIPLIER path, empty/NULL, bad-unit → -1) + the tkconf lifecycle with highest_fd pinned -1 on both sides (reap loop inert): do_kline head-insert, dedup re-add (bumps hold, no new node — inverse), tkline_expire(0) partial removal with head+middle relink + next-min return (inverse), tkline_expire(1) empties (inverse), re-add post-teardown is a clean single node. Zero-diff vs cref_.
    • L2 (ircd-golden/tests/golden_s_conf_tkline.rs, fixture tkline_oper.conf, O-line flag T = ACL_TKLINE): oper_tkline_notices (OPER → JOIN &NOTICES → TKLINE insert notice → UNTKLINE unlink notice → bad-format error to self); oper_tkline_reap (two clients — the oper TKLINEs a connected victim's exact ~victim@127.0.0.1; victim gets 465 + ERROR Closing Link, the oper — different ident, unmatched — sees the TKLINE + "TKill line active for" notices; the only layer exercising the reap loop); nonoper_tkline_denied (the dispatch-gate inverse — a non-oper TKLINE → 481 m_nopriv, never do_kline). All byte-identical vs reference-C.
    • No S2S — TKLINE has no IsServer branch, formats no remote-user fields, and does not propagate to servers (local &NOTICES notices + local-client reaping only), so the S2S path is genuinely unreachable under the locked config.
  • 2026-06-04 — P6b (res.c m_dns) merged. The DNS resolver/cache debug command — the last command handler still in C — is ported to ircd-common/src/res.rs; every msgtab handler is now Rust.

    • Cluster choicem_dns only: it walks the DNS cache (cachetop) and prints the cainfo/reinfo stats structs. The resolver proper (cache management, queries, parsing) stays C for the full P6 DNS port.
    • Guard / partial portres_link.o second-compile (-DPORT_DNS_M_DNS, mirroring s_conf_link.o): #ifndef PORT_DNS_M_DNS brackets m_dns, and a RES_DNS_STATIC macro de-static's the three file-statics the handler reads (cachetop, cainfo, reinfo) so the Rust port resolves them. The plain res.o oracle keeps everything static. res.o → res_link.o substitution added to link_objs(); res.o stays in CREF_OBJS for the archive (not in PORTED — the resolver is still C).
    • Config-resolved bodycainfo/reinfo are res.c-private struct tags (cacheinfo/resinfo), not in any header, so mirrored as #[repr(C)] int-only structs in res.rs. is_allowed/m_nopriv/inetntop are the Rust ports (linker-resolved); sendto_one is the still-C variadic trampoline. AFINET=AF_INET6 → inetntop(AF_INET6=10, …).
    • Classification — handler cluster, msgtab DNS 0 MPAR { m_nop, m_nopriv, m_dns, m_nop, m_nop }: col 2 (STAT_OPER) is the only reach; the handler-internal is_allowed(sptr, ACL_DNS) needs O-line flag d.
    • L2golden_s_dns (booted with dns_oper.conf): oper_dns (OPER → 381, then DNS = the three Ca/Re/Ru stats NOTICE lines + DNS l = empty cache dump) and nonoper_dns_denied (the dispatch-gate inverse — non-oper DNS → 481 m_nopriv, never the stats). Both byte-identical vs reference-C.
    • No S2Sm_dns has no IsServer branch and formats no remote-user fields (it reads only the local cache + stats). The cache-dump line formatting (Ex/= … (CN)/= … (IP)) has no L2 path under the project-wide NO_DNS_LOOKUP (the cache is always empty); covered by the faithful port + review.
  • 2026-06-04 — P6c (s_conf.c flags cluster) merged. The second P6 sub-phase and the first bite into the config grammar TU. Ported the six pure config-flag converters — iline_flags_parse/iline_flags_to_string (I-line CFLAG_*), pline_flags_parse/pline_flags_to_string (P-line/port PFLAG_*), oline_flags_parse/oline_flags_to_string (O-line ACL_*) — to ircd-common/src/s_conf.rs.

    • Cluster choice / why now. The user picked the config grammar over the resolver (the resolver swap — faithful vs hickory-dns — is still an open design decision; NO_DNS_LOOKUP means res*.c isn't exercised at runtime, so that work risks being throwaway). The flags converters are the cleanest self-contained connected component of s_conf.c: six pure char*<->long functions, no shared statics with the rest of the TU, no conf-list dependency — a P1-style leaf port.
    • Guard / partial port. Extended the existing P6a s_conf_link.o second-compile with -DPORT_SCONF_FLAGS_P6 (alongside -DPORT_TKLINE_P6); a single #if !defined(PORT_SCONF_FLAGS_P6) … #endif brackets the contiguous s_conf.c:91-376 block. No new link_objs() entry (the s_conf.o -> s_conf_link.o substitution already exists); the cref oracle keeps the full unguarded s_conf.o for the L1 diff.
    • Config-resolved body. XLINE off → no e/CFLAG_XEXEMPT (iline); CLIENTS_CHANNEL off → no &/ACL_CLIENTS (oline); ENABLE_SIDTRACE off → no v/ACL_SIDTRACE letter (oline). TKLINE on → the #ifndef TKLINE → #undef OPER_TKLINE (config.h:911) is skipped, so all OPER_* are onoline_flags_parse's only effective post-mask is if (tmp & ACL_LOCOP) tmp &= ~ACL_ALL_REMOTE (every #ifndef OPER_x → tmp &= ~ACL_x compiled out). KLINE off but OPER_KLINE on → the q/ACL_KLINE bit is kept. Flag-bit values come from ircd_sys::bindings (CFLAG_*/PFLAG_*/ACL_*, u32 consts bindgen evaluates from struct_def.h) — no hardcoded magic. The 'A' arm is the exact C expression ACL_ALL & ~(ACL_LOCOP|ACL_CLIENTS|ACL_NOPENALTY|ACL_CANFLOOD) (not a hand-enumerated bit list — it carries ACL_TKLINE/ACL_SIDTRACE since ACL_ALL does, independent of the gated letters). The three _to_string static output buffers are module-private static mut [c_char; BUFSIZE] (not ABI), the NUL written without bumping the index (0-warning).
    • Classification. Utility/callee cluster — none of the six are in msgtab. *_parse run from initconf (config load, stays C → resolves to Rust); iline/oline _to_string render in m_stats (s_serv.c, already Rust) STATS i/o. pline_flags_to_string is only on the volatile STATS l/L link-info path (sendQ + timeofday-firsttime) → no non-volatile L2 path, L1-only. → L1 + L2, NO S2S (pure converters, no IsServer branch, no remote-user fields).
    • L1 (ircd-testkit/tests/s_conf_flags_diff.rs): all six fns vs cref_ over a per-fn corpus — every flag letter, the full set, dup/mixed/unknown chars, empty, iline NULL→0, the gated e/&/v (ignored), the LOCOP-masked combo (exercises the K/k, S/s, C/c either-branches in to_string), plus a parse→to_string round-trip. Zero-diff (7 tests).
    • L2 (ircd-golden/tests/golden_s_conf_flags.rs, fixture flags_oper.conf): the shared oper.conf has empty flags (only the - branch), so a new fixture carries rich flags — O-line …|10|LKSCdr → STATS o renders the LOCOP-masked local variants Lkscdr (243); I-line …|10|DIRE| → STATS i renders DIRE (215). Exercises Rust *_parse at config load and Rust *_to_string at STATS end-to-end; both byte-identical vs reference-C. Regression: the existing golden_s_serv_stats STATS i/o (- branch on oper.conf) stays green.
    • No S2S — the flag converters have no IsServer branch and format no remote-user fields; genuinely unreachable from a server link.
  • 2026-06-04 — P6d (s_conf.c match_ipmask) merged. Ported the CIDR/#IP-bitmask matcher match_ipmask(char *mask, aClient *cptr, int maskwithusername) (s_conf.c:400-467) into ircd-common/s_conf.rs. A pure, side-effect-free predicate over cptr->ip (an in6_addr under AFINET6) + cptr->username; returns -1 on error, 0 on match, 1 when NO match. The next bite into s_conf.c after P6c — a leaf IP helper, not config grammar.

    • Cluster choice / why now. The cleanest remaining self-contained s_conf.c function: no socket, no conf-list dependency, no unmade design decision. High leverage — it's the CIDR matcher behind Rust channel.rs:971 ban-matching and the already-Rust TKLINE do_kline/prep_kline + surviving-C conf lookups. ipv6_convert (the sibling IP-normalization helper) is left for a later bite: config-load-only, no clean L2 numeric path, shares no statics.
    • Guard / partial port. #if !defined(PORT_SCONF_IPMASK_P6) wraps the def; s_conf_link.o's make-eval recipe gains -DPORT_SCONF_IPMASK_P6 (alongside the P6a/P6c guards). The s_conf_ext.h prototype uses EXTERN→empty, stays visible. The cref oracle keeps the unguarded s_conf.o (→ cref_match_ipmask). RED was the undefined-symbol link error: match_ipmask referenced by Rust channel.rs (ban), Rust s_conf.rs (do_kline/prep_kline), and surviving C s_conf.c — exactly one symbol to port, confirming the checklist.
    • Config-resolved body. AFINET=AF_INET6 (=10) → struct IN_ADDR==in6_addr; an IPv4 joiner is V4-mapped (::ffff:7f00:0001). NO_DNS_LOOKUP on, but the matcher reads only the binary cptr->ip, so faithful regardless. Removed match_ipmask from s_conf.rs's bindings import (now a local #[no_mangle] def; same-module callers bind it directly); channel.rs keeps importing the bindgen extern decl (resolves to the Rust symbol at link).
    • Faithfulness. strncpyzt(dummy,mask,128) = strncpy + force dummy[127]=0, operate on the 128-byte copy, keep omask=original for the error message; sscanf(p+1,"%d",&m) via libc; IN6_IS_ADDR_V4MAPPED = addr32[0..2]==0,0,htonl(0xffff) (0xffffu32.to_be()); lmask = htonl(0xffffffffL<<(32-j))(0xffff_ffffu32<<(32-j)).to_be() (the C u_long shift truncated by htonl == a u32 shift; j is 1..31 in that block, so the shift amount is in range — j==0 skips it). s6_addr accessed via the bindgen union __in6_u.__u6_addr8/__u6_addr32; addr32[m] reaches at most index 3 when j!=0 (m 97..127 → m>>5==3), index 4 only when m==128 → j==0 (not indexed). The badmask: sendto_flag(SCH_ERROR,…) notice only fires when maskwithusername.
    • Classification. Utility/callee — not in msgtab. → L1 + L2, NO S2S (pure local-IP predicate, no IsServer branch, no remote-user field formatting, no sendto_serv).
    • L1 (ircd-testkit/tests/s_conf_match_ipmask_diff.rs): Rust match_ipmask vs cref_ over a corpus on parallel make_client'd clients (ip set via the project inetpton, exactly how the I/O layer fills it): v4-mapped /32 //24 //0 with one-off inverses; the sub-32-bit j-boundary (/28, /20) with below/above inverses; user@host/cidr (maskwithusername=1) user-match-then-IP + user-mismatch inverse; badmask (maskwithusername=0 so no sendto_flag): no-slash, m<0, m>128, v4-mapped m>96-after-+96, inetpton fail, non-numeric prefix → all -1; native v6 /32 //48 //0 (the m+=96 branch NOT taken). Zero-diff (5 tests).
    • L2 (ircd-golden/tests/golden_s_conf_match_ipmask.rs): the end-to-end gate that the Rust matcher fires on the real JOIN path. alice (chanop) +b *!*@127.0.0.0/24 on #banc → a second client from 127.0.0.1 JOIN is banned 474 (the literal glob-match of 127.0.0.0/24 vs the string-IP misses → is_banned falls to match_ipmask, which matches via CIDR); the inverse +b *!*@128.0.0.0/24 on #okc lets the JOIN through (echo + 366). The 127-vs-128 contrast isolates match_ipmask as the sole discriminator. Byte-identical ref-C vs Rust.
    • No S2Smatch_ipmask has no IsServer branch and formats no remote-user fields; genuinely unreachable from a server link.
  • 2026-06-04 — P6e (s_conf.c find_admin/find_me) merged. Ported the two leaf conf-list scans find_admin(void) / find_me(void) (s_conf.c:899-918) into ircd-common/s_conf.rs. Each is a read-only walk of the global conf list returning the first aConfItem whose status carries a single bit (CONF_ADMIN=1024 / CONF_ME=256), else NULL. The next bite into s_conf.c after P6d — still read-only accessors over the conf list, not the config grammar.

    • Cluster choice / why now. The cleanest remaining self-contained s_conf.c functions: trivial list scans, no socket, no unmade design decision. They share only the conf global — already a bindgen extern (pub static mut conf), not a private static, so no de-static'ing needed. High leverage: find_admin is the accessor behind the already-Rust m_admin (P5ii, the ADMIN command); find_me gates boot.
    • Guard / partial port. #if !defined(PORT_SCONF_FINDADMIN_P6) wraps both C defs; s_conf_link.o's make-eval recipe gains -DPORT_SCONF_FINDADMIN_P6 (alongside the P6a/P6c/P6d guards). The s_conf_ext.h prototypes (EXTERN→empty) stay visible. The cref oracle keeps the unguarded s_conf.o (→ cref_find_admin/cref_find_me). RED was the undefined-symbol link error: find_admin referenced by Rust s_serv.rs (m_admin), find_me by surviving C ircd.c:1047 + s_bsd.c:2932 — exactly two symbols, confirming the checklist.
    • Config-resolved body. Both functions are config-independent; the only build fact is the bit values (CONF_ME=256, CONF_ADMIN=1024, bindgen consts). Faithful to the C for (aconf=conf; aconf; aconf=aconf->next) if (aconf->status & BIT) break; return aconf; — the loop variable is NULL at the end, so a no-match returns NULL. Walk the raw intrusive list via (*aconf).next, read (*aconf).status (u_int).
    • Classification. Utility/callee — neither is in msgtab. → L1 + L2, NO S2S (no IsServer branch, no remote-user field formatting, no sendto_serv; m_admin's hunt_server forward is already-Rust and separately covered).
    • L1 (ircd-testkit/tests/s_conf_find_admin_me_diff.rs): build ONE hand-chained conf list (via make_conf) and point both the live conf global and the renamed oracle cref_conf at the same head, so the Rust port and the cref_ oracle walk identical memory and must return the identical pointer + null verdict. Cases: admin-not-at-head; the no-match inverse for each finder (→ NULL); ADMIN/ME on distinct entries (no aliasing); both bits on one entry (same entry); the empty list (both NULL). A process-wide Mutex serializes the tests (cargo's parallel #[test] threads otherwise race the shared global — an empty-list test setting conf=NULL raced a non-empty test mid-scan; caught in RED→GREEN). Zero-diff (6 tests).
    • L2 (ircd-golden/tests/golden_s_conf_find_admin.rs): drive ADMIN from a registered local client against minimal.conf's standard A-line (A|Admin|admin@test|info||TestNet|); assert RPL_ADMINME (256) / ADMINLOC1 (257, host="Admin") / ADMINLOC2 (258, passwd="admin@test") / ADMINEMAIL (259, name="info") byte-identical ref-C vs Rust. Rust m_admin calls the now-Rust find_admin. find_me's coverage is the boot itself — both servers refuse to start without their M-line, so the successful boot exercises its scan.
    • No S2S — neither scan has an IsServer branch or formats remote-user fields; genuinely unreachable from a server link.
  • 2026-06-04 — P6f (s_conf.c openconf/initconf — the config-file parser) merged. The config-grammar bite of P6: ported openconf() (s_conf.c:1340, plain open(O_RDONLY) under M4_PREPROC-off) and initconf() (s_conf.c:1495, the whole ircd.conf line reader + semantic switch) into Rust, building byte-identical aConfItem lists + side effects. This is the first bite into the config grammar proper (P6c/d/e were leaf converters/scans); rehash, the conf lookups, and the res.c resolver stay C.

    • Rust-native parsing library. Per the user directive, the line lexer is a new nom-based module ircd-common/src/config_parse.rs (nom 7, added to ircd-common/Cargo.toml) — a faithful two-stage port of initconf's inline lexing: process_escapes (s_conf.c:1551-1574 escape/comment pass) → split_fields (parse.c getfield |-splitter). It reproduces the historical dead-quotes[] quirk (the in-place shift overwrites the translated byte, so every \X→literal X; trailing \ truncates; # is a comment) and getfield's trailing-\| edge (*(end+1)==0 leaves the backslash literal, | a final separator). The semantic switch lives in s_conf.rs.
    • Config-resolved body. CONFIG_DIRECTIVE_INCLUDE/M4_PREPROC off → the dgets/getfield reader compiles (NOT the config_read.c #include path), openconf is the plain-open #else branch; XLINE off (no name3/CONF_XLINE); ENABLE_CIDR_LIMITS on (tmp5 9th field → add_class cidr arg); FASTER_ILINE_REHASH on (find_conf_entry only for CONF_LISTEN_PORT); SPLIT_SERVERS=10/SPLIT_USERS=5000 pinned (bindgen drops the object-macros). Callees: Rust make_conf/free_conf/ delist_conf/find_class/add_class/collapse/{iline,pline,oline}_flags_parse/ match_ipmask; C ipv6_convert/add_listener/setup_ping/find_conf_entry/ check_split/check_class/lookup_confhost/dgets.
    • Guard / partial port. #if !defined(PORT_INITCONF_P6) wraps openconf + initconf; lookup_confhost (a file-static, the only initconf-private callee that stays C — DNS/socket) is un-static'd under the guard so Rust can call it. s_conf_link.o's eval recipe gains -DPORT_INITCONF_P6 (alongside the P6a/c/d/e guards). The cref oracle keeps the unguarded s_conf.ocref_initconf/ cref_openconf/cref_getfield. RED was the undefined-symbol link error for initconf (ircd.c:1016 boot) / openconf; GREEN confirmed via nm target/debug/ircd (initconf/openconf from Rust, lookup_confhost from s_conf_link.o).
    • Faithful simplification. The C tmp2 heap copy of the port field (DupString'd for CONNECT/ZCONNECT, read at ping setup, carrying a latent cross-line double-free on the if(tmp2) MyFree(tmp2) leftover path) is elided — its only observable effect, ping->port, is computed identically from the still-live port field. No structural difference. The me.serv->sid fixed 5-byte toupper loop is bounds-read to the field NUL (the realistic 4-char SID "000A" reads exactly field+NUL; only a malformed <4-char SID would diverge, where C reads adjacent line-buffer bytes).
    • Classification. Neither in msgtabL1 + L2, NO S2S (no IsServer branch, no remote-user fields).
    • L1a (ircd-testkit/tests/config_parse_diff.rs): nom split_fields vs cref_getfield over plain/empty/escaped-delimiter/realistic lines — byte-identical field sequences (4 tests).
    • L1b (ircd-testkit/tests/s_conf_initconf_diff.rs): Rust initconf vs cref_initconf parse the same on-disk config into separate conf/kconf lists (both seeded via initclass/cref_initclass, BOOT_QUICK to skip DNS); walk both in lockstep asserting every field (status/port/flags/host/passwd/name/name2/ source_ip/class#/ping.port) + networkname byte-identical, over A/I/i/O/o/Y/C/c/N/D/H/L/V/Q/B/K/k + skipped comment/indented/no-delimiter lines. M (me.serv) + P (listen socket) lines excluded here → covered by L2 boot.
    • L2 (ircd-golden/tests/golden_s_conf_initconf.rs): reference-C vs Rust parse the same flags_oper.conf; STATS y (218, the Y-line→add_class path), i (215), o (243) byte-identical. The boot itself exercises the M-line (me.serv name/sid) + P-line (add_listener). Regression: registration banner, S2S link burst (C/N lines → lookup_confhost), STATS i/o flags, channel/debug goldens all stay green.
    • No S2Sinitconf has no IsServer branch and formats no remote-user fields.
  • 2026-06-04 — P6g (s_conf.c find_Oline) merged. Ported the O-line lookup m_oper uses to resolve OPER <name> <pass> to an O-line; the cleanest remaining self-contained s_conf.c leaf, same shape as the P6e find_admin/find_me bite.

    • Cluster choice / why now. Of the remaining s_conf.c symbols (find_conf*, attach_conf/detach_conf, rehash, lookup_confhost), find_Oline is the only read-only leaf with a client L2 path — m_oper (already Rust, s_user.rs:1996) calls it directly. The find_conf* family (find_conf/find_conf_host/find_conf_host_sid/find_conf_ip/find_conf_exact/find_conf_name/find_conf_entry) is reached only from still-C s_bsd.c/s_serv.c server-link/registration handling → a separate later bite (P6h) with an S2S gate; the attach/detach/rehash mutating cluster is later still.
    • Guard / partial port. #if !defined(PORT_SCONF_FINDOLINE_P6) wraps the C def (s_conf.c:1033-1061), mirroring the P6e PORT_SCONF_FINDADMIN_P6 guard; -DPORT_SCONF_FINDOLINE_P6 added to the s_conf_link.o compile in build.rs. The plain s_conf.o (cref oracle) keeps the C def → cref_find_Oline stays available for L1.
    • Config-resolved body. CONF_OPS == CONF_OPERATOR == 128 (bindgen const). Faithful translation: match/match_ipmask keep the IRC convention (0 == matched, nonzero == no match), the match(host,userhost) && match(host,userip) && (!strchr('/') || match_ipmask) triple-continue is preserved verbatim, and the clients < MaxLinks(Class(tmp)) early-return vs the tmp2 over-limit fallback is byte-for-byte. Depends only on already-ported leaves (match_, match_ipmask [P6d], mycmp, max_links).
    • Classification. Utility/callee — not in msgtab. → L1 + L2, NO S2S: find_Oline has no IsServer(cptr) branch and formats no remote-user fields; it reads only the local client (m_oper is a local-client command).
    • L1 (ircd-testkit/tests/s_conf_find_oline_diff.rs): hand-chained conf list of O-line entries (name/host strings, a make_class() with set maxLinks, a clients counter) + a make_client/make_user client; both conf and cref_conf pointed at the same head; c_find_Oline == cref_find_Oline across name match + the inverse name mismatch, host glob mismatch, CIDR in-range hit vs out-of-range (/24 adjacent) miss, the clients >= MaxLinks over-limit tmp2 fallback (and an in-limit entry preferred over an earlier over-limit one), wrong-status skip, and empty list. Zero diff.
    • L2 (ircd-golden/tests/golden_s_conf_find_oline.rs, fixture find_oline.conf): a registered alice runs OPER carol secret (name matches the O|*@10.0.0.0/8|secret|carol|… O-line but its host CIDR misses the 127.0.0.1 client → find_Oline NULL → 491 ERR_NOOPERHOST) then OPER alice secret (*@* → 381 RPL_YOUREOPER); byte-identical vs reference-C. This adds the host/CIDR-miss branch the existing golden_s_user_oper.rs *@* O-line cannot reach (it only exercises the hit + name-miss). No canonicalizer masking needed.
  • 2026-06-04 — P6h (s_conf.c find_conf* family) merged. Ported the seven read-only conf/Link-list lookups — the bite the P6g note named ("a separate later bite (P6h) with an S2S gate"). These are the read-only half of s_conf.c's remaining surface; the mutating attach/detach family, the K-line lookups, and rehash are still C.

    • Cluster choice. find_conf_exact (global conf, user@host match + CONF_OPERATOR over-limit skip), find_conf_name (global conf, NULL-name/match), find_conf/find_conf_host/find_conf_host_sid/find_conf_ip (a passed Link* chain), find_conf_entry (global conf, full port+host+passwd+name match with CONF_ILLEGAL masked off). Every one is read-only; find_conf_ip does an in-place @-split of tmp->host it restores before continuing/returning — preserved byte-for-byte.
    • Guard / partial port. One shared #if !defined(PORT_SCONF_FINDCONF_P6) wraps s_conf.c:994-1212 (nesting the existing P6g find_Oline guard inside); -DPORT_SCONF_FINDCONF_P6 added to the s_conf_link.o compile. The plain s_conf.o (cref oracle) keeps the C defs for L1.
    • Config-resolved body. Config-independent loops; only the bit values (CONF_OPERATOR=128, CONF_SERVER_MASK=56, CONF_HUB, CONF_ILLEGAL — bindgen consts) + struct layout matter. Reused the existing bad_ptr()/max_links()/match_/mycmp helpers; indexstrchr, bcmpmemcmp, sizeof(IN_ADDR)size_of::<in6_addr>() (AFINET=AF_INET6).
    • Classification. Utility/callee — not in msgtab. Callers (all still C, resolve to the Rust defs via the link seam): s_serv.c m_server/m_server_estab (the C/N-line find_conf, find_conf_host CONF_LEAF, find_conf_host_sid CONF_HUB, find_conf_name CONF_QUARANTINED_SERVER), s_bsd.c (server connect + registration: find_conf, find_conf_ip, find_conf_host, find_conf_exact), s_misc.c (find_conf_name). → L1 + S2S, no client-only L2.
    • L1 (ircd-testkit/tests/s_conf_find_conf_diff.rs): hand-built parallel conf lists / Link chains, both conf and cref_conf pointed at the same head; c_find_conf* == cref_find_conf* pointer-for-pointer across each fn's positive + inverse — name/host/status/port/passwd-BadPtr-asymmetry misses, the CONF_OPERATOR over-limit skip (later in-limit op preferred), the server-mask mycmp-exact vs non-server match-glob split, the >HOSTLEN/BadPtr NUL early-outs, the empty-passwd-vs-sid-mask gate, and the find_conf_ip host-string-restored assertion. The differential caught a real bug in the first draft: I had translated C's !match(tmp->passwd, sid) sid gate as match_(...) != 0 (inverted) — C's !match is true when the passwd-mask matches the sid (entry eligible), so it is == 0. Fixed; zero diff.
    • S2S (ircd-golden/tests/golden_s2s_s_conf_find_conf.rs): the s2s.conf fixture's C|…peer.test/N|…peer.test + H|*||peer.test lines mean Peer::link() drives find_conf (C/N accept), find_conf_host (no L line → not leaf), and find_conf_host_sid (empty-passwd hub gate) during m_server_estab. A faithful port → byte-identical burst (EOB + UNICK alice) + post-link routing (remote-user introduce → local WHOIS 311). The existing 37-test golden_s2s_* suite + registration goldens are the broader regression gate for the same establishment path.
  • 2026-06-05 — P6i (s_conf.c K-line lookup family) merged. Ported the four read-only lookups named in the PLAN P6 row — find_kill, find_two_masks, find_conf_flags, find_denied — the read-only remainder of s_conf.c after the P6h find_conf* bite.

    • Cluster choice. Of the remaining s_conf.c symbols (find_kill/find_two_masks/find_conf_flags/find_denied, the mutating attach/detach/attach_confs* family, rehash, find_bounce), the K-line lookups are the read-only leaves — the established P6 bite shape. They share no private statics (each walks the global kconf/tkconf/conf lists or svrtop); find_bounce (the RPL_BOUNCE sender) sits between find_conf_flags and find_denied and stays C (separate concern, its own future bite). The mutating attach/detach/rehash cluster is later still.
    • Guard / partial port. One macro PORT_SCONF_FINDKILL_P6, three #if !defined(...) regions (find_kill at s_conf.c:2143, find_two_masks+find_conf_flags at :2302, find_denied at :2484 — find_bounce between them left C). -DPORT_SCONF_FINDKILL_P6 added to the s_conf_link.o eval recipe in build.rs. The plain s_conf.o cref oracle keeps the C defs → cref_find_kill etc. for L1. RED was the undefined-symbol link error for find_kill/find_two_masks/find_conf_flags/find_denied (callers: s_serv.rs check_version, s_user.rs register_user, ircd.c:289/471); GREEN confirmed via nm target/debug/ircd (all four T from Rust).
    • Config-resolved body. TKLINE on → find_kill's tklines ? tkconf : kconf two-pass findkline: goto (translated to a labeled 'findkline: loop) compiles. KLINE off (kconf seeded from config K/k lines, not the runtime /KLINE). TIMEDKLINES off → the reply/now/check_time_interval block is not compiled, so timedklines is an inert _ arg. Bit values (CONF_TKILL/TOTHERKILL/KILL/OTHERKILL/DENY/NOCONNECT_SERVER/VER) + ConfClass(x)=x->class->class are the only config inputs.
    • Faithfulness notes. find_kill: the unresolved (ip==NULL) vs resolved host branches, the =-prefixed numeric-only skip, the match/match_ipmask /-split, the status==CONF_(T)KILL ? username : ident check selection, the !port || port==acpt->port gate, and the ERR_YOUREBANNEDCREEP sendto_one (with the BadPtr"*" / ": "/"" comment formatting) are byte-for-byte. find_denied: the isdigit(passwd)atoi→CONF_NOCONNECT_SERVER find_client cross-check and the !-reversed svrtop bcptr->name scan (returning conf head for a satisfied reversed mask). Reused match_/match_ipmask/mycmp/bad_ptr/reply/me_name/max_links; added inetntop/ipv6string/find_client/svrtop/aServer imports + strncasecmp/strpbrk.
    • Classification. Utility/callees — none in msgtab. → L1 + L2 (find_kill) + S2S (find_two_masks); find_denied L1-only.
    • L1 (ircd-testkit/tests/s_conf_find_kill_diff.rs): hand-built parallel kconf/tkconf/conf/svrtop state, both live globals + cref_* globals pointed at the same heads; c_find_* == cref_find_* over each fn's positive + inverse (9 tests, zero diff). find_kill: tkconf hit, expired-TK→kconf fallthrough, OTHERKILL→ident vs KILL→username, port match/mismatch, CIDR in/out, '='-numeric-only skip on a resolved client, resolved-hostname match, IsKlineExempt(user->flags) short-circuit, no-user guard (the HOSTLEN guard is unreachable — sockhost/username are fixed 64/11-byte buffers — so it is documented, not tested). find_two_masks/find_conf_flags: status/host/name/no-slash/prefix/strpbrk-empty inverses. find_denied: host-scan hit, reversed present/absent, name+port miss, digit-passwd empty-inner-loop, no-D-line. A test bug (set cptr->flags instead of user->flags) was caught by the differential agreeing both returned -1 — confirming the port reads user->flags correctly.
    • L2 (ircd-golden/tests/golden_s_conf_find_kill.rs, kline.conf): a client from 127.0.0.1 hits K|127.0.0.1|Banned from this server|*|0| during register_user → killed mid-registration with 465 ERR_YOUREBANNEDCREEP + ERROR :Closing Link … (K-lined: …), byte-identical. Exercises the unresolved host match (sockhost == ip string → ip=NULL → match(host,sockhost)), the * user-mask, the zero-port gate, the *comment=passwd path. The miss path is covered by every other registration golden.
    • S2S (ircd-golden/tests/golden_s2s_s_conf_find_two_masks.rs, vline_reject.conf = s2s.conf + V|*||peer.test|): the peer's SERVER line reaches m_server_estab check_versionfind_two_masks(name,"id/info",CONF_VER) matches the * version mask + peer.test name mask → "Bad version" exit before the burst (no EOB), byte-identical. The no-match path (no V-line → 0 → link proceeds) runs on every other golden_s2s_* link. find_conf_flags's match path needs a 3-token id|misc version (its misc branch) → covered at L1; its no-match path runs on every link. find_denied is reachable only from try_connections (server auto-connect timer + outbound connect) — not reachable in the golden harness → L1 only, documented.
  • 2026-06-05 — P6j (s_conf.c find_bounce) merged. Ported the RPL_BOUNCE sender named in the P6i note ("find_bounce (RPL_BOUNCE sender — separate concern), … its own future bite"). It is the read-only remainder of s_conf.c after the P6i K-line lookups; what stays C is now only the mutating cluster (attach_conf/detach_conf/attach_confs/attach_confs_host/det_confs_butmask/attach_Iline/count_cnlines), rehash, and the initconf-private lookup_confhost/ipv6_convert.

    • Cluster choice. find_bounce (s_conf.c:2407) is a self-contained read-only walk of the global conf list for CONF_BOUNCE (B-line) entries; it shares no private statics with the mutating family. The P6i bite deliberately left it C ("find_bounce sits between find_conf_flags and find_denied and stays C") because it is a sender, not a lookup — its observable is emitted bytes, not a return value, so it needs a different L1 shape.
    • Guard / partial port. New macro PORT_SCONF_FINDBOUNCE_P6, one #if !defined(...) region around s_conf.c:2407–2480. -DPORT_SCONF_FINDBOUNCE_P6 added to the s_conf_link.o eval recipe in build.rs. The plain s_conf.o cref oracle keeps the C def → cref_find_bounce for L1. RED was the undefined-symbol link error for find_bounce (callers: Rust s_user.rs m_nick/m_user, C s_bsd.c:1809, C s_conf.c:631/637 in still-C count_cnlines); GREEN confirmed via nm target/debug/ircd (T find_bounce from Rust).
    • Config-resolved body. CONF_BOUNCE=262144, RPL_BOUNCE=10 (bindgen consts). Three fd-keyed dispatch modes preserved byte-for-byte: fd>=0 early rejection (cptr==NULL, class+host unknown → for an empty-host B-line, sprintf(rpl, replies[RPL_BOUNCE], ME, "unknown", name, port) + strcat("\r\n") + a raw sendto(fd,…) syscall); fd==-1 ("too many", class known → a bare-number B-line host !strchr('.')&&(isdigit||'-') is matched against class via atoi); fd==-2 (host known, class not a number → the class-number branch is skipped, host glob/match_ipmask only). On a match the reply goes via the variadic sendto_one(cptr, …, BadTo(cptr->name), name, port). The if/else if ladder faithfully reproduces C's outer-if (class-number) / outer-else (strchr('/')→match_ipmask else match) nesting. Reused reply/me_name/bad_to/match_/match_ipmask; added strcat/sendto to the module's libc extern block (sprintf/strlen/strchr/isdigit/atoi were already there).
    • Classification. Utility/callee — not in msgtab. → L1 + L2, NO S2S (no IsServer branch, no remote-user fields; reads only the global conf + the local cptr).
    • L1 (ircd-testkit/tests/s_conf_find_bounce_diff.rs): find_bounce returns void, so the differential captures sent bytes, not a return value. Both conf and cref_conf point at the same hand-built B-line head; me.name/cref_me.name set identically. fd<0 cases diff cptr->sendQ (dbuf_map) after sendto_one; fd>=0 cases pass a socketpair write end as fd and non-blocking recv the peer. 9 tests, byte-identical: host-glob hit (:irc.test 010 * bounce.elsewhere 7000 :…\r\n) vs miss (continue→empty), CIDR /8 in-range vs out-of-range, numeric host on fd==-1 (class==atoi hit, class≠atoi miss) vs the fd==-2 skip (numeric host falls to host-match → miss), non-CONF_BOUNCE-entry skip + first-matching-B-line-wins (later B-line never reached), the fd<0 && cptr==NULL up-front guard (no-op, no UB), empty conf list, and the fd>=0 empty-host unknown write vs non-empty-host no-op. (Fixed mid-port: person() must set acpt non-NULL — send_message does (*to).acpt->sendM += 1.)
    • L2 (ircd-golden/tests/golden_s_conf_find_bounce.rs, fixture bounce.conf): the only reachable client path under the locked config is the server-only P-line rejection in m_nick (Rust). bounce.conf makes the listener server-only (P||||16667|0|S|, the S=PFLAG_SERVERONLY flag in P-line field tmp3) and adds B|127.0.0.1||bounce.elsewhere|7000|. A client connecting from 127.0.0.1 sends NICK → IsConfServeronly(cptr->acpt->confs->value.aconf)find_bounce(cptr,-1,-1)010 bounce (host-match branch: match("127.0.0.1","127.0.0.1")==0, BadTo("*")) → exit_client(… "Server only port"). Byte-identical reference-C vs Rust.
    • No S2Sfind_bounce has no IsServer branch. The fd>=0 raw-sendto early-rejection branch (caller s_bsd.c all-connections-in-use) needs MAXCLIENTS live connections → unreachable in the golden harness, covered at L1 only.
  • 2026-06-05 — P6k (s_conf.c attach/detach mutating cluster) merged. Ported the connected component of conf-attachment mutators named in the P6h–P6j "still C: the mutating family" notes — the bulk of s_conf.c's remaining surface.

    • Cluster choice. attach_conf (s_conf.c:800), detach_conf (722), det_confs_butmask (389), attach_confs (938), attach_confs_host (969), + the private statics is_attached (781), add_cidr_limit/remove_cidr_limit (667/698). These form one connected component over shared statics + the cptr->confs/global-conf/class-refcount/CIDR-patricia mutation: det_confs_butmaskdetach_confremove_cidr_limit/free_class/free_conf; attach_confs/attach_confs_hostattach_confis_attached/add_cidr_limit. attach_Iline + count_cnlines (callers of this core) stay C — a later bite (P6l) — and resolve to the Rust defs at link.
    • Guard / partial port. New macro PORT_SCONF_ATTACH_P6, three #if !defined(...) regions (det_confs_butmask at :389; the CIDR statics + detach_conf + is_attached + attach_conf at :666–905; attach_confs + attach_confs_host at :932–996 — the existing P6e find_admin + P6h find_conf guards sit between them). -DPORT_SCONF_ATTACH_P6 added to the s_conf_link.o eval recipe in build.rs. The plain s_conf.o cref oracle keeps the C defs → cref_attach_conf etc. for L1. RED was the undefined-symbol link error for the five exported symbols (callers: s_serv.rs m_server_estab, s_user.rs m_oper/m_umode, s_bsd.c register/connect, s_conf.c:631 count_cnlines); GREEN confirmed via nm target/debug/ircd (all five T from Rust).
    • Config-resolved body. ENABLE_CIDR_LIMITS on → add_cidr_limit/remove_cidr_limit compile and are called from attach/detach. pnode->data is a *mut c_void used as an integer counter (C void-ptr arith = +1 byte; cast to long for the cap compare) → ((*pnode).data as usize).wrapping_add(1) as *mut c_void + (*pnode).data as usize as c_long >= cidr_amount. YLINE_LIMITS_IPHASH on → attach_conf's per-host limit loop walks hash_find_ip(cptr->user->sip)/iphnext (ported faithfully, only entered when a ConfMax*Local/Global > 0). YLINE_LIMITS_OLD_BEHAVIOUR off → the max-links gate is ConfLinks(aconf) >= ConfMaxLinks(aconf) && ConfMaxLinks > 0 → -3.
    • Faithfulness notes. detach_conf's global-conf-removal loop (for (aconf2=&conf; ...)) doesn't break after free — it reads aconf3->next into *aconf2, NULs aconf3->next, frees, and re-reads *aconf2 (the successor) next iteration — preserved verbatim via raw *mut *mut aConfItem walking. The --aconf->clients <= 0 && IsIllegal short-circuit, the ConfMaxLinks==-1 && ConfLinks==0 free_class+NULL, and attach_conf's get_client_ping/ConfLinks++ tail are byte-for-byte. attach_confs/attach_confs_host keep the non-server match / server-mask mycmp-exact split and always call attach_conf when the outer condition holds (the first check is the inner guard, not a gate on the attach). Reused match_/mycmp/bad_ptr; added free_class/free_link/make_link/get_client_ping/hash_find_ip/patricia_match_ip/patricia_make_and_lookup_ip/patricia_remove imports + the conf_is_illegal/my_connect inline macro helpers.
    • Classification. Utility/callees — none in msgtab. → L1 + L2, NO S2S (no IsServer branch, no remote-user fields).
    • L1 (ircd-testkit/tests/s_conf_attach_diff.rs): unlike the read-only P6e–P6j lookups, these MUTATE shared state, so the two impls can't share it. Each scenario runs identically on two fully-isolated worlds via an Api of fn-pointers — LIVE (Rust port) over conf/istat/classes, CREF over cref_conf/cref_istat/cref_classes, each built with its own allocators — returning a Vec<i64> of observations (return codes, refcounts, chain shape, is_conflink delta, freed-ness) asserted equal. 8 tests, zero diff: attach happy-path + is_attached idempotency + detach round-trip (re-attach clean, counts back to baseline); IsIllegal -1 + Y-line-max -3 gates + the under-cap inverse; ENABLE_CIDR_LIMITS amount=2 admits 2 same-CIDR clients, rejects the 3rd with -4, freed slot reusable after detach; free-class on maxLinks==-1; illegal-conf removal from the global conf list (pointer gone, survivor head); det_confs_butmask keeping only the masked conf; attach_confs glob-vs-mycmp + attach_confs_host host match, each with its miss. mk_class wipes the struct (make_class is a bare MyMalloc — uninitialized maxH* would trip the IPHASH loop and differ between worlds; caught as the first SIGSEGV).
    • L2 (ircd-golden/tests/golden_s_conf_attach.rs, ylinemax.conf): class 10 maxlinks 1 + an I-line binding 127.0.0.0/8. alice registers and takes the only slot (ConfLinks 0→1); bob hits the same I-line and is rejected mid-registration via attach_conf -3 EXITC_YLINEMAX → "Too many connections" exit, byte-identical. Adds the one client-reachable attach branch the existing all-success registration goldens never hit; the success + detach paths are covered pervasively by the registration + golden_s2s_* suite (confirmed green: golden_registration, golden_s2s_link/server/squit/quit/find_conf).
    • No S2S — attach_conf/detach_conf have no IsServer branch and format no remote-user fields. The server-link wrappers (attach_confs C/N in m_server_estab, det_confs_butmask on SQUIT) are exercised by the 37-test golden_s2s_* link-establishment + teardown suite.
  • 2026-06-05 — P6l (s_conf.c attach_Iline + count_cnlines) merged. Ported the two callers of the P6k attach/detach core that the P6k note explicitly deferred ("attach_Iline/count_cnlines (callers of this core) stay C — a later bite (P6l)"). With these gone, the only s_conf.c that stays C is rehash + the initconf-private lookup_confhost/ipv6_convert.

    • Cluster choice. attach_Iline (s_conf.c:485) — the I-line lookup used during local client registration: scan the global conf for the best CONF_CLIENT (I) line whose port/name/host/password match cptr, set the +r / kline-exempt conf flags onto cptr->user->flags, copy the resolved host into cptr->sockhost if the conf permits, then attach_conf(cptr, aconf). count_cnlines (s_conf.c:648) — read-only walk of a passed Link chain returning the single CONF_NOCONNECT_SERVER (N) line (NULL if 0 or >1 of each C/N). Both are direct callers of the P6k core (attach_Ilineattach_conf/find_bounce); grouping them is the natural final s_conf.c bite before rehash.
    • Guard / partial port. New macro PORT_SCONF_ILINE_P6, one #if !defined(...) region wrapping s_conf.c:483–664 (the UHConfMatch macro — used only by attach_Iline — through the end of count_cnlines). -DPORT_SCONF_ILINE_P6 added to the s_conf_link.o eval recipe in build.rs. The plain s_conf.o cref oracle keeps the C defs → cref_attach_Iline/cref_count_cnlines for L1. RED was the undefined-symbol link error for attach_Iline (s_bsd.c:929 check_client) + count_cnlines (s_bsd.c:1025 check_server); GREEN confirmed via nm target/debug/ircd (both T from Rust).
    • Config-resolved body. UNIXPORT off → the IsUnixSocket(cptr) / aconf->host[0]=='/' unix-path branch and the #ifdef UNIXPORT block drop out; only the strchr('/')match_ipmask (CIDR) vs UHConfMatch (glob) host split compiles. XLINE off → the IsConfXlineExemptClearXlined block drops out. NO_DNS_LOOKUP on → clients register numeric, so attach_Iline is normally called with hp == NULL (the add_local_domain/uhost name-match path is the rare branch, ported faithfully + exercised at L1 with a constructed hostent). UHConfMatch(x,y,z) = match(x, index(x,'@') ? y : y+z). ConfClass(aconf) = (*(*aconf).class).class.
    • Faithfulness notes. The for-loop with many continues is rendered as a loop that advances aconf = (*aconf).next at the top so each continue mirrors the for-increment (the break paths — password-no-fall + the final attach — still exit cleanly). The password check is !BadPtr(passwd) && strcmp(cptr->passwd, aconf->passwd) != 0 (C's !StrEq = strcmp != 0); SetRestricted/SetKlineExempt write cptr->user->flags; get_sockhost(cptr, uhost+ulen) copies the host tail. count_cnlines keeps the dead cline tracking verbatim (C assigns it but returns only nline). Reused attach_conf/find_bounce/match_/match_ipmask/bad_ptr/bad_to/reply/me_name; added add_local_domain/get_sockhost/hostent/ERR_PASSWDMISMATCH/FLAGS_GOTID imports + six is_conf_* flag helpers + conf_class/uh_conf_match.
    • Classification. Utility/callees — neither in msgtab. → L1 + L2 (attach_Iline), L1 (count_cnlines), NO S2S.
    • L1 (ircd-testkit/tests/s_conf_iline_diff.rs): attach_Iline MUTATES (attach_conf) → two fully-isolated worlds via an Api of fn-pointers (the P6k s_conf_attach_diff.rs pattern); each scenario returns a Vec<i64> (return code, aconf->clients, class links, cptr->confs chain length, cptr->user->flags, cptr->sockhost bytes, sendQ bytes) asserted equal LIVE vs CREF. 8 tests, zero diff: happy glob match + the no-matching-I-line -2 inverse; the port gate (skip vs attach); CIDR *@10.0.0.0/8 in-range hit / out-of-range miss; empty host/name match-any; password fall-through (F flag → skip to the next I-line vs no-F464 sent + -8, second never tried) + correct-passwd attach; CFLAG_RESTRICTED/CFLAG_KEXEMPTFLAGS_RESTRICT/FLAGS_EXEMPT on user->flags + the no-flag inverse; hp!=NULL name match on user@fullname + get_sockhost copy + the non-matching-name -2 inverse. count_cnlines: exactly-one-N → that N, zero-N → NULL, two-N → NULL, non-server entries skipped, empty chain → NULL.
    • L2 (ircd-golden/tests/golden_s_conf_iline.rs, iline_pass.conf): the only I-line — I|*@127.0.0.0/8|secret|||10|| — carries a password and no F flag, so a client from 127.0.0.1 sending no PASS mismatches and, with no fall-through, is rejected mid-registration → attach_Iline emits 464 ERR_PASSWDMISMATCH + returns -8 (EXITC_BADPASS) → register_user exits with ERROR :Closing Link … (Bad password), byte-identical reference-C vs Rust. Exercises the !BadPtr && !StrEq mismatch + the no-fall-through sendto_one path the all-success registration goldens (passwordless I-lines) never hit. The success path is covered pervasively by every other registration golden.
    • No S2S — neither function has an IsServer branch or formats remote-user fields. count_cnlines's server-link reachable path (check_server on every inbound SERVER) is exercised by the existing 37-test golden_s2s_* link-establishment suite (golden_s2s_link/server/squit/quit confirmed green).
  • 2026-06-05 — P6m (s_conf.c finalize: rehash + ipv6_convert + lookup_confhost + the data globals) merged. Ported the last three s_conf.c functions and moved its last three data globals — s_conf.c is now FULLY Rust, s_conf.o dropped outright (the P6a..P6l s_conf_link.o partial port is gone). This closes the s_conf.c strand the P6l note named ("the only s_conf.c that stays C is rehash + the initconf‑private lookup_confhost/ipv6_convert").

    • Cluster choice. The complete remaining nm --defined-only s_conf_link.o set was rehash/ipv6_convert/lookup_confhost (T) + conf/kconf/networkname (B) + the file-static rcsid (r, unreferenced → vanishes for free). Porting all six drops the .o. rehash reaches initconf/lookup_confhost/ipv6_convert, so the three functions are one natural final bite; the globals must move with them (they were the last s_conf.c-owned data).
    • Drop mechanism. Added s_conf.o to PORTED, removed the "s_conf.o" => "s_conf_link.o" entry in link_objs() + the whole s_conf_link.o make --eval recipe in build.rs. The cref oracle keeps the full unguarded s_conf.o (CREF_OBJS) so the cref_* symbols survive for every existing s_conf L1 diff. RED was the undefined-symbol link error for rehash/ipv6_convert/lookup_confhost/conf/kconf/networkname (callers: list.rs delist_conf, s_serv.rs report_configured_links/m_connect/m_rehash, s_conf.rs's own already-ported lookups, ircd.c SIGHUP); GREEN confirmed via nm target/debug/ircd (all six T/B from Rust).
    • Data-global move. conf/kconf/networkname flip from use ircd_sys::bindings::{…} to #[no_mangle] pub static mut defs in s_conf.rs (the tkconf/classes/msgtab pattern). list.rs + s_serv.rs keep importing them from bindings — those externs resolve to the new Rust defs at link. Only s_conf.rs had to stop importing them (can't import + define the same name); also removed its now-conflicting ipv6_convert import + the P6f extern { fn lookup_confhost } decl + the P6f networkname import.
    • Config-resolved bodies. rehash (s_conf.c:1231): ULTRIX off (no fork/pidfile), USE_SYSLOG off, TKLINE on (the sig=='t'tkline_expire(1) block compiles). The two aConfItem** walk loops (the in-use/ILLEGAL marking pass + the post-initconf flush) are rendered as raw *mut *mut aConfItem loops preserving C's *tmp = tmp2->next vs tmp = &tmp2->next branch exactly; IsMe(acptr) = status == STAT_ME inline; NextClass(FirstClass()) = (*classes).next, MaxLinks(x)=-1 = (*x).maxLinks=-1; mysrand(timeofday) casts to u32; the read_motd(IRCDMOTD_PATH) path comes from ircd_sys::MOTD_PATH. ipv6_convert (s_conf.c:1444): faithful pointer arithmetic — the user@ copy-with-'@', the /cidr NUL-off before inetpton, the j=len-1/buf+j splice, the *(s-1)='/' restore (C's post-incremented s = my slash.add(1)), non-IP passthrough (t=orig). lookup_confhost (s_conf.c:2101): BadPtr guards, @-tail, isalpha||isdigit first-char gate, inetpton (nonzero=stored) else gethost_byname (still-C) h_addr_list[0] copy else minus_one fill + badlookup; AND16(ipnum)==255 (all 0xff) → bzero 16 bytes.
    • IRCDMOTD_PATH plumbing. read_motd is already Rust (s_misc.rs) but IRCDMOTD_PATH is a recursively-expanded Makefile path var, not a header #define (absent from bindgen). build.rs expands it via make --eval=__ircd_print_motd and emits cargo:rustc-env=IRCD_MOTD_PATH=…; ircd-sys/src/lib.rs re-exports it as pub const MOTD_PATH = env!("IRCD_MOTD_PATH") (rustc-env applies only to the emitting crate, so the const bridges it to ircd-common). The Rust rehash builds a temporary CString from it — read_motd only opens the path, never retains it.
    • Classification. Utility/callees — none in msgtab. rehash is reached from Rust m_rehash (oper REHASH) + still-C ircd.c (SIGHUP); ipv6_convert/lookup_confhost from the Rust initconf at boot/rehash. → L1 (ipv6_convert) + L2 (existing REHASH + initconf goldens), NO S2S — none has an IsServer branch or formats remote-user fields (REHASH server propagation is m_rehash's concern, ported in P5rr).
    • L1 (ircd-testkit/tests/s_conf_rehash_diff.rs): c_ipv6_convert vs cref_ipv6_convert over bare-v6 (::1), 2001:db8::1, v4-mapped (::ffff:127.0.0.1), user@v6, v6/cidr, user@v6/cidr, and non-IP passthrough (*@*, notanip, notanip/24, bob@notanip) on independent input copies, asserting byte-identical output and that the in-place @// poke was restored. lookup_confhost is static in the unguarded oracle (the symbol-rename only renames globals → no cref_lookup_confhost) so it has no L1 shape → covered by the initconf/boot golden (initconf calls it on every conf host). rehash reopens listeners / forks iauth / resets the resolver fd → no isolated-L1 shape → covered by the L2 REHASH transcript.
    • L2 (golden_s_serv_maint REHASH: rehash_success_matches_reference 382 + rehash_reject_matches_reference 481 now drive the Rust rehash, byte-identical; golden_s_conf_initconf STATS y/i/o over the boot-parsed config exercises lookup_confhost/ipv6_convert). Regression: golden_registration, golden_s2s_link, golden_s2s_s_conf_find_conf, golden_s_conf_iline/attach + all s_conf L1 diffs stay green (the conf/kconf data-global move touches every conf consumer).
    • No S2S — none of the three has an IsServer branch or remote-field formatting. The REHASH command's server-to-server propagation lives in m_rehash (P5rr); rehash itself only reloads local config + the resolver/iauth fds.
  • 2026-06-05 — P6n (res_comp.c — BIND DNS wire-format primitives) merged. The pure wire-format leaf of the resolver, ported outright to ircd-common/res_comp.rs; res_comp.o added to PORTED and dropped (no partial _link.o — every symbol is portable).

    • Cluster choiceres_comp.c is the foundation the rest of the resolver builds on: res_mkquery.c (ircd_dn_comp/ircd__putshort/ircd__putlong) and res.c proper (ircd_dn_expand/ircd_getshort/ircd_getlong/__ircd_dn_skipname) both call into it. It is the natural first DNS bite — fully self-contained, pure, no event-loop entanglement. The remaining resolver TUs (res_mkquery.c, res_init.c, res.c proper) stay C for a later P6 cluster.
    • Config — no gates touch this TU. The #if 0 block (res_hnok/res_ownok/res_mailok/res_dnok) is dead code (not compiled); the NEXT-gated res_getshort is absent. nm --undefined-only cbuild/res_comp.o = only libc (memcpy/strchr/__errno_location) → zero ircd-internal deps.
    • Classification — utility/leaf TU: none of its 7 public symbols appear in msgtab. The seven are global (T) so each has a cref_ oracle → fully L1-diffable. The 7 BIND static helpers (ns_name_ntop/_pton/_unpack/_pack/_uncompress/_compress/_skip, special/printable/mklower/dn_find) have no oracle → ported as private Rust fns, covered transitively through the public entry points.
    • Faithfulness — faithful raw-pointer/unsafe port mirroring the C pointer arithmetic exactly (label-length bytes, the 0xc0 compression-pointer encode/decode, the dnptrs/lastdnptr table walk + mutation in dn_find/ns_name_pack, dn_comp's post-increment *dnptrs++ so dn_find sees dnptrs[1..]). errno set via *libc::__errno_location() to EMSGSIZE/ENOENT on every error path so callers observe the identical side effect. The goto next in dn_find became a labelled 'outer loop. Constants NS_CMPRSFLGS=0xc0/NS_MAXCDNAME=255/MAXCDNAME=255 from nameser_def.h.
    • L1 (ircd-testkit/tests/res_comp_diff.rs, 9 tests, zero diff) — get/put value match + big-endian byte order + put→get round-trip; dn_compdn_expand pack/expand round-trip (incl. root .); the compression-pointer reuse (two suffix-sharing names) diffed as buffer-relative dnptrs offsets + the emitted wire bytes against cref_; a hand-built compressed-pointer message (0xc0 indirection) → identical decoded string + consumed-octet count; dn_skipname over plain/compressed/truncated names; and the inverse error paths — dstsiz-too-small (expand + comp), malformed out-of-range pointer — each returning -1 from both with matching errno.
    • L2 / S2S — none. Pure wire-format leaf with no client- or server-reachable command path under the locked config (the resolver proper that uses these is still C; m_dns debug output does not surface compression internals). End-to-end DNS coverage arrives when the resolver proper is ported under a fixture nameserver.
    • Plandocs/superpowers/plans/2026-06-05-p6n-res_comp.md.
  • 2026-06-05 — P6o (res_mkquery.cres_mkquery.rs) merged. The BIND-derived DNS query-packet builder ircd_res_mkquery, the second resolver leaf after P6n. res_mkquery.o dropped outright (added to PORTED; no _link.o).

    • Cluster choicenm cbuild/res_mkquery.o: one defined symbol (ircd_res_mkquery); undefined deps are ircd_dn_comp/ircd__putshort/ircd__putlong (Rust, P6n), ircd_res/ircd_res_init (stay C, res_init.o), memmove/__h_errno_location/libc. Dropping it leaves every dep defined, so it's a clean single-function bite.
    • Layout hazard / HEADER bitfields — the function writes the 12-byte DNS header through a HEADER * bitfield struct (nameser_def.h:249: id:16/opcode:4/rd:1/rcode:4/qdcount:16/ancount:16/arcount:16). Rather than hand-reproduce gcc's little-endian bitfield packing, the port uses the bindgen HEADER type (ircd_sys::bindings, asserted size 12) and its set_* accessors, which are byte-identical to the C layout by construction. htons reproduced as u16::to_be; h_errno = NETDB_INTERNAL via an extern "C" { fn __h_errno_location() }. bzeroptr::write_bytes, bcopyptr::copy.
    • Config-resolved bodyDEBUG off → the RES_DEBUG printf block is not compiled (not ported). Three op cases: QUERY/NS_NOTIFY_OP (shared question-section path + the optional additional-record branch when data != NULL), IQUERY (answer section), default → -1.
    • Classification — utility/leaf, not in msgtab; callers are the still-C res.c resolver (query_name/do_query_name/do_query_number). No client/server command path under NO_DNS_LOOKUP ⇒ L1 only, like P6n.
    • L1 (ircd-testkit/tests/res_mkquery_diff.rs) — c_res_mkquery (#[link_name] → Rust) vs cref_ircd_res_mkquery, byte-identical packets (return length + emitted bytes) over: QUERY with/without RES_RECURSE (the rd bit), NS_NOTIFY_OP with data==NULL (matches QUERY) and data!=NULL (the additional-record dn_comp+T_NULL+ttl+rdlength+arcount path), IQUERY with datalen>0 and ==0; plus the -1 inverses — unknown op, NULL buf, buflen<HFIXEDSZ, and the question-section buflen -= QFIXEDSZ < 0 / dn_comp < 0 overflow at several small buflens. Both resolver-state globals (ircd_res, cref_ircd_res) are pinned identically with RES_INIT set (skips the nondeterministic ircd_res_init/res_randomid gettimeofday+getpid path) and the shared ++id side effect is diffed after every call. A process-global RES_LOCK serializes the parallel #[test] threads (the globals are shared across threads in the one test process — without the lock the id races and the header bytes diverge). Zero diff over 7 tests.
    • No S2S/L2 — pure wire-format leaf, no IsServer/remote-field/command path. End-to-end DNS coverage lands with the resolver proper (res.c + res_init.c) under a fixture nameserver.
  • 2026-06-05 — P6p (res_init.c resolver-state bootstrap) merged. Ported ircd/res_init.c outright to ircd-common/res_init.rsres_init.o dropped; only res.c (the resolver proper) is still C in P6.

    • Cluster choice. The smaller, self-contained of the two remaining DNS files. nm cbuild/res_init.o defines ircd_res (B), ircd_res_init/ircd_res_randomid (T), and the file-static res_setoptions (.isra.0); nm --undefined-only shows zero ircd-internal deps — only libc + inetpton (the P1 support.rs symbol). Same self-contained-leaf shape as P6n/P6o → drop the .o (add to PORTED), no _link.o.
    • Config-resolved body. Verified DEBUG/NEXT/RESOLVSORT/RFC1535/HAVE_GETIPNODEBYNAME/USELOOPBACK/__BIND_RES_TEXT all OFF in cbuild/{setup,config}.h + build.rs defines. So: ircd_res is a plain zero-init BSS global (no = {RES_TIMEOUT,}); ircd_res_init is the simple path only (defaults → LOCALDOMAIN block → /etc/resolv.conf parse loop with the MATCH macro + inetpton(AF_INET6,…) nameservers → gethostname fallback → the #ifndef RFC1535 default-dnsrch derivation LOCALDOMAINPARTS=2/MAXDFLSRCH=3RES_OPTIONS env → options |= RES_INIT), no NetInfo/ircd_netinfo_res_init, no sortlist; res_setoptions handles ndots: (clamp to RES_MAXNDOTS=15), inet6 (→RES_USE_INET6=0x2000), debug (no-op, DEBUG off); res_randomid = 0xffff & (tv_sec ^ tv_usec ^ getpid()).
    • Faithfulness. Raw-pointer port mirroring the C: strncpyztstrncpy+NUL; the two near-identical search-list tokenizers inlined faithfully (the LOCALDOMAIN one breaks on \n, the file search one doesn't); dnsrch[] pointers walk into defdname; nsaddr sin6_addr=in6addr_any (16 zero bytes) / sin6_family=AF_INET6 / sin6_port=htons(53); ndots written via the bindgen set_ndots bitfield accessor. ircd_res defined as #[no_mangle] static mut via MaybeUninit::zeroed().assume_init() (the s_misc.rs::ircst idiom); res_mkquery.rs/res.c/s_bsd.c keep importing ircd_res/ircd_res_init from bindgen and resolve to the Rust defs at link (the conf/kconf-globals mechanism).
    • Classification. Utility/leaf (none in msgtab); no IsServer/remote-field path.
    • L1. ircd-testkit/tests/res_init_diff.rs: zero diff vs cref_ircd_res_init. Both ircd_res/cref_ircd_res zeroed + .id pinned 0x4321 per test (skip the nondeterministic res_randomid); env-set + both inits + env-restore under a process-global RES_LOCK (parallel #[test] threads share the env + globals). The result structs diffed field-by-field (scalars/bitfields, 3× nsaddr_list sockaddr_in6 bytes, defdname bytes, dnsrch[] as offsets into defdname). The differential makes the /etc/resolv.conf+inetpton path host-robust (both read the same file → identical, or both skip it); the host-independent branches run on every CI host via env — plain (default-dnsrch), LOCALDOMAIN (search tokenizer), RES_OPTIONS ndots:3 inet6 and ndots:99 (the RES_MAXNDOTS clamp). res_randomid nondeterministic → only & 0xffff smoke-tested (the one symbol with no L1 diff).
    • L2/S2S. None — pure leaf, no client/server command path under NO_DNS_LOOKUP. Boot+registration (golden_registration) and the resolver-adjacent golden_s_dns goldens stay byte-identical, confirming the Rust ircd_res_init/ircd_res (called from s_bsd.c at startup) boot the server identically to reference-C. End-to-end DNS coverage (L2/L4) arrives when the resolver proper res.c is ported under a fixture nameserver.
  • 2026-06-05 — P6q (res.c cache-hash leaves) merged. First bite of the res.c port — the last C file. res.c is one large connected cluster (resolver socket + request queue + hostent cache) over file-statics with only 11 _ext.h-exported entry points; the dependency floor is the hostent cache, whose foundation is the two pure hash leaves hash_number/hash_name. Ported bottom-up so the cache core (make_cache/update_list/find_cache_*/rem_cache, P6r+) can build on Rust hashes.

    • Cluster choice — only hash_number/hash_name (pure: read solely their argument, touch no module state → zero data-global exposure), keeping this bite about establishing the new res.c static seam on a trivially verifiable pair before the gnarly cache core.
    • Guard / seam — new three-way RES_CACHE_LINKAGE macro in res.c: extern under -DPORT_RES_CACHE_P6q (res_link.o LINK build: bodies #ifndef'd out → Rust defines them, still-C walkers resolve to Rust at link), empty under -DRES_CACHE_EXPOSE (cref oracle res.o: GLOBAL → cref_hash_* for L1), else static (plain). build.rs: res_link.o gains -DPORT_RES_CACHE_P6q; a new step recompiles the cref res.o with -DRES_CACHE_EXPOSE (overwrites the plain one — res.o is consumed only by libcref.a, the link set uses res_link.o, so the real binary is untouched). RED confirmed: dropping the C bodies left 8 undefined hash_name/hash_number refs from the still-C walkers (res.c:1084/1088/1282/1316/1337/1382/1583/1600) — the exact port checklist.
    • Config-resolved body — neither function is config-gated; ARES_CACSIZE=1009.
    • Faithfulnesshash_number: hashv = ip[0]; for i in 1..16 { hashv = 2*hashv + ip[i] } over a wrapping u_int (the doubling overflows 32 bits for large addresses — % ARES_CACSIZE applied only at the end), reads exactly 16 bytes (sizeof struct in6_addr; AFINET=AF_INET6) → wrapping_add/wrapping double. hash_name: sum bytes until the first ./NUL, % 1009; char is signed on x86-64, so a high-bit byte promotes to a negative int before the unsigned += — replicated via *p as i32 as u32 + wrapping_add for 8-bit-clean parity. Mutable *mut args (C u_char*/char*). Returns c_int in [0,1009).
    • Classification — utility/leaf, not in msgtab, no client/server command path (the resolver is inert under NO_DNS_LOOKUP; the cache never populates at runtime).
    • L1ircd-testkit/tests/res_hash_diff.rs: Rust hash_number/hash_name (#[link_name] → the only def once res_link.o drops the C bodies) vs cref_ over crafted corpora — 16-byte v6 addrs (zero, all-0xff overflow, single-bit, ::ffff: v4-mapped, every-byte-high-bit, random), names (empty, dot-first, no-dot, embedded-dot, case-sensitive, UTF-8/0xff high-bit, long label); each result range-checked [0,1009). Zero diff. Both pure → no global state / no RES_LOCK.
    • L2 / S2S — none. No client/server command reaches these under the locked config; golden_s_dns + the registration/boot goldens (which boot the full ircd + still-C resolver, exercising the C cache walkers that now call the Rust hashes) confirm no regression. End-to-end DNS coverage arrives when the resolver proper is ported under a fixture nameserver.
  • 2026-06-05 — P6r (res.c cache removal/expiry core) merged. Second bite of the res.c port (after P6q's hash_number/hash_name). Ported the removal/expiry connected component — the half of the hostent cache that deletes entries — to ircd-common/res.rs, built directly on the P6q Rust hashes. res.c's only remaining strands are the resolver proper (socket + request queue + answer parser) and the cache build/insert/lookup half.

    • Cluster choice. Bottom-up from the cache floor: rem_list (static), rem_cache (static), expire_cache (global, res_ext.h), flush_cache (global, res_ext.h). They form a clean connected component — expire_cache/flush_cache both call rem_cache; rem_cache/rem_list call only the P6q hashes + touch the cache data structures — with no dependency on the still-C build/insert/lookup half (add_to_cache/update_list/find_cache_*/make_cache). The two public entry points give clean cref_ L1 symbols; the two statics are exposed via the generalized RES_CACHE_LINKAGE.
    • Seam. -DPORT_RES_CACHE_REM_P6r added to the res_link.o recipe. RES_CACHE_LINKAGE now fires extern under either PORT_RES_CACHE_P6q or PORT_RES_CACHE_REM_P6r (res_link.o defines both), so the rem_cache/rem_list forward decls + definitions de-static; their bodies (+ expire_cache/flush_cache) are wrapped in #ifndef PORT_RES_CACHE_REM_P6r. RED (after fixing a static-vs-non-static decl mismatch on the definitions) was the four undefined refs: flush_cache (Rust s_conf.rs rehash), expire_cache (C ircd.c:1195), rem_list (C res.c:1379 find_cache_number), rem_cache (C res.c:1119 add_to_cache) — the exact port checklist. GREEN confirmed via nm target/debug/ircd (all four T from Rust).
    • Data-global exposure. The Rust removal core + the L1 live world need cachetop/cainfo (already RES_DNS_STATIC-exposed for m_dns) plus hashtable[]/incache. RES_DNS_STATIC now also blanks static under RES_CACHE_EXPOSE (so the cref oracle res.o exposes cref_cachetop/cref_cainfo/cref_hashtable/cref_incache/cref_reinfo for the L1 isolated worlds), and hashtable/incache moved onto it. In res_link.o (which has PORT_DNS_M_DNS) all four stay defined-global — C owns the storage, the Rust core + the L1 live world extern them, same as P6k's conf. No extern/definition split needed (so no = NULL/array-initializer churn). res.rs adds static mut hashtable: [CacheTable; 1009] + static mut incache: c_int to its extern block + local_base()/hashtable_base() helpers.
    • Faithfulness. Raw-pointer walks mirroring the C aCache ** chains exactly. rem_list captures cp->list_next first, unlinks from cachetop only, MyFrees, returns the saved successor — and (the C quirk preserved) does not decrement incache, touch cainfo, or unlink the hash buckets (leaving the freed entry's bucket refs dangling). rem_cache's hostp-clearing loop walks local[] from highest_fd down; the bucket-by-hash_name/hash_number unlinks use hp->h_addr = h_addr_list[0]; MyFree(x) → guarded libc::free (the macro's re-NULL is moot — the struct is freed; *h_addr_list is one contiguous block freed once, then the pointer array). expire_cache captures list_next before the possible free and returns (next > now) ? next : now + AR_TTL.
    • Classification. Utility/leaf — none in msgtab. L1 only — the resolver is inert under NO_DNS_LOOKUP, the cache never populates at runtime, so no client/server command path reaches these. No S2S (no IsServer/remote-field path).
    • L1 (ircd-testkit/tests/res_cache_rem_diff.rs): the P6k two-isolated-worlds pattern. A World bundles fn-pointers + global base pointers (cachetop/hashtable/incache/cainfo and local/highest_fd — the hostp loop reads s_bsd globals, which are renamed to cref_local/cref_highest_fd in the archive, so each world must own its own). Entries are hand-built mimicking add_to_cache (one contiguous 16×n address block with h_addr_list[i] pointing into it + per-name strdup'd aliases, so rem_cache's frees match make_cache's layout), then the public fns are driven and Vec<i64> observations asserted equal. 4 tests, zero diff: flush_cache (3 mixed single/multi-alias/multi-addr entries → cachetop/buckets empty, incache delta 0, ca_dels==3; round-trip re-insert + re-flush clean), expire_cache (two past + two future at now=5000 → past removed, survivors kept in order, incache -=2, ca_expires delta 2, return = min-future 7000; the all-future inverse returns min expireat not now+AR_TTL; empty-cache returns now+AR_TTL), rem_cache hostp (a per-world local[0]->hostp at the doomed entry is NULLed, an unrelated local[1]->hostp untouched), rem_list (returns the successor, unlinks the middle from cachetop, incache unchanged — the leak-by-design quirk — then head + last removal empties the list). A process-global Mutex (poison-tolerant) serializes the #[test]s (shared cache + s_bsd globals).
    • No L2/S2S. Resolver inert under NO_DNS_LOOKUP; golden_s_dns (oper DNS stats + empty DNS l cache dump) + golden_registration stay byte-identical, confirming the still-C add_to_cache/find_cache_number now call the Rust rem_cache/rem_list (+ the P6q hashes) without regression. End-to-end DNS coverage arrives when the resolver proper is ported under a fixture nameserver.
    • Plandocs/superpowers/plans/2026-06-05-p6r-res-cache-rem.md.
  • 2026-06-05 — P6s (res.c cache lookup/reorder half) merged. Third bite of the res.c port (after P6q's hashes + P6r's removal core). Ported the lookup/reorder connected component — the half of the hostent cache that finds entries and reorders/merges them — to ircd-common/res.rs. res.c's only remaining strands are the resolver proper (socket + request queue + answer parser) and the cache build/insert half (make_cache/add_to_cache, → P6t).

    • Cluster choice. Bottom-up from the cache floor: update_list (static) is the shared leaf both finders call; find_cache_name/find_cache_number (static) are the lookup entry points. A clean connected component — the finders call update_list (+ the already-Rust P6r rem_list in find_cache_number's degenerate branch) and touch only cachetop/hashtable/cainfo + mycmp/MyRealloc/mystrdup; no dependency on the still-C build/insert half. Splitting the cache "build/insert/lookup" remainder into lookup (P6s) then build/insert (P6t) keeps the bite small, matching the P6q/P6r cadence.
    • Seam. -DPORT_RES_CACHE_LOOKUP_P6s added to the res_link.o recipe; the three-way RES_CACHE_LINKAGE now fires extern under PORT_RES_CACHE_P6q or PORT_RES_CACHE_REM_P6r or PORT_RES_CACHE_LOOKUP_P6s (res_link.o defines all three guards), GLOBAL under RES_CACHE_EXPOSE (cref oracle), else static. The three forward decls (res.c:84/85/91) + definitions flip staticRES_CACHE_LINKAGE; the bodies (update_list … find_cache_number, res.c:1140–1406) wrap in #ifndef PORT_RES_CACHE_LOOKUP_P6s. RED was the undefined-symbol link error for find_cache_name/find_cache_number (still-C callers gethost_byname_type:410, proc_answer:776, gethost_byaddr:440, make_cache:1440/1456 — the exact port checklist; update_list had no still-C reference once the finders dropped, but Rust defines it for the finders to call). GREEN via nm target/debug/ircd | grep -E ' T (update_list|find_cache_name|find_cache_number)$'. (A C-comment gotcha: gethost_*/proc and find_cache_*/cref each contain a literal */ that prematurely closed the seam comment → reworded.)
    • Config-resolved bodies. No gates touch these (DEBUG off → the Debug(...) blocks vanish). IN_ADDR=in6_addr (sizeof 16, AFINET=AF_INET6); WHOSTENTP(x)=OR of the 16 s6_addr bytes; S_ADDR=s6_addr. ResRQ.he is the inline hent (h_addr_list:[in6_addr;35], h_aliases:[*char;35]); aCache.he is the libc hostent (**char lists). Constants T_A=1/T_PTR=12/T_AAAA=28, MAXALIASES/MAXADDRS=35, FLG_*.
    • Faithfulness. Raw-pointer translation of the C for-loops with post-increment index walks (for (i=0,s=h_name; s; s=h_aliases[i++])s = *aliases.offset(i); i+=1 at the loop tail), incl. the find_cache_name full-list fallback's fixed-s=alias[0] quirk (s set only in the init, never in the increment → it only ever compares alias[0]). update_list's T_PTR alias-merge reallocs h_aliases by one + mystrdups the new name; the T_A/T_AAAA addr-merge reproduces the double-MyRealloc exactly — realloc the contiguous IP block (*h_addr_list[0]) to addrcount*16, realloc the pointer array to addrcount+1, repoint each pointer into the (possibly moved) block, NULL-terminate, bcopy the new IP into the last slot (*--ab) — then the addrcount>1 → clear FLG_PTR_FWD|VALID vs the single-addr FLG_PTR_PEND_REV/FLG_PTR_PEND_FWD logic. find_cache_number's second loop preserves the C for-loop continue→increment semantics (every guard incl. the cp = rem_list(cp) degenerate-reap falls through to the trailing cp = cp->list_next). bcmp→16-byte slice compare; MyRealloc/mystrdup/mycmp are the Rust ports resolved at link.
    • Classification. Utility/leaf — none in msgtab. → L1 only. No IsServer/remote-field path; the resolver is inert under NO_DNS_LOOKUP so no client/server command populates the cache at runtime.
    • L1 (ircd-testkit/tests/res_cache_lookup_diff.rs): the P6k/P6r two-isolated-Worlds pattern (fn-pointers + global base pointers: cachetop/hashtable/cainfo + find_cache_*/update_list/hash_*). insert() builds entries à la add_to_cache (contiguous addr block); make_rptr() builds a ResRQ per world (each owns its pointers → compare CONTENTS not pointers). 6 tests, zero diff: update_list reorder-only (rptr=NULL: head move keeping relative order, ca_updates bump, repeat on the head = no-op); find_cache_name hashed hit + reorder + ca_na_hits / name-miss (counter unchanged, order kept) / flags-gate miss / the alias full-list fallback (h_name & alias[0] in distinct hash buckets); find_cache_number hashed hit + ca_nu_hits / absent-addr miss / FLG_PTR_VALID gate miss / the multi-addr fallback (lookup the 2nd of two addresses, found only via the full-list scan); update_list T_PTR alias merge (append a new name) + the already-present (= cached h_name) no-growth inverse; update_list T_A addr merge (append a new IP, addrcount→2 flag clear) + the already-present no-growth inverse (single-addr FLG_PTR_PEND_FWD set). A process-global Mutex serializes the #[test]s; each scenario reset()s its world's cache + buckets and snapshots counter deltas.
    • No L2/S2S. Resolver inert under NO_DNS_LOOKUP; golden_s_dns (oper DNS stats + empty DNS l) + golden_registration stay byte-identical, confirming the still-C proc_answer/gethost_*/make_cache now call the Rust lookup/reorder core (+ the P6q hashes + P6r removal) without regression. End-to-end DNS coverage arrives when the resolver proper is ported under a fixture nameserver.
    • Plandocs/superpowers/plans/2026-06-05-p6s-res-cache-lookup.md.
  • 2026-06-05 — P6t (res.c cache build/insert half add_to_cache/make_cache) merged. The fourth and final bite of the res.c hostent-cache cluster — the two functions that create and link in cache entries — closing the cache out: with this res.c's hostent cache is now FULLY Rust, and only the resolver proper (request queue + answer parser + socket I/O) remains C.

    • Cluster choice / why now. The dependency floor was already in place: P6q (the hash_number/hash_name leaves), P6r (the rem_list/rem_cache/expire_cache/flush_cache removal core), P6s (the update_list/find_cache_name/find_cache_number lookup/reorder half). add_to_cache/make_cache are the only remaining cache functions and form a tight connected component (make_cachefind_cache_*/MyMallocadd_to_cacherem_cache), every callee already Rust → the natural last bite.
    • Guard / partial port. #ifndef PORT_RES_CACHE_BUILD_P6t wraps both C bodies (res.c:1086 add_to_cache, res.c:1416 make_cache); -DPORT_RES_CACHE_BUILD_P6t added to the res_link.o compile in build.rs alongside the P6q/r/s defines. Both functions' decl+def flipped from static to the existing three-way RES_CACHE_LINKAGE macro (→ extern under any PORT_RES_CACHE_* in res_link.o so the still-C proc_answer caller of make_cache resolves to the Rust def; → GLOBAL under -DRES_CACHE_EXPOSE in the cref oracle res.o so cref_add_to_cache/cref_make_cache exist for L1). make_cache's forward decl at res.c:88 updated to match.
    • Config-resolved body. DEBUG off → all Debug((…)) blocks drop out (no inetntop/ipv6string debug formatting in add_to_cache). AFINET=AF_INET6struct IN_ADDR = in6_addr (16 bytes); WHOSTENTP tests all 16 s6_addr bytes; make_cache's address-count loop + contiguous-block fill use sizeof(struct IN_ADDR) = 16. MAXCACHED 512, MAXADDRS/MAXALIASES 35.
    • Faithfulness notes. make_cache steals the ResRQ's alias pointers (rptr->he.h_aliases[n] → cache, source nulled) and h_name (nulled after copy) — preserved exactly, so the L1 asserts the source ResRQ fields go NULL. The *t++ = s; s += sizeof(IN_ADDR) two-array block-fill is reproduced with raw-pointer t.add(1)/s.add(IN_ADDR_SIZE) walks; bzerowrite_bytes(_,0,_). The ttl < 600 clamp bumps reinfo.re_shortttl and forces 600; expireat = timeofday + ttl. add_to_cache's ++incache > MAXCACHED LRU evict walks cachetop to the tail (while !(*cp).list_next.is_null()) and rem_caches it. MyMalloc declared returning *mut c_char to match dbuf.rs's decl (avoids the redeclared-signature warning), cast at each use.
    • Classification. Utility/leaf — not in msgtab. → L1 only: no client/server command path under the locked config (NO_DNS_LOOKUP on → the resolver is inert, the cache never populates at runtime). End-to-end DNS coverage arrives when the resolver proper is ported under a fixture nameserver.
    • L1. ircd-testkit/tests/res_cache_build_diff.rs — two fully-isolated Worlds of fn-pointers + global base pointers (cachetop/hashtable/incache/cainfo/reinfo/timeofday), copying res_cache_lookup_diff.rs: LIVE (#[link_name] → the Rust port over the res_link.o globals) vs CREF (cref_* over the renamed oracle). 8 tests with inverse/round-trip invariants — build-by-name T_A (entry contents + linkage + ca_adds/incache deltas + the steal inverse) and T_AAAA (flag), the short-ttl clamp (ttl<600→600 + re_shortttl) with the ≥600 inverse, dedup-by-name (T_A HIT → existing entry, no add) and dedup-by-addr (T_PTR HIT via find_cache_number → name merged as alias, no add), multi-addr contiguous-block build (distinct pointers, correct bytes), add_to_cache LRU boundary at MAXCACHED (incache pre-set near the cap; evict observed via ca_dels delta + list length) with the under-cap inverse, and both-bucket linkage (a single add is findable via both find_cache_name and find_cache_number). timeofday pinned to a fixed value per scenario so expireat is deterministic; process-global LOCK serializes the parallel #[test] threads. Zero diff. make_cache/add_to_cache confirmed T (Rust-defined) in target/debug/ircd.
    • No L2/S2S. Resolver inert under NO_DNS_LOOKUP (cache never populates at runtime); no IsServer/remote-field path. golden_s_dns (oper DNS stats / DNS l empty dump / non-oper 481) + golden_registration confirm the still-C resolver calls the Rust build/insert core without regression.
  • 2026-06-05 — P6u (res.c request-queue management core) merged. First bite into the res.c resolver proper — the only DNS file still in C after the P6q–P6t hostent-cache cluster — ported bottom-up from the outstanding-DNS-request queue floor.

    • Cluster choice. add_request/rem_request/make_request/find_id: the connected component over the file-statics first/last (the request queue heads) + reinfo.re_requests. No socket I/O, no answer parsing (none call resend_query/query_name/do_query_*/proc_answer), so it isolates cleanly as the floor the rest of the resolver builds on. The public queue walkers del_queries/timeout_query_list stay C this bite (timeout_query_list pulls in resend_query → the socket half).
    • Guard / seam. res_link.o gains -DPORT_RES_REQ_P6u. A new three-way RES_REQ_LINKAGE macro (parallel to RES_CACHE_LINKAGE): extern under PORT_RES_REQ_P6u (res_link.o — the four bodies #ifdef'd out, Rust defines them, the still-C callers del_queries/timeout_query_list/gethost_*/do_query_*/get_res resolve at link), blank under RES_CACHE_EXPOSE (cref oracle — GLOBAL so cref_add_request/cref_rem_request/cref_make_request/cref_find_id exist for L1), else static. The first/last queue heads flip from static ResRQ *last, *first; to RES_DNS_STATIC (already exposed in res_link.o via PORT_DNS_M_DNS + the cref oracle via RES_CACHE_EXPOSE) so the Rust port and the still-C walkers share one storage — exactly the P6r/P6t cache-globals pattern.
    • Config-resolved body. AFINET=AF_INET6 → he.h_addrtype = AF_INET6; DEBUG off → the Debug(...) traces in rem_request/timeout_query_list drop out; MyFree is a macro → libc::free; MyMalloc+bzeroMyMalloc + ptr::write_bytes(…,0,…); the bcopy(lp, &cinfo, sizeof(Link))ptr::copy_nonoverlapping (or a zero-fill when lp==NULL).
    • Faithfulness notes. rem_request's for (rptr=&first; *rptr; r2ptr=*rptr, rptr=&(*rptr)->next) rendered as a raw *mut *mut reslist walk advancing r2ptr/rptr at the bottom so the last==old → last=r2ptr tail fix-up matches; the post-loop free order (he.h_name, the MAXALIASES h_aliases[], name, the node) preserved. make_request sets next=NULL before add_request (which sets it again) verbatim. add_request(NULL) returns -1 (the C no-op) before touching the queue.
    • Classification. Utility/callees — neither in msgtab. → L1 only, NO L2, NO S2S. The resolver is inert under NO_DNS_LOOKUP (no client/server command path populates the queue at runtime; end-to-end queue coverage arrives under a fixture nameserver when the socket/answer half lands).
    • L1. res_request_diff — two fully-isolated Worlds of fn-pointers + first/last/reinfo base pointers (LIVE #[link_name] → the Rust port over the res_link.o globals; CREF cref_* over cref_first/cref_last/cref_reinfo). Positive + inverse/round-trip: make_request seeds every default field & appends (NULL lp zeroes cinfo); add_request arrival ordering + re_requests increments, and add_request(NULL) → -1 with the queue untouched; find_id hit (first/middle/tail) / miss / misses-after-its-target-is-rem_request'd; rem_request head/middle/tail unlink with last repointed on tail removal + the node freed; a drained queue is first==last==NULL and reusable by a fresh make_request. timeofday pinned (deterministic sentat); a process-global LOCK serializes the parallel #[test] threads over the shared queue. Zero diff over 8 tests.
    • L2/S2S. None (see Classification). cargo test -p ircd-golden golden_s_dns (2) + golden_registration (2) green — the still-C resolver init_resolver/walkers call the Rust queue primitives at boot without regression. 0 warnings.
    • Still C in res.c. del_queries/timeout_query_list (the public queue walkers) + the resolver socket/answer half: send_res_msg, do_query_name/do_query_number, query_name, resend_query, proc_answer, get_res, gethost_byname/gethost_byname_type/gethost_byaddr, init_resolver, cres_mem, bad_hostname.
  • 2026‑06‑05 — P6v (res.c queue walkers del_queries/timeout_query_list) merged. The second bite of the res.c resolver proper, built on the P6u queue core — the reaper/timeout layer that walks the outstanding-DNS request queue (first/last).

    • Cluster choice. Continuing bottom-up after the P6u request-queue floor: del_queries/timeout_query_list are the two queue walkers the PLAN names as the next nameable unit. del_queries deps are purely Rust (rem_request P6u) + the shared first global → fully portable. timeout_query_list additionally calls the still-C resend_query (the resend/socket path) — the one inverse-direction dependency.
    • Guard / seam. res_link.o gains -DPORT_RES_WALK_P6v (build.rs); res.c wraps both bodies in #ifndef PORT_RES_WALK_P6v. New RES_RESEND_LINKAGE macro: extern under PORT_RES_WALK_P6v (res_link.o exposes resend_query as a global so the Rust timeout_query_list links against it), static otherwise — the cref oracle keeps resend_query static, and cref_timeout_query_list resolves to the objcopy-renamed local cref_resend_query inside libcref.a (self-contained: resend_query has no out-of-TU caller). Both del_queries/timeout_query_list are already public (res_ext.h) → the still-C callers (s_bsd.c:1361/3288, ircd.c:580/1193) and the Rust list.rs:free_conf resolve to the Rust defs at link.
    • Config‑resolved body. DEBUG off → the Debug(())/myctime traces drop out of both functions. USE_IAUTH on → the ASYNC_CLIENT exhausted arm calls sendto_iauth("%d d", cptr->fd). ClearDNS(x) = (*x).flags &= ~FLAGS_DOINGDNS (0x100; flags is c_long). The cinfo.flags switch: ASYNC_CLIENT=0 (iauth+ClearDNS), ASYNC_CONNECT=1 (sendto_flag SCH_ERROR), default (ASYNC_CONF=2 etc.) → reap only. Platform note: reslist.retries/resend render as c_char = u8 here, so --rptr->retries is ported as wrapping_sub(1) (faithful to C's modular char; the <= 0 test then means == 0, matching unsigned C on the same platform) — panic-free even though the live values (1‑3) never underflow.
    • Faithfulness. timeout_query_list mirrors the C for (rptr=first; rptr; rptr=r2ptr) walk with r2ptr captured before rem_request frees the node; the exhausted arm uses continue (skipping the next update) exactly as C; next seeded 0 with the !next || tout < next min; return (next > now) ? next : now + AR_TTL. resend_query short-circuits on resend == 0, so the L1 retry test exercises the call with no socket I/O.
    • Classification. Utility/callees — neither is in msgtab (callers: s_bsd.c registration/cleanup, ircd.c io_loop/exit, list.rs free_conf). L1 only.
    • L1. ircd-testkit/tests/res_walk_diff.rs (modeled on res_request_diff.rs): two fully-isolated Worlds (LIVE #[link_name] → the Rust port over res_link.o globals; CREF cref_* over the renamed oracle), each carrying base pointers to first/last/reinfo + the four fn-pointers. 9 tests, positive + inverse: del_queries head/middle/tail match, absent-cp no-op, all-match drain (first==last==NULL); timeout_query_list future-dated survive (returns min tout), exhausted ASYNC_CONF (no-notify reap → now+AR_TTL), exhausted ASYNC_CLIENT (ClearDNS clears the DNS bit but leaves an unrelated bit, diffed on two per-world clients), exhausted ASYNC_CONNECT, retry back-off (retries--, sentat=now, timeout 4→8, resend_query no-op via resend=0), and a mixed survivor+reaped+retried walk — return value, surviving ids, per-node retries/sentat/timeout, and re_timeouts all diffed. timeofday pinned; adfd/cref_adfd=-1 so sendto_iauth no-ops; svchans BSS-zero so sendto_flag no-ops; process LOCK serializes the parallel #[test] threads. Zero diff.
    • No L2/S2S. The resolver is inert under NO_DNS_LOOKUP — the request queue never populates at runtime, so there's no client/server command path that reaches these walkers. golden_s_dns (2) + golden_registration (2) stay green, confirming the still-C resolver calls the Rust walkers without boot regression. End-to-end DNS coverage arrives when the answer parser + socket I/O half is ported under a fixture nameserver.
    • Still C in res.c: only the answer parser + socket I/O half — send_res_msg/do_query_name/do_query_number/query_name/resend_query/proc_answer/get_res/gethost_byname/gethost_byname_type/gethost_byaddr/init_resolver/cres_mem/bad_hostname.
  • 2026-06-05 — P6w (res.c bad_hostname) merged. The next bottom-up leaf of the still-C res.c resolver proper.

    • Cluster choicebad_hostname (res.c:1832), the pure hostname-character validator used by the answer parser proc_answer (res.c:802,851) to reject malformed PTR/A results. No socket, no globals, no allocation — the cleanest remaining res.c leaf to extract before the socket/answer-parser core. Single function → atomic cluster.
    • Config-resolved bodyRESTRICT_HOSTNAMES is defined (cbuild/config.h:608), HOSTNAMES_UNDERSCORE is not. So the compiled body is the restrictive branch without the underscore escape hatch: isalnum ok; interior hyphen ok unless leading / after-dot / trailing (len==1) / before-dot; . ok unless leading or doubled; else -1.
    • Guard / seam — upstream file-static, so a new three-way RES_HOSTNAME_LINKAGE (mirroring P6q/P6r/P6s RES_CACHE_LINKAGE): extern when building res_link.o (-DPORT_RES_HOSTNAME_P6w, body #ifndef'd out → Rust def wins, still-C proc_answer resolves at link), GLOBAL in the cref oracle (-DRES_CACHE_EXPOSEcref_bad_hostname for L1), static in the plain build. Applied to the forward decl (res.c:132) + the definition. build.rs appends -DPORT_RES_HOSTNAME_P6w to the res_link.o compile line.
    • Faithfulness notes — the C for (s=name; (c=*s) && len; s++, len--) runs the s++, len-- increment on every continue (C continue jumps to the increment) and never falls through to it (each path continues or return -1s); the Rust port advances s/len before each continue and skips it on the -1 exit. isalnum(c) is called as libc::isalnum(c as c_int) with the same (signed-char-promoted) value → byte-identical shared-libc table lookup. s[1] is read only when len != 1 — the Rust && short-circuits identically, so *s.add(1) is never evaluated at the cap.
    • Classification — utility/callee TU (not in msgtab; called only from the still-C proc_answer). L1 is the gate. No client command reaches it under the locked config without the still-C answer parser running.
    • L1ircd-testkit/tests/res_bad_hostname_diff.rs: declare c_bad_hostname (#[link_name="bad_hostname"] → Rust def via res_link.o) + cref_bad_hostname (oracle), drive both over a corpus × every prefix length 0..=len+1 (the len cap is load-bearing — only the first len bytes are scanned) and assert identical return. Covers valid names (incl. punycode interior double hyphen), hyphen edge cases (leading/trailing/after-dot/before-dot + the len==1 trailing-hyphen guard), dot edge cases (leading/trailing/doubled), illegal chars (space, underscore, /, *, :, control \x07, high-bit), and the len boundary (illegal char at len vs len-1, len==0, empty string). 6 tests, zero diff.
    • S2S — no path (the function is unreachable without the still-C answer parser; no IsServer/remote-field/propagation path exists).
  • 2026-06-05 — P6x (res.c cres_mem) merged. The DNS-cache memory-usage reporter — the next bottom-up leaf of the res.c resolver proper, ported to ircd-common/src/res.rs.

    • Cluster choicecres_mem(aClient *sptr, char *nick) (res.c:1810): walks the hostent cache list (cachetop, via list_next) summing each aCache/hostent's struct bytes (sm), IP storage (imsizeof(char*)+sizeof(struct IN_ADDR) per addr, +1 for the NULL terminator) and name storage (nmsizeof(char*)+strlen per alias, then the i-1/+sizeof(char*) tail, then strlen(h_name)); ts = ARES_CACSIZE(1009) * sizeof(CacheTable)(16) = 16144; sends two RPL_STATSDEBUG (249) lines and returns ts+sm+im+nm.
    • Seam / Guardcres_mem is non-static, exported in res_ext.h, so the cref oracle already carries cref_cres_memno new RES_*_LINKAGE/*_EXPOSE seam (contrast the file-static cache/queue leaves P6q–P6w). #ifndef PORT_RES_CRES_MEM_P6x wraps the C body; ircd-sys/build.rs adds -DPORT_RES_CRES_MEM_P6x to the res_link.o compile so the body is dropped and the Rust #[no_mangle] cres_mem resolves it for s_debug.c's count_memory at link. The cref oracle res.o is untouched.
    • Config-resolved body — DEBUG off; the whole body compiles unconditionally. me (bindgen static mut me) read for the server name; sendto_one via the existing res.rs variadic extern.
    • Faithfulness notes — the nm += i - 1 step is unsigned modular arithmetic: with 0 aliases i == 0, so it adds (int)-1 widened to u_long (= u_long::MAX, i.e. nm -= 1), and the following nm += sizeof(char*) nets +7. Reproduced with wrapping_add throughout the nm accumulation so it may transit u_long::MAX without a debug-mode overflow panic. The %d format applied to the u_long args (ts/sm/im/nm) is preserved verbatim (reads the low 32 bits) — the values pass as u_long so the live trampoline and cref_sendto_one push identical bytes.
    • Classification — utility/callee (not in msgtab); reached from count_memory via STATS z. Read-only on the cache.
    • L1 (ircd-testkit/tests/res_cres_mem_diff.rs) — two-world (LIVE over cachetop/me/the Rust cres_mem; CREF over cref_cachetop/cref_me/cref_cres_mem) build of identical caches + a buffering local client (make_client, P2) per world with a pinned me.name. Diffs both the return value and the sendQ bytes (the two RPL lines). Cases: empty cache (return == ts = 16144, Structs 0 IP storage 0 Name storage 0); single entry / 0 aliases (the nm += i-1 wrap → +7); single entry / 3 aliases / 2 addrs; NULL h_name (trailing strlen skipped); 3 entries (sm scales per node). Hand-computed totals match the cref oracle. Zero diff.
    • L2 — already covered by golden_debug::stats_memory_match_reference (STATS z → count_memorycres_mem); stays green (empty boot cache under NO_DNS_LOOKUP → ts-only walk).
    • S2S — none. No IsServer/remote-field path (oper STATS only).
    • Still C in res.c — the socket/answer-parser core only: send_res_msg/do_query_*/query_name/resend_query/proc_answer/get_res/gethost_*/init_resolver.
  • 2026-06-05 — P6y (res.c send_res_msg) merged. The resolver UDP datagram-send leaf — the literal bottom of the send tree (do_query_name/do_query_number/resend_queryquery_namesend_res_msg). Continues the bottom-up res.c resolver-proper port; after this only the answer-parser + socket-I/O core remains C (query_name/do_query_*/resend_query/proc_answer/get_res/gethost_*/init_resolver).

    • Seam. send_res_msg is upstream file-static → new three-way RES_SEND_LINKAGE (same shape as RES_RESEND_LINKAGE/RES_HOSTNAME_LINKAGE): extern in res_link.o (-DPORT_RES_SEND_P6y; body #ifndef-dropped, Rust defines it, the still-C query_name resolves at link), GLOBAL in the cref oracle (-DRES_CACHE_EXPOSEcref_send_res_msg for L1), else static. The resolver socket fd resfd it sends on is a real exported global (s_bsd_ext.h) — declared extern in res.rs, no extra seam; ircd_res is the Rust res_init.rs global (P6p).
    • Config-resolved body / faithfulness. max = MIN(nscount, rcount), overridden to 1 on RES_PRIMARY (=16), then floored to 1 — the clamp order is load-bearing (rcount=0 → 1 send; RES_PRIMARY with nscount=3 → 1 send). Each iteration force-sets nsaddr_list[i].sin6_family = AF_INET6 (=AFINET) then sendto(resfd, msg, len, 0, &nsaddr_list[i], sizeof(sockaddr_in6)); reinfo.re_sent/sent bump only on a full-length send (== len); the DEBUGMODE-off failure branch is a no-op. Returns sent, or -1 on NULL msg / all-failed.
    • Classification. Utility/callee — not in msgtab; only caller is query_name (res.c). No client command reaches it under the locked config (NO_DNS_LOOKUP disables the DNS path).
    • L1. ircd-testkit/tests/res_send_msg_diff.rs: two fully isolated Worlds (LIVE via #[link_name] → Rust over res_link.o/res_init globals; CREF via cref_*). Each world+scenario binds its own loopback (::1) UDP recv socket(s) with a 300ms SO_RCVTIMEO + its own send socket, points ircd_res.nsaddr_list[] at the recv port(s), zeroes reinfo.re_sent, drives send_res_msg, then recvfroms and snapshots {return value, datagram bytes received, re_sent delta, nsaddr_list[0].sin6_family}. Six cases diff byte-identical: single-nameserver one-send; NULL msg → -1/no send; rcount=0 floor-to-1; RES_PRIMARY (nscount=2) override-to-1 (only ns[0] receives); two-nameserver both-receive; bad resfd (-1) → all sends fail → -1, counter untouched.
    • L2/S2S. None. No DNS command path under NO_DNS_LOOKUP; boot/registration (golden_registration) and oper-DNS (golden_s_dns) goldens stay byte-identical, confirming the dropped C body doesn't perturb the binary. No IsServer/remote-field path.
  • 2026-06-05 — P6z (res.c query_name — the DNS query builder/sender) merged. The next bottom-up leaf of the resolver proper, sitting directly above the P6y send_res_msg leaf in the send tree. Ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_QUERY_P6z).

    • Cluster choicestatic int query_name(char *name, int class, int type, ResRQ *rptr) (res.c:618). Its only callees are already Rust: ircd_res_mkquery (P6o), find_id (P6u), send_res_msg (P6y). Its callers are still C: do_query_name/do_query_number/resend_query/get_res. So it is the next clean leaf up the tree.
    • Linkage / seam — upstream file-static → new three-way RES_QUERY_LINKAGE (mirrors RES_SEND_LINKAGE): extern under -DPORT_RES_QUERY_P6z (res_link.o body #ifdef'd out → Rust def; still-C do_query_*/get_res resolve at link), GLOBAL under -DRES_CACHE_EXPOSE (cref oracle → cref_query_name for L1), static otherwise. No new data-global seam — it touches only ircd_res/resfd/first/reinfo, all already exposed (P6p/P6u/P6y). (Gotcha hit while writing the C comment: do_query_*/ contains a literal */ that closes the block comment early → reworded to do_query_name/do_query_number.)
    • Config-resolved bodyLRAND48 absent from cbuild/setup.h/config.h → the id source is gettimeofday(&tv,NULL) + tv.tv_usec & 0xffff (the lrand48() branch is dead); DEBUG off → no Debug() trace. MAXPACKET=1024, QUERY=0, NO_RECOVERY=3, TRY_AGAIN=2. reslist.sends is a signed charsends++ is an i8 wrapping_add, sign-extended to int as send_res_msg's rcount. hptr->id is the HEADER 16-bit bitfield → (*hp).id()/set_id (stored big-endian; ntohs=u16::from_be), the res_mkquery.rs:84 pattern. The do/while collision loop is a faithful loop { …; k += 1; if find_id(...).is_null() { break } }.
    • Classification — utility/callee leaf, not in msgtab; reachable only through the still-C resolver entry points, inert project-wide under NO_DNS_LOOKUP. L1 is the gate.
    • L1ircd-testkit/tests/res_query_name_diff.rs: two fully-isolated Worlds (LIVE = Rust port over res_link.o/res_init globals; CREF = cref_* oracle), each with its own loopback ::1 UDP recv+send sockets + ircd_res/reinfo/resfd/first base pointers (the res_send_msg_diff.rs shape). Determinism via a strong interposed #[no_mangle] gettimeofday returning a fixed tv — both worlds (the Rust libc::gettimeofday call and the cref-archive call) resolve to it at link, so packets are byte-identical including the id field and the find_id collision loop is reproducible (ircd_res.id pinned to 0 → mkquery writes htons(1), first computed id = 1 + (tv_usec & 0xffff)). Diff on {ret, datagram bytes, rptr.{id,sends,sent}, re_sent delta, h_errno} over: A-record query (1 send, id == header id, sends 0→1), PTR query (qtype trailer == T_PTR), a forced one-collision id advance (seed the queue with a node whose id == the first computed id) vs the no-collision inverse (k stays 0 → id == first computed) with the exact second-iteration id checked against the C formula, mkquery-failure (300-char single label → dn_comp fails → r<=0, NO_RECOVERY, no send, sends/sent/re_sent untouched), and send-failure (resfd=-1send_res_msg -1 → TRY_AGAIN, but sends still bumped before the send, sent/re_sent untouched). Zero diff over 5 tests.
    • L2 / S2S — none. The DNS path is disabled project-wide via NO_DNS_LOOKUP; the boot goldens golden_registration + golden_s_dns stay byte-identical, confirming the still-C resolver calls the Rust query_name without regression. End-to-end DNS coverage arrives when the resolver proper (answer parser + socket I/O) is ported under a fixture nameserver.
  • 2026-06-05 — P6aa (res.c query-dispatch layer do_query_name/do_query_number) merged. The next bottom-up cluster of the still-C res.c resolver proper, sitting directly above the P6z query_name leaf and below the still-C gethost_*/get_res/resend_query callers.

    • Cluster choice. do_query_name (res.c:541) builds the forward-lookup hostname and do_query_number (res.c:580) builds the reverse-lookup name; both allocate the ResRQ via make_request (P6u) when none was passed and then call query_name (P6z) — all three callees are already Rust, so this is the clean next leaf up the send tree. resend_query (which dispatches to both) is the next unit above and stays C this bite (now calling the Rust do_query_*).
    • Guard / seam. res_link.o gains -DPORT_RES_DOQUERY_P6aa. Both functions are upstream file-static → a new three-way RES_DOQUERY_LINKAGE (mirrors RES_QUERY_LINKAGE): extern under PORT_RES_DOQUERY_P6aa (res_link.o — the two bodies #ifndef'd out, Rust defines them, the still-C callers gethost_byname_type/gethost_byaddr/get_res/resend_query resolve at link), GLOBAL under RES_CACHE_EXPOSE (cref oracle — cref_do_query_name/cref_do_query_number for L1), else static. No new data-global seam — they touch only ircd_res/first/last/reinfo/resfd, all already exposed (P6p/P6u/P6y). (Gotcha re-hit from P6z: a gethost_*/ in the macro comment closes the block comment early via the literal */ — reworded to spell out the caller names.)
    • Config-resolved body. AFINET=AF_INET6rptr->addr/he.h_addr are in6_addr (16 bytes); IN6ADDRSZ = sizeof(struct IN_ADDR) = 16. RES_DEFNAMES = 0x80 (resolv_def.h). DEBUG off → the Debug((…)) trace drops out. strncpyzt(x,y,N) = strncpy(x,y,N); x[N-1]=0 (struct_def.h:871); HOSTLEN=63hname[HOSTLEN+1] = [c_char; 64]. index=strchr; MyMalloc returns *mut c_char.
    • Faithfulness notes. The domain-append branch is gated on rptr != NULL (the resend path) in the C — faithful: a fresh do_query_name (rptr==NULL) never appends defdname, and stores the original (un-truncated/un-appended) name, not hname. do_query_number's two sprintf branches are reproduced by building the exact byte string in Rust ({} decimal for the IPv4 form, single-nibble lowercase {:x} for the ip6.arpa form, byte-reversed low-then-high nibble) and copy_nonoverlapping into the C ipbuf; bcopycopy_nonoverlapping; &rptr->he.h_addr = &he.h_addr_list[0].
    • Classification. Utility/callee leaf — neither in msgtab; reachable only through the still-C resolver entry points, inert project-wide under NO_DNS_LOOKUP. → L1 only, NO L2, NO S2S (no IsServer/remote-field path).
    • L1. ircd-testkit/tests/res_do_query_diff.rs — two fully-isolated Worlds (LIVE #[link_name] → the Rust port over the res_link.o/res_init globals; CREF cref_* over the renamed oracle), each with its own loopback ::1 UDP recv+send sockets + ircd_res/reinfo/resfd/first/last base pointers (the res_query_name_diff.rs shape). A strong interposed gettimeofday pins query_name's id randomization so packets are byte-identical including the id. Diffs {ret, datagram bytes, the resulting ResRQ (type/name/id/sends/sent), he.h_addr bytes, he.h_length, addr bytes, re_sent delta, h_errno} over 7 tests: do_query_name fresh (original name stored verbatim, no append, 1 send); do_query_name resend + RES_DEFNAMES (.example.org appended → longer packet) vs the already-dotted inverse (no append); the overlong-defdname overflow → -1/no send; do_query_number IPv4-mapped (::ffff:127.0.0.1in-addr.arpa, he.h_addr/addr/he.h_length=16 seeded) + the all-zero-prefix arm + the full-IPv6 ip6.arpa nibble form; and do_query_number resend (no make_request, he/addr untouched, queue stays empty). Zero diff. do_query_name/do_query_number confirmed T (Rust-defined) in target/debug/ircd.
    • L2/S2S. None (see Classification). cargo test -p ircd-golden golden_s_dns (2) + golden_registration (2) stay byte-identical — the still-C gethost_*/get_res/resend_query call the Rust do_query_* without boot regression. 0 warnings.
    • Still C in res.c. Only the answer parser + remaining socket I/O: resend_query/proc_answer/get_res/gethost_byname/gethost_byname_type/gethost_byaddr/init_resolver.
  • 2026-06-05 — P6bb (res.c gethost_byname_type/gethost_byname/gethost_byaddr) merged. The public host-lookup entry points — the next bottom-up cluster of the resolver proper, sitting directly above the Rust find_cache_* (P6s) + do_query_* (P6aa) and below the still-C get_res/s_bsd callers.

    • Cluster choice. The three exported lookup fns are thin dispatchers: bump a counter → try the cache → on a miss issue an async query (when a client lp is present) or give up. They depend only on already-ported Rust (find_cache_name/find_cache_number, do_query_name/do_query_number), so they are the clean next leaf above the do_query layer.
    • Mechanism / no seam. Ported into ircd-common/res.rs; res_link.o drops the C bodies via -DPORT_RES_GETHOST_P6bb. Because all three are exported res_ext.h symbols (not file-statics), no linkage seam is needed — the cref oracle's unguarded res.o already exports cref_gethost_* for L1 (same pattern as P6x cres_mem).
    • Config-resolved body. DEBUG off → the Debug(...) trace in the miss path vanishes. gethost_byname delegates with T_AAAA (AFINET=AF_INET6 → IPv6-first; get_res falls back to T_A). gethost_byname_type rejects any non-T_A/T_AAAA type with NULL after bumping re_na_look; the cache flag is FLG_AAAA_VALID/FLG_A_VALID by type. gethost_byaddr gates the number cache on FLG_PTR_VALID (via find_cache_number). (struct hostent *)&(cp->he)addr_of_mut!((*cp).he) (already *mut hostent, no cast).
    • Faithfulness. Counter bump precedes the type guard; cache consulted before any query; lp == NULL short-circuits to NULL before do_query_*; the async query path always returns NULL (the result arrives later via get_res).
    • Compiled-out caller. gethost_byaddr's sole C caller (s_bsd.c:1681) lives in the #else of #ifdef NO_DNS_LOOKUP; the project builds with NO_DNS_LOOKUP, so the call is compiled out and the linker omits the (unreferenced) Rust fn from the ircd binary. It is still ported faithfully and L1-verified — nm confirms gethost_byname/gethost_byname_type resolve to Rust (T) in the binary; gethost_byaddr simply has no caller to pull it in.
    • Classification. Utility/leaf — none in msgtabL1 only. No IsServer/remote-field path; the resolver is inert under NO_DNS_LOOKUP, so no client/server command reaches these at runtime.
    • L1. ircd-testkit/tests/res_gethost_diff.rs (8 cases, zero diff). Two fully isolated worlds (live #[link_name] Rust port vs cref_* oracle) spanning both the DNS cache globals (cachetop/hashtable/cainfo) and the resolver query globals (ircd_res/reinfo/resfd/first/last), each binding its own loopback ::1 UDP recv socket + a pinned gettimeofday (deterministic query id, matching res_do_query_diff). Branch matrix + inverses: bad-type early-out (re_na_look bumped, cache untouched, no query), cache HIT (returns &he, ca_na_hits/ca_nu_hits bumped, no datagram) vs MISS, lp==NULL short-circuit vs lp!=NULL async query (AAAA datagram + queued T_AAAA node for byname; PTR/ip6.arpa datagram + queued T_PTR node for byaddr), the byname→byname_type(T_AAAA) delegation, and the re_na_look (byname) vs re_nu_look (byaddr) split.
    • S2S. None — no server-reachable path (see Classification).
  • 2026-06-05 — P6cc (res.c resend_query) merged. The DNS request-retry dispatcher — the next bottom-up unit of the still-C resolver proper, sitting directly above the P6aa query-dispatch layer (do_query_name/do_query_number) and below the still-C get_res. Ported to ircd-common/res.rs via the res_link.o partial port (-DPORT_RES_RESEND_P6cc); after this only the answer parser + socket I/O remain C (proc_answer/get_res/init_resolver).

    • Cluster choice. RES_RESEND_LINKAGE void resend_query(ResRQ *rptr) (res.c:703). Its callees are all already Rust — do_query_number/do_query_name (P6aa) → query_name (P6z) → send_res_msg (P6y) — so it is the clean next leaf up the tree. Its callers are the Rust timeout_query_list (P6v, the retry branch) and the still-C get_res (res.c:1149, on a truncated/failed reply).
    • Seam. resend_query is upstream file-static. The RES_RESEND_LINKAGE macro already existed (P6v, where it was 2-way: extern under PORT_RES_WALK_P6v to satisfy the Rust timeout_query_list, else static). P6cc promotes it to the standard three-way shape (mirrors RES_QUERY_LINKAGE/RES_DOQUERY_LINKAGE): extern under PORT_RES_RESEND_P6cc (res_link.o — the body #ifndef'd out, Rust defines it, the still-C get_res + the Rust timeout_query_list resolve at link), GLOBAL under RES_CACHE_EXPOSE (cref oracle → cref_resend_query for L1), else static. res_link.o sets both PORT_RES_WALK_P6v and PORT_RES_RESEND_P6cc; the P6cc arm is ordered first so it wins (the legacy P6v arm is kept for clarity but superseded). No new data-global seam — it touches only reinfo/first/last/ircd_res/resfd, all already exposed (P6p/P6u/P6y).
    • Config-resolved body. Faithful 1:1 with the C: early-return on resend == 0; bump reinfo.re_resends; match (*rptr).type_ on T_PTRdo_query_number(NULL, &rptr->addr, rptr), T_AAAA | T_Ado_query_name(NULL, rptr->name, rptr, type), _ → no-op (the counter still bumped before the switch, matching the C ordering). The Rust extern decl for resend_query (added in P6v so timeout_query_list could call the still-C fn) was removed — the module now defines it.
    • Classification. Utility/callee (file-static, not in res_ext.h → not in msgtab). L1 is the gate. No L2 path — the resolver is dead code under the locked NO_DNS_LOOKUP config (the golden binary never drives a live lookup), same as every P6q–P6bb resolver bite. No S2S (no IsServer/remote-field/propagation path).
    • L1. ircd-testkit/tests/res_resend_query_diff.rs (modeled on res_do_query_diff.rs): two fully isolated Worlds (LIVE #[link_name] → the Rust port over the res_link.o/res_init globals; CREF cref_* → the renamed reference-C oracle), each binding its own loopback ::1 UDP recv+send sockets so the two never cross-talk. gettimeofday interposed (fixed tv) to pin query_name's id randomization → byte-identical datagrams. Snapshots {datagram bytes, node.{type,name,id,sends,sent}, re_resends delta, re_sent delta, h_errno}. Branch matrix + inverses: resend==0 → no-op (no datagram, zero counter deltas, sends untouched); T_Ado_query_name dispatch (datagram, re_resends+1, re_sent+1, sends+1); T_PTRdo_query_number dispatch (in-addr.arpa reverse datagram, re_resends+1); default (type=99) → counter bumps but nothing dispatched (no datagram, re_sent delta 0). Zero-diff. The P6v res_walk_diff (which calls resend_query via timeout_query_list, now resolving to the Rust def) + res_do_query_diff re-run green.
  • 2026-06-05 — P6dd (res.c proc_answer) merged. The DNS-reply answer-section parser — the next bottom-up unit of the still-C resolver proper — is now Rust (ircd-common/res.rs), leaving only the socket I/O (get_res/init_resolver) in res.c.

    • Cluster choiceproc_answer (res.c:732) is the floor below the still-C get_res (its only caller): a pure parse-into-rptr->he routine (no socket I/O), so it is fully L1-testable by feeding constructed DNS wire buffers and diffing the resulting hent + return value. All of its callees were already Rust: ircd_dn_expand/__ircd_dn_skipname/ircd_getshort/ircd_getlong (res_comp, P6n), bad_hostname (P6w), find_cache_name (P6s), MyMalloc (P2); plus the still-C variadic sendto_flag (wrong-type / bad-IP-length error notices).
    • Seamproc_answer is upstream file-static → new three-way RES_PROC_ANSWER_LINKAGE (mirrors RES_DOQUERY_LINKAGE): extern under -DPORT_RES_PROC_ANSWER_P6dd (res_link.o drops the #ifndef'd body so the still-C get_res resolves to the Rust def at link), GLOBAL under -DRES_CACHE_EXPOSE (cref oracle exports cref_proc_answer for L1), else static. Added -DPORT_RES_PROC_ANSWER_P6dd to the res_link.o compile in ircd-sys/build.rs. The res.c-private scratch hostbuf[HOSTLEN+1+100] is touched ONLY by proc_answer (verified by grep) → it moved into the Rust module as a private static mut [c_char; 164] (NOT ABI, like debugbuf); get_res stays C and never reads it, so its #ifndef PORT_RES_PROC_ANSWER_P6dd-guarded C definition simply drops.
    • Config-resolved body — AFINET=AF_INET6 → A/AAAA addresses are 16-byte in6_addr; T_A builds the v4-mapped ::ffff:a.b.c.d form. sizeof(HEADER)=12, QFIXEDSZ=4, C_IN=1, HOSTLEN=63, RES_DEFNAMES=0x80, MAXALIASES=35. hptr->qdcount/->ancount read+decremented as loop counters via the bindgen bitfield *_raw accessors (get_res already ntohs-converted them to host order before the call — faithful). DEBUG off → the Debug((…)) traces drop.
    • Classification — utility/callee (file-static, not in msgtab, not in res_ext.h); sole caller the still-C get_res. L1 is the gate; no L2 path (the resolver is dead code under the locked NO_DNS_LOOKUP config — the golden binary never drives a live lookup, same as every P6q–P6cc bite). No S2S (no IsServer/remote-field/propagation).
    • L1res_proc_answer_diff (12 cases): two isolated worlds (LIVE #[link_name="proc_answer"] → the Rust port over res_link.o globals; CREF cref_proc_answer over cref_ircd_res) parse constructed DNS reply buffers (a small uncompressed-name wire encoder + a header whose qd/ancount are set host-order via the bindgen accessors) and diff the return value + a fingerprint of rptr->he (h_name, alias bytes/count, addr-slot byte-sums/count, h_addrtype, h_length). All scenarios keep the DNS cache empty so find_cache_name misses identically. Cases + inverses: single A; single AAAA; multi-A (two addrs, equal name NOT re-added as a duplicate alias); good PTR (ans=1) vs bad-hostname PTR (-1); CNAME-then-A; wrong-reply-type record skipped (ans unchanged); bad-IP-length A (-2); RES_DEFNAMES dotless-append (hosthost.example.org) vs dotted name untouched; two-question header skip; empty answer section. Zero diff vs cref_.
    • No L2/S2S path — resolver dead-code under NO_DNS_LOOKUP; covered later when get_res lands with fixture DNS.
  • 2026-06-05 — P6ee (res.c init_resolver) merged. The resolver subsystem's bit-flag-driven one-shot bootstrap ported to ircd-common/res.rs — the second-to-last function in res.c.

    • Cluster choiceinit_resolver(int op) (res.c:243), op an OR of RES_INITLIST/RES_CALLINIT/RES_INITSOCK/RES_INITDEBG/RES_INITCACH (res_def.h:7-11). Called only from the still-C I/O layer (s_bsd.c boot/rehash, ircd.c). The recvfrom-driven get_res is now the only function left C in res.c.
    • Guard / seam#ifndef PORT_RES_INIT_P6ee around the C body; -DPORT_RES_INIT_P6ee added to the res_link.o compile in ircd-sys/build.rs. init_resolver is an exported res_ext.h symbol → the cref_init_resolver oracle already exists, so no linkage seam (cf. P6x/P6bb); only minus_one (support.c:32, unsigned char[17]) was added to res.rs's extern block.
    • Config-resolved bodyLRAND48 off → the srand48(time(NULL)) line drops; DEBUG off → the RES_INITDEBG RES_DEBUG block + all Debug() drop. What ports: INITLIST (bzero reinfo, null first/last), CALLINIT (ircd_res_init then the nscount==0::1 / all-ones minus_one fallback nameserver), INITSOCK (socket(AF_INET6,SOCK_DGRAM,0)resfd, clear SO_BROADCAST with optlen 0, bind to in6addr_any), INITCACH (bzero cainfo + hashtable), and the bare op==0→return resfd. Calls the Rust ircd_res_init (P6p) + inetpton (P1/support).
    • Classification — utility/callee (no msgtab entry; the call sites are the still-C I/O bootstrap, unreachable from a scripted client under the locked config) → L1 is the gate, no L2/L2-S2S path.
    • L1res_init_resolver_diff (isolated-worlds, the P6p/P6bb pattern): c_init_resolver writes the real globals, cref_init_resolver the cref_* twins; diff per op flag. INITLIST/INITCACH pre-dirty then assert both worlds zeroed/nulled + identical; CALLINIT zeroes both ircd_res, diffs retrans/retry/options/nscount/nsaddr_list bytes masking the nondeterministic .id (covers the ::1 fallback host-robustly); INITSOCK asserts return==resfd>=0 and getsockname→AF_INET6 in both worlds (fd values legitimately differ → structural assert); op==0 pins resfd and asserts the return. Zero diff (5/5).
    • S2S — none (no IsServer/remote-field/propagation path; pure state-init + socket setup).
  • 2026-06-05 — P6ff (res.c get_res — the LAST C function in res.c) merged. The recvfrom DNS answer driver ported to ircd-common/res.rs; after it res.c has zero C function bodies left.

    • Cluster choiceget_res is the final C function in res.c and the top of the resolver read path: s_bsd.c's io‑loop calls it when resfd is readable. Every function it calls was already ported bottom‑up across P6q–P6ee (find_id, proc_answer, make_cache, rem_request, gethost_byname_type, query_name, resend_query), so this port is the keystone that closes the resolver.
    • Guard / linkage#ifndef PORT_RES_GET_RES_P6ff around the C body; no linkage seam (exported res_ext.h symbol → cref_get_res already in the oracle, and the still‑C s_bsd.c caller resolves to the Rust def at link). -DPORT_RES_GET_RES_P6ff appended to the res_link.o recipe in ircd-sys/build.rs. After this, res_link.o defines only the resolver data globals (reinfo/cachetop/cainfo/hashtable/incache/first/last); the final res.o drop (move those to Rust) is the remaining P6 res step.
    • Config‑resolved body — DEBUG off → all Debug(...) traces gone. AFINET = AF_INET6SOCKADDR_IN = sockaddr_in6, the anti‑spoof bcmps over the 16‑byte sin6_addr. The C !nsaddr_list[a].SIN_ADDR.S_ADDR clause is dead (array decays to a non‑NULL address) → faithfully replicated as a pure memcmp match. rptr->he.h_addr = h_addr_list[0]. Header counts are 16‑bit bitfields → ntohs via the bindgen *_raw/set_*_raw accessors; rcode_raw for the nibble. h_errno values TRY_AGAIN=2/NO_RECOVERY=3/NO_DATA=4.
    • Faithfulness notegetres_err: (the C goto label) factored into a small unsafe fn getres_err(rptr, lp) returning NULL, so each goto getres_err becomes an early return. The && short‑circuit in the retry path (options & RES_DEFNAMES && ++srch == 0) is preserved — srch is bumped only when RES_DEFNAMES is set. Verified behavior: TRY_AGAIN (NXDOMAIN/SERVFAIL) takes the else if (lp) branch → no resend and the request is kept; the resend/query_name retry fires only on the non‑TRY_AGAIN goto paths where resend is still 1.
    • Classification — utility/callee, no msgtab entry; sole caller s_bsd.c:3272 is the still‑C io‑loop, inert project‑wide under NO_DNS_LOOKUP. L1 is the gate; no L2/S2S path.
    • L1ircd-testkit/tests/res_get_res_diff.rs: two fully isolated Worlds (LIVE = Rust port over res_link.o/res_init globals; CREF = cref_* oracle), each with its own resfd recv socket + an ::1 "nameserver" socket that both sends the crafted reply (so the anti‑spoof source = nsaddr_list[0] = ::1) and captures any follow‑up query. A strong interposed gettimeofday pins the query_name id randomization so follow‑up packets are byte‑identical across worlds. Crafted network‑order DNS reply datagrams; diffed post‑state {ret null?, returned hostent (h_name fp + addr count + addrtype/length), reinfo deltas (replies/errors/unkrep/resends/na_look/requests/sent), incache, cainfo.ca_adds, request‑removed?, h_errno, follow‑up bytes} across 9 scenarios: A‑success (entry cached + request rem_request'd), unknown‑id (find_id miss, re_replies still bumped), short‑packet (bail before re_replies++), NXDOMAIN/SERVFAIL (TRY_AGAIN, no resend, request kept), REFUSED (NO_RECOVERY, resend zeroed), NOERROR/ancount‑0 (NO_DATA), anti‑spoof mismatch (re_unkrep++, nothing cached), and PTR‑relookup (async gethost_byname_type fires a follow‑up, original rem_request'd). Zero diff. cargo build 0 warnings; golden_registration/golden_s_dns stay green (still‑C s_bsd.c calls the Rust get_res).
  • 2026-06-05 — P6gg (res.c data globals → Rust; the final res.o drop) merged. res.c is now FULLY Rust.

    • Cluster choice — after P6ff (get_res, the last C function), res_link.o defined ONLY the resolver's seven private data globals: cachetop (aCache* cache list head), incache (int cache count), hashtable (CacheTable[ARES_CACSIZE]), first/last (ResRQ* request-queue heads), cainfo (struct cacheinfo, 7×int), reinfo (struct resinfo, 10×int). All upstream res.c file-statics (no *_ext.h symbol), de-static'd via RES_DNS_STATIC under -DPORT_DNS_M_DNS. This step moves their storage into Rust and drops res.o.
    • Port — removed the seven static mut … lines from the extern "C" block in ircd-common/src/res.rs; added seven #[no_mangle] pub static mut definitions: pointers = null_mut(), incache = 0, and hashtable/cainfo/reinfo = unsafe { MaybeUninit::zeroed().assume_init() } (the s_misc.rs::ircst idiom). C BSS zero-init → these reproduce it byte-for-byte. CacheInfo/ResInfo promoted to pub (now in pub static signatures). ircd_res/resfd/minus_one stay extern (owned by res_init.rs / s_bsd.c / support.c).
    • build.rs"res.o" added to PORTED; the "res.o" => "res_link.o" arm dropped from link_objs(); the res_link.o compile run() block deleted (the whole -DPORT_RES_CACHE_*..P6ff chain). Kept the cref-oracle res.o compile (-DRES_CACHE_EXPOSE) — the existing res L1 tests still diff against cref_*.
    • Classification — data-global move, no new function → no new L1 test. The seven globals carry the same symbol names, so the L1 tests' #[link_name=…] live-world decls now resolve to the Rust storage instead of res_link.o's.
    • L1 — all 19 res_*_diff suites zero-diff (130 assertions): nm target/debug/ircd confirms the seven globals defined (B) by the Rust object; res_get_res_diff/res_proc_answer_diff/res_request_diff/etc. exercise live-world reinfo/cachetop/cainfo/hashtable/incache/first/last deltas and still match cref_*.
    • L2golden_registration + golden_s_dns byte-identical (the latter drives the DNS/DNS l command that reads cachetop/cainfo/reinfo). No further L2 resolver path: reverse-DNS is dead-code under NO_DNS_LOOKUP.
    • S2S — none (no IsServer/remote-field path in a data-global move).
    • Buildcargo build -p ircd-rs 0 warnings; res.c has zero remaining C symbols in the link set.
  • 2026-06-05 — P6hh (chkconf.c → standalone chkconf-rs binary) merged. The IRCnet config-file checker — the final C translation unit in P6 — ported to its own workspace crate. With this, P6 (Config & DNS) is COMPLETE.

    • Scope / classification. chkconf is a standalone validator binary (CHKCONF_OBJS in support/Makefile.in), NOT a msgtab command handler, so the command-cluster-port seam (drop the .o, L1 vs cref_) does not apply — it is a greenfield-style standalone port like iauth-rs, verified black-box by running both binaries and diffing output. It #includes config_read.c; under the locked config (M4_PREPROC/CONFIG_DIRECTIVE_INCLUDE/DEBUGMODE/XLINE/UNIXPORT/CLIENTS_CHANNEL/ENABLE_SIDTRACE OFF, TKLINE ON) the compiled surface is: main, new_class/get_class, dgets, getfield, initconf, validate, confchar, checkSID, openconf, showconf, and config_read.c's config_error (the CHKCONF_COMPILE non-CDI branch). The M4 (simulateM4Include/mystrinclude) and CDI (config_read/config_free/new_config_file/findConfLineNumber-CDI) bodies are #ifdef'd out and not ported.
    • Crate. New chkconf-rs workspace member (Cargo.toml [[bin]] chkconf), pure-std, NO ircd-sys dependency (per the PLAN: chkconf is never routed through ircd-sys). Constants resolved to the locked config and probed from a C harness: IRCDCONF_DELIMITER='|', SIDLEN=4, QUEUELEN=19120, SPLIT_SERVERS=10, SPLIT_USERS=5000, CONF_SERVER_MASK=0x38, CONF_CLIENT_MASK=0x20ba, IRCDCONF_PATH/IRCDCONF_DIR from the Makefile.
    • Single-file collapse. Under the locked config there is exactly one config file, so findConfLineNumber always returns the lone files node (min=0) → every per-line config_error location is the constant pair ("ircd.conf", physical line nr). The mywc/files/wordcount machinery produces NO output (DEBUGMODE off) and only resets its dgets buffer before initconf (which resets it itself), so it is collapsed away — output-equivalent and verified. config_read.c needs no separate Rust lib: the ircd binary already uses the P6f config_parse.rs, and chkconf-rs reimplements config_error inline.
    • Faithfulness notes (reproduced byte-for-byte). (1) The getfield escape/#-comment loop quirk where the switch translation of \n/\r/\t/\0 is immediately overwritten by the subsequent left-shift (net effect: backslash removed, next char kept literally; a trailing backslash truncates). (2) aconf->port is the ONE field C does NOT reset per line (the struct is reused; only host/passwd/name/class/clients/status/flags are cleared each iteration), so under -d9 a line with <5 fields prints the previous line's port — modelled as a carried port_reg register, zeroed after an append (the C aconf=NULL→re-malloc). (3) get_class always returns the same static &cls, so validate's bconf->class == aconf->class pointer compare is "both non-NULL" — modelled by class_set: bool. (4) mycmp uses libc toupper (ASCII fold), not a table. (5) ctop is prepend-order, so the unmatched-conf warnings print in reverse file order. (6) maxsendq is a file static carried across lines (debug-only read).
    • C UB excluded (documented). The reference dgets returns a buffer that is NOT NUL-terminated, so the -d/-s line echo reads uninitialised/stale stack in two cases: a line ≥511 bytes echoed under -d/-d9 (stack-frame-dependent; -s and non-debug happen to match), and a config file with no trailing final newline (last-line echo). Both are genuine UB and non-reproducible; all fixtures end with a newline, and long.conf is only run without an echoing flag. The Rust port implements the well-defined NUL-terminated behaviour, which matches C on every non-UB path.
    • L1. chkconf-rs/tests/differential.rs: builds the reference C oracle (make -C cbuild chkconf, links match.o+chkconf.o+-lcrypt) and the Rust binary (CARGO_BIN_EXE_chkconf), runs both over 13 committed fixtures (tests/fixtures/: empty, fully-valid + matching C/N pair, all conf letters incl. unknown + I/O-line flags + unmatched, SID validity matrix, M-line split-servers/users checks, wrong-delimiter/blank/comment/bare-letter, class-not-found + P-line slash/null-host, V-line slash warnings, unmatched servers + hub/leaf, backslash escapes + inline # + line continuation, CRLF, multiple M/A, overlong-line) × {none, -d, -d3, -d9, -s} (long.conf: none only) plus an argv0-masked -h test — asserting byte-identical stdout, stderr and exit status. Zero diff. cargo build -p chkconf-rs 0 warnings; clippy clean (faithful-port allows documented). No L2/S2S path (standalone CLI, no IsServer/remote-field/msgtab entry).
    • P6 status. With chkconf-rs landed, every P6 module — s_conf.c (+config_read.c), res.c+res_comp.c+res_init.c+res_mkquery.c, chkconf.c — is ported. P6 (Config & DNS) is COMPLETE. Remaining migration: P7 (I/O core + event loop + s_bsd.c/packet.c/bsd.c/fileio.c/ircd.c).