In enqueue_otr_fragment(), once a ?OTR: reassembly is open, each
continuation fragment is appended to opc->full_msg and the buffer is
grown only if there isn't enough room:
if (msg_len > (opc->msg_size - opc->msg_len)) { realloc(...); }
memcpy(opc->full_msg + opc->msg_len, msg, msg_len);
opc->msg_len += msg_len;
opc->full_msg[opc->msg_len] = '\0';
The comparison uses '>' instead of '>='. When a fragment's length is
exactly equal to the remaining space (opc->msg_size - opc->msg_len),
the condition is false, so no realloc happens; the memcpy itself still
fits, but the following NUL-terminator write at
opc->full_msg[opc->msg_len] lands exactly one byte past the end of the
allocation, corrupting the adjacent heap chunk.
This is remotely reachable the same way as the other reassembly bugs
in this file: any user who can send the victim a private message can
drive the running remaining-space counter to land on an exact match
(remaining space grows by a small, attacker-observable amount on every
realloc, and fragment lengths are fully attacker controlled), then send
one more fragment of that exact length to trigger the overflow.
Fix the comparison to '>=' so the buffer is grown whenever there isn't
room for both the fragment bytes and the terminator.
In enqueue_otr_fragment(), after a ?OTR: reassembly has been opened
(opc->full_msg != NULL), the end-tag check evaluates
msg[msg_len - 1] where msg_len is size_t. If a PRIVMSG whose body
decodes to an empty string reaches this path, msg_len is 0 and
msg_len - 1 wraps to SIZE_MAX, indexing far off the page and crashing
irssi (SIGSEGV). The initial-fragment path is safe because it is gated
by 'pos &&' (strstr of an empty string returns NULL), but the
open-reassembly path has no such guard.
recode_in() returns a non-NULL empty string for an empty PRIVMSG body,
and sig_message_private (signal_add_first on "message private") feeds
it straight to otr_receive()/enqueue_otr_fragment(). Relays/bouncers
such as bitlbee/znc and some ircds pass PRIVMSGs with an empty trailing
parameter through to the client.
Drop an empty fragment early: if a reassembly is open, keep waiting for
more (OTR_MSG_WAIT_MORE); otherwise treat it as an original message
(OTR_MSG_ORIGINAL), matching the existing behaviour of the else branch.