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.

fix(leveva): +R auto-reop enforced for local members only — P11 slice 278

The enforce_reop chokepoint claimed it skipped remote members but never
filtered by locality — it relied on the false premise that remote members
lack a registry record. A UNICK-introduced remote user does hold one, so a
+R mask matching its nick!user@host would wrongly op it locally and relay a
server-sourced +o we have no authority to emit. Add an explicit
is_local_uid filter to the one chokepoint every reop trigger funnels through
(JOIN, local +R-set, inbound S2S +R burst, CHGHOST, CHANTS/NJOIN merge).

Tests: strengthened inbound_reop_mask_skips_remote_members to give the
remote member a real registry record (RED before fix); new unit
enforce_reop_ops_only_local_matching_members + inverse; proptest fuzz
enforce_reop_never_ops_remote over a random local/remote mix.

+239 -14
+1 -1
PLAN.md
··· 90 90 | **P8 — Delete the C + retire the oracle harness ✅ DONE** (per-sub-phase: [`PLAN-P8-progress.md`](PLAN-P8-progress.md) + [`docs/progress-log/p8.md`](docs/progress-log/p8.md)) | remaining C (the ~25 variadic sender trampolines + residual data globals) + `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 with a non‑variadic Rust sender API at every call site (faithful `wire!`/`reply_one!`/`format!` first; leveva typed messages adopted alongside the IRC senders), drop the residual data globals via the data‑symbol seam (bindgen `extern` decls resolve to `#[no_mangle]` Rust defs at link), then drop **all** C compilation from `ircd-sys` (no more `cc::Build`), retire the differential harness (`cref_*`/L1 `ircd-testkit`/`ircd-golden` L2 can no longer be built once the oracle is gone), and **migrate the load‑bearing tests into `ircd-common`** as self‑contained Rust tests (own fixtures, no `cref_` oracle). **Progress (P8a–P8r — full entries in the progress logs):** **every variadic sender trampoline is now Rust** (P8a–P8m, incl. the keystones `sendto_one`/`sendto_flag`/`esendto_*`), and the five data‑global `.o`s are dropped outright (`s_auth.o` iauth globals P8n, `send.o` P8o, `s_bsd.o` `local[]`/`highest_fd`/… P8p, `ircd.o` `me`/`client`/timers/… P8q). **P8r dropped the last C‑logic TU `support.o`** — `dgets`/`make_isupport` ported to Rust + the `ipv6string`/`minus_one` data globals defined as `#[no_mangle]` statics (`snprintf_append` is dead in Rust — WHOX is field‑by‑field — so it died with the `.o`; `irc_sprintf` is deleted not ported, in P11). **Every ircd C TU is now Rust:** the link set holds only the generated `version.o` + the L1 harness's `ctruth.o`. **P8s migrated all 115 L1 `cref_` differentials into self‑contained `ircd-common` `insta` snapshot tests** (drive only the Rust port via `link_anchor()`, no oracle; soundness via the capture chain) — the suite is green + 0 warnings. **P8t ported the last *generated* C TU `version.c` to Rust** (`ircd-common/src/version.rs`): the data globals `generation`/`creation`/`pass_version`/`infotext`/`isupport` are now `#[no_mangle]` statics via the data‑symbol seam — `generation`/`creation` sourced from crate metadata at build time (`CARGO_PKG_VERSION` + an `ircd-common/build.rs` UTC build stamp), `pass_version`/`infotext` copied verbatim. `version.c.SH` generation + the `version.o` compile/archive are gone from `ircd-sys/build.rs`; `libircd_c.a` now holds **only** the L1 harness `ctruth.o`. (`creation` is now genuinely build‑time volatile, so the golden canonicalizer masks RPL_CREATED 003, matching the existing 371 Birth‑Date rule.) **P8u (FINAL) retired the oracle.** Ran the differential suite a last time green — L1 `ircd-testkit` all pass; L2 `ircd-golden` 86 pass + only the documented reference‑C‑garbage `s_serv_stats` flake (where Rust is the *correct* side) — then **dropped all C compilation** from `ircd-sys/build.rs`: the `make` object build, the `cref_*`/`ctruth.o`/`res.o` recompiles, the `libircd_c.a` archive, and the whole‑archive link + `-lz`/`-lm`/`-lcrypt` (none needed by Rust — `pow` resolves from glibc 2.29+). `build.rs` now only runs `configure` + bindgen over the C *headers* (the struct view `ircd-common` still uses until P9–P12) and expands the install‑path Makefile vars. `ircd-testkit` (L1) + `ircd-golden` (L2) are `exclude`d from the workspace (mothballed, git‑recoverable); the `ctruth.c`/`layout.rs` drift‑net is deleted. **The workspace is now 100% Rust — zero C TUs compiled.** **P8v (literal end of C) deleted the C *source tree* itself + retired `ircd-sys`:** froze the generated `bindings.rs` into a committed `ircd-common/src/bindings.rs` (a self‑alias `extern crate self as ircd_sys;` keeps every `ircd_sys::bindings::*` path resolving, zero src churn) + the install‑path consts as literals, then deleted `ircd-sys`, the mothballed `ircd-testkit`/`ircd-golden` oracle crates, the `iauth-rs` C‑iauth differential, and **all** C source (`common/ ircd/ iauth/ support/ contrib/ cbuild/ configure .clang-format* clangformat.yml`). `ircd-rs` calls `ircd_common::ircd::c_ircd_main` directly. **Not one line of C remains in the repo.** | **MET. Final differential run green; oracle mothballed; `cargo build --workspace` 0 warnings links pure Rust; `cargo test --workspace` 695 pass / 0 fail (P8v; was 698 before deleting the C‑oracle tests); the `ircd-common` snapshot tests carry the reference‑C correctness forward. C source tree deleted — repo is 100% Rust at the file level.** | 91 91 | **P9 — std‑library cleanup ❌ RETIRED (2026‑06‑08)** | — (was: whole Rust tree) | **Skipped by decision — superseded by the `leveva` greenfield.** P9 assumed an *in‑place* idiomatic transformation of the mechanical port (`MyMalloc`/`MyFree`→`Box`/`Vec`, `dbuf` freelist→owned, raw `libc`→`std`/`nix`, `[c_char;N]`→`[u8;N]`). Because the idiomatic end‑state is now a separate greenfield crate (`leveva`), `ircd-common` is throwaway code (the oracle, deleted at parity), so polishing it earns nothing. Nothing was implemented; the row is kept for provenance. | n/a — retired. | 92 92 | **P10 — `leveva` greenfield foundations 🔶 IN PROGRESS** ([`docs/progress-log/p10.md`](docs/progress-log/p10.md)) | new `leveva` crate (lib) + `leveva-integration` differential tests | The idiomatic‑Rust product, built from scratch (not transformed from the port). **Done so far:** RFC 1459 case‑folding (`casemap`); typed identifier strings (`IrcStr`/`IrcString` + `Nick`/`ChanName`/`Uid`/`Sid`/`Cid`/`ServerName`/`UserName`/`HostName` validating newtypes via `ident_newtype!`); `Message`/`MessageBuilder`; the full `Numeric` reply/error enum (184 codes, discriminant = wire code); a generic safe `patricia` trie; glob `matching`; `mode`; KDL `config` (model/parse/password/privilege); `rustls`/`tokio` `tls` endpoint with hot reload; `ident`. The async boot path (`main.rs`) binds listeners + watches certs but currently drops accepted connections — no protocol layer yet. Idiomatic Rust allowed everywhere; pulls real crates (tokio/rustls/kdl/nom/clap). | `cargo test -p leveva` green + clippy clean per module; `leveva-integration` pins each greenfield reimplementation against `ircd-common` (the oracle) within their shared domain (documented divergences asserted explicitly). | 93 - | **P11 — `leveva` protocol layer → feature parity 🔶 IN PROGRESS** ([`docs/progress-log/p11.md`](docs/progress-log/p11.md)) | `leveva` (client/server state machine + handlers) | The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and `leveva-iauth` (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, `Result`/`thiserror`, `format!`/typed builders — **no** `sprintf`/global `buf`), unit‑tested + boot‑golden + proptest‑fuzzed, and **differentially pinned against `ircd-common`** (or `iauth-rs` for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. **Done: 277 slices** (277 = netsplit batches are no longer duplicated to `batch`-capable clients under concurrent teardown — when a dying peer is detected on two paths at once (abrupt socket drop + a sibling's inbound `SQUIT`, in separate tasks), the `NJOIN`-only ghost-member sweep raced (`co_members` then `remove_everywhere`, no single-shot gate) and double-relayed a casualty's `QUIT`; new atomic `Channels::detach_member` fuses recipient-gather + removal in one critical section so only the first teardown to reach a member relays it (the loser gets `None`), mirroring the already-atomic `remove_behind_sid` mirror drain; new `s2s_netsplit_concurrency_proptest` fuzz; 276 = remote-originated channel messages are now recorded into chat-history — the S2S relay's inbound channel fan (`s2s::relay::inbound_message`) records the delivered line on this server too, keyed by the carried network msgid (slice 130) + `@time` (slice 134), gated on this server having a local member of the channel (only a local member can `CHATHISTORY` it; no whole-network accumulation on a hub); the `+z` op-redirect/non-channel branches never record, mirroring the local plane; closes the slice-274 gap where `CHATHISTORY` held only locally-sent lines; 275 = IRCv3 `message-redaction` now deletes from chat-history — an accepted channel `REDACT <chan> <msgid>` deletes the slice-274 stored line (`History::delete_by_msgid`, keyed `(target, msgid)`) so `CHATHISTORY` can never replay it; wired in both the local `CanSend::Ok` path and inbound `ENCAP * REDACT`, per-server; mirrors recording — a `+z` op-moderated/"shown to ops" attempt is never recorded so never deleted; authorization stays a stateless relay; 274 = IRCv3 `draft/chathistory` — SQLite-backed (`rusqlite`, bundled) channel message history behind an optional `ircd.kdl` `database {}` block; all channels auto-enrolled (PRIVMSG/NOTICE + JOIN/PART/QUIT recorded at the delivery plane), joining clients auto-replayed `replay-lines` recent lines unless they negotiated `draft/chathistory` (then exempt, per spec), full `CHATHISTORY LATEST/BEFORE/AFTER/BETWEEN/AROUND/TARGETS` (timestamp=/msgid= selectors) framed as a `chathistory` batch, `draft/chathistory`+`draft/event-playback` dynamic caps + `CHATHISTORY`/`MSGREFTYPES` ISUPPORT, age-based `retention-days` prune ticker; `Message` gained serde; 273 = registration-time nick `RESV` enforcement — a connection may no longer *register* directly as a reserved nick, only the post-registration `NICK`/JOIN were gated before; refuse `432` + reopen registration, mirroring the `433` nick-in-use path; 272 = charybdis `RESV`/`UNRESV` nick/channel reservation — JOIN→`437`/NICK→`432` gates, opers exempt, new `OperPrivilege::Resv`; defaults declared in `resv {}` config blocks, boot-seeded + REHASH-re-seeded keeping runtime `RESV`s). The slices span the full client command surface (registration; channels + the complete channel-mode surface; WHO/WHOIS/WHOX; OPER/STATS; and the query/util commands incl. OPERWALL/LOCOPS/USERIP); the UID-based S2S link/burst/netsplit protocol (keepalive, live introduction, NJOIN chunking, TS merge arbitration); `leveva-iauth` (dnsbl/socks/webproxy/pipe/ident); config live-rehash across every block; CertFP; the **IRCv3 cap track** (message-tags/server-time/msgid/account/bot/oper/echo/labeled-response/CAP, `draft/metadata-2`, caller-id `+g`/`+G`, the **STS/TLS** sub-track); elemental channel modes + extbans; KNOCK; command-rate flood protection (fakelag); the **complete charybdis channel-mode set** (`+C/+c/+S/+j/+z/+T/+g/+r/+f/+Q/+F/+P/+L`, with `+L` the last standard one) and **user-mode set** (`+D/+R/+Q/+G/+z/+S/+l`, plus the product decision that leveva has no local-only umodes — `SEND_UMODES` is every umode); and the `+s` server-notice **snomask** (a charybdis category mask with a producer wired for every category — the matrix is complete). Most const limits (`max-penalty`/`max-bans`/`service-string`/`default-snomask`/…) have been promoted to REHASH-able `options { … }` knobs. **Full per-slice detail (scope, mechanism, divergences, gate) for every slice lives in [`docs/progress-log/p11.md`](docs/progress-log/p11.md); the standing testing plan is [`docs/superpowers/specs/2026-06-10-leveva-testing-plan.md`](docs/superpowers/specs/2026-06-10-leveva-testing-plan.md). This row stays thin.** | Per‑slice: leveva boots and serves the slice; leveva golden snapshot + proptest green; the `leveva-integration` differential agrees with `ircd-common` on the shared skeleton with divergences asserted. Across the eventual matrix: registration, channels, WHO/WHOIS/WHOX, OPER/STATS, server burst, netsplit, rehash, restart; iauth end‑to‑end. | 93 + | **P11 — `leveva` protocol layer → feature parity 🔶 IN PROGRESS** ([`docs/progress-log/p11.md`](docs/progress-log/p11.md)) | `leveva` (client/server state machine + handlers) | The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and `leveva-iauth` (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, `Result`/`thiserror`, `format!`/typed builders — **no** `sprintf`/global `buf`), unit‑tested + boot‑golden + proptest‑fuzzed, and **differentially pinned against `ircd-common`** (or `iauth-rs` for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. **Done: 278 slices** (278 = `+R` auto-reop is now enforced for **local** members only — the `enforce_reop` chokepoint gained an explicit `is_local_uid` filter, so a `+R` mask matching a `UNICK`-introduced remote user's `nick!user@host` no longer wrongly ops it locally and relays a `+o` we have no authority to emit (the locality guarantee previously rested on the false premise that remote members lack a registry record); 277 = netsplit batches are no longer duplicated to `batch`-capable clients under concurrent teardown — when a dying peer is detected on two paths at once (abrupt socket drop + a sibling's inbound `SQUIT`, in separate tasks), the `NJOIN`-only ghost-member sweep raced (`co_members` then `remove_everywhere`, no single-shot gate) and double-relayed a casualty's `QUIT`; new atomic `Channels::detach_member` fuses recipient-gather + removal in one critical section so only the first teardown to reach a member relays it (the loser gets `None`), mirroring the already-atomic `remove_behind_sid` mirror drain; new `s2s_netsplit_concurrency_proptest` fuzz; 276 = remote-originated channel messages are now recorded into chat-history — the S2S relay's inbound channel fan (`s2s::relay::inbound_message`) records the delivered line on this server too, keyed by the carried network msgid (slice 130) + `@time` (slice 134), gated on this server having a local member of the channel (only a local member can `CHATHISTORY` it; no whole-network accumulation on a hub); the `+z` op-redirect/non-channel branches never record, mirroring the local plane; closes the slice-274 gap where `CHATHISTORY` held only locally-sent lines; 275 = IRCv3 `message-redaction` now deletes from chat-history — an accepted channel `REDACT <chan> <msgid>` deletes the slice-274 stored line (`History::delete_by_msgid`, keyed `(target, msgid)`) so `CHATHISTORY` can never replay it; wired in both the local `CanSend::Ok` path and inbound `ENCAP * REDACT`, per-server; mirrors recording — a `+z` op-moderated/"shown to ops" attempt is never recorded so never deleted; authorization stays a stateless relay; 274 = IRCv3 `draft/chathistory` — SQLite-backed (`rusqlite`, bundled) channel message history behind an optional `ircd.kdl` `database {}` block; all channels auto-enrolled (PRIVMSG/NOTICE + JOIN/PART/QUIT recorded at the delivery plane), joining clients auto-replayed `replay-lines` recent lines unless they negotiated `draft/chathistory` (then exempt, per spec), full `CHATHISTORY LATEST/BEFORE/AFTER/BETWEEN/AROUND/TARGETS` (timestamp=/msgid= selectors) framed as a `chathistory` batch, `draft/chathistory`+`draft/event-playback` dynamic caps + `CHATHISTORY`/`MSGREFTYPES` ISUPPORT, age-based `retention-days` prune ticker; `Message` gained serde; 273 = registration-time nick `RESV` enforcement — a connection may no longer *register* directly as a reserved nick, only the post-registration `NICK`/JOIN were gated before; refuse `432` + reopen registration, mirroring the `433` nick-in-use path; 272 = charybdis `RESV`/`UNRESV` nick/channel reservation — JOIN→`437`/NICK→`432` gates, opers exempt, new `OperPrivilege::Resv`; defaults declared in `resv {}` config blocks, boot-seeded + REHASH-re-seeded keeping runtime `RESV`s). The slices span the full client command surface (registration; channels + the complete channel-mode surface; WHO/WHOIS/WHOX; OPER/STATS; and the query/util commands incl. OPERWALL/LOCOPS/USERIP); the UID-based S2S link/burst/netsplit protocol (keepalive, live introduction, NJOIN chunking, TS merge arbitration); `leveva-iauth` (dnsbl/socks/webproxy/pipe/ident); config live-rehash across every block; CertFP; the **IRCv3 cap track** (message-tags/server-time/msgid/account/bot/oper/echo/labeled-response/CAP, `draft/metadata-2`, caller-id `+g`/`+G`, the **STS/TLS** sub-track); elemental channel modes + extbans; KNOCK; command-rate flood protection (fakelag); the **complete charybdis channel-mode set** (`+C/+c/+S/+j/+z/+T/+g/+r/+f/+Q/+F/+P/+L`, with `+L` the last standard one) and **user-mode set** (`+D/+R/+Q/+G/+z/+S/+l`, plus the product decision that leveva has no local-only umodes — `SEND_UMODES` is every umode); and the `+s` server-notice **snomask** (a charybdis category mask with a producer wired for every category — the matrix is complete). Most const limits (`max-penalty`/`max-bans`/`service-string`/`default-snomask`/…) have been promoted to REHASH-able `options { … }` knobs. **Full per-slice detail (scope, mechanism, divergences, gate) for every slice lives in [`docs/progress-log/p11.md`](docs/progress-log/p11.md); the standing testing plan is [`docs/superpowers/specs/2026-06-10-leveva-testing-plan.md`](docs/superpowers/specs/2026-06-10-leveva-testing-plan.md). This row stays thin.** | Per‑slice: leveva boots and serves the slice; leveva golden snapshot + proptest green; the `leveva-integration` differential agrees with `ircd-common` on the shared skeleton with divergences asserted. Across the eventual matrix: registration, channels, WHO/WHOIS/WHOX, OPER/STATS, server burst, netsplit, rehash, restart; iauth end‑to‑end. | 94 94 | **P12 — Retire the mechanical port; `leveva` is the product ✅ DONE (2026‑06‑13)** ([`docs/progress-log/p12.md`](docs/progress-log/p12.md)) | deleted `ircd-common`/`ircd-rs` + C‑era `iauth-rs` + `leveva-integration` + the mechanical port's deployment rigging; workspace = `leveva` + `leveva-iauth` | Ran the differential suite a **final time green** (`cargo test -p leveva-integration`, skeleton‑identical to the oracle), then **deleted the mechanical port** (`ircd-common`, `ircd-rs`), its C‑era auth scaffolding (`iauth-rs`, long since subsumed by the native `leveva-iauth`), and the oracle differential crate (`leveva-integration`). The oracle tests retired with their oracle — the load‑bearing behavior was already carried by leveva's self‑contained 212‑file golden + proptest suite (verified: leveva/leveva-iauth have zero Cargo/`use` dep on the deleted crates). Also stripped the mechanical port's **deployment rigging** (`docker/Dockerfile.ircd`, the `ircd` bake target, the entire `docs/k8s/` example network) and updated the README to describe the finished strangler. This is the literal end of the strangler — same lifecycle the C tree had at P8. | **MET. Final differential run green; `ircd-common`/`ircd-rs`/`iauth-rs`/`leveva-integration` deleted; `cargo build --workspace` 0 warnings; `cargo clippy --workspace --tests` clean; `cargo test -p leveva -p leveva-iauth` green — the behavioral guarantees carry forward. The workspace is now `leveva` + `leveva-iauth`.** | 95 95 96 96 ---
+44
docs/progress-log/p11.md
··· 15215 15215 **Plan:** [`docs/superpowers/plans/2026-06-19-p11-slice277-netsplit-batch-dedup.md`](../superpowers/plans/2026-06-19-p11-slice277-netsplit-batch-dedup.md) 15216 15216 15217 15217 **Gate:** `cargo test -p leveva` (squit + channel + golden netsplit/squit suites) green; `cargo clippy -p leveva --tests` clean; `cargo build -p leveva` 0 warnings. 15218 + 15219 + ## 2026-06-19 — P11 slice 278: `+R` auto-reop is enforced for local members only 15220 + 15221 + The `enforce_reop` chokepoint (`leveva/src/command/mod.rs`) — the single seam every auto-reop 15222 + trigger funnels through (JOIN, a local `+R`-set, an inbound S2S `+R` burst, a `CHGHOST` host 15223 + change, a CHANTS/NJOIN TS-merge) — could wrongly op a **remote** member. 15224 + 15225 + ### Mechanism 15226 + The doc-comment and every call site claimed `enforce_reop` "skips remote members", but the code 15227 + never filtered by locality: it resolved each candidate via `Registry::record_of` and skipped only 15228 + candidates that *lacked a registry record* (or were restricted `+r`). The locality guarantee 15229 + rested on the false premise that remote members have no registry record. A remote user introduced 15230 + over `UNICK` **does** get one (`s2s/unick.rs` → `Registry::try_claim`), so once a real remote user 15231 + was present, a `+R` reop mask matching its `nick!user@host` would (1) locally op that remote member 15232 + — diverging from its home server's authoritative op state — and (2) relay a server-sourced 15233 + `MODE +o <remote-uid>` onward, a `+o` we have no authority to emit. The merge/burst paths 15234 + (`s2s/mode.rs`, `s2s/chants.rs`, `s2s/njoin.rs`) all pass **all** members (local + remote) into 15235 + `enforce_reop`, so they are the realistic trigger. 15236 + 15237 + The fix adds an explicit `.filter(|u| ctx.is_local_uid(u))` to the candidate-resolution closure — 15238 + the one chokepoint that covers every trigger — making the long-standing comments true. The 15239 + restricted-`+r` skip and the rest of the reop machinery are unchanged. 15240 + 15241 + ### Divergences 15242 + None — leveva-native. Wire shapes unchanged; this only removes an erroneous local op-and-relay for 15243 + remote members. 15244 + 15245 + ### Tests 15246 + - `s2s::mode::tests::inbound_reop_mask_skips_remote_members` **strengthened** — the remote member 15247 + now carries a real `try_claim` registry record (modelling a `UNICK`-introduced user). RED before 15248 + the fix (the remote member got opped), green after. 15249 + - `command::tests::enforce_reop_ops_only_local_matching_members` (unit) — a `+R` mask matching both 15250 + a local (`0ABC…`) and a remote (`0XYZ…`) member, both with full registry records, ops the local 15251 + one only. Inverse `enforce_reop_skips_a_nonmatching_local_member` — a non-matching local member 15252 + is left un-opped (gate keys on the mask, not merely locality). 15253 + - `command::tests::enforce_reop_never_ops_remote` (**proptest fuzz**) — a random mix of local + 15254 + remote members (random hosts, random mask shape); no remote member is ever opped, the local 15255 + member is opped iff the mask matches. RED before the fix (minimal input `mask = *!*@a.aa`). 15256 + 15257 + **Plan:** [`docs/superpowers/plans/2026-06-19-p11-slice278-reop-local-only.md`](../superpowers/plans/2026-06-19-p11-slice278-reop-local-only.md) 15258 + 15259 + **Gate:** `cargo test -p leveva` (command + s2s::mode reop suites + `golden_s2s_reop` + 15260 + `golden_restricted_reop` + `golden_s2s_njoin_live`) green; `cargo clippy -p leveva --tests` clean; 15261 + `cargo build -p leveva` 0 warnings.
+58
docs/superpowers/plans/2026-06-19-p11-slice278-reop-local-only.md
··· 1 + # P11 slice 278 — `+R` auto-reop is enforced for **local** members only 2 + 3 + ## Problem 4 + 5 + The `enforce_reop` chokepoint (`leveva/src/command/mod.rs`) is the single seam every 6 + auto-reop trigger funnels through (JOIN, a local `+R`-set, an inbound S2S `+R` burst, a 7 + `CHGHOST` host change, a CHANTS/NJOIN TS-merge). Its doc-comment and every call site claim 8 + it "skips remote members — a remote member's reop is the responsibility of its own server." 9 + 10 + But the code never actually filtered by locality. It resolved each candidate via 11 + `ctx.registry.record_of(u)` and skipped only candidates that **lacked a registry record** or 12 + were restricted (`+r`). The locality guarantee rested on the false premise that remote members 13 + have no registry record. 14 + 15 + A remote user introduced over `UNICK` **does** get a registry record (`s2s/unick.rs` → 16 + `Registry::try_claim`). So once a real remote user is present, a `+R` reop mask matching its 17 + `nick!user@host` would: 18 + 19 + 1. locally op that remote member in our channel state (`apply_reop` sets `+o`), diverging from 20 + the member's home server's authoritative op state, and 21 + 2. relay a server-sourced `MODE +o <remote-uid>` onward (`server_channel_mode`) — a `+o` that 22 + should never originate from us. 23 + 24 + The merge/burst call paths (`s2s/mode.rs`, `s2s/chants.rs`, `s2s/njoin.rs`) pass **all** 25 + members (local + remote) into `enforce_reop`, so they are the realistic trigger. 26 + 27 + The existing `inbound_reop_mask_skips_remote_members` test only passed because it added the 28 + remote member via `njoin_member` **without** a registry record — not modelling production. 29 + 30 + ## Fix 31 + 32 + Add an explicit `ctx.is_local_uid(u)` filter to the candidate-resolution closure in 33 + `enforce_reop`. This makes the long-standing comments true and is the one chokepoint that 34 + covers every trigger. Update the doc-comment to state the locality filter as a real gate 35 + (not an accident of registry membership). 36 + 37 + ## Tests (TDD — RED first) 38 + 39 + 1. **Strengthen** `s2s::mode::tests::inbound_reop_mask_skips_remote_members` to `try_claim` 40 + a real registry record for the remote member (matching the mask). RED before the fix 41 + (the remote member gets opped), green after. 42 + 2. **Unit** `command::tests::enforce_reop_ops_only_local_matching_members`: a channel with a 43 + local member and a remote member (real registry records, both matching the same `+R` mask). 44 + `enforce_reop` ops the local one only; the remote stays un-opped. Inverse: a non-matching 45 + local member is not opped. 46 + 3. **Proptest** `command::tests::enforce_reop_never_ops_remote`: a random mix of local/remote 47 + members with random hosts and a random mask; assert no remote member is ever opped and a 48 + local member is opped iff the mask matches. Never panics. 49 + 50 + ## Gate 51 + 52 + `cargo test -p leveva` (command + s2s::mode reop suites + golden reop/merge) green; 53 + `cargo clippy -p leveva --tests` clean; `cargo build -p leveva` 0 warnings. 54 + 55 + ## Divergences 56 + 57 + None — leveva-native. The wire shapes are unchanged; this only removes an erroneous local 58 + op-and-relay for remote members.
+7
leveva/proptest-regressions/command/mod.txt
··· 1 + # Seeds for failure cases proptest has generated in the past. It is 2 + # automatically read and these particular cases re-run before any 3 + # novel cases are generated. 4 + # 5 + # It is recommended to check this file in to source control so that 6 + # everyone who runs the test benefits from these saved cases. 7 + cc 3e7d0dd4c25afc09d48374141a186ad183a78a7a4357a739b11e20ef7a88331f # shrinks to local_host = "a.aa", remote_host = "a.aa", mask_wild = false
+118 -9
leveva/src/command/mod.rs
··· 489 489 /// `MODE` line the **`actor`** should see synchronously is returned for the caller to 490 490 /// append to its replies. 491 491 /// 492 - /// Masks are resolved from the local [`Registry`](crate::registry::Registry) only — a 493 - /// remote member's reop is the responsibility of its own server, so non-local candidates 494 - /// are skipped here. 492 + /// Auto-reop is enforced for **local** members only — a remote member's reop is the 493 + /// responsibility of its own server, so non-local candidates are skipped here by an explicit 494 + /// [`is_local_uid`](crate::server::ServerContext::is_local_uid) check. (A `UNICK`-introduced 495 + /// remote user *does* hold a registry record, so locality cannot rest on the registry missing 496 + /// it — without the gate a `+R` mask matching a remote member's `nick!user@host` would wrongly 497 + /// op it locally and relay a server-sourced `+o` we have no authority to emit. Slice 278.) 495 498 /// 496 499 /// `actor` is the local client whose command triggered the reop (a JOIN, a `+R`-set, a 497 500 /// `CHGHOST`); it is excluded from the synchronous local fan-out because it receives the MODE ··· 504 507 candidates: &[Uid], 505 508 actor: Option<&Uid>, 506 509 ) -> Vec<Message> { 507 - // Resolve each local candidate's mask; remote members have no registry record → skipped. 508 - // A restricted (`+r` / `FLAGS_RESTRICT`) member is **also** skipped here: it can hold no 509 - // channel-operator status, so it is never auto-reopped — by whichever trigger reaches this 510 - // chokepoint (JOIN, a local `+R`-set, an inbound S2S `+R` burst, a CHGHOST host change, or a 511 - // CHANTS/NJOIN merge). This generalizes slice 193's join-site gate to every reop path 512 - // (slice 195). 510 + // Resolve each **local** candidate's mask; a remote member (a peer's SID prefix) is its own 511 + // server's reop responsibility and is skipped (slice 278). A restricted (`+r` / 512 + // `FLAGS_RESTRICT`) member is **also** skipped here: it can hold no channel-operator status, 513 + // so it is never auto-reopped — by whichever trigger reaches this chokepoint (JOIN, a local 514 + // `+R`-set, an inbound S2S `+R` burst, a CHGHOST host change, or a CHANTS/NJOIN merge). This 515 + // generalizes slice 193's join-site gate to every reop path (slice 195). 513 516 let resolved: Vec<(Uid, String)> = candidates 514 517 .iter() 518 + .filter(|u| ctx.is_local_uid(u)) 515 519 .filter_map(|u| { 516 520 ctx.registry.record_of(u).and_then(|r| { 517 521 if crate::mode::is_restricted(r.modes) { ··· 934 938 stats.iter().any(|s| s.command == "FLOOBLE"), 935 939 "unknown command is counted too: {stats:?}" 936 940 ); 941 + } 942 + 943 + // ----- slice 278: `enforce_reop` ops LOCAL members only ----- 944 + 945 + use crate::channel::{MemberStatus, ModeChange}; 946 + use crate::ident::Uid; 947 + use crate::server::ServerContext; 948 + use crate::ChanMode; 949 + use tokio::sync::mpsc::unbounded_channel; 950 + 951 + /// Seed a `+R` reop mask onto `chan` (trusted server path, no op/actor gate). 952 + fn set_reop(ctx: &ServerContext, chan: &str, mask: &str) { 953 + ctx.channels.apply_modes_as_server( 954 + chan, 955 + &[ModeChange::ListMask { 956 + add: true, 957 + mode: ChanMode::ReopList, 958 + mask: mask.into(), 959 + }], 960 + ); 961 + } 962 + 963 + /// Register a user with an explicit `uid` (so a remote SID prefix can be used) and a real 964 + /// registry record `nick!u@host`, then add it as a plain member of `chan`. 965 + fn member_with_uid(ctx: &ServerContext, uid_s: &str, nick: &str, host: &str, chan: &str) { 966 + let u = Uid::try_from(uid_s).unwrap(); 967 + ctx.registry 968 + .try_claim(&u, nick, "u", host, "real", unbounded_channel().0) 969 + .expect("free nick"); 970 + ctx.channels 971 + .njoin_member(chan, &u, MemberStatus::default(), 0); 972 + } 973 + 974 + /// A `+R` mask matching both a local (`0ABC…`) and a remote (`0XYZ…`) member ops the local 975 + /// one only — the remote member's reop is its home server's job, even though it carries a 976 + /// full registry record (as a `UNICK`-introduced user does in production). 977 + #[test] 978 + fn enforce_reop_ops_only_local_matching_members() { 979 + let ctx = ctx(); 980 + let local = Uid::try_from("0ABCAAAAA").unwrap(); 981 + let remote = Uid::try_from("0XYZAAAAA").unwrap(); 982 + member_with_uid(&ctx, local.as_str(), "alice", "h", "#c"); 983 + member_with_uid(&ctx, remote.as_str(), "rover", "h", "#c"); 984 + set_reop(&ctx, "#c", "*!*@h"); // matches both alice!u@h and rover!u@h 985 + 986 + let members = ctx.channels.members("#c").unwrap(); 987 + super::enforce_reop(&ctx, "#c", &members, None); 988 + 989 + assert!(ctx.channels.is_op("#c", &local), "local member is reopped"); 990 + assert!( 991 + !ctx.channels.is_op("#c", &remote), 992 + "remote member is never reopped by us" 993 + ); 994 + } 995 + 996 + /// Inverse — a local member whose mask does NOT match is left un-opped (so the gate keys on 997 + /// the mask, not merely on locality). 998 + #[test] 999 + fn enforce_reop_skips_a_nonmatching_local_member() { 1000 + let ctx = ctx(); 1001 + let local = Uid::try_from("0ABCAAAAA").unwrap(); 1002 + member_with_uid(&ctx, local.as_str(), "alice", "h", "#c"); 1003 + set_reop(&ctx, "#c", "*!*@trusted.example"); // no match for alice!u@h 1004 + 1005 + let members = ctx.channels.members("#c").unwrap(); 1006 + super::enforce_reop(&ctx, "#c", &members, None); 1007 + assert!(!ctx.channels.is_op("#c", &local), "no match → no reop"); 1008 + } 1009 + 1010 + use proptest::prelude::*; 1011 + 1012 + proptest! { 1013 + #![proptest_config(ProptestConfig::with_cases(96))] 1014 + 1015 + /// Fuzz the locality gate: a random mix of local + remote members (real registry 1016 + /// records, random hosts) meets a random `+R` mask. No **remote** member is ever opped; 1017 + /// a **local** member is opped iff the mask matches its `nick!u@host`. Never panics. 1018 + #[test] 1019 + fn enforce_reop_never_ops_remote( 1020 + local_host in "[a-z]{1,6}\\.[a-z]{2,4}", 1021 + remote_host in "[a-z]{1,6}\\.[a-z]{2,4}", 1022 + mask_wild in any::<bool>(), 1023 + ) { 1024 + let ctx = ctx(); 1025 + let local = Uid::try_from("0ABCAAAAB").unwrap(); 1026 + let remote = Uid::try_from("0XYZAAAAB").unwrap(); 1027 + member_with_uid(&ctx, local.as_str(), "lalice", &local_host, "#c"); 1028 + member_with_uid(&ctx, remote.as_str(), "rrover", &remote_host, "#c"); 1029 + // `*` matches everyone; `*!*@<local_host>` matches only the local member. 1030 + let mask = if mask_wild { "*".to_string() } else { format!("*!*@{local_host}") }; 1031 + set_reop(&ctx, "#c", &mask); 1032 + 1033 + let members = ctx.channels.members("#c").unwrap(); 1034 + super::enforce_reop(&ctx, "#c", &members, None); 1035 + 1036 + prop_assert!( 1037 + !ctx.channels.is_op("#c", &remote), 1038 + "a remote member is never reopped (mask {mask})" 1039 + ); 1040 + // The mask was chosen to always match the local member, so it is always opped. 1041 + prop_assert!( 1042 + ctx.channels.is_op("#c", &local), 1043 + "the local member matches the mask and is reopped (mask {mask})" 1044 + ); 1045 + } 937 1046 } 938 1047 }
+11 -4
leveva/src/s2s/mode.rs
··· 407 407 } 408 408 409 409 /// A **remote** member (homed behind the peer) is never reopped by us on an inbound `+R` — its 410 - /// own server owns that. Only our locals are opped here. 410 + /// own server owns that. Only our locals are opped here. The remote member carries a **real 411 + /// registry record** (as one introduced over `UNICK` does in production, slice 278): the 412 + /// locality gate must be an explicit `is_local_uid` check, not an accident of the registry 413 + /// missing the remote user. 411 414 #[test] 412 415 fn inbound_reop_mask_skips_remote_members() { 413 416 let ctx = ctx(); 414 417 let mut link = peer(); 415 418 link.end_burst(); 416 - // A remote member (SID 0XYZ) — no local registry record. 419 + // A remote member (SID 0XYZ) WITH a registry record matching the `*` mask. 420 + let remote = uid("0XYZAAAAA"); 421 + ctx.registry 422 + .try_claim(&remote, "rover", "u", "h", "real", unbounded_channel().0) 423 + .expect("free remote nick"); 417 424 ctx.channels 418 - .njoin_member("#c", &uid("0XYZAAAAA"), MemberStatus::default(), 0); 425 + .njoin_member("#c", &remote, MemberStatus::default(), 0); 419 426 handle_mode(&mut link, &mode(":0XYZ MODE #c +R *"), &ctx); 420 427 assert!( 421 - !ctx.channels.is_op("#c", &uid("0XYZAAAAA")), 428 + !ctx.channels.is_op("#c", &remote), 422 429 "a remote member is its own server's reop responsibility" 423 430 ); 424 431 }