fix: bound RTP header offsets in voice_courier_loop to packet size - #1635
fix: bound RTP header offsets in voice_courier_loop to packet size#1635Mounika2456 wants to merge 2 commits into
Conversation
✅ Deploy Preview for dpp-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
are these the result of real usage? or a theoretical output? |
|
I’m not accepting this in its current form. The stated issue only requires a small bounds check, but this patch changes types, moves unrelated logic and introduces a new parked_payloads.pop() path without explaining the queue semantics. Please reduce it to the minimal validation necessary, preserve the existing control flow, and provide evidence that normal voice receive and DAVE receive were tested. |
24f87d6 to
601beef
Compare
|
Fair, that was too invasive. I've cut it down to just the two length checks and left everything else alone: The ASan numbers aren't from live traffic. They're from a small harness that runs this function's header block verbatim against the shortest datagram A well-formed packet takes the identical path, so normal receive is unaffected, and DAVE runs after the AEAD step on the decrypted |
|
the unit tests don't test and can't test voice receive. but if discord isn't sending this, and can't send this, how could we test it properly? making arbitrary shims to test data discord never sends (always one csrc) seems like it isn't going to get us much, you're testing things the library doesn't support. |
|
Agreed on the test side, a shim that fakes csrc counts isn't worth adding and I'm not asking to add one. The unittest can't reach voice receive, so that's not where this belongs. Where I think the framing slips is "discord isn't sending this, and can't send this." The bytes that drive offset_to_data don't have to come from Discord. The voice UDP socket is created unconnected (bound to INADDR_ANY, no connect()) and read with recv(), and recv() on an unconnected socket takes a datagram from any source that can reach the port. read_ready() only rejects packet_size < 44 and the RTCP payload types, so a crafted 44 byte datagram with the first byte set to 0x1F reaches the courier, and all of this header math runs before the Poly1305 tag is checked. So the csrc_count of 15 isn't Discord sending something odd, it's an unauthenticated sender setting four bits. The guard isn't trying to support multi-csrc packets. For a real Discord packet (one csrc, header fits) the check is always true and the path is byte for byte identical, nothing changes. It only fires when the header the first byte declares doesn't fit in the bytes that actually arrived, which is exactly the case that underflows ciphertext_len and reads the extension length off the end of the vector today. It's the same shape as the existing packet_size < 44 reject, just covering the offset the csrc bits can push it to. |
|
and how is the unauthenticated sender going to know which port or your bots ip? what about nat? and even then, what will they do? I dont see a crash here. |
|
On the crash: it's a heap-buffer-overflow read, not just odd numbers. With csrc_count 15 the extension-length read at You're right on reachability though. An attacker needs the bot's public IP and the current ephemeral UDP port, and has to land a datagram through NAT, so this isn't spray-the-internet and I won't pretend it's high severity. Where I'd still place it is robustness: it's the same shape as the existing |
|
sure but as before, we can't merge anything until the boxes are ticked in the pr description and the cla contributor covenant is done. |
|
Makes sense. I'll get the CLA signed through cla-assistant today so that check clears. On the checklist, four of the five boxes have been ticked since the PR went up; the one I left open is the tooling attestation, since I did use ASan and a bit of light tool assistance for research and sanity checks while running this down, and I didn't want to tick it as an absolute. The diff itself was written and tested by hand, and the guard's behavior on valid packets is as described above. |
braindigitalis
left a comment
There was a problem hiding this comment.
changes needed, see comments
| */ | ||
| if (ciphertext_len < sizeof(uint16_t) * 2) { | ||
| /* Invalid Discord RTP payload. */ | ||
| return; |
There was a problem hiding this comment.
this actually causes the whole courier thread to terminate! this hasn't been tested.
There was a problem hiding this comment.
Same fix here, this one now pops and continues too rather than returning. So a packet whose extension header doesn't fit gets dropped and the courier keeps draining the rest of the queue.
| */ | ||
| if (packet_size < static_cast<size_t>(offset_to_data) + nonce_size) { | ||
| /* Invalid Discord RTP payload. */ | ||
| return; |
There was a problem hiding this comment.
causes thread termination.
There was a problem hiding this comment.
Good catch, that return was wrong. Swapped both guards to pop the offending payload and continue, so the malformed packet is dropped and the loop moves on to the next one instead of tearing down the thread. It matches the pop() at the end of the normal iteration.
There was a problem hiding this comment.
have you any way of testing this with real discord clients
Signed-off-by: Mounika HJ <mounika@digiscrypt.com>
|
Pushed the fix for both guards. To confirm the thread doesn't die, I ran the courier's inner drain loop with a two-entry queue: seq 0 is the malformed 44-byte packet (first byte 0x1F, so offset_to_data works out to 72) and seq 1 is a well-formed one. Under ASan seq 0 is now popped and skipped by the first guard with no out-of-bounds read, the loop keeps going, and seq 1 is processed with the same offsets as before. Library builds clean with the same flags and the offline unittest is still Failed: 0 Passed: 64. |
ASan, replaying the courier's header block against a 44 byte datagram whose first byte is 0x1F (extension bit set, CSRC count 15), the shortest packet
read_ready()forwards:offset_to_datais12 + 4 * csrc_countand reaches 72, whileread_ready()only guarantees 44 bytes, sociphertext_lenwraps, the extension length is read off the end of the vector holding the datagram, andtotal_header_lenwalks the AEAD past that same allocation for its AAD. The voice UDP socket is bound without aconnect()and read withrecv(), so the sender is any host that can reach the port, and none of this sits behind the Poly1305 check. Drop the packet when the header its first byte describes does not fit in what actually arrived.Code change checklist