# Mechanically rewrite IRCnet ircd 2.11 from C to Rust (strangler fig)

## Context

`ircd.rs` is the IRCnet ircd 2.11 IRC daemon: ~51k LOC of C across `common/`, `ircd/`, and `iauth/`,
producing 5 binaries (`ircd`, `iauth`, `chkconf`, `ircdwatch`, `mkpasswd`). It is a single‑threaded
`select()/poll()` server with a table‑driven command dispatcher, a bundled BIND DNS resolver, pervasive
fixed‑size buffers, and ~230 `strcpy/sprintf`‑family calls — i.e. exactly the memory‑safety profile a
Rust rewrite is meant to eliminate. There are **no existing tests** (only a clang‑format CI check).

Goal: convert the codebase to Rust **module‑by‑module**, keeping the program building and running at
every step, with **every important part fully tested** against the original C as a reference oracle.

This plan reflects four locked decisions from the user:

1. **Faithful mechanical port now**, then **one dedicated idiomatic‑Rust pass at the very end** (after C is gone). No async/ownership redesign mid‑migration.
2. **Rust‑driven build from day one**: `fn main()` is Rust; remaining C compiles as a `*-sys` crate; we strangle _inward_.
3. **iauth rewritten first**, standalone greenfield Rust binary speaking the identical pipe protocol.
4. **Full 4‑layer testing** (differential unit, golden, fuzz, conformance) + a **permanent C reference build** in CI.

The leverage point: every `foo.c` already ships `foo_ext.h` (its public `extern` API) — **41 headers / ~417 symbols** that form a ready‑made per‑module C ABI contract. Dispatch is a function‑pointer table (`msgtab[]` in `common/parse.c`). These two facts make the strangler seam tractable.

---

## The migration model (the seam)

**Workspace.** A cargo workspace at repo root, coexisting with the untouched autoconf tree:

- `ircd-sys/` — `build.rs` runs `support/configure` to generate `setup.h`/`config.h`, then `cc::Build` compiles the C objects (mirroring `IRCD_COMMON_OBJS`+`IRCD_OBJS` from `support/Makefile.in:144‑149`) **minus** whatever has been ported, plus `bindgen` over a `wrapper.h` that `#include`s `common/struct_def.h` + all `*_ext.h`/`*_def.h`.
- `ircd-rs/` — the binary crate; `src/main.rs` owns the real `fn main()`. Ported modules live here / in leaf sub‑crates.
- `iauth-rs/` — standalone greenfield `iauth` binary (no `ircd-sys` dep).
- `ircd-testkit/` — the L1–L4 harness. `ircd-fuzz/` — `cargo-fuzz` targets.

**Runnable baseline (day one).** Compile the C `main` (`ircd/ircd.c:766`) as `c_ircd_main` via `cc::Build::define("main", Some("c_ircd_main"))`. Rust `main()` marshals argc/argv and calls it. The product is byte‑for‑byte the C ircd, just entered from Rust. (Also generate `version.c` from `ircd/version.c.SH.in` in `build.rs` and compile it — the C link needs `version.o`.)

**Strangling inward (per module).** To port `foo.c`: read `foo_ext.h` for the exact ABI; write Rust `#[no_mangle] pub extern "C" fn …` (and `#[no_mangle] pub static …` for exported data) matching the **exact** signatures (note: C uses mutable `char*`, not `const`); **remove `foo.c` from `build.rs`'s source list**. Because the `.c` is _dropped_ (not overridden), exactly one definition of each symbol exists at all times → no duplicate‑symbol/LTO games, program always links. Cross‑language LTO stays **off** during migration. CI builds + boots the server after every move.

**Single source of truth for compile defines.** `build.rs` must mirror `Makefile.in` **per‑object** defines (e.g. `-DFNAME_USERLOG=…`, `-DIRCDCONF_PATH=…`, `CLIENT_COMPILE`), and feed the **identical** `-include config.h/setup.h` + `-D` set to `cc::Build`, to `bindgen`, **and** to the reference‑archive build. A divergence here silently corrupts `aClient` layout. Default config to lock: `USE_IAUTH` on, `XLINE` off, `ZIP_LINKS` off, `AFINET=AF_INET6` (so `IN_ADDR` = libc `in6_addr`).

**Non‑ircd binaries are out of strangler scope.** `ircdwatch`, `chkconf`, `mkpasswd` and the `cl*.o` duplicate‑compiled common objects (`-DCLIENT_COMPILE`) stay in the **permanent C reference tree only**; they are never routed through `ircd-sys`. `chkconf` gets a Rust port opportunistically in P6.

---

## Keystone hazards & how we neutralize them (all verified in‑tree)

| Hazard                                 | Reality (verified)                                                                                                                                                                                   | Neutralization                                                                                                                                                                                                                                                                                                                                                                           |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **C‑variadic exports**                 | ~14 in `send_ext.h` (`sendto_one`, `sendto_flag`, …) + `irc_sprintf`; stable Rust _cannot define_ an `extern "C"` variadic fn. Each already has a `va_list` sibling (`vsendto_one`, `irc_vsprintf`). | Keep a **thin C trampoline** per variadic symbol (cc‑compiled): `va_start` → format via `irc_vsprintf` into a `BUFSIZE` buffer → call a **non‑variadic** Rust core `rs_sendto_one_str(to, msg)`. Rust never sees `va_list`. These ~25 trampolines persist through the faithful port and are deleted in **P8** (call sites switch to Rust `format!`). ⇒ "100% Rust" is a P8 goal, not P7. |
| **`aClient` interior self‑pointers**   | `make_client` sets `cptr->name = cptr->namebuf` (`list.c:121,226`); `me` is a by‑value global with `client = &me`.                                                                                   | **Invariant:** never bind/move/copy an `aClient` by value in Rust. All access through stable raw pointers; `name` set via `addr_of_mut!((*c).namebuf)`.                                                                                                                                                                                                                                  |
| **`CLIENT_REMOTE_SIZE` trick**         | Remote clients allocated `offsetof(aClient,count)` bytes; local‑only tail must never be touched when `from != self`.                                                                                 | Port `make_client` to branch on `offset_of!(aClient,count)` vs `size_of::<aClient>()`; build‑test asserts both equal the C `offsetof/sizeof`, **plus** `offset_of!` of every local‑tail field (`count,buffer,sendQ,recvQ,ip,hostp,…`).                                                                                                                                                   |
| **`repr(C)` layout drift**             | Layout gated by `USE_IAUTH/XLINE/ZIP_LINKS/INET6/TKLINE/…`.                                                                                                                                          | bindgen + cc share one define set (above); compile‑time `static_assert`s on size/offset; incomplete‑array externs (`local[]`, `tolowertab[]`) get element counts from **bindgen constants** (`MAXCONNECTIONS`) with a C `static_assert` that `sizeof(arr)/sizeof(arr[0])` matches the Rust length.                                                                                       |
| **Mutable dispatch table**             | `msgtab[]` entries are `struct Cmd{handler;count;rcount;bytes;rbytes}` **mutated every dispatch**, read by STATS.                                                                                    | Rust `static mut msgtab: [Message; …]` of the bindgen type, exact field order; handlers point at still‑C `m_*` until ported; L1 test asserts identical `count/bytes` after N dispatches.                                                                                                                                                                                                 |
| **Function‑like macros**               | bindgen drops `IsServer`, `MyConnect`, `MyFree`, `MyMalloc`, `DBufLength`, `SetXxx`, …                                                                                                               | Hand‑audited `macros.rs` of inline equivalents, each cross‑checked by an L1 test (C macro via tiny accessor vs Rust inline).                                                                                                                                                                                                                                                             |
| **Intrusive lists + manual refcounts** | `next/prev/hnext`, `Link` union, `anUser.refcnt`/`aServer.refcnt`.                                                                                                                                   | **Invariant during faithful port:** lists stay raw‑pointer intrusive lists — **no `Box`/`Drop`/Rust ownership**; refcnt inc/dec byte‑for‑byte as C. Enum‑`Link` and real ownership are **P8 only**.                                                                                                                                                                                      |
| **`find_*` ownership**                 | `find_client/uid/server/name/person` live in **`parse.c:144‑417`** (not hash.c), wrapping `hash_find_*`.                                                                                             | Port these with parse (**P4**); `hash_find_*` with hash (**P2**).                                                                                                                                                                                                                                                                                                                        |

---

## The 4‑layer test harness (built in P0, used every phase)

- **Permanent C reference build.** The pristine C tree stays in‑repo and buildable for the project's life; CI builds it every run as the oracle.
- **L1 — differential unit.** Build the reference `.c` into a **separate static archive** whose symbols are bulk‑renamed (`objcopy --redefine-syms` / `--prefix-symbols=cref_`, applied over the **full transitive** symbol set so internal callers follow the rename), exporting `cref_match`, `cref_MyMalloc`, … Link that archive beside the Rust crate in the test binary and assert Rust‑vs‑`cref_` identical outputs (and, for data structures, identical bytes/pointer‑walk order/counters). This is the workhorse for leaf/data/format tiers.
- **L2 — golden / characterization.** Run reference‑C ircd and the in‑progress Rust ircd side‑by‑side, diff wire output. Determinism is load‑bearing:
    - **Clock pinning:** `LD_PRELOAD` shim overriding `time()`/`gettimeofday()` to a fixed value in _both_ processes (the loop reads `time(NULL)` directly every iteration — a tune/faketime file is insufficient).
    - **Output canonicalizer:** mask known‑volatile token positions (WHOISIDLE idle/signon, STATSUPTIME, RPL_TIME/RPL_CREATED, fd‑derived ids). Gate = byte‑identical _after_ clock‑pin + canonicalization.
    - **Strict serialization:** drive scripted clients **one action at a time with barriers** — hash buckets prepend at head, so WHO/NAMES/LINKS/LIST order follows arrival order. No concurrent clients.
    - **S2S:** server‑burst diffing uses a **TS‑aware linking peer** (or link‑against‑reference‑C‑and‑observe), not canned replay.
- **L3 — differential fuzzing.** Feed identical bytes **through the real reassembly path** (`dbuf_getmsg` in `packet.c`, to hit 512‑byte truncation/framing) to C `parse()` vs Rust; diff the **full post‑state** — mutated `aClient` bytes, `ircstp` counters, and selected handler identity — not just `parv[]`.
- **L4 — conformance.** Rescoped: (1) extract the numeric→format table from `s_err.c` `replies[]` and assert the Rust port reproduces it (an L1 test); (2) curated scripted‑client assertions on numeric **code + arg arity/semantics** from `doc/whox.md`, `doc/whoistls.txt`, `rfc2812.txt`. (There is no machine‑readable RFC oracle; `replies[]` _is_ the truth.)

---

## Phases

> Each phase keeps the program building + L2/L4 green. "Drop" = remove the `.c` from `build.rs`.

| Phase                                                   | Modules                                                                                                                                                                    | Approach & gotchas                                                                                                                                                                                                                                                                                    | Test gate (exit)                                                                                                                                                                                                               |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **P0 — Scaffold + baseline + harness 🔶 PARTIAL**       | `ircd-sys` (cc+bindgen, `main`→`c_ircd_main`, `version.c.SH`), `ircd-rs/main.rs`, `ircd-testkit`, `ircd-fuzz`, frozen C reference build                                    | Establish the Rust‑driven build; lock the configure‑define single source of truth; emit layout `static_assert`s; stand up all 4 layers. **Done as P0a (baseline), P0b (bindgen + layout drift-net), P0c (L1 differential harness).** **Not yet built:** L2 golden (clock-pin + canonicalizer), L3 fuzz (`ircd-fuzz`), L4 conformance, `iauth-rs`.                                                                                                                                                               | `cargo build` boots an ircd that links iauth and **diffs byte‑identical (post‑canonicalization) vs reference‑C** across the full scripted suite; layout asserts pass. **(L1 + layout MET; L2/L4 gate pending those layers.)**                                                          |
| **P‑IAUTH — greenfield iauth (parallel, starts at P0)** | `iauth/*` → `iauth-rs`                                                                                                                                                     | Idiomatic Rust allowed (separate process, narrow text ABI). Reimplement event loop (mio), `cldata` fd‑slab, the `<id> <category> <info>` parser, the 8‑fn `aModule` vtable as a trait with 5 impls (`dnsbl/socks/webproxy/rfc931/pipe`); replace dlopen DSM with a static registry from `iauth.conf`. | Drive C‑iauth vs iauth‑rs with an **identical scripted `<id>` sequence** (ids are fixed in input, since `<id>`=ircd fd); byte‑identical replies per category + per‑module fixtures; end‑to‑end ircd+iauth‑rs golden identical. |
| **P1 — Leaf / pure utils ✅ DONE**                       | `common/match.c` (+`tolowertab/touppertab/char_atribs`), `dbuf.c`, **pure** subset of `support.c` (`mystrdup/strtoken/myctime/mybasename/inetntop/inetpton/irc_memcmp`). ~~`irc_sprintf.c`~~ → **deferred to P8** (variadic `va_arg` engine; stable Rust can't consume `va_list`, and it has no post-format core to hand a string to Rust → stays C, no trampoline). `snprintf_append`, `dgets`, `make_isupport`, `ipv6string` also stay C for now.                                                | Faithful; exact `char*` signatures; tables `#[no_mangle] static [u8;256]` (byte-equality asserted); `dbuf` owns `poolsize/freelist`. **`support.c` partial port** via inert `#ifndef PORT_SUPPORT_P1` guards + a `support_link.o` second-compile substituted into the link set; `MyMalloc/MyRealloc/MyFree` + `outofmemory()` **defer to P2**.                                                                | **MET:** L1 zero-diff over all corpora (match incl. tables + `MAX_ITERATIONS` cap; dbuf incl. `getmsg` framing + over-poolsize rollback; support incl. 8-case inet round-trip). Rust-driven `ircd` binary links; layout net green; 0 warnings. (L3 fuzz / L2/L4 not built yet — deferred from P0.)                                                                                                                                                                   |
| **P2 — Data & lookup ✅ DONE**                           | `patricia.c`, `class.c`, `whowas.c` (ring; `m_whowas` stays C), `hash.c` (6 tables; `m_hash` stays C), `list.c` (allocators, `make_client`, `svrtop/numclients`) + `MyMalloc`/`MyRealloc` + `outofmemory`            | Faithful; preserved intrusive pointers + refcounts (raw‑pointer invariant); replicated `CLIENT_REMOTE_SIZE` branch + hash **iteration order** exactly. **Verified findings:** `ENABLE_CIDR_LIMITS` ON (patricia live, `add_class` carries `cidrlen_s`); `USE_HOSTHASH`+`USE_IPHASH` ON (6 tables); **`MyFree` is a macro** (trio = `MyMalloc`+`MyRealloc`+`outofmemory`). `m_whowas`/`m_hash` kept in C via `whowas_link.o`/`hash_link.o` partial‑ports (`-DPORT_*_P2`).                                                                                                                                                  | **MET:** L1 zero‑diff over all corpora (patricia tree walk; class list + CIDR; whowas ring/index + uwas; hash client/channel/uid bucket chains + counters + stored hashv; `make_client` local/remote self‑pointers). Rust‑driven `ircd` binary links with `list.o` dropped; layout net green; 0 warnings. **whowas/class notification (`m_whowas`/`report_classes`) `sendto_*` path deferred to post‑P3 (L2).**                          |
| **P3 — Transport & format ✅ DONE**                     | `s_err.c` (`replies[]`), `s_id.c` (UID/SID/CID), `s_numeric.c` (`do_numeric`), `send.c` **delivery core** (`send_message`/`send_queued`/`flush_*`). **Finding:** the whole `sendto_*`/`esendto_*` family is irreducibly C trampolines (variadic; `vsendpreprep` consumes `va_arg` per‑recipient) → no non‑variadic cores extractable; senders + `sendto_flog`/`logfiles`/`setup_svchans` stay C (move to P7/P8). | Faithful; `replies[]` as Rust static (dumped from the oracle so config‑gated `#if` entries resolve); exact UID/SID base‑N; `send.c` partial‑ported via `send_link.o` (`-DPORT_SEND_P3`, `dead_link` exposed). ZIP/USE_SERVICES/LOG_SERVER_CHANNELS/etc. all OFF. | **MET (L1):** zero‑diff vs `cref_` on `replies[]` (all 1000), `ltoid`/`idtol`/`sid_valid`/`cid_ok`/`check_uid`, `do_numeric` guards, `send_message` sendQ buffering + `send_queued` early returns. **L2 (post‑P3):** `do_numeric`/sender wire broadcasts + `deliver_it` write path. |
| **P4 — Parser & dispatch ✅ DONE**                      | `common/parse.c` (tokenizer, `msgtab[]`, all 11 `find_*` wrappers, `getfield`, `m_nop`/`m_nopriv`/`m_unreg`/`m_reg`, `find_sender`/`cancel_clients`/`remove_unknown`)        | Faithful; `msgtab` as `#[no_mangle] static mut [Message; 66]` (config‑resolved: TKLINE on, SERVSET/KLINE/SIDTRACE off, MOTD→m_unreg; mutable counters); the ~60 real `m_*` handlers stay C and are referenced as fn pointers; **`parse.o` dropped outright** (every symbol ported — no `parse_link.o`). Rust **calls** the C variadic senders (`sendto_one`/`sendto_flag`/`sendto_serv_butone`) via bindgen decls (the s_numeric.rs pattern). Exact tokenize semantics (512 trunc, leading/trailing colon, empty trailing param, `MPAR` cap). **Deliverable:** per‑handler‑file static‑symbol cluster map (`specs/2026-06-02-p5-handler-cluster-map.md`).                            | **MET (L1):** zero‑diff vs `cref_` on the `msgtab[]` structure (cmd/min/max per row + row count), `getfield` (delimiter/escape/trailing/empty), `find_mask`/`find_name` fallbacks, and `parse()`'s no‑server paths (IsDead, empty, unknown‑cmd, numeric, ENCAP→m_nop dispatch — return code + `cptr.flags` + `ircstp` deltas). **Full `parse()` post‑state + hash‑populated `find_*` = L3/L2** (need the server fixture). |
| **P5 — Command handlers (bulk)**                        | `channel.c`, `s_user.c`+`s_auth.c`, `s_serv.c`+`s_service.c`+`s_zip.c` (zlib→flate2, **bit‑exact**), `s_misc.c` (`exit_client`), `s_debug.c`                               | Faithful, but **cluster‑based, not per‑function**: `channel.c` has 37 file‑statics (`set_mode`, `can_join`, `get_channel`, …); migrate each **connected component over shared statics** as a unit via the `msgtab` fn‑pointer seam.                                                                   | Per‑cluster L2 golden + L4 conformance byte‑identical incl. WHO/WHOIS/WHOX, full server‑burst/netsplit; zlib bytes match.                                                                                                      |
| **P6 — Config & DNS**                                   | `s_conf.c`+`config_read.c` (rehash; `conf/kconf/networkname`), `chkconf.c`, `res.c`+`res_comp.c`+`res_init.c`+`res_mkquery.c`                                              | Faithful config grammar + rehash. Resolver: faithful port **or** hickory‑dns behind `res_ext.h`, but golden runs use a **fixture nameserver** for determinism.                                                                                                                                        | Config‑parse + chkconf L1/L3 zero‑diff on fixtures; resolver encode/decode identical; L2/L4 identical with fixture DNS.                                                                                                        |
| **P7 — I/O core + event loop + glue (last)**            | `s_bsd.c` (select/poll loop, `local[]`, `highest_fd`, `timeofday`, `FdAry`), `fileio.c`, `packet.c`, `bsd.c`, `ircd.c` (main, io_loop, signals, rehash/restart, tune file) | Faithful: keep **single‑threaded select/poll via mio** (not tokio); preserve per‑client state machine; globals become Rust‑owned statics; signals via signal‑hook/nix + atomics. Removes `c_ircd_main`.                                                                                               | All C _logic_ gone (only variadic trampolines remain); full L2+L3+L4 + soak byte‑identical under connect storms, partial reads, sendQ backpressure, netsplit, rehash, restart.                                                 |
| **P8 — Idiomatic pass (after C is gone)**               | whole Rust tree                                                                                                                                                            | The single allowed redesign: async (tokio) _if desired_, owned state (slotmap/arena) replacing intrusive lists, `enum Link`, delete variadic trampolines (call sites → `format!`), remove `unsafe`, real error types. **Also: port `irc_sprintf`/`snprintf_append` (varargs) here as call sites adopt `format!`.**                                                                                 | Behavior parity maintained via the same L2/L4 suite against the now‑archived reference‑C.                                                                                                                                      |

---

## Progress log

- **2026‑06‑02 — P0a/P0b/P0c merged** (scaffold + Rust‑driven baseline; bindgen full ABI + layout drift‑net; L1 differential harness `ircd-testkit` with the `cref_*` symbol‑renamed oracle archive). See `docs/superpowers/plans/2026-06-01-p0{a,b,c}-*.md`.
- **2026‑06‑02 — P1 (Leaf / pure utils) merged.** New library crate **`ircd-common`** holds the ports (`#[no_mangle] pub extern "C"`, exact `*_ext.h` signatures); `ircd-rs` + `ircd-testkit` depend on it via a `link_anchor()`. Ported & dropped from the C link set: `match.c` (5 fns + 3 tables), `dbuf.c` (8 fns + `poolsize`/`freelist`), and a pure subset of `support.c` (`mystrdup`, `strtoken`, `myctime`, `mybasename`, `inetntop`, `inetpton`, `irc_memcmp`).
  - **Seam wiring (resolves P0c's deferred task):** `ircd-sys/build.rs` now splits `CREF_OBJS` (full set → feeds the `cref_*` oracle) from the link set (`CREF_OBJS \ PORTED`, with `support.o`→`support_link.o` substitution). `support.c` is partially ported via inert `#ifndef PORT_SUPPORT_P1` guards + a second compile, leaving the pristine reference build and the oracle untouched.
  - **Scope corrections vs. original plan:** `irc_sprintf.c` + `snprintf_append` moved to **P8** (variadic `va_arg`, unportable on stable Rust, no post‑format core); `dgets`/`make_isupport`/`ipv6string` left C (not pure leaves). `dbuf_copy` is dead/commented‑out C (not a compiled symbol) → not ported.
  - **Verification:** `cargo build` warning‑free; all L1 + layout tests green; `match`/`dbuf_*`/`mystrdup`/`irc_memcmp` resolve to Rust in `libircd_c.a` while `cref_*` remain in `libcref.a`. Spec: `docs/superpowers/specs/2026-06-02-p1-leaf-utils-design.md`; plan: `docs/superpowers/plans/2026-06-02-p1-leaf-utils.md`.
  - **Faithfulness notes (caught in review):** reproduced `match()`'s always‑active `MAX_ITERATIONS 512` cap; `dbuf_get` mirrors C's `&&` short‑circuit (avoids `dbuf_map`'s `tail` side effect); `dbuf` `poolsize` hard‑codes the compile‑time `(BUFFERPOOL>1500000)?…:1500000` value (correct for this config, pinned by an L1 test); `irc_memcmp` = unsigned byte compare (only the sign is observable); `istat` externed from C, not redefined.
- **2026‑06‑02 — P2 (Data & lookup) merged.** All five modules ported into `ircd-common` (`patricia.rs`, `class.rs`, `whowas.rs`, `hash.rs`, `list.rs`), each with an L1 differential test in `ircd-testkit`. Dropped from the link set: `patricia.o`, `class.o`, `list.o`; substituted: `whowas.o`→`whowas_link.o`, `hash.o`→`hash_link.o`, and `support.o`→`support_link.o` now also `-DPORT_SUPPORT_P2` (so `MyMalloc`/`MyRealloc` come from Rust). Spec: `docs/superpowers/specs/2026-06-02-p2-data-lookup-design.md`; plan: `docs/superpowers/plans/2026-06-02-p2-data-lookup.md`.
  - **Verified config findings that shaped the port:** `ENABLE_CIDR_LIMITS` is **ON** (config.h:738) → `patricia.c` is live, `add_class` carries the trailing `cidrlen_s`, `aClass` has the CIDR fields, `free_class` calls `patricia_destroy`. `USE_HOSTHASH`+`USE_IPHASH` **ON** → `hash.c` has 6 tables. **`MyFree` is a macro** (`sys_def.h:26`), not a symbol → the "trio" is really `MyMalloc`+`MyRealloc`+`outofmemory`; Rust ports call libc `free`.
  - **Partial‑port mechanism (extends P1's `support_link.o`):** `m_whowas` and `m_hash` are command handlers (P5) that format replies through the variadic `sendto_one`/`replies[]` path, so they stay C. The CREF oracle compiles the pristine unguarded `.o`; the link set compiles a `<mod>_link.o` with `-DPORT_<MOD>_P2` that `#ifdef`s out the ported fns + their owning globals and `extern`s the now‑Rust‑owned ones. To make the oracle export the needed `cref_*`, file‑static data (`was`/`locked` in whowas; the tables/counters/`hashtab` in hash) was given external linkage — behaviourally inert.
  - **Determinism preserved (pinned by L1):** bit‑exact hash fns (`hashtab[i]=tolower((char)i)*109`, nick `(<<4)+`, uid `(<<4)^`, channel start‑5/cap‑30/`+(i<<1)`, host/ip `31*+`), head‑prepend insertion, stored‑hashv‑then‑`mycmp` find; whowas ring index advance/wraparound + the `uwas` link storing the **index** not a pointer; `make_client` self/interior pointers + the `CLIENT_REMOTE_SIZE = offset_of!(Client,count)` branch; `make_user`/`make_server` refcnt=1 and `free_user`/`free_server` `-211000/-211001` recursion sentinels.
  - **Also fixed:** a pre‑existing P1 flake — the four `dbuf_diff` tests raced on the shared `poolsize`/`freelist` under parallel `cargo test`; now serialized through a process‑wide mutex.
  - **Verification:** `cargo build` warning‑free; the Rust‑driven `ircd` binary links with `list.o` dropped (all alloc/list logic now Rust); every `*_diff` L1 test zero‑diff vs `cref_*`; layout net green. Deferred to L2/post‑P3: the `m_whowas`/`report_classes` `sendto_*` wire output.
- **2026‑06‑02 — P3 (Transport & format) merged.** Ported into `ircd-common`: `s_err.rs` (the 1000‑entry `replies[]` numeric→format table), `s_id.rs` (UID/SID/CID), `s_numeric.rs` (`do_numeric`), `send.rs` (the non‑variadic delivery core `send_message`/`send_queued`/`flush_connections`/`flush_fdary`). Dropped from the link set: `s_err.o`, `s_id.o`, `s_numeric.o`; substituted: `send.o`→`send_link.o`. Spec: `docs/superpowers/specs/2026-06-02-p3-transport-format-design.md`; plan: `docs/superpowers/plans/2026-06-02-p3-transport-format.md`.
  - **Keystone finding (refines the hazard table):** the entire `sendto_*` (send.c) + `esendto_*` (s_send.c) family is **irreducibly C trampolines** — every one is C‑variadic and consumes `va_list` through a formatter (`vsendprep`/`vsendpreprep`/`build_suffix`), and `vsendpreprep` (send.c:384) consumes `va_arg` **per recipient**, so there is no "format once → Rust core" reduction. The Rust "cores" P3 actually extracts are the **delivery** fns, not the senders. The senders + `vsendto_one`/`sendto_flog`/`logfiles_*`/`setup_svchans` + the static buffers/`anon`/`svchans` stay C; they vanish in **P8** when call sites adopt `format!`. **s_send.c is therefore 100% C in P3 (nothing ported).**
  - **Verified config gates (all OFF):** `USE_SERVICES`, `LOG_SERVER_CHANNELS`, `USE_SYSLOG`, `LOG_OLDFORMAT`, `ZIP_LINKS`, `CLIENTS_CHANNEL`, `JAPANESE`, `POOLSIZE_LIMITED` → the corresponding branches of `send_message`/`send_queued`/`sendto_flog`/`setup_svchans` are omitted from the Rust port. `SCH_MAX`=14.
  - **`send_link.o` partial‑port (extends P1/P2):** `send.c` compiled twice — pristine `send.o` for the oracle; `send_link.o` with `-DPORT_SEND_P3` that `#ifndef`‑guards the 4 ported delivery fns out of the link set **and exposes `dead_link`** (drops `static`) so the Rust core can call that still‑variadic C fn. Built via `make --eval` so the recursive `-DFNAME_USERLOG/CONNLOG/SCH_PREFIX` path vars expand identically for the surviving C `logfiles_open`. Pristine `send.o` (`dead_link` static, `send_message` defined) is unchanged.
  - **Faithfulness notes (caught in review):** `replies[]` is **not** mechanically text‑extractable — it contains `#if/#else/#endif` config branches (WHOIS_SIGNON_TIME/USERS_RFC1459/ENABLE_SUMMON/TXT_NOSTATSK), two bare‑`NULL` entries, and an escaped `\r\n` (entry 384) that Rust source‑level CRLF normalization would silently corrupt → it is **dumped from the compiled `cref_replies` oracle** with byte‑level escaping. `alphabet_id[256]` in s_id.c is partially initialized (249 explicit values) so indices 249..=255 default to **0** (reproduced verbatim). `init_sid` is `#if 0` (not ported). `poolsize *= 1.1` reproduced as `(poolsize as f64 * 1.1) as u_int`.
  - **L1 coverage / L2 deferrals:** L1 zero‑diff vs `cref_` on the `replies[]` table (all 1000 slots), `ltoid`/`idtol` round‑trip + `sid_valid`/`cid_ok`/`check_uid`, `do_numeric`'s early‑out guards, and `send_message` sendQ buffering (bytes + `sendM` + `lastsq`) + `send_queued`'s dead/empty early returns. **Deferred to L2** (need full server state — initialized hash tables, registered `me`, `cref_`'s separate globals, a live fd): the `do_numeric` broadcast wire output and the `send_queued` `deliver_it` write path.
  - **Verification:** `cargo build` warning‑free; Rust‑driven `ircd` links (`send_link.o`'s variadic senders call the Rust `send_message`; the Rust core calls C `dead_link`/`deliver_it`); all `*_diff` L1 tests zero‑diff; layout net green.
- **2026‑06‑02 — P4 (Parser & dispatch) merged.** All of `common/parse.c` ported into `ircd-common/src/parse.rs` and **`parse.o` dropped outright** (added to `PORTED`, like `list.o`/`match.o`) — no `parse_link.o`, because **every** symbol parse.c defined is portable now: the `msgtab[]` table, all 11 `find_*` wrappers, `parse`, `getfield`, the four stub handlers (`m_nop`/`m_nopriv`/`m_unreg`/`m_reg`), and the three file‑statics (`find_sender`/`cancel_clients`/`remove_unknown`). Spec: `docs/superpowers/specs/2026-06-02-p4-parse-dispatch-design.md`; plan: `docs/superpowers/plans/2026-06-02-p4-parse-dispatch.md`.
  - **Refines the hazard table (variadic dispatch):** the ~14 `sendto_*` senders parse.c **calls** stay C, but Rust **calls** them through bindgen variadic declarations (`sendto_one(to, c"…".as_ptr(), …)`) — the exact s_numeric.rs pattern. *Calling* a C variadic is stable Rust; only *defining* one is not. So no trampoline is needed on the call side; the senders themselves still vanish in P8.
  - **`msgtab[]` as a Rust `#[no_mangle] static mut [Message; 66]`:** built with a `row!`/`m!` macro; handler fn‑pointers reference the still‑C `m_*` (and the four Rust stubs) and coerce in the `static` initializer; the mutable `count`/`rcount`/`bytes`/`rbytes` counters mutate in place exactly as C (read by STATS in still‑C `s_serv.o`). **Config‑resolved** for this build: `TKLINE` **on** (TKLINE+UNTKLINE rows), `USE_SERVICES`/`KLINE`/`ENABLE_SIDTRACE` **off** (no SERVSET/KLINE/SIDTRACE rows), `MOTD_UNREG` **off** (MOTD unreg slot = `m_unreg`), `IDLE_FROM_MSG` **on** (idle guard = `fhandler == m_private`), `DEBUGMODE` **off** (every `Debug(())` omitted). The msgtab L1 test compares cmd/min/max/row‑count, **not** handler pointers (Rust rows point at `m_join`, the oracle at `cref_m_join`).
  - **Bug caught by L1:** `IRCDCONF_DELIMITER` is `'|'` (config.h:500), **not** `':'` — the first `getfield` draft used `':'` and the differential failed immediately. (find_mask/find_name L1 also surfaced that the hash tables are null until `inithashtables()`; the test now inits both sets.)
  - **L1 coverage / L2‑L3 deferrals:** L1 zero‑diff vs `cref_` on the `msgtab[]` structure, `getfield` (delimiter/escape/trailing/empty), `find_mask`/`find_name` empty‑hash fallbacks, and `parse()`'s no‑`me`/no‑hash/no‑sender paths (IsDead early‑out, empty message, unknown‑command‑from‑non‑person, 3‑digit numeric, and an `ENCAP→m_nop` dispatch — asserting return code + `cptr.flags` + `ircstp` counter deltas). **Deferred to L3 (parser fuzz) / L2:** the full `parse()` post‑state differential (dispatch into the still‑C `m_*`, mutated `aClient` + handler counters) and hash‑populated `find_*` — both need the populated‑server fixture.
  - **Deliverable:** the per‑handler‑file static‑symbol **cluster map** for P5 (`docs/superpowers/specs/2026-06-02-p5-handler-cluster-map.md`): clusters + entry points + suggested porting order (s_zip/s_debug → s_auth/s_service → s_user → s_misc → channel.c → s_serv.c).
  - **Verification:** `cargo build` warning‑free; the Rust‑driven `ircd` binary links with `parse.o` dropped (`parse`/`msgtab`/`find_*`/`getfield`/`m_nop` resolve to Rust); full `cargo test` green (all prior `*_diff` + `parse_diff` 4/4 + layout); 0 warnings.

---

## Key decisions & open risks

- **"100% Rust" is P8, not P7.** ~25 trivial C variadic trampolines persist through the faithful port (stable Rust can't define variadic `extern "C"`). They vanish in P8 when call sites adopt `format!`. (Alternative: nightly `c_variadic` — not recommended; pins the toolchain.)
- **Determinism is the project's spine.** Hash/list iteration order, clocks, UID/SID sequence, DNS, and fd ordering must all be pinned, or L2 becomes noisy and stops catching regressions. A "faithful" port that changes iteration order passes L1 but fails L2 — by design.
- **zlib bit‑exactness** (`ZIP_LINKS` server links): flate2 must match compression level/strategy/flush points, or P5 golden fails. Validate early in P5.
- **Resolver swap risk:** hickory‑dns caching/timeout drift is visible via WHOIS/WHO hostnames; keep a faithful `res*.c` port as fallback if golden diffs appear (P6).
- **crypt() legacy:** keep libc `crypt` for oper‑password dual‑validation; divergence locks out opers; availability varies by platform.
- **Reference‑build longevity:** the pristine C tree must keep compiling as the Rust product diverges; OS/toolchain drift is a maintenance cost (pin a container image).

---

## Verification (end‑to‑end)

1. **Per module:** `cargo test -p <crate>` runs L1 differential (Rust vs `cref_…`) to zero diffs; `cargo fuzz run <target>` for parser/format/config tiers.
2. **Per phase:** `ircd-testkit` boots reference‑C + Rust ircd under the `LD_PRELOAD` clock shim, replays the scripted serialized session suite (registration, JOIN/PRIVMSG/MODE/KICK/TOPIC, WHO/WHOIS/WHOX, OPER/STATS, server link burst, netsplit, rehash, restart), and asserts canonicalized wire output is byte‑identical. L4 conformance assertions run in the same harness.
3. **iauth:** `iauth-rs` vs C‑iauth replay with fixed `<id>` scripts + per‑module fixtures; then end‑to‑end ircd+iauth‑rs golden.
4. **CI (every commit):** build reference‑C; build Rust workspace; boot the server; run L1 (all crates), L2 golden, L4 conformance; nightly L3 fuzz + soak. Green CI after a module's `.c` is dropped is the definition of "that module is migrated and fully tested."
5. **Final (P7 exit):** `ircd-sys` compiles no C logic; the entire L1–L4 + soak suite is byte‑identical to the archived reference‑C across the full scenario matrix.
