enqueue_otr_fragment() reassembles ?OTR: multi-fragment OTR messages
into a per-peer heap buffer (opc->full_msg), grown by realloc on every
fragment that does not end with the OTR end tag '.'. The buffer has no
total size limit.
A remote IRC user can open a reassembly by sending a query PRIVMSG whose
body begins with ?OTR: and lacks the trailing '.', then keep sending
further PRIVMSGs: each is appended to opc->full_msg (roughly one
IRC-line-length worth of bytes per fragment) with no bound, growing it
until the victim irssi is OOM-killed. No OTR session is required -- the
peer context is created lazily by otr_find_context(..., create=1) on
first contact in otr_receive(), and sig_message_private
(signal_add_first on "message private") feeds every query PRIVMSG
straight to otr_receive()/enqueue_otr_fragment().
Add an OTR_REASM_MAX_SIZE (256 KiB) cap on the total reassembled size.
When appending a fragment would exceed it, free and reset the
reassembly state and return OTR_MSG_ERROR, on both the
subsequent-fragment and initial-fragment paths. 256 KiB is far above
any legitimate OTR message, so there is no functional regression.
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.