Summary
When a LIST pauses because the client's sendq is over half full, the resumed tick re-sends the whole hash bucket it stopped on, so the client receives duplicate RPL_LIST (322) entries.
Where
ircd/hash.c, list_next_channels() (main, lines 436-472):
for (args = cli_listing(cptr); args->bucket < HASHSIZE; args->bucket++)
{
/* Send all the matching channels in the bucket. */
for (chptr = channelTable[args->bucket]; chptr; chptr = chptr->hnext)
{
...
send_reply(cptr, RPL_LIST, chptr->chname, chptr->users, chptr->topic);
...
}
/* If, at the end of the bucket, client sendq is more than half
* full, stop. */
if (MsgQLength(&cli_sendQ(cptr)) > get_sendq(cptr) / 2)
break;
}
The break fires after the bucket has been fully sent but before the loop's args->bucket++. When s_bsd.c later calls list_next_channels() again on a writable socket, the walk restarts at the same args->bucket and every matching channel in it is sent a second time.
Fix
Advance past the completed bucket before pausing:
if (MsgQLength(&cli_sendQ(cptr)) > get_sendq(cptr) / 2)
{
args->bucket++;
break;
}
Reproduction
Observed while testing on a fork, on a connection whose kernel write actually blocked (client not reading, small SO_RCVBUF, connected directly rather than through a userspace proxy), with 1500 channels created over a server link and a class with sendq = 3000. The LIST reply contained 1552 322 lines: 1500 distinct channels and 52 repeats. The count of repeats is the number of channels in the buckets the listing paused on.
Note that on an ordinary connection this rarely shows: send_buffer() flushes to the kernel every 1KB, so the userspace sendq only crosses sendq/2 when the socket write itself blocks. Large LIST replies to slow clients are exactly the case where it will.
Summary
When a
LISTpauses because the client's sendq is over half full, the resumed tick re-sends the whole hash bucket it stopped on, so the client receives duplicateRPL_LIST(322) entries.Where
ircd/hash.c,list_next_channels()(main, lines 436-472):The
breakfires after the bucket has been fully sent but before the loop'sargs->bucket++. Whens_bsd.clater callslist_next_channels()again on a writable socket, the walk restarts at the sameargs->bucketand every matching channel in it is sent a second time.Fix
Advance past the completed bucket before pausing:
Reproduction
Observed while testing on a fork, on a connection whose kernel write actually blocked (client not reading, small
SO_RCVBUF, connected directly rather than through a userspace proxy), with 1500 channels created over a server link and a class withsendq = 3000. TheLISTreply contained 1552322lines: 1500 distinct channels and 52 repeats. The count of repeats is the number of channels in the buckets the listing paused on.Note that on an ordinary connection this rarely shows:
send_buffer()flushes to the kernel every 1KB, so the userspace sendq only crossessendq/2when the socket write itself blocks. LargeLISTreplies to slow clients are exactly the case where it will.