HexBrain

curl · Report #3575250

RTSP RTP Interleaved Parser Assertion Failure (Zero-Length RTP Payload)

Informativestatus
28upvotes
26 Feb 2026disclosed

How it was found

Summary:

I am submitting this as a security issue primarily due to how it was discovered and that it's my first Curl submission, but I suspect I might be overly cautious here.

This issue was discovered as part of the AIXCC competition, and I am assisting on reporting true positive findings to upstream repositories, facilitated by way of of OpenSSF and OSTIF.

AI was used to find and root-cause this issue.

The issue is that a debug assertion (https://github.com/curl/curl/blob/3cf86508fdc3f54bb2a3f42c8c0bd464ea39883d/lib/rtsp.c#L784) is triggerable from the curl_fuzzer_rtsp (build from OSS-Fuzz). When I disable that specific assertion and run the crashing input, it passes without issues so I suspect release mode is fine.

Affected version

latest commit on GitHub.

Steps To Reproduce:

# 1. Clone oss-fuzz (or use existing checkout)
git clone https://github.com/google/oss-fuzz.git
cd oss-fuzz

# 2. Build the curl fuzzers (requires Docker)
python3 infra/helper.py build_fuzzers curl

# 3. Replay against the attached payload
python3 infra/helper.py reproduce curl curl_fuzzer_rtsp /tmp/curl_crash_input.bin
...
Running: /testcase
curl_fuzzer_rtsp: /src/curl/lib/rtsp.c:784: CURLcode rtsp_filter_rtp(struct Curl_easy *, struct rtsp_conn *, const char *, size_t, size_t *): Assertion `rtp_len < rtspc->rtp_len' failed.
AddressSanitizer:DEADLYSIGNAL
=================================================================
==14==ERROR: AddressSanitizer: ABRT on unknown address 0x00000000000e (pc 0x7fde2c69eb2c bp 0x7fff96291630 sp 0x7fff962915f0 T0)
SCARINESS: 10 (signal)
    #0 0x7fde2c69eb2c in pthread_kill (/lib/x86_64-linux-gnu/libc.so.6+0x9eb2c) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
    #1 0x7fde2c64527d in raise (/lib/x86_64-linux-gnu/libc.so.6+0x4527d) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
    #2 0x7fde2c6288fe in abort (/lib/x86_64-linux-gnu/libc.so.6+0x288fe) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
    #3 0x7fde2c62881a  (/lib/x86_64-linux-gnu/libc.so.6+0x2881a) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
    #4 0x7fde2c63b516 in __assert_fail (/lib/x86_64-linux-gnu/libc.so.6+0x3b516) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
    #5 0x5564d89cb51e in rtsp_filter_rtp /src/curl/lib/rtsp.c:784:7
    #6 0x5564d89c8c8f in rtsp_rtp_write_resp /src/curl/lib/rtsp.c:893:16
    #7 0x5564d8893930 in Curl_xfer_write_resp /src/curl/lib/transfer.c:772:14
    #8 0x5564d888fd2a in sendrecv_dl /src/curl/lib/transfer.c:298:14
    #9 0x5564d888fd2a in Curl_sendrecv /src/curl/lib/transfer.c:370:14
    #10 0x5564d88572a6 in state_performing /src/curl/lib/multi.c:1938:12
    #11 0x5564d88572a6 in multi_runsingle /src/curl/lib/multi.c:2672:17
    #12 0x5564d884e9a2 in multi_perform /src/curl/lib/multi.c:2775:17
    #13 0x5564d87fef55 in fuzz_handle_transfer(fuzz_data*) /src/curl_fuzzer/curl_fuzzer.cc:341:3
    #14 0x5564d87fe317 in LLVMFuzzerTestOneInput /src/curl_fuzzer/curl_fuzzer.cc:97:3
    #15 0x5564d869a57d in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:619:13
    #16 0x5564d8685302 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:329:6
    #17 0x5564d868b1d0 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:865:9
    #18 0x5564d86b6cf2 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
    #19 0x7fde2c62a1c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
    #20 0x7fde2c62a28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
    #21 0x5564d867e3e4 in _start (/out/curl_fuzzer_rtsp+0x6723e4)

DEDUP_TOKEN: pthread_kill--raise--abort
==14==Register values:
rax = 0x0000000000000000  rbx = 0x000000000000000e  rcx = 0x00007fde2c69eb2c  rdx = 0x0000000000000006  
rdi = 0x000000000000000e  rsi = 0x000000000000000e  rbp = 0x00007fff96291630  rsp = 0x00007fff962915f0  
 r8 = 0x00000000000000bc   r9 = 0x00007e1e2b7e0000  r10 = 0x0000000000000008  r11 = 0x0000000000000246  
r12 = 0x0000000000000006  r13 = 0x00005564d955a520  r14 = 0x0000000000000016  r15 = 0x00005564d955b200  
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: ABRT (/lib/x86_64-linux-gnu/libc.so.6+0x9eb2c) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb) in pthread_kill

Root Cause

The RTSP protocol supports interleaving RTP data within the TCP connection using a $-framed binary format (RFC 2326, Section 10.12):

  '$' <1-byte channel> <2-byte big-endian length> <length bytes of data>

curl's RTP interleaved parser (rtsp_filter_rtp) uses a 4-state machine:

  1. RTP_PARSE_SKIP — scanning for $ frame start
  2. RTP_PARSE_CHANNEL — reading the 1-byte channel number
  3. RTP_PARSE_LEN — reading the 2-byte payload length field
  4. RTP_PARSE_DATA — reading the RTP payload data

The bug is in the transition from RTP_PARSE_LEN to RTP_PARSE_DATA.

In RTP_PARSE_LEN (line 762), once both length bytes have been collected (buffer contains 4 bytes: $, channel, len_hi, len_lo), the expected total frame size is computed:

rtspc->rtp_len = RTP_PKT_LENGTH(rtp_buf) + 4;

Where RTP_PKT_LENGTH extracts the 16-bit big-endian value from bytes 2-3:

#define RTP_PKT_LENGTH(p) ((((unsigned int)((unsigned char)((p)[2]))) << 8) | \
                            ((unsigned int)((unsigned char)((p)[3]))))

When the payload length field is 0x0000 (zero):

  • RTP_PKT_LENGTH(rtp_buf) returns 0
  • rtspc->rtp_len = 0 + 4 = 4
  • The dynamic buffer (rtspc->buf) already contains exactly 4 bytes ($ + channel + 2 length bytes)
  • State transitions to RTP_PARSE_DATA

In RTP_PARSE_DATA (line 781):

  • rtp_len = curlx_dyn_len(&rtspc->buf) → 4
  • rtspc->rtp_len → 4
  • DEBUGASSERT(rtp_len < rtspc->rtp_len) → DEBUGASSERT(4 < 4) → FAILS

The assertion correctly detects an invariant violation: the parser entered the "read data" state when there is no data to read.

Impact

Summary:

The impact is that in debug builds an assertion is triggerable. In release builds, as far as I can tell, the issue does not have adverse effect other than the rtsp_filter_rtp state machine processes an invalid zero-length RTP frame and delivers a 4-byte header to the RTP write callback as if they were valid RTP data.

I considered making this public due to issues such as https://github.com/curl/curl/issues/12701 but, again, I'm being a bit cautious here. I am happy to submit this issue on GitHub if Curl maintainers confirm it's okay to do so.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original