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

Configure Feed

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

ircd.rs / PLAN-P8-progress.md
33 kB

P8 — Delete the C + retire the oracle harness (per-cluster progress)#

One-line summary per cluster. Detailed entries: docs/progress-log/p8.md. P8 rips out the ~25 variadic sender trampolines (replacing each with a non-variadic Rust sender API + format! at the call sites), then drops all C compilation and mothballs the L1/L2 oracle. "100% Rust" lands here.

  • P8a DONE: sendto_iauth trampoline ripped out — the C sendto_iauth/vsendto_iauth variadic pair (s_auth.c:120-170) → a non-variadic pub unsafe fn sendto_iauth(&[u8]) in ircd-common/src/s_auth.rs; all 24 Rust call sites (s_auth/s_user/s_bsd/ircd/res) now format! the iauth line and pass its bytes. Guarded out of s_auth_link.o (-DPORT_S_AUTH_SENDTO_IAUTH_P8a); only s_auth.c data globals remain C. Gate: new L1 sendto_iauth_diff.rs (socketpair byte-diff vs cref_sendto_iauth, 2 tests) + the iauth L1 suite + golden_registration all green; cargo build 0 warnings.

  • P8b DONE: sendto_serv_butone + sendto_ops_butone trampolines ripped out — the two coupled server-broadcast senders (send.c:517/989). ops_butone was serv_butone's only in-send.c caller, so porting serv_butone forced porting ops_butone to close the C→C edge. Non-variadic Rust cores in ircd-common/src/send.rs + a wire!/WirePart raw-byte line builder (faithful: quit/kill comments pass through verbatim, no UTF-8 lossy step); 18 call sites (parse/s_misc/s_user/channel/s_serv) now build the body with wire!. Guarded out of send_link.o (-DPORT_SEND_SERV_BUTONE_P8b/-DPORT_SEND_OPS_BUTONE_P8b). Gate: new L1 sendto_serv_butone_diff.rs (4 cases: QUIT broadcast, >510 truncation, one-uplink exclusion, ops WALLOPS) + the existing golden_s2s_{away,quit,nick,kill,wallops, membership,join,save,s_serv_connect,s_serv_squit} all byte-identical; cargo build 0 warns.

  • P8c DONE: sendto_serv_v trampoline ripped out — the third variadic server-broadcast sender (send.c:541-572) and near-identical sibling of P8b's sendto_serv_butone: same fdas.highest→0 loop over local[fdas.fd[i]] (skip one's uplink + me, build the line once via vsendprep, send_message each). Its ver parameter gates on cptr->serv->version & ver but that gate is #if 0'd out, so ver is dead and it always returns rc = 0. No live C caller (every caller — ircd.c/s_serv.c/ s_user.c/channel.c — already ported + dropped), so ripped out alone. Non-variadic pub unsafe fn sendto_serv_v(one, _ver, body: &[u8]) -> c_int in ircd-common/src/send.rs (reuses prep_line); 10 call sites (ircd s_die; channel NJOIN tail; s_user KILL+SAVE; s_serv 4× EOB + SDIE relay + m_sdie) now build the body with wire!. Guarded out of send_link.o (-DPORT_SEND_SERV_V_P8c). Gate: new L1 sendto_serv_v_diff.rs (3 cases: EOB broadcast + rc==0, >510 truncation, one-uplink exclusion) + the existing golden_s2s_{save,join,membership,kill,s_serv_connect,s_serv_squit} all byte-identical; cargo build 0 warnings.

  • P8d DONE: sendto_match_servs + sendto_match_servs_v trampolines ripped out — the channel-mask server-broadcast pair (send.c:790-876). Both walk fdas.highest→0 over local[], skip cptr == from + me, and — when the channel name carries a trailing :server-mask (get_channelmask = rindex(name,':'), JAPANESE off) — also skip servers whose name doesn't match the mask; a leading-& channel returns at once. sendto_match_servs_v's ver gate is #if 0'd out (dead → always rc = 0), the same serv_butone/serv_v shape as P8b/c. Clean leaves (no in-send.c callers), so ripped out as a pair; all 12 call sites are in channel.rs (TOPIC/PART/JOIN/MODE+o + KICK/NJOIN/MODE), now built with wire!. Non-variadic Rust cores reuse prep_line. Guarded out of send_link.o (-DPORT_SEND_MATCH_SERVS_P8d). Gate: new L1 sendto_match_servs_diff.rs (5 cases: broadcast, &-early-return, mask filtering + inverse, from-exclusion, _v >510 trunc + rc==0) + the existing golden_s2s_{topic,kick,membership,mode,njoin,join} all byte-identical; cargo build 0 warnings.

  • P8e DONE: sendto_prefix_one trampoline ripped out — the first of the vsendpreprep-based prefix senders (send.c:1017-1027), a clean leaf with no in-send.c caller (internal paths use the static vsendto_prefix_one; only external — now-Rust — callers reach the public one). Introduces the prep_preprep Rust core: a faithful port of vsendpreprep (send.c:388-419) reproducing the special path — when to is MyClient and from is a Person, the :%s prefix arg par collapses to :from->name!user@host (if par == from->name) else stays :par; otherwise the line is emitted verbatim — then hands the body to the shared prep_line (≤510 + CRLF). The C from == &anon disjunct is unreachable from Rust callers (only the still-C channel senders substitute &anon), so it lands when those are ported. Every caller leads with :%s, so the C !strncmp(pattern, ":%s", 3) guard reduces to the to/from test. The static vsendto_prefix_one + vsendpreprep stay C (the channel senders still use them). Non-variadic pub unsafe fn sendto_prefix_one(to, from, par, rest: &[u8]); 10 call sites (s_numeric numeric relay; channel INVITE ×2; s_user PRIVMSG/NOTICE ×4 + KILL) build the post-:%s rest with wire!. Guarded out of send_link.o (-DPORT_SEND_PREFIX_ONE_P8e). Gate: new L1 sendto_prefix_one_diff.rs (4 cases: special-collapse, special-no-collapse, non-special server-to verbatim, collapse >510 truncation) + the existing golden_channel_invite, golden_s_user_message, golden_s2s_message, golden_s_user_kill, golden_s2s_kill all byte-identical; cargo build 0 warnings.

  • P8f DONE: sendto_match_butone trampoline ripped out — the $$/$#-mask broadcast prefix sender (send.c:935-987, "enhanced PRIVMSG"). A clean leaf: no in-send.c caller, one live call site (m_message in s_user.rs). Reuses the P8e prep_preprep/sendto_prefix_one core verbatim — its delivery is vsendto_prefix_one(cptr, from, …) with the real from (NEVER &anon), so no new anon machinery is needed (that lands with the channel senders). Non-variadic pub unsafe fn sendto_match_butone(one, from, mask, what, par, rest: &[u8]) in ircd-common/src/send.rs: walks i = 0..=highest_fd over local[i], skips NULL + the origin one; for a server recipient walks cptr->prev for the first registered dependent whose from == cptr and whose host/server matches the mask (the new match_it + is_registered_user helpers; MATCH_HOST=2 / MATCH_SERVER=1), for a non-server requires the client itself match; each survivor gets the P8e sendto_prefix_one. The single call site (:%s %s %s :%s → par = parv0, rest = <cmd> <nick> :<parv2> built with wire!) converted. Guarded out of send_link.o (-DPORT_SEND_MATCH_BUTONE_P8f). Gate: new L1 sendto_match_butone_diff.rs (4 cases: my-client MATCH_HOST match + inverse, server prev-walk MATCH_SERVER match + inverse, origin exclusion, unregistered-client skip) + the existing golden_s_user_message (7) / golden_s2s_message byte-identical; cargo build 0 warnings.

  • P8g DONE: sendto_channel_butone ripped out + the anon machinery ported to Rust — the channel broadcaster (send.c:459-510; JOIN/PART/KICK/MODE/TOPIC, the s_numeric channel-numeric relay, and channel PRIVMSG/NOTICE). It is the first anon-needing channel prefix sender, so this step also moves the anonymous-identity placeholder (anon/ausr/initanonymous, send.c:344-365) from C to Rust: anon/ausr become #[no_mangle] pub static mut globals and the still-C vsendpreprep / sendto_channel_butserv (send_link.o) reference them via extern decls that resolve to the Rust symbols. prep_preprep gains the C from == &anon disjunct (collapse to :anonymous!anonymous@anonymous.). The non-variadic Rust core routes BOTH the C raw (vsendprep → server recipients) and prefixed (vsendpreprep → MyClient recipients) paths through the P8e sendto_prefix_one/prep_preprep core (byte-identical: a server recipient lands in the else-path :par, a MyClient recipient collapses). 7 call sites (s_numeric ×1, channel ×5, s_user ×1) build the post-:%s rest with wire!. Guarded out of send_link.o (-DPORT_SEND_CHANNEL_BUTONE_P8g -DPORT_SEND_ANON_P8g). Deferred: sendto_channel_butserv (still-C variadic sendto_flag at send.c:1147 calls it — needs sendto_flag's 247 Rust call sites ported first) and sendto_common_channels (a clean leaf, no anon, its own step). Gate: new L1 sendto_channel_butone_diff.rs (4 cases: broadcast collapse/verbatim/IsMe-skip, one exclusion + inverse, self-send-not-doubled, anonymous-channel collapses to anon) + the existing channel goldens (join/part/kick/topic/mode_set/invite ×21) + s_user_message (7) + the S2S goldens (message/join/kick/topic/mode ×8) all byte-identical; 0 warnings.

  • P8h DONE: sendto_common_channels trampoline ripped out — the local-channel-peers broadcaster (send.c:620-729) used by NICK changes + QUIT so a client's peers see the event once. A clean leaf: no in-send.c caller; live callers are the now-Rust exit_client (s_misc) + the two NICK-change paths (s_user). No anon machinery — it explicitly skips anonymous channels (if IsAnonymous(...) continue), so from is always the real user. Non-variadic pub unsafe fn sendto_common_channels(user, par, rest: &[u8]) in ircd-common/src/send.rs. The faithful subtlety: C preps the line once (if (!len), DEBUGMODE off) into psendbuf and reuses it for every recipient, so the Rust port holds a local [u8;520] + len and calls prep_preprep only when len==0 — NOT sendto_prefix_one per recipient (which re-preps against each to); in the big-client branch a remote clist member (not MyClient) still receives the cached, FIRST-recipient-shaped line, which per-recipient prep would diverge on. Both size-gated branches ported (highest_fd<50 HUB + the big-client clist/sentalong path); sentalong becomes a per-call vec![0; MAXCONNECTIONS] indexed via raw .offset((*cptr).fd as isize) to match C's fd==-1 (remote) pointer arithmetic without a Vec bounds panic. New private is_member/is_quiet helpers (via the ported find_channel_link). 3 call sites converted (:%s QUIT :%s / :%s NICK :%s → par + wire! rest). Guarded out of send_link.o (-DPORT_SEND_COMMON_CHANNELS_P8h — the now-unused sentalong static guarded too). Gate: new L1 sendto_common_channels_diff.rs (4 cases: shared-channel broadcast + self-send + different-channel/server inverse, quiet-channel skip, anonymous-channel skip, remote-user no-self-send) + golden_s_user_nick (5) / golden_s_user_quit (4) / golden_s2s_nick / golden_s2s_quit (2) byte-identical; 0 warnings.

  • P8i DONE: sendto_flog logfile cluster (the first non-variadic leaf) — ripped the sendto_flog logger + its companion logfiles_open/logfiles_close + the file-private userlog/connlog fd statics out of send_link.o into ircd-common/src/send.rs; formatting via libc::sprintf with the exact C format string (byte-identical by construction); setup_svchans/svchans stay C (svchans shared with the still-C variadic sendto_flag); FNAME paths via new IRCD_USERLOG_PATH/IRCD_CONNLOG_PATH rustc-envs; -DPORT_SEND_FLOG_P8i. Gate: new L1 sendto_flog_diff.rs (write_pidfile pattern — diff rust-vs-cref logfile bytes, no-op identical in sandbox / byte-pinned where writable; 2 cases REG→userlog, non-REG→connlog) + golden boot smoke; no L2/S2S (file-only, USE_SERVICES off).

  • P8j DONE: sendto_flag (the keystone) + dead_link ripped out — the server-notice broadcaster (send.c:1141, ~220 call sites across 16 ircd-common files) → a non-variadic Rust core sendto_flag(chan, body: &[u8]) in ircd-common/src/send.rs that clamps chan, looks up the server-owned channel in the now-Rust svchans[] (its connected component — svchans + setup_svchans — ported too), and wraps :me NOTICE <chname> :<body> via the still-C variadic sendto_channel_butserv (called through the bindgen decl; channel_butserv's only C caller was sendto_flag, so it's now the next step). dead_link (send.c:50, the last C caller of sendto_flag) ported alongside (non-variadic; SetDead + DBufClear + the SCH_ERROR notice). Call sites pre-render the notice text with wire!; WirePart grew %u/%08x/%x/%X/%#x/%02d/ %2d/%c/%p helpers (integers via format! = byte-identical; %#x/%p via libc). All 203 literal-format call sites machine-verified byte-faithful vs the original C format strings (the one NULL %s renders the literal (null) glibc emits). Guarded out of send_link.o (-DPORT_SEND_FLAG_P8j -DPORT_SEND_DEAD_LINK_P8j). Gate: new L1 sendto_flag_diff.rs (4 cases: basic wrap, mixed %d/%s/%u, chan clamp, NULL-svc_ptr no-op — drives cref_sendto_flag via cref_setup_svchans + a hashed &NOTICES) + 16 golden binaries (registration, s_user nick/quit, channel join/part/kick/topic, s2s nick/quit/kill/join/eob/server/squit/njoin/ s_serv_connect) byte-identical; cargo build 0 warnings.

  • P8k DONE: sendto_channel_butserv ripped out — the channel local-members broadcaster (send.c:746-780; TOPIC/PART/KICK/JOIN/MODE relays to MyClient members + the anonymous PART + the now-Rust sendto_flag). Unblocked by P8j (its only in-send.c caller, sendto_flag, went Rust). A clean leaf like P8g minus the server broadcast: it only ever sends to MyClient recipients, so per-recipient sendto_prefix_one (P8e core) is byte-identical to the C psendbuf/if (!len) cache. Non-variadic pub unsafe fn sendto_channel_butserv(chptr, from, par, rest: &[u8]) in ircd-common/src/send.rs: MyClient(from) self-send (real prefix) then IsQuiet→return; IsAnonymous && IsClient(from)lfrm=&anon; loop chptr->clist delivering to each MyClient(acptr) && acptr != from via lfrm. 14 call sites converted to par+wire! rest (12 channel.rs, 1 s_misc anon-PART, the sendto_flag wrapper in send.rs); the local variadic extern decls in channel.rs/s_misc.rs replaced with use crate::send::sendto_channel_butserv. Its now-orphaned static helpers vsendpreprep + vsendto_prefix_one + the psendbuf buffer (last unguarded caller was channel_butserv) go with it under the same guard; vsendprep/sendbuf stay (live: vsendto_one). Guarded out of send_link.o (-DPORT_SEND_CHANNEL_BUTSERV_P8k). Gate: new L1 sendto_channel_butserv_diff.rs (4 cases: local members + server/remote inverse, quiet early-return, anonymous &anon prefix with real-prefix self-send, remote-from no-self-send — both worlds' anon initialized via initanonymous/cref_initanonymous) + sendto_flag_diff (4) still green + 14 golden binaries byte-identical (channel join/part/kick/topic/mode_set/modes + s2s join/topic/kick/mode/njoin); cargo build 0 warnings.

  • P8l DONE: sendto_one (the keystone single-client sender) ripped out — the C variadic sendto_one/vsendto_one/vsendprep (send.c:450-468) → a non-variadic Rust core send::sendto_one(to, body: &[u8]) (preps ≤510+CRLF via prep_line, delivers via send_message). All 435 call sites across 15 ircd-common files convert the line body: 58 literal-pattern sites via wire!, 377 replies[]/runtime-pattern sites via a new reply_one! macro (libc snprintf — the same printf engine the C trampoline used via vsprintf, so byte-identical, and the only faithful path for the ~6 runtime reply(var) sites — reply(num)/reply(cj)/reply(REPORT_ARRAY[idx][1]) — that wire! cannot bind). The 2 rlen += sendto_one(...) accumulator sites (channel.rs) call the core directly for its c_int return. vsendprep/vsendto_one/sendto_one + the orphaned sendbuf guarded out (-DPORT_SEND_ONE_P8l). Key finding: the plan's "blocked on leveva typed-numerics" note overstated it — only ~6 sites are genuinely-runtime reply(var); the ~430 reply(ERR_CONST)/literal sites have a compile-time-known format, and since vsendprep uses libc vsprintf (not the custom irc_vsprintf), reply_one!/snprintf is byte-identical for ALL sites. Gate: new L1 sendto_one_diff.rs (5 cases: wire! literal, reply_one! %s template, %d/%02d numerics, NULL %s(null), >510 trunc) + the full 111-test golden suite byte-identical (the only 2 failures are the pre-existing STATS garbage flakes — uninitialized per-process sq/Sq/Yg/Fl counters, not sendto_one output); cargo build 0 warnings. All variadic sender trampolines are now Rust — only the P1-deferred irc_sprintf format engine (deleted, not ported, in P11) remains C in send.

  • P8m DONE: the esendto_* cluster ripped out — the last variadic SENDER trampolines (ircd/s_send.c: the UID-aware service-routing esendto_one/serv_butone/channel_butone/ match_servs). USE_SERVICES off → ZERO callers, so no call-site conversion and no L2 path; ported faithfully to a non-variadic ircd-common/src/s_send.rs (the variadic fmt/... collapses to suffix: &[u8]) with all quirks (the maxplen+slen>512 suffix truncation, the UID-selection + newplen=-1 bail, the hardcoded :anonymous!anonymous@anonymous. local prefix, the &-channel return + match() mask filter). The :%s %s %s prefixes defer to libc::sprintf (byte-identical incl. glibc's (null) for a NULL oname). s_send.o dropped outright (PORTED, no s_send_link.o, no -DPORT_* guard; cref keeps the unguarded .o for L1). Gate: new L1 s_send_diff.rs (9 cases, branches + inverses) zero-diff + golden_registration byte-identical.

  • P8n DONE: the s_auth.c iauth data globals ripped out → s_auth.o dropped outright — the five file-scope iauth globals (iauth_options/iauth_spawn/iauth_version/iauth_conf/ iauth_stats, s_auth.c:102-112) were the LAST C symbols s_auth_link.o defined (all of s_auth.c's logic went Rust over P7u..P8a). Moved them to Rust-owned #[no_mangle] pub static mut statics in ircd-common/src/s_auth.rs (zero/NULL-init exactly as the C BSS defs); the iauth-timeout readers in ircd.rs/s_user.rs/s_bsd.rs reach iauth_options/iauth_spawn through their bindgen extern decls, which resolve to these defs at link (the data-symbol case of the function-symbol seam). s_auth.o added to PORTED (dropped outright — no more s_auth_link.o, the P7u..P8a -DPORT_S_AUTH_* chain gone); cref keeps the unguarded s_auth.o for cref_iauth_*/cref_report_iauth_*/cref_sendto_iauth. Gate: the existing iauth L1 suite (read_iauth_diff 10 — writes all 5 globals — + iauth_reporters_diff 6 + sendto_iauth_diff 2 + start_iauth_diff 3, all zero-diff vs cref) + golden_registration 2 byte-identical; nm confirms the 5 resolve as Rust BSS defs (none undefined); cargo build 0 warnings.

  • P8o DONE: send.o dropped outright — the second P8 data-global drop. Over P3d + P8b..P8l every send.c sender (plus dead_link, the logfile cluster, the svchans component, the anon machinery) was #ifdef'd out of send_link.o and reimplemented in ircd-common/src/send.rs, leaving send_link.o defining ONLY the module-private rcsid version string (an unreferenced static, internal linkage). nm --defined-only cbuild/send_link.o = rcsid and nothing else, so send.o is added to PORTED and dropped outright: the entire send_link.o second-compile step + the -DPORT_SEND_P3 .. -DPORT_SEND_ONE_P8l guard chain + the "send.o" => "send_link.o" link-map arm are deleted from ircd-sys/build.rs. The Rust logfiles_open still reads FNAME_USERLOG/FNAME_CONNLOG via the IRCD_USERLOG_PATH/IRCD_CONNLOG_PATH rustc-envs; cref keeps the unguarded send.o (native Makefile recipe, FNAME_* expanded) for the L1 send diffs. Gate: all 14 send L1 differentials (send_diff/sendto_one/sendto_flag/sendto_channel_butone/ sendto_channel_butserv/sendto_serv_butone/sendto_serv_v/sendto_match_servs/ sendto_match_butone/sendto_prefix_one/sendto_common_channels/sendto_flog/sendto_iauth/ s_send, 57 tests) zero-diff + golden_registration 2 byte-identical; nm target/debug/ircd shows logfiles_open/svchans resolving as Rust defs and no undefined send symbols; cargo build 0 warnings.

  • P8p DONE: s_bsd.o dropped outright — the third P8 data-global drop. Over P7d..P7ff every s_bsd.c function went Rust, leaving s_bsd_link.o defining ONLY its data globals (local[]/fdas/fdall/highest_fd/readcalls/udpfd/resfd/adfd/timeofday/mysk

    • the module-private rcsid). Those globals now live in ircd-common/src/s_bsd.rs as Rust-owned #[no_mangle] pub static mut statics with byte-exact C inits (udpfd/resfd/ adfd = -1.data; local=all-NULL, fdas/fdall=FdAry{fd:[0;50],highest:0}, highest_fd/readcalls=0, timeofday=0, mysk=MaybeUninit::zeroed → BSS). The data-symbol seam carries the rest: the other modules (send/parse/s_user/s_auth/s_service/ s_send + s_bsd.rs itself) reach them through their bindgen extern static mut decls, which resolve to these defs at link (proven for anon/ausr P8g, svchans P8j, the iauth globals P8n). s_bsd.o added to PORTED; the whole s_bsd_link.o second-compile (the long -DPORT_S_BSD_* chain) + the s_bsd.os_bsd_link.o link-map arm deleted from build.rs; cref keeps the unguarded s_bsd.o so cref_local/cref_highest_fd/cref_mysk/… survive for the L1 differentials. Two in-module clears: dropped the old local extern "C" { static mut mysk } decl, and renamed the read_message local var locallocal_p (it shadowed the new static). Gate (pure-data drop, per P8n/P8o): the existing s_bsd L1 differentials that exercise the globals end-to-end — read_message_diff/add_connection_diff/close_connection_diff/ close_listeners_diff/list_diff (add_fd/del_fd on fdas/fdall)/s_bsd_leaves_diff/ connect_server_diff/check_client_diff/setup_ping_diff/send_ping_diff/check_pings_diff/ get_my_name_diff (mysk)/init_sys_diff/daemonize_diff/start_iauth_diff/delayed_kills_diff/ calculate_preference_diff — all zero-diff + the cross-module wire seam (sendto_one/sendto_flag/ sendto_common_channels read local via the seam) + golden_registration byte-identical; nm target/debug/ircd shows all 10 resolving as Rust defs (B/D, none U); cargo build 0 warnings.
  • P8q DONE: ircd.o dropped outright — the fourth P8 data-global drop. Over P7c..P7oo every ircd.c function went Rust, leaving ircd_link.o defining ONLY its data globals (nm --defined-only ircd_link.o = no T symbols): me/client/istat/iconf/myargv/ rehashed/portnum/configfile/debuglevel/bootopt/serverbooting/firstrejoindone/ sbrk0/tunefile/dorehash/dorestart/restart_iauth/the next* timers/ListenerLL (+ the module-private rcsid). Those now live in ircd-common/src/ircd.rs as Rust-owned #[no_mangle] pub static mut statics with byte-exact C inits (me/istat/iconf zeroed via MaybeUninit::zeroed; client = addr_of_mut!(me); portnum/debuglevel = -1; serverbooting = 1; bootopt = BOOT_PROT|BOOT_STRICTPROT; the six =1 timers in .data, the rest BSS). The me self-pointer hazard is a non-issue at definition — the interior me.name → me.serv->namebuf pointers are set at runtime by the already-ported make_server/setup_me, so the static is just a zeroed aClient (the C BSS def). DATA-symbol seam carries the rest: the other modules reach the bindgen-surfaced globals through their ircd_sys::bindings::{me,client,…} extern static mut decls, which resolve to these defs at link (proven local/anon/svchans/iauth). The 5 non-bindgen flags/timers (dorehash/dorestart/restart_iauth/nextpreference/nextiarestart), formerly reached via two local extern "C" blocks in ircd.rs, are now module statics (the blocks removed, bare refs resolve directly); the three bindgen import-lists trimmed of the now-defined names to clear the clash. configfile/tunefile default to IRCDCONF_PATH/IRCDTUNE_PATH via new IRCD_CONF_PATH/IRCD_TUNE_PATH rustc-envs (re-exported ircd_sys::CONF_PATH_Z/TUNE_PATH_Z, NUL-terminated). ircd.o added to PORTED; the ircd_link.o second-compile (the long -DPORT_IRCD_* chain) + the ircd.oircd_link.o link-map arm deleted from build.rs; cref keeps the unguarded ircd.o so cref_me/cref_try_connections/cref_dorehash/… survive for the L1 differentials. Gate (pure-data drop, per P8n/o/p): the ircd L1 differentials that exercise the globals end-to-end — try_connections_diff/check_pings_diff/ calculate_preference_diff/delayed_kills_diff/ircd_signals_diff (writes dorehash/ dorestart/restart_iauth)/ircd_tune_diff/ircd_cli_helpers_diff/setup_signals_diff/ activate_delayed_listeners_diff — all zero-diff + golden_registration 2 byte-identical (boots the Rust ircd, which now owns me/the timers/configfile/tunefile); nm target/debug/ircd shows all ~26 resolving as Rust defs (B/D, none U) with the correct .data/.bss split; cargo build --workspace 0 warnings. Now only support_link.o (the format remnants dgets/make_isupport/snprintf_append/ipv6string/minus_one) keeps C logic; every other TU is Rust-or-data-only.

  • P8r DONE: support.o dropped outright — the LAST C-logic TUdgets + make_isupport ported to ircd-common/src/support.rs, the ipv6string ([c_char;46] BSS) + minus_one ([c_uchar;17] = 16×0xff+0, .data) data globals defined as #[no_mangle] pub static mut statics. The variadic snprintf_append is NOT ported: it has no live Rust caller (WHOX is built field-by-field in s_user.rs) so it died with the .o. support.o added to PORTED; the support_link.o second-compile (-DPORT_SUPPORT_P1 -DPORT_SUPPORT_P2) + the "support.o" => "support_link.o" link-map arm deleted from build.rs (link_objs() is now a plain set difference — no more *_link.o variants). cref keeps the unguarded support.o (13 syms) so cref_dgets/cref_make_isupport/… survive for L1. Every ircd C TU is now Rust — the product link set holds only the generated version.o + the L1 harness ctruth.o. Gate: new L1 differentials dgets_matches_reference (pipe-driven: \n/\r lines, odd-backslash continuation splice, even-backslash run, EOF tail, dgets(_,_,0) reset) + make_isupport_matches_reference (walk the char**) both zero-diff; full support_diff (7) zero-diff; golden_registration (2) byte-identical (005 ISUPPORT flows through make_isupport); s_conf_match_ipmask_diff/res_gethost_diff (read ipv6string/minus_one via the seam) zero-diff; nm target/debug/ircd shows dgets/make_isupport as T, ipv6string B, minus_one D (none U), snprintf_append absent; cargo build --workspace 0 warnings.

  • P8s DONE: all 115 L1 cref_ differentials migrated to self-contained ircd-common insta snapshots — the oracle-retirement test step. Each ircd-testkit/tests/<n>_diff.rs now has an ircd-common/tests/<n>_snap.rs that drives ONLY the Rust port (#[no_mangle] syms via link_anchor(); no cref_, no ircd_testkit). Dual-World diffs collapse to a single Rust world; raw pointers/timestamps/fd-numbers/ephemeral-ports become derived invariants (bools/offsets/byte-seqs); clock interposition + global Mutexes preserved. Sound via the capture chain: the matching differential is green at capture (Rust==cref), so snapshot==cref golden. insta added as a dev-dep. Built hand-proven (s_err/s_id) + 6-tier pilot + a throttled multi-agent workflow (guide: docs/superpowers/plans/2026-06-08-p8-test-migration-guide.md). Gate: 115 tests / 707 snapshots, 1:1 coverage, ircd-common suite stably green over repeated runs (determinism), cargo build --workspace 0 warnings. ircd-testkit/cref_ oracle left intact (still the proof oracle); mothballing it + dropping cc::Build is the remaining P8 work.

  • P8t DONE: version.c ported to Rust — the last generated C TU — its five data globals (generation/creation/pass_version/infotext/isupport) are now #[no_mangle] statics in ircd-common/src/version.rs via the data-symbol seam (the bindgen extern decls in s_serv/s_user/s_debug/ircd/s_bsd resolve to them at link). generation=CARGO_PKG_VERSION, creation=a UTC build-stamp captured in a new ircd-common/build.rs (user-directed: sourced from crate metadata at build time); pass_version=PATCHLEVEL + the infotext[] credits array copied verbatim. The version.c.SH generation step + the version.o compile/archive deleted from ircd-sys/build.rslibircd_c.a now holds only the L1 harness ctruth.o. creation is now build-time volatile so the L2 canonicalizer masks RPL_CREATED (003), matching the existing 371 Birth-Date rule. Gate: cargo build --workspace 0 warnings; nm shows all five syms as Rust defs (D/B, none U), version.o absent from the archive; ircd-common snapshot suite green (117 results); golden suite 58 pass + the one known pre-existing golden_s2s_s_serv_stats flake (reference-C uninitialized-sendq garbage, unrelated).

  • P8u DONE (FINAL — oracle retired, workspace 100% Rust): dropped all C compilation + mothballed the L1/L2 oracle — ran the differential suite a last time green (L1 ircd-testkit all pass; L2 ircd-golden 86 pass + only the documented s_serv_stats reference-C-garbage flake, Rust the correct side), then deleted from ircd-sys/build.rs the make object build / the cref_*/ctruth.o/res.o recompiles / the libircd_c.a archive / the whole-archive link + -lz/-lm/-lcrypt (none needed by Rust). build.rs now only runs configure + bindgen over the C headers + expands the install-path Makefile vars. ircd-testkit/ircd-golden excluded from the workspace (mothballed, git-recoverable); ctruth.c/layout.rs drift-net deleted. Gate: cargo build --workspace 0 warnings links pure Rust (binary boots, 0 cref_/ctruth syms, c_ircd_main a Rust T); cargo test --workspace 698 pass / 0 fail across 125 binaries. Every ircd C TU is gone from the build — P8 COMPLETE.

  • P8v DONE (C source tree DELETED, ircd-sys retired — workspace now has zero C files at all): P8u stopped one step short of literal C removal — it dropped all C compilation but kept ircd-sys, whose build.rs still ran configure + bindgen over the C headers to produce the struct/type view ircd-common builds against. P8v finishes the job: froze the generated bindings.rs (4953 lines, bindgen 0.72.1 over the locked-config headers) into a committed ircd-common/src/bindings.rs (pub mod bindings; + a self-alias extern crate self as ircd_sys; so the 28 ported modules' ircd_sys::bindings::* / ircd_sys::MOTD_PATH paths resolve unchanged to the in-crate module + frozen install-path consts — zero src churn). Then deleted the ircd-sys crate, the mothballed ircd-testkit/ircd-golden oracle crates, the iauth-rs/tests/differential_c.rs C-iauth differential (per-module unit tests remain the iauth gate), and the entire C source tree (common/ ircd/ iauth/ support/ contrib/ cbuild/ configure .clang-format* .github/workflows/clangformat.yml). ircd-rs now calls ircd_common::ircd::c_ircd_main directly (dropped its ircd-sys dep). Four clashing_extern_declarations surfaced once bindings + the modules' local forward-decls shared a crate (unregister_server/del_from_hostname_hash_table/del_from_ip_hash_table missing -> c_int; mystrdup decl'd *const vs the real *mut) — aligned the 4 decls to the real signatures (one line.as_ptr() as *mut c_char cast at the s_misc MOTD call site). Gate: cargo build --workspace 0 warnings links pure Rust; cargo test --workspace 695 pass / 0 fail. The repo now contains no C whatsoever. Next: P9 (std-library cleanup).