Channel rename support: RN handler, AC R rename arbitration, registered channels marked +R - #57
Draft
MrLenin wants to merge 15 commits into
Draft
Channel rename support: RN handler, AC R rename arbitration, registered channels marked +R#57MrLenin wants to merge 15 commits into
MrLenin wants to merge 15 commits into
Conversation
Factor the GetUserN numeric-to-userNode lookup into a static GetUserN_impl(numeric, quiet) helper, expose both the original GetUserN (noisy — keeps WARNING for every failure mode) and a new GetUserN_silent (suppresses all warnings on lookup miss). Flip the BX P handler's two probe lookups (cmd_bouncer_transfer: both old_primary and new_node) to the silent variant. Probing is the whole point — the old client may already be gone, and new_node is normally absent in the swap-path case. Command handlers and protocol parsers (opserv command targets, FAKEHOST/MARK/etc. dispatchers, channel-mode victims) keep using the noisy GetUserN — a miss there is a real protocol bug or config issue worth surfacing. mod-snoop / mod-track stay on the noisy variant too (those modules are off by default and operators who enable them want the misses). Symptom this addresses: noisy networks running the fork's bouncer subsystem were seeing repeated x3 warning: GetUserN(GkAAp) couldn't find user! snotices to O3 channels from the BX P probe path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The original cmd_bouncer_transfer BX P handler logged a warning and bailed when both old_primary and new_node existed locally. Modern fork peers can emit BX P in that exact shape when an N-introduced client is being absorbed into an existing primary on the same account — typically because we received the would-be-alias's N before its BX C in a burst, so both userNodes landed in our tables. Add a merge branch gated on same-handle: if both nodes exist AND share handle_info, delete old_primary's userNode via DelUser(announce=0) — channels, dict entry, oper list, etc. all get cleaned up by the standard DelUser path without a network QUIT/KILL. The surviving identity is new_node. The mismatched-handle case keeps the original "log and ignore" behaviour — we don't silently merge across unrelated accounts. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement Task 1 of the channel-rename foundation: RenameChannel() primitive and rename hook array in X3's hash layer. The implementation follows the existing del-channel hook pattern (dcf_list): - reg_channel_rename_func() registers rename handlers with optional context - Hook array (crf_list) and extra-data list (crf_list_extra) with dynamic growth on first use (8-entry initial, doubling on capacity exhaustion) - RenameChannel(old_node, new_name) creates the new node with all state copied, updates dict keys, then fires hooks with BOTH nodes alive Key design constraint: old node's dict key is an interior pointer to name[]; removal must happen before free(). Hooks receive both nodes because pointer-compare holders need old node address while name-keyed dicts need old->name intact for any final cleanup lookups. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task 2 of the channel-rename plan: each module that keeps a persistent struct chanNode * now registers a channel_rename_func_t hook (Task 1, 83af825) next to its existing reg_del_channel_func/reg_new_channel_func call, so a RenameChannel() call re-points every holder instead of leaving it dangling on the freed old node. - chanserv.c: new_chan->channel_info->channel back-pointer (the memcpy in RenameChannel moves channel_info onto new_chan but leaves chanData->channel targeting old_chan); walks adduser_pendings; swaps matches in chanserv_conf.support_channels. - opserv.c: compare-swap debug_channel/alert_channel/staff_auth_channel; walks opserv_user_alerts re-pointing each alert's discrim->channels[0..channel_count); removes the pending opserv_part_channel purge-lock timer for old_chan without re-adding (mirrors opserv_channel_delete, but ignores func too since the node is about to be freed); recomputes bad_channel on the new node. - spamserv.c: follows spamserv_cs_move_merge's existing chanInfo re-point + registered_channels_dict re-key pattern; additionally walks every connected_users_dict entry's spam/flood/joinflood node chains re-pointing ->channel. - mod-helpserv.c: walks helpserv_bots_dict re-pointing hs->helpchan and each hs->page_targets[PGSRC_COUNT]; re-keys the single helpserv_bots_bychan_dict entry for the renamed channel (key is the interior helpchan->name pointer). - mod-snoop.c, mod-track.c, mod-blacklist.c: single compare-swap of each module's channel config slot. mod-blacklist.c has a pre-existing compile break unrelated to this change (reg_new_user_func/reg_exit_func called with too few args, predating this commit) that already excludes it from the configured --enable-modules set; the new hook and its registration call are correctly typed and were verified with a standalone compile of just those lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The legacy P10 "AC <target> R <account>" stamp and the new ircd rename
permission query "AC <unum> R <cookie> <#chan> RENAME <new>" both use
subcommand R, and both send the SAME low argc/argv[3] positioning that
made them collide: naively treating every R as an account stamp would
poison the account cache with a cookie or a channel name instead of a
handle. Disambiguate on shape (argc >= 7 && argv[5] == "RENAME") before
falling through to the unchanged legacy call_account_func() path.
The rename branch replies with the cookie as parv[1] ("AC <cookie> A"
or "AC <cookie> D :<reason>"), NOT the LOC reply shape ("AC <servnum>
A <cookie>") — ircd's m_account.c keys pending renames on parv[1]
failing a server-numeric lookup, so the cookie must lead.
Authorization policy (chanserv_rename_allowed, chanserv.c/.h): owner
(UL_OWNER, access 500) only, mirroring cmd_move's DNR-against-the-new-
name gate, minus the IsHelping/"force" bypass — staff override here
comes only from _GetChannelUser()'s override=1 synthetic access entry.
Also denies on unauthenticated requester, protected/suspended source
channel, a blocked or already-registered new name, and unregistered
target channels are allowed through untouched (nothing to protect).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…name Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…annelName) IsChannelName() has no length ceiling, allowing overlong channel names to enter fixed-size sprintf buffers in hash.c and chanserv.c. Added length validation in RenameChannel() and chanserv_rename_allowed() to reject names exceeding CHANNELLEN (200 bytes). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…RED z->R) X3's MODE_REGISTERED bit (hash.h, comment "Bahamut +r") was wired to the channel-mode letter 'z' in the P10 parser/formatter. Two problems made this dead weight: - On the nefarious fork, '+z' means the persist exmode -- an unrelated server-settable mode. X3 was misreading incoming '+z' from the ircd as a MODE_REGISTERED toggle, and would happily set/clear its own registration bit off a mode letter the fork uses for something else. - Announcing MODE_REGISTERED at all was gated behind `off_channel > 0` in every call site that set it (register, unregister, move, DB-load), and the deployed conf runs off_channel=no. So X3 never actually put +R (or, previously, +z) on the wire in the first place. Both nefarious and nefarious-upstream's m_rename only ask services for permission to rename a channel when it carries MODE_REGISTERED (channel.c:2402/2113 -- server-settable only, MODE_PARSE_FORCE required). With X3 never setting the bit, the whole services-arbitration path for channel rename was unreachable. Fix, at the root: - Remap the wire letter from 'z' to 'R' everywhere MODE_REGISTERED is parsed or emitted (mod_chanmode_parse, mod_chanmode_announce, mod_chanmode_format, clear_chanmode/CLEARMODE, the cmd_burst registration-correction path). 'z' is no longer a recognized channel mode letter in X3 at all -- it now falls through to the same "unrecognized letter" handling every other unknown letter already gets (silently ignored from server origin, rejected from user-typed strings), so an incoming fork +z persist exmode is simply left alone instead of being misread. - Announce +R/-R unconditionally on registration, unregistration, and channel move, instead of gating it behind off_channel (which governs only whether ChanServ itself occupies the channel, not whether the ircd should know the channel is registered). Also unconditional in the saxdb channel-load path for channels with a stored modelock. - Self-heal in handle_join(): if a registered, non-suspended channel's ircd-side modes don't carry +R, re-announce it. Covers ircd restarts, X3 restarts racing the first post-restart JOIN, and channel re-creation. Not gated on burst state (unlike the nearby dynamic-limit and automode/greeting guards) since it sends no message and touches no timer -- mod_chanmode_announce applies the change to local state immediately, so only the first joiner per convergence gap triggers the wire MODE. MCP_REGISTERED's guard semantics (block MODE_REGISTERED toggles from the two user-facing ChanServ mode-string paths, cmd_mode and the SET MODES modelock) are unchanged -- only the letter moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wipeout_channel() (called from AddChannel() whenever an incoming BURST's channel timestamp predates X3's own -- the normal case on X3 restart, since chanserv_channel_read() creates placeholder chanNodes at timestamp=now before the real BURST arrives) reparsed the ircd's burst mode string via mod_chanmode(NULL, cNode, modes, modec, 0), missing MCP_FROM_SERVER. It was the only call site parsing server-origin mode data without that flag; AddChannel()'s own two mod_chanmode() calls already pass it. Without MCP_FROM_SERVER, mod_chanmode_parse()'s default case aborts the whole parse on the first mode letter it doesn't recognize, leaving cNode->modes stranded at the zero wipeout_channel just set -- losing every mode, not just +R. This fires on the ordinary X3-restart path, and in particular for any channel still carrying a stray +z on the live ircd side (the fork's persist exmode, previously planted there by X3's old z-as-registered announce before 129a0d8) -- exactly the population the +R remap needed to heal. Fix: pass MCP_FROM_SERVER, the semantically correct flag for burst data, so an unrecognized letter is skipped instead of aborting the parse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
reg_channel_rename_func(chanserv_channel_rename, NULL) was registered
inside init_chanserv()'s if(nick) block, but saxdb_register("ChanServ",
...) is unconditional. With the bot nick disabled (the "." convention),
registered channels still load from the DB via chanserv_saxdb_read, so
channel_info exists and is live -- but with the rename hook never
registered, an RN arriving over the wire runs RenameChannel() with no
callback to repoint channel_info->channel at the new node. It's left
pointing at the chanNode RenameChannel() just freed: a use-after-free at
the next saxdb write (or any other access through channel_info->channel).
chanserv_channel_rename() only touches channel_info, adduser_pendings,
and chanserv_conf.support_channels -- none of which depend on the bot
nick being enabled -- so it's safe to register unconditionally. Moved it
out of if(nick), alongside the other already-unconditional hook
registrations (reg_handle_rename_func, reg_unreg_func).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cmd_bouncer_transfer's same-handle merge branch (BX P where new_node already exists with old_primary's handle_info) called DelUser on old_primary without first moving its channel memberships to new_node. Both nefarious implementations of the both-exist case transfer memberships before killing the old node (upstream m_bouncer_transfer.c:88-113, fork bouncer_session.c:7070-7078); X3's merge branch skipped that step. Reachable in steady state via cross-server BOUNCER RESUME: old_primary is a ghost holding all of the session's channels, new_node is a fresh client with none. DelUser's own channel-removal loop calls DelChannelUser(..., NULL, 0), and DelChannelUser's tail destroys an unregistered channel when the removal empties it. With no prior transfer, any unregistered channel where old_primary was the only member from X3's point of view got destroyed mid-merge, even though the surviving identity (and the rest of the network) was still in it. Fix: before DelUser, walk old_primary's channel list the same way DelUser's own loop does (always pop the last element, since Add/DelChannelUser mutate both the channel's member list and the user's channel list). For each channel, add new_node with the same modes/oplevel if it isn't already a member, then remove old_primary with no announce. Doing this before DelUser means old_primary's channel list is already empty when DelUser runs, so its loop body never executes and never risks the auto-destroy path. AddChannelUser unconditionally fires call_join_funcs (chanserv join-time hooks) -- there's no hook-free "just add the membership" primitive, and the numeric-swap path above never touches channel membership at all so there's no hook-free both-exist precedent to follow instead. Accepted as a known side effect since membership state correctness is the priority; it does not emit anything on the wire (irc_join only fires for IsLocal users, i.e. X3's own service bots, never for a bounced network client). Applies the PR evilnet#56 review fix (fix/bx-p-merge-and-silent-probes) on this stacked branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Change 1: irc_server() now emits +s6or (was +s6o) on both the initial J10 self-burst line and the relay P10/J10 line, advertising the ircu upstream-standard r (rename-capable) SERVER flag so an r-aware ircd delivers RN tokens to this link. No-op on the fork, which routes channel-rename traffic via IsService rather than the r flag. Change 2 (F2): the AC rename-permission reply (cmd_account, the R subtype) now carries an explicit RENAME discriminator token right after the A/D type: "AC <unum> A RENAME" / "AC <unum> D RENAME :<reason>". Previously the reply shape was indistinguishable on the wire from an AC LOC reply once the receiving ircd fell through its FindNServer() check — a decimal rename cookie could alias a live server numeric and get misrouted. The discriminator lets ms_account() on both ircd trees route by cookie unconditionally, without ever calling FindNServer() on the rename path. Companion ircd-side changes land in the nefarious and nefarious-upstream repos (m_account.c), same commit message, same branch strategy (feature/backport channel-rename); all three must ship together since X3 now always emits the RENAME token. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Walk back the letter half of 129a0d8: X3 MUST keep emitting 'z' as the registered-channel mode letter for now (user decision 2026-08-01; see the testnet registered-mode z->R transition plan). The announce-gating half of that commit (off_channel un-gating, handle_join self-heal) stays -- it was the load-bearing fix; with off_channel=no, X3 had never emitted ANY registered marker, which left every MODE_REGISTERED-gated fork feature (rename arbitration, chathistory store-registered, metadata persistence) silently dead. - Emit conservatively: all four mod_chanmode format sites emit 'z'. - Parse liberally: both 'z' and 'R' read as MODE_REGISTERED (the fork bursts +R alongside +z once its persist-mirror sets the bit). - CLEARMODE parse accepts both; the cmd_burst unregistered-correction strips -zR. On nefarious, 'z' is the persist exmode (services-settable only), so X3 marking registered channels +z doubles as the empty-channel keep-alive those channels want -- the semantics off_channel>0 networks always had. The ircd-side companion (FEAT_REGISTERED_FROM_PERSIST mirror) makes the fork derive MODE_REGISTERED from services 'z'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Supersedes the previous letter dance (z->R remap in 129a0d8, then the z-primary walk-back): final decision keeps BOTH letters with their true meanings, orthogonal from day one: - 'R' = MODE_REGISTERED, the ircd's registration marker. X3 emits it unconditionally on register/unregister/move/DB-load (the off_channel un-gating and handle_join self-heal from 129a0d8 stand). - 'z' = new MODE_PERSIST, nefarious's persist exmode, used as originally intended: set alongside +R only when off_channel>0 -- exactly the case where no ChanServ presence holds the registered channel open while empty. Cleared (with R) on unregistration so the emptied channel can destruct; moved on cmd_move; healed by handle_join. Parse side: 'z' now reads as MODE_PERSIST (never MODE_REGISTERED), with the same MCP_REGISTERED guard as 'R' so user-typed modelocks can't toggle server-managed markers. CLEARMODE accepts both letters. The cmd_burst correction for channels we don't recognize strips both (-zR). No ircd-side companion needed: the fork's MODE_REGISTERED machinery (rename arbitration, chathistory store-registered, metadata persistence) keys off the R that X3 now actually emits. The FEAT_REGISTERED_FROM_PERSIST mirror has been reverted from both ircd trees. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MrLenin
force-pushed
the
feature/channel-rename
branch
from
August 1, 2026 06:38
b44a20b to
1d80d7d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds services-side support for IRCv3 draft/channel-rename against nefarious2's services-arbitration machinery. Draft: functionally blocked on the upstream nefarious2 patch that emits the rename query and RN token — the X3 side is complete and E2E-verified against the fork carrying that machinery.
What this does
RenameChannel()primitive + rename hook array (hash.c/h): re-keys a live channel under a new name — new tail-allocated node, members/banlists moved with backpointers re-pointed,channelsdict re-keyed (the key is an interior pointer into the node), locks inherited, per-module hooks (reg_channel_rename_func) fired with both nodes alive. Hooks added for chanserv, opserv, spamserv, helpserv, snoop, track, blacklist covering every persistentchanNode*holder (audited exhaustively).Rdisambiguation (proto-p10.c): the rename permission queryAC <unum> R <cookie> <#chan> RENAME <new>is now distinguished from the legacy account stamp (argc>=7 +RENAMEkeyword). This also fixes a real bug: the stamp path previously consumed the query's cookie as an account stamp, silently discarding the requesting user's legitimate stamp for the session.chanserv_rename_allowed): denies below UL_OWNER, protected/suspended channels, bad/DNR'd/already-registered/overlong target names; repliesAC <cookie> A/AC <cookie> D :<reason>.rename_dnr_duration, default 1d) so it can't be re-registered out from under the rename.+Ron the wire: X3'sMODE_REGISTEREDletter was the Bahamut-eraz— which nefarious2 doesn't use for registered. Remapped toR(nefarious2's actual server-settable registered mode), announced unconditionally at register/unregister/move/db-load with heal-on-join. Without this, no ircd-side registered-channel logic could ever trigger.wipeout_channelnow parses burst modes as server-origin (an unknown mode letter previously aborted the whole parse, stranding a channel's mode mirror at zero on every X3 restart); NULL-bot guards; CHANNELLEN cap on rename targets (IsChannelNamehas no length ceiling).Verification
Live E2E on the Afternet testnet against the fork: owner rename approved and applied with X3's registration following (INFO/ACCESS/MYACCESS track the new name), op-level user denied with the owner-only reason surfaced to the client, account stamp intact after a denied query, old name DNR-refused post-rename. Vitest suite (6 cases) in the testnet repo.
🤖 Generated with Claude Code