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.

feat(p7ii): port delayed_kills io_loop K-line sweep from ircd.c

Port the second io_loop timer helper (sibling of P7hh check_pings,
ircd.c:447) to ircd-common/src/ircd.rs. De-static + guard the C body
under -DPORT_IRCD_DELAYED_KILLS_P7ii on ircd_link.o so the cref archive
exports cref_delayed_kills; #else extern proto for the still-C io_loop
caller.

Config-resolved: MAXDELAYEDKILLS=200 > MAXCONNECTIONS=50 -> the batch
clamp j always reaches 0, so the sweep completes in one call (persistent
statics reset every call; the return-rehashed incomplete-batch path is
unreachable). TIMEDKLINES off -> find_kill timedklines arg is 0.

The kflag==-1 kill arm (exit_client) is destructive and reached only
with both a matching K-line conf and rehashed>0, which the golden suite
never produces -> L1 is the gate, no L2 path. golden_registration stays
byte-identical.

+207 -9
+1
PLAN-P7-progress.md
··· 38 38 - **P7ff DONE: `s_bsd.c` event-loop cluster `read_message` + 8 statics** (`completed_connection`/`read_listener`/`read_packet`/`client_packet`/`ircd_no_fakelag`/`do_dns_async`/`polludp`/`check_ping`) — the body of `io_loop` and the last *logic* in `s_bsd.c`: build the read/write fd_set over `fdp`+`udpfd`/`resfd`/`adfd` → `select()` → dispatch resolver/UDP/iauth reads + auth-fd I/O + listener `accept()`s + writable sendQ flushes + readable packets + dead-socket `exit_client`. `USE_POLL` off → the `select()` path; the 8 statics → module-private Rust twins (no `cref_` oracle); guard `-DPORT_S_BSD_READ_MESSAGE_P7ff`. **With `read_message` ported, `s_bsd_link.o` has zero functions left — only the data globals.** L1 = the differentially-safe empty-loop path (`read_message_diff.rs`, 2 cases zero-diff); L2 = the *entire* `golden_*`+`golden_s2s_*` suite byte-identical (every byte flows through it; only the known `golden_s2s_s_serv_stats` reference-C uninitialised-sendq flake fails — [[p5-s2s-stats-flake]]); destructive accept/read/write/exit + `polludp`/`do_dns_async` are L2-only by design. 39 39 - **P7gg DONE: `ircd.c` `try_connections` (auto-connect timer)** — ported the static io_loop AC timer (walks `conf`, picks the best AC-able `connect{}` C-line by lowest-hold/best-`pref`/highest-class, splices the winner to the list tail, penalises its `hold`, then defers or dials via `connect_server`); de-static'd (mysk precedent) so the cref archive exports `cref_try_connections`, guarded `-DPORT_IRCD_TRY_CONNECTIONS_P7gg` in `ircd_link.o` with an `#else extern` proto for the still-C io_loop caller. `DISABLE_DOUBLE_CONNECTS` undef → dup-IP scan dead. L1 8-case zero-diff (all non-`connect_server` paths incl. the tail-splice + deferred-notice via `iconf.aconnect=0`); the dial path is L2-only (golden dials *into* the ircd — P7cc precedent); no S2S. 40 40 - **P7hh DONE: `ircd.c` `check_pings` (io_loop ping/timeout sweep)** — ported the static io_loop timer helper (ircd.c:525): walks `local[highest_fd..0]`, pings idle registered links (`FLAGS_PINGSENT` state machine), times out unresponsive ones (`exit_client`; servers get a "No response … closing link" notice; DNS/auth waiters reset or handed to iauth via `sendto_iauth`), and folds the soonest next-check time into the return value. De-static'd under `-DPORT_IRCD_CHECK_PINGS_P7hh` (the try_connections P7gg precedent) on the `ircd_link.o` recipe; `#else extern` proto for the still-C io_loop caller. Config-resolved: TIMEDKLINES off → `kflag` permanently 0 (find_kill/kill-notice + static `lkill` gone, timeout arm always "Ping timeout"); USE_IAUTH on; DEBUGMODE off. C `goto ping_timeout` modelled by guarding the if/else on the negated recently-active condition; `bysptr` kept outside the loop (never reset). L1 (`check_pings_diff.rs`, 7 cases) zero-diff vs `cref_check_pings` over isolated worlds (real `local`/`highest_fd`/`me` vs `cref_*`), serialized on `GLOBALS_LOCK`: empty, listener-skip, recently-active-no-ping, the PINGSENT ping-send branch (its inverse), young-unregistered, already-pinged-idle-below-2*ping, mixed multi-fd sweep — asserts per-client (flags,lasttime)+return. The `exit_client` timeout paths are L2-gated (destructive+variadic, the read_message/P7ff precedent). L2 = the ircd-golden suite runs check_pings every io_loop tick; `golden_registration`+`golden_channel_join` byte-identical, only the documented `stats_t` reference-C garbage flake remains. No S2S (no command-driven remote-field formatting). 41 + - **P7ii DONE: `ircd.c` `delayed_kills` (io_loop K-line sweep)** — ported the second io_loop timer helper (sibling of P7hh `check_pings`, ircd.c:447): runs only while `rehashed > 0`, walks `local[dk_lastfd..j]` and `find_kill`s every local person, `exit_client`ing K-line matches. De-static'd under `-DPORT_IRCD_DELAYED_KILLS_P7ii` on `ircd_link.o` (the check_pings precedent; `#else extern` proto for the still-C io_loop caller). Config-resolved: MAXDELAYEDKILLS=200 > MAXCONNECTIONS=50 → `j` always clamps to 0 → the sweep always completes in one call (persistent statics reset every call; the `return rehashed` incomplete-batch path unreachable); TIMEDKLINES off → find_kill `timedklines`=0. L1 (`delayed_kills_diff.rs`, 6 cases) zero-diff vs `cref_delayed_kills` over isolated worlds (real `local`/`highest_fd`/`rehashed` vs `cref_*` + empty kconf → no kill), serialized on `GLOBALS_LOCK`: empty across rehashed∈{0,1,2}→return{0,0,1}, person-no-kline completes & survives (the inverse of the kill arm), non-person/NULL skipped, mixed sweep — asserts return + per-client (status,flags,still-in-local). The `kflag==-1` kill arm (`exit_client`, destructive) is untested-by-design AND has no L2 path (golden never rehashes → delayed_kills' body isn't reached); `golden_registration` byte-identical (no-regression). No S2S (no remote-user wire fields).
+1 -1
PLAN.md
··· 84 84 | **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). | 85 85 | **P5 — Command handlers (bulk) ✅ DONE (all `m_*` handlers; `s_auth.c` socket/ident/iauth‑pipe I/O → P7)** | `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`. <br>**Per‑cluster sub‑phase progress log (P5a–P5whowas, 56 clusters): see [`PLAN-P5-progress.md`](PLAN-P5-progress.md).** **`channel.c` (P5hh), `s_serv.c` (P5bbb) and `s_user.c` (P5ccc) are now FULLY ported → all three `.o`s dropped.** The P2‑deferred `m_whowas` handler is also ported (P5whowas) → **`whowas.o` dropped, whowas.c fully Rust**; the P2‑deferred `m_hash` (HAZH) handler is ported (P5hash) → **`hash.o` dropped, the `hash_link.o` partial‑compile gone**. Of the P5 TUs, only `s_auth.c` remains C (no `m_*` handlers; pure socket/ident/iauth‑pipe I/O → deferred to P7/P‑IAUTH). | 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. **s_zip.o is empty (ZIP_LINKS off) → drop for free, no port.** | Per‑cluster L2 golden + L4 conformance byte‑identical incl. WHO/WHOIS/WHOX, full server‑burst/netsplit; zlib bytes match. | 86 86 | **P6 — Config & DNS ✅ DONE** | `s_conf.c`+`config_read.c`, `chkconf.c`, `res.c`+`res_comp.c`+`res_init.c`+`res_mkquery.c`. <br>**Per‑cluster sub‑phase progress log (P6a–P6hh): see [`PLAN-P6-progress.md`](PLAN-P6-progress.md)** (one‑line summaries) and [`docs/progress-log/p6.md`](docs/progress-log/p6.md) (full entries). **Outcome:** `s_conf.c` (config grammar + `rehash` + the `conf`/`kconf`/`networkname` globals) and the whole resolver (`res.c` + the `res_comp.c`/`res_init.c`/`res_mkquery.c` leaves) are now FULLY Rust → all `.o`s dropped; **every `msgtab` handler is Rust**; `chkconf.c` is a standalone `chkconf-rs` binary. → **P6 COMPLETE.** | 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. | 87 - | **P7 — I/O core + event loop + glue (last) 🔶 STARTED** | `s_bsd.c` (select/poll loop, `local[]`, `highest_fd`, `timeofday`, `FdAry`), `packet.c`, `bsd.c`, `ircd.c` (main, io_loop, signals, rehash/restart, tune file), `s_auth.c` (socket/ident/iauth‑pipe I/O, deferred from P5). <br>**Per‑cluster sub‑phase progress log (P7a–P7hh): see [`PLAN-P7-progress.md`](PLAN-P7-progress.md)** (one‑line summaries) and [`docs/progress-log/p7.md`](docs/progress-log/p7.md) (full entries). Ported bottom‑up (leaf‑first), as in P6. **Done:** `bsd.c` (`deliver_it`) + `packet.c` (`dopacket`) dropped outright; `s_bsd.c` fully drained of *logic* (P7d–P7ff: socket/file leaves → fd/connection teardown → listeners → `add_connection`/`connect_server` → `start_iauth`/`daemonize` → the `read_message` select/poll event loop) — **only its data globals remain**; `s_auth.c` ident/iauth‑pipe I/O ported (P7u–P7y) — **only the variadic `sendto_iauth`/`vsendto_iauth` remain**; `ircd.c` partial (P7c/P7t/P7gg/P7hh: `ircd_writetune`/`ircd_readtune`/`calculate_preference`/`try_connections`/`check_pings` via `ircd_link.o`). **Remaining C *logic*:** `ircd.c` `c_ircd_main`/`io_loop`/signals/`server_reboot` + the `delayed_kills`/`setup_me`/`setup_signals` helpers. | 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. | 87 + | **P7 — I/O core + event loop + glue (last) 🔶 STARTED** | `s_bsd.c` (select/poll loop, `local[]`, `highest_fd`, `timeofday`, `FdAry`), `packet.c`, `bsd.c`, `ircd.c` (main, io_loop, signals, rehash/restart, tune file), `s_auth.c` (socket/ident/iauth‑pipe I/O, deferred from P5). <br>**Per‑cluster sub‑phase progress log (P7a–P7ii): see [`PLAN-P7-progress.md`](PLAN-P7-progress.md)** (one‑line summaries) and [`docs/progress-log/p7.md`](docs/progress-log/p7.md) (full entries). Ported bottom‑up (leaf‑first), as in P6. **Done:** `bsd.c` (`deliver_it`) + `packet.c` (`dopacket`) dropped outright; `s_bsd.c` fully drained of *logic* (P7d–P7ff: socket/file leaves → fd/connection teardown → listeners → `add_connection`/`connect_server` → `start_iauth`/`daemonize` → the `read_message` select/poll event loop) — **only its data globals remain**; `s_auth.c` ident/iauth‑pipe I/O ported (P7u–P7y) — **only the variadic `sendto_iauth`/`vsendto_iauth` remain**; `ircd.c` partial (P7c/P7t/P7gg/P7hh/P7ii: `ircd_writetune`/`ircd_readtune`/`calculate_preference`/`try_connections`/`check_pings`/`delayed_kills` via `ircd_link.o`). **Remaining C *logic*:** `ircd.c` `c_ircd_main`/`io_loop`/signals/`server_reboot` + the `setup_me`/`setup_signals` helpers. | 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. | 88 88 | **P8 — Delete the C + retire the oracle harness** | remaining C (the ~25 variadic sender trampolines) + `ircd-sys` C build + `ircd-golden` (L2) + `ircd-testkit`/`cref_*` archives (L1) + the frozen reference‑C tree | With all C _logic_ already Rust (P7 exit), delete the last C: replace the variadic sender trampolines (`sendto_one`/`sendto_flag`/`sendto_serv_butone`/…) with a non‑variadic Rust sender API at every call site, then drop **all** C compilation from `ircd-sys` (no more `cc::Build`; bindgen kept only if still needed for libc structs, else removed). Retire the differential harness — once the oracle is gone, `cref_*` archives, L1 `ircd-testkit`, and the `ircd-golden` L2 binary can no longer be built. **Migrate the load‑bearing tests into `ircd-common`** as ordinary self‑contained Rust unit/integration tests (own fixtures, no `cref_` oracle). | **Run the full L1/L2/L4 suite byte‑identical vs reference‑C one last time, _then_ mothball the oracle.** After: `cargo build` is a 100% Rust workspace (zero C TUs); the tests ported into `ircd-common` are green. | 89 89 | **P9 — std‑library cleanup** | whole Rust tree | Behavior‑preserving replacement of the C‑isms the mechanical port kept: `MyMalloc`/`MyRealloc`/`MyFree`/`outofmemory()` → `Box`/`Vec`/`alloc`; the `dbuf` freelist + client/link free pools → idiomatic owned containers where safe; raw `libc` syscalls → `std`/`nix`; `[c_char; N]` scratch buffers → `[u8; N]`/`String` where lifetimes allow. Do **not** yet touch intrusive lists/refcounts/enum `Link` (deferred to P12) — keep iteration order identical. | `ircd-common` test suite green; no `My*` allocator symbols remain; `cargo clippy` clean. | 90 90 | **P10 — Rename to `leveva` + typed identifier string types** | the workspace (crate/binary rename) + new string‑type module in `ircd-common` | **Phase 10 is where the project starts being called `leveva`** — the idiomatic‑Rust end state ships under that name. Rename the crates/binaries/workspace from the `ircd*` mechanical‑port names to `leveva` (update `Cargo.toml` package names, the binary artifact, README/docs), then define validating newtype strings, one per IRC identifier class: `Nick` (RFC2812 nick chars + length), `ChanName` (`#&+!` prefix + channel chars), `Uid`/`Sid`/`Cid` (the base‑N ID alphabets from `s_id.rs`), `ServerName` (hostname grammar), plus `UserName`/`HostName` as needed. Each validates on construction (`TryFrom<&str>`/`FromStr`), rejecting illegal characters. Add a `format!`‑adjacent macro (e.g. `nick!("…")`) that const‑validates a checked literal at compile time — the typed analogue of `format!`. | Unit tests: each type round‑trips every legal input and rejects each illegal‑char class; the macro fails to compile on an invalid literal (`trybuild`). |
+9
docs/progress-log/p7.md
··· 613 613 - **Classification.** `io_loop` callee (no `msgtab` entry), reached only from the periodic ping recompute. The only remote read is `bysptr` (a remote oper) for a NOTICE on server timeout — a server-link path exercised by the L2/S2S loop, not a client command. → **L2-gated**, with an L1 over the deterministic non-destructive paths. 614 614 - **L1** — `check_pings_diff.rs`, 7-case zero-diff vs `cref_check_pings` over two isolated worlds (Rust `local`/`highest_fd`/`me` vs the `cref_` copies), each building its own parallel client set, serialized on `GLOBALS_LOCK` (the documented P7 shared-global hazard). Covers every path that does NOT reach `exit_client` or the server-timeout `find_uid`/`sendto_one`: empty `local[]` (oldest stays 0 → `now+PINGFREQUENCY`), listener-skip (`FLAGS_LISTEN`), recently-active registered (`ping >= now-lasttime` → goto, no ping, flags untouched), the `FLAGS_PINGSENT` ping-send branch (idle 121 > ping 120, below 2*ping → sets `FLAGS_PINGSENT` + rewrites `lasttime = now-ping` + sends PING — the inverse of recently-active), young unregistered (firsttime < ACCEPTTIMEOUT, left alone), already-pinged idle below 2*ping (folds oldest only), and a mixed multi-fd sweep (listener + recent + idle-pinged + young-unreg) exercising the oldest-fold + final clamps. Asserts each client's post-`(flags,lasttime)` + the return value identical. Finding: a fresh `calloc`'d client needs `acpt` set (real `make_client` sets it to the listener) or `send_message`'s `acpt != &me` sendM bump null-derefs; `fd = -1` + a sub-1024-byte PING never trips `send_queued` so there is no real socket write. 615 615 - **L2** — no new file. The entire `ircd-golden` suite runs `check_pings` every `io_loop` tick (it is the registration-timeout + ping path). `golden_registration` (boot/loop/timeout) and `golden_channel_join` byte-identical; the only failure is the documented pre-existing `golden_s_misc` `stats_t` reference-C garbage flake ([[p5-s2s-stats-flake]] / P7ff) — a `%lu`/`u_int` width mismatch in the still-C variadic `sendto_one` (`281470681743360Sq` from `va_arg`'d call-frame garbage in the frozen reference-C binary vs the correct `0Sq/0Yg/0Fl` from Rust), surfaced not caused. No S2S (no command-driven remote-field formatting). 616 + 617 + - **2026-06-06 — P7ii (`ircd.c` `delayed_kills` — the io_loop K-line sweep) merged.** Ported the static `io_loop` timer helper `delayed_kills` (ircd.c:447) to `ircd-common/src/ircd.rs`, the sibling of `check_pings` (P7hh). The other of the two remaining loop helpers; `io_loop` calls it (ircd.c:1294, `rehashed = delayed_kills(timeofday)`) **only while `rehashed > 0`** — i.e. after a rehash queues a K-line re-evaluation. 618 + - **Cluster choice / Scope.** Walks `local[dk_lastfd..j]` (a batch of at most `MAXDELAYEDKILLS`); for every local fully-registered person it runs `find_kill`, and on a match (`kflag == -1`) notices the oper channel (`sendto_flag "Kill line active for %s"`), stamps `cptr->exitc = EXITC_KLINE`, and `exit_client`s the connection with the kill reason. Returns 1 if work remains queued (incomplete batch, or `rehashed == 2` — a rehash arrived mid-sweep), else 0. Persistent function-local statics (`dk_rehashed`/`dk_lastfd`/`dk_checked`/`dk_killed`) carry the batch cursor across calls; `dk_rehashed == 0` re-arms the sweep from `highest_fd`. 619 + - **Guard / de-static.** `delayed_kills` is `static` → no `cref_` oracle symbol. De-static'd in ircd.c under `#ifndef PORT_IRCD_DELAYED_KILLS_P7ii` (the `check_pings`/`try_connections`/`mysk` precedent) so the cref archive's prefix-rename exports `cref_delayed_kills`; the `#else` branch is an `extern int delayed_kills(time_t)` proto so the still-C `io_loop` caller keeps a declaration. `-DPORT_IRCD_DELAYED_KILLS_P7ii` added to the `ircd_link.o` make recipe (alongside the P7c/P7t/P7gg/P7hh defines). The full `ircd.o` (cref oracle, no PORT define) keeps the non-static body. 620 + - **Config-resolved body.** `MAXDELAYEDKILLS` = 200 (config.h:423) and `MAXCONNECTIONS` = 50 (config.h:150): the `#ifdef MAXDELAYEDKILLS` batch block IS compiled, but `j = dk_lastfd - 199` with `dk_lastfd ≤ 49` is **always < 0 → clamped to 0**, so the whole `local[]` is swept in a single call and `dk_lastfd` always reaches -1 → the persistent statics reset every call (no multi-call drain) and the trailing `return rehashed` (incomplete-batch path) is **unreachable** under the locked config — ported faithfully regardless. `TIMEDKLINES` off → `find_kill`'s `timedklines` arg is 0 (the `check_pings` finding); `DEBUGMODE` off → the `Debug((DEBUG_DEBUG, "DelayedKills killed…"))` no-op. 621 + - **Faithfulness.** The C `for (i = dk_lastfd; i >= j; i--)` with its `continue` (which still runs `i--`) and in-loop `j--` (on a NULL/non-person slot, only while `j > 0`) is modelled by a `while i >= j` loop that decrements `i` before every `continue`/iteration end, so the post-loop `dk_lastfd = i` lands on `j-1` identically. `IsPerson` = `user && (STAT_CLIENT || STAT_OPER)` and `BadPtr` inlined as private `unsafe fn` helpers. The summary `sendto_flag(SCH_NOTICE, "DelayedKills checked %d killed %d in %d sec", dk_checked, dk_killed, currenttime - dk_rehashed)` passes a `time_t` (long) to a `%d` — a faithful C quirk, passed verbatim (the variadic trampoline reads 32 bits; a no-op until the notice channel exists). The variadic `sendto_flag` is *called* (→ P8); `find_kill` (Rust P6i)/`get_client_name`/`exit_client` resolve through the link seam. 622 + - **Classification.** `io_loop` callee (no `msgtab` entry), reached only after a rehash queues K-line re-evaluation (`rehashed > 0`). → **L1-gated**: the kill arm is destructive (`exit_client`) AND requires both a matching K-line conf and `rehashed > 0`, neither of which the golden suite produces — so unlike `check_pings`, `delayed_kills`' body has no L2 path at all (golden never rehashes). 623 + - **L1** — `delayed_kills_diff.rs`, 6-case zero-diff vs `cref_delayed_kills` over two isolated worlds (Rust `local`/`highest_fd`/`rehashed` + empty `kconf`/`tkconf` → `find_kill` returns 0, no kill, vs the `cref_` copies), serialized on `GLOBALS_LOCK` (the documented P7 shared-global hazard). Covers the **non-kill** sweep (the inverse of the destructive kill arm): empty `local[]` across `rehashed` ∈ {0,1,2} (→ return {0,0,1} — exercises the requeue logic), an `IsPerson` client over empty conf (checked, no kill → completes, **client left alive**: status/flags unchanged + still in `local[]`), non-person (no `user`) + userless STAT_CLIENT slots (both `!IsPerson` → skipped before `find_kill`), and a mixed multi-fd sweep. Asserts identical return + per-client `(status, flags, still-in-local)`. A `with_user` client gets a zeroed `anUser` (username "") so `find_kill` walks the empty conf and returns 0. 624 + - **Untested-by-design / L2** — the `kflag == -1` kill arm (`exit_client` is destructive — the `check_pings`/`read_message` L2-gating precedent — and needs both a K-line conf match and a queued rehash). No new L2 file: `golden_registration` confirmed byte-identical (no-regression; the boot/event-loop path is unaffected and `delayed_kills` is never entered without a rehash). No S2S (formats no command-driven remote-user wire fields).
+68
docs/superpowers/plans/2026-06-06-p7ii-ircd-delayed_kills.md
··· 1 + # P7ii — port `ircd.c` `delayed_kills` (io_loop K-line sweep) 2 + 3 + ## What 4 + 5 + Port `delayed_kills` (ircd.c:447), the second of the two `io_loop` timer helpers 6 + (sibling of P7hh `check_pings`). Called from `io_loop` **only while `rehashed > 0`**: 7 + it walks `local[dk_lastfd..j]` and, for every local fully-registered person, runs 8 + `find_kill`; a matching K-line (`kflag == -1`) notices the oper channel, stamps 9 + `EXITC_KLINE`, and `exit_client`s the connection. Returns 1 if work remains queued 10 + (an incomplete batch, or a rehash arrived mid-sweep), else 0. 11 + 12 + ## Classification 13 + 14 + Utility / callee TU — **not a `msgtab` handler**. It is reached only from `io_loop` 15 + (`ircd.c:1286`) when `rehashed > 0` (a rehash/SIGHUP queued K-line re-evaluation). 16 + L1 is the gate; the golden suite never rehashes, so `delayed_kills`' body is not 17 + reached in L2 — `golden_registration` stays byte-identical (no-regression). No S2S 18 + (no command-driven remote-user wire fields). 19 + 20 + ## Config-resolved findings (locked config) 21 + 22 + - `MAXDELAYEDKILLS` = 200 (config.h:423), `MAXCONNECTIONS` = 50 (config.h:150). The 23 + `#ifdef MAXDELAYEDKILLS` block IS compiled, but `j = dk_lastfd - 199` with 24 + `dk_lastfd ≤ 49` is **always < 0 → clamped to 0** → the whole `local[]` is swept in 25 + one call and `dk_lastfd` always reaches -1. So the persistent function-local statics 26 + (`dk_rehashed`/`dk_lastfd`/`dk_checked`/`dk_killed`) **reset every call** — no 27 + multi-call drain. The trailing `return rehashed` (incomplete-batch path) is 28 + unreachable here, but ported faithfully. 29 + - `TIMEDKLINES` off → `find_kill`'s `timedklines` arg is 0 (matches the `check_pings` 30 + finding); `DEBUGMODE` off → no `Debug`. 31 + - The summary `sendto_flag(... "in %d sec", currenttime - dk_rehashed)` passes a 32 + `time_t` (long) to a `%d` — a faithful C quirk; the long is passed verbatim (the 33 + trampoline reads 32 bits; no-op until the notice channel exists). 34 + 35 + ## Mechanism 36 + 37 + - De-static `delayed_kills` in `ircd.c` (the P7gg/P7hh precedent) so the cref archive's 38 + prefix-rename exports `cref_delayed_kills` for the L1 differential; guard the body 39 + `#ifndef PORT_IRCD_DELAYED_KILLS_P7ii ... #else extern int delayed_kills(time_t); #endif` 40 + (the still-C `io_loop` caller keeps a prototype). 41 + - Add `-DPORT_IRCD_DELAYED_KILLS_P7ii` to the `ircd_link.o` recipe in `ircd-sys/build.rs`. 42 + - Port to `ircd-common/src/ircd.rs`: function-local statics → `static mut` inside the fn; 43 + `is_person`/`bad_ptr` helpers; `find_kill`/`get_client_name`/`exit_client` via 44 + `ircd_sys::bindings`; variadic `sendto_flag` via the existing module extern block. 45 + 46 + ## Tests 47 + 48 + - **RED → GREEN L1** (`ircd-testkit/tests/delayed_kills_diff.rs`, the `check_pings_diff.rs` 49 + isolated-worlds + `GLOBALS_LOCK` structure): Rust `delayed_kills` (resolves to 50 + ircd-common via `ircd_link.o`) vs `cref_delayed_kills`. Each world reads its own 51 + `local`/`highest_fd` and an empty `kconf`/`tkconf` (find_kill → 0, no kill). Cases: 52 + empty (rehashed 0/1/2 → 0/0/1 return), person-no-kline completes & leaves clients 53 + alive (the inverse of the kill arm), non-person/NULL slots skipped, mixed sweep. Assert 54 + identical return + per-client post-state (clients NOT exited, `local[i]` intact). 55 + - **Untested-by-design:** the `kflag == -1` kill arm (`exit_client` is destructive — the 56 + `check_pings`/`read_message` L2-gating precedent) AND it requires both a matching 57 + K-line conf and `rehashed > 0`; the golden suite does neither, so it has no L2 path 58 + either. Noted in the commit/PLAN. 59 + - **No S2S** (formats no remote-user wire fields). 60 + 61 + ## Procedure 62 + 63 + 1. RED: write `delayed_kills_diff.rs` (copy `check_pings_diff.rs`). 64 + 2. Force RED: add the guard to `build.rs` → undefined `delayed_kills`. 65 + 3. GREEN: de-static + guard `ircd.c`; port to `ircd.rs`. 66 + 4. L1 green: `cargo test -p ircd-testkit --test delayed_kills_diff`. 67 + 5. Verify: `cargo build` (0 warnings); confirm `nm target/debug/ircd | grep delayed_kills` → `T` from Rust. 68 + 6. Commit (port + test separately per the one-test-per-commit rule); update PLAN row + the two P7 logs.
+118 -6
ircd-common/src/ircd.rs
··· 20 20 use ircd_sys::bindings::{ 21 21 aClient, aConfItem, bootopt, conf, connect_server, find_denied, find_mask, find_name, 22 22 get_con_freq, highest_fd, iauth_options, iconf, local, lk_size, me, mybasename, poolsize, 23 - send_ping, timeofday, ww_size, BOOT_BADTUNE, BOOT_STANDALONE, ServerChannels_SCH_ERROR, 24 - ServerChannels_SCH_NOTICE, CONF_CONNECT_SERVER, CONF_ZCONNECT_SERVER, EXITC_AUTHTOUT, EXITC_PING, 25 - FLAGS_AUTH, FLAGS_DOINGDNS, FLAGS_LISTEN, FLAGS_PINGSENT, FLAGS_WXAUTH, FLAGS_XAUTH, 26 - FLAGS_XAUTHDONE, Status_STAT_CONNECTING, Status_STAT_HANDSHAKE, Status_STAT_ME, 27 - Status_STAT_SERVER, XOPT_EXTWAIT, XOPT_NOTIMEOUT, _CHANNELHASHSIZE, _HASHSIZE, _HOSTNAMEHASHSIZE, 28 - _IPHASHSIZE, _SIDSIZE, _UIDSIZE, 23 + rehashed, send_ping, timeofday, ww_size, BOOT_BADTUNE, BOOT_STANDALONE, ServerChannels_SCH_ERROR, 24 + ServerChannels_SCH_NOTICE, CONF_CONNECT_SERVER, CONF_ZCONNECT_SERVER, EXITC_AUTHTOUT, EXITC_KLINE, 25 + EXITC_PING, FLAGS_AUTH, FLAGS_DOINGDNS, FLAGS_LISTEN, FLAGS_PINGSENT, FLAGS_WXAUTH, FLAGS_XAUTH, 26 + FLAGS_XAUTHDONE, Status_STAT_CLIENT, Status_STAT_CONNECTING, Status_STAT_HANDSHAKE, 27 + Status_STAT_ME, Status_STAT_OPER, Status_STAT_SERVER, XOPT_EXTWAIT, XOPT_NOTIMEOUT, 28 + _CHANNELHASHSIZE, _HASHSIZE, _HOSTNAMEHASHSIZE, _IPHASHSIZE, _SIDSIZE, _UIDSIZE, 29 29 }; 30 30 use std::os::raw::{c_char, c_int, c_long}; 31 31 ··· 388 388 unsafe fn is_listener(x: *mut aClient) -> bool { 389 389 (*x).flags & FLAGS_LISTEN as c_long != 0 390 390 } 391 + /// `IsPerson(x)` (struct_def.h:212) — `(x)->user && IsClient(x)`, where `IsClient` is 392 + /// `status == STAT_CLIENT || status == STAT_OPER`. 393 + #[inline] 394 + unsafe fn is_person(x: *mut aClient) -> bool { 395 + !(*x).user.is_null() 396 + && ((*x).status as c_int == Status_STAT_CLIENT as c_int 397 + || (*x).status as c_int == Status_STAT_OPER as c_int) 398 + } 399 + /// `BadPtr(x)` (struct_def.h:786) — `!(x) || (*(x) == '\0')`. 400 + #[inline] 401 + unsafe fn bad_ptr(x: *const c_char) -> bool { 402 + x.is_null() || *x == 0 403 + } 391 404 /// `IsRegistered(x)` — `status >= STAT_SERVER || status == STAT_ME`. 392 405 #[inline] 393 406 unsafe fn is_registered(x: *mut aClient) -> bool { ··· 608 621 } 609 622 oldest 610 623 } 624 + 625 + /// `static int delayed_kills(time_t currenttime)` (ircd.c:447) — the io_loop K-line 626 + /// sweep, the sibling of `check_pings`. `io_loop` calls it only while `rehashed > 0`: 627 + /// it walks `local[dk_lastfd..j]` and, for every local fully-registered person, runs 628 + /// `find_kill`; a matching K-line (`kflag == -1`) notices the oper channel, stamps 629 + /// `EXITC_KLINE`, and `exit_client`s the connection. Returns 1 if work remains queued 630 + /// (an incomplete batch, or a rehash arrived mid-sweep), else 0. 631 + /// 632 + /// Config-resolved (locked config): `MAXDELAYEDKILLS` = 200 (config.h:423) and 633 + /// `MAXCONNECTIONS` = 50 (config.h:150), so `j = dk_lastfd - 199` is always < 0 → 634 + /// clamped to 0 → the whole `local[]` is swept in one call and `dk_lastfd` always 635 + /// reaches -1; the persistent function-local statics therefore reset every call (no 636 + /// multi-call drain) and the trailing `return rehashed` incomplete-batch path is 637 + /// unreachable here (ported faithfully). `TIMEDKLINES` off → `find_kill`'s `timedklines` 638 + /// arg is 0 (the `check_pings` finding); `DEBUGMODE` off → no `Debug`. 639 + /// 640 + /// The variadic `sendto_flag` is *called* (→ P8 trampoline; no-ops until the 641 + /// server-notice channel exists). The summary `"in %d sec"` is fed `currenttime - 642 + /// dk_rehashed` (a `time_t` passed to a `%d` — a faithful C quirk, passed verbatim). 643 + /// `find_kill`/`get_client_name`/`exit_client` resolve through the link seam. 644 + #[no_mangle] 645 + pub unsafe extern "C" fn delayed_kills(currenttime: c_long) -> c_int { 646 + const MAXDELAYEDKILLS: c_int = 200; // config.h:423 647 + 648 + static mut DK_REHASHED: c_long = 0; // time of last rehash we're processing 649 + static mut DK_LASTFD: c_int = 0; // fd we last checked 650 + static mut DK_CHECKED: c_int = 0; // # clients we checked 651 + static mut DK_KILLED: c_int = 0; // # clients we killed 652 + 653 + if DK_REHASHED == 0 { 654 + DK_REHASHED = currenttime; 655 + DK_CHECKED = 0; 656 + DK_KILLED = 0; 657 + DK_LASTFD = highest_fd; 658 + } 659 + // checking only this many clients each time 660 + let mut j = DK_LASTFD - MAXDELAYEDKILLS + 1; 661 + if j < 0 { 662 + j = 0; 663 + } 664 + 665 + let mut i = DK_LASTFD; 666 + while i >= j { 667 + let cptr = *local_base().add(i as usize); 668 + 669 + if cptr.is_null() || !is_person(cptr) { 670 + // for K:lines we're interested only in local, fully registered clients 671 + if j > 0 { 672 + j -= 1; 673 + } 674 + i -= 1; 675 + continue; 676 + } 677 + 678 + DK_CHECKED += 1; 679 + let mut reason: *mut c_char = std::ptr::null_mut(); 680 + let kflag = ircd_sys::bindings::find_kill(cptr, 0, &mut reason); 681 + 682 + // If the client is a user and a KILL line was found active, close it. 683 + if kflag == -1 { 684 + let mut buf = [0 as c_char; 100]; 685 + DK_KILLED += 1; 686 + sendto_flag( 687 + ServerChannels_SCH_NOTICE, 688 + c"Kill line active for %s".as_ptr() as *mut c_char, 689 + ircd_sys::bindings::get_client_name(cptr, 0 /* FALSE */), 690 + ); 691 + (*cptr).exitc = EXITC_KLINE as c_char; 692 + if !bad_ptr(reason) { 693 + libc::sprintf(buf.as_mut_ptr(), c"Kill line active: %.80s".as_ptr(), reason); 694 + } 695 + let comment = if !reason.is_null() { 696 + buf.as_ptr() 697 + } else { 698 + c"Kill line active".as_ptr() 699 + }; 700 + ircd_sys::bindings::exit_client(cptr, cptr, me_ptr(), comment); 701 + } 702 + i -= 1; 703 + } 704 + DK_LASTFD = i; // from which fd to start next time 705 + 706 + if DK_LASTFD < 0 { 707 + sendto_flag( 708 + ServerChannels_SCH_NOTICE, 709 + c"DelayedKills checked %d killed %d in %d sec".as_ptr() as *mut c_char, 710 + DK_CHECKED, 711 + DK_KILLED, 712 + currenttime - DK_REHASHED, 713 + ); 714 + DK_REHASHED = 0; 715 + if rehashed == 2 { 716 + // there was rehash queued, start again 717 + return 1; 718 + } 719 + return 0; 720 + } 721 + rehashed 722 + }
+1 -1
ircd-sys/build.rs
··· 232 232 .current_dir(&cbuild) 233 233 .arg(format!("CC={cc_override}")) 234 234 .arg( 235 - "--eval=ircd_link.o: ../ircd/ircd.c\n\t$(CC) $(S_CFLAGS) -DPORT_IRCD_TUNE_P7c -DPORT_IRCD_CALC_PREF_P7t -DPORT_IRCD_TRY_CONNECTIONS_P7gg -DPORT_IRCD_CHECK_PINGS_P7hh \ 235 + "--eval=ircd_link.o: ../ircd/ircd.c\n\t$(CC) $(S_CFLAGS) -DPORT_IRCD_TUNE_P7c -DPORT_IRCD_CALC_PREF_P7t -DPORT_IRCD_TRY_CONNECTIONS_P7gg -DPORT_IRCD_CHECK_PINGS_P7hh -DPORT_IRCD_DELAYED_KILLS_P7ii \ 236 236 -DIRCDCONF_PATH=\"\\\"$(IRCDCONF_PATH)\\\"\" \ 237 237 -DIRCDTUNE_PATH=\"\\\"$(IRCDTUNE_PATH)\\\"\" \ 238 238 -DIRCDMOTD_PATH=\"\\\"$(IRCDMOTD_PATH)\\\"\" \
+9 -1
ircd/ircd.c
··· 444 444 ** Only MAXDELAYEDKILLS at a time or all, if not defined. 445 445 ** Returns 1, if still work to do, 0 if finished. 446 446 */ 447 - static int delayed_kills(time_t currenttime) 447 + #ifndef PORT_IRCD_DELAYED_KILLS_P7ii /* ported to ircd-common/src/ircd.rs (P7ii) */ 448 + /* de-static'd so the cref archive's prefix-rename exports cref_delayed_kills for the 449 + ** L1 differential (delayed_kills_diff.rs); the check_pings (P7hh) precedent. */ 450 + int delayed_kills(time_t currenttime) 448 451 { 449 452 static time_t dk_rehashed = 0; /* time of last rehash we're processing */ 450 453 static int dk_lastfd; /* fd we last checked */ ··· 521 524 } 522 525 return rehashed; 523 526 } 527 + #else 528 + /* P7ii: defined in Rust (ircd-common/src/ircd.rs); the only caller is io_loop() 529 + ** below, which loses the in-file definition's implicit prototype when guarded. */ 530 + extern int delayed_kills(time_t currenttime); 531 + #endif /* PORT_IRCD_DELAYED_KILLS_P7ii */ 524 532 525 533 #ifndef PORT_IRCD_CHECK_PINGS_P7hh /* ported to ircd-common/src/ircd.rs (P7hh) */ 526 534 /* de-static'd so the cref archive's prefix-rename exports cref_check_pings for the