✓ProveIt
Examples / Report #1269242 Saved analysis

CVE-2021-22945: UAF and double-free in MQTT sending

curl · 2021-07-1912.3s analysis · $0.016 in model calls

What the report claims

Use-After-Free and double-free in libcurl's MQTT client when handling partial writes.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2021-22945 UAF and double free in MQTT sending

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Code supports the claim

The code supports the reported flaw. A maintainer should review its impact and decide what to do next.

Why this finding?
  • The code in lib/mqtt.c lines 556-563 frees mq->sendleftovers without setting it to NULL or resetting mq->nsend.
  • mqtt_send (lines 112-132) sets mq->sendleftovers and mq->nsend only on partial writes; on a later full write it leaves them unchanged.
  • Curl_write can return 0 bytes written with CURLE_OK (lines 332-335 in lib/sendf.c), which triggers the partial-write path in mqtt_send.
  • This creates a reachable use-after-free and double-free when the network socket blocks temporarily (EAGAIN/EWOULDBLOCK), a realistic condition an attacker can influence by not reading data fast enough.
How this analysis ran 3 claims · 13 citations verified

Checked against curl permalink curl-7_77_0 (6b951a692881). Test evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
grounded
no model · 17 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
3 claims · 2188 tokens · $0.0002
3 · Evidence
6 code snippets
real curl source at 6b951a692881 · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
9645 tokens · 10.1s · $0.0159
5 · Receipts
13 of 13 citations verified
every citation must point at code the judge was shown
Review a draft reply to the reporter

Edit and verify before sending.

Thank you for the detailed report and reproduction steps. The code confirms the use-after-free and double-free in mqtt_doing: mq->sendleftovers is freed at line 560 without being nulled, and mq->nsend is not reset. When Curl_write returns 0 due to blocking (CURLE_AGAIN path in lib/sendf.c:332-335), mqtt_send sets the leftovers for the full buffer; a subsequent successful send frees the buffer but leaves the stale pointer and length, causing the double-free on the next mqtt_doing call. This is a valid security issue. We will prepare a fix that clears mq->sendleftovers and mq->nsend after freeing.

Inspect all claims and source code 3 claims · 6 code excerpts

Report claims

C1flaw supported

mq->sendleftovers is freed without being set to NULL, leading to use-after-free if mqtt_doing is called again

Read the reasoning

The code at lines 556-563 frees mq->sendleftovers via ptr but never nulls out mq->sendleftovers or resets mq->nsend. On a subsequent call to mqtt_doing with unchanged mq->nsend, the dangling pointer is passed to mqtt_send (use-after-free) and freed again (double-free).

C2flaw supported

mq->nsend and mq->sendleftovers are set in mqtt_send when Curl_write cannot send all bytes, but they are not reset after successful send

Read the reasoning

mqtt_send sets mq->nsend and mq->sendleftovers only on partial writes (len != n). On a subsequent full write, mqtt_send does not modify these fields, but mqtt_doing frees the buffer without clearing them, leaving stale values for the next iteration.

C3flaw supported

When Curl_write returns 0 due to blocking, the code may trigger the double-free path on subsequent calls

Read the reasoning

Curl_write returns CURLE_OK with *written=0 when the socket would block (CURLE_AGAIN path). This makes mqtt_send see n=0, so len != n is true for any non-zero len, causing it to allocate leftovers for the entire buffer and set mq->nsend. The subsequent mqtt_doing call frees that buffer but leaves mq->nsend non-zero, enabling the double-free on the next call.

Read the original report (excerpt)

Full report on HackerOne ↗

# Vulnerability Description libcurl version 7.77.0 has a [Use-After-Free](https://github.com/curl/curl/blob/curl-7_77_0/lib/mqtt.c#L559) and a [Double-Free](https://github.com/curl/curl/blob/curl-7_77_0/lib/mqtt.c#L560) in `lib/mqtt.c` in the function `mqtt_doing` on [lines 556 - 563](https://github.com/curl/curl/blob/curl-7_77_0/lib/mqtt.c#L556): ```c if(mq->nsend) { /* send the remainder of an outgoing packet */ char *ptr = mq->sendleftovers; result = mqtt_send(data, mq->sendleftovers, mq->nsend); free(ptr); if(result) return result; } ``` As can be seen in the code above `mq->sendleftovers` gets freed in line 560 but not set to `NULL`. If `mqtt_doing` gets called repeatedly and the values of `mq->nsend` and `mq->sendleftovers` don't change this can result in 1. Sending the metadata of the freed chunk over the network via `mqtt_send` 2. Freeing `mq->sendleftovers` multiple times `mq->nsend` and `mq->sendleftovers` get set in the function `mqtt_send` if `Curl_write` cannot send all bytes in the write-buffer at once. This can e.g. happen if `write()` returns `EAGAIN` or `EWOULDBLOCK`. Then `Curl_write` sets the number of written bytes to `0` and returns `CURLE_OK`. This can trigger the vulnerabilities as follows: 1. Supply an `mqtt://` URL to curl 2. Have some successfull transmissions with `mqtt_send` 3. At some point have an unsuccessfull transmission such that not all bytes of the write-buffer can be sent. This causes `mq->sendleftovers` and `mq->nsend` to be set. 4. Have another invocation of `mqtt_doing`. The code mentioned above gets executed. `mq->sendleftovers` gets freed. If `mqtt_send` could send all remaining bytes successfully `mq->sendleftovers` and `mq->nsend` don't get reset. 5. Have another invocation of `mqtt_doing`. Since `mq->nsend` didn't change curl tries to send the leftover bytes again, triggering the vulnerabilities # How to reproduce the bug 1. Checkout tag `curl-7_77_0` in the curl repository 2. Apply the following patch that artificially creates a scenario as described above: ``` diff --git a/lib/sendf.c b/lib/sendf.c index e41bb805f..773d4b5b6 100644 --- a/lib/sendf.c +++ b/lib/sendf.c @@ -294,6 +294,7 @@ void Curl_failf(struct Curl_easy *data, const char *fmt, ...) * If the write would block (CURLE_AGAIN), we return CURLE_OK and * (*written == 0). Otherwise we return regular CURLcode value. */ +static i …

Source code

lib/mqtt.clines 531–636 · lines 559-559 cited by the report; lines 560-560 cited by the report; lines 556-556 cited by the report; lines 556-563 cited by the report; definition of mqtt_doing
531 CURLcode result = CURLE_OK;
532 *done = FALSE; /* unconditionally */
533
534 result = mqtt_connect(data);
535 if(result) {
536 failf(data, "Error %d sending MQTT CONN request", result);
537 return result;
538 }
539 mqstate(data, MQTT_FIRST, MQTT_CONNACK);
540 return CURLE_OK;
541}
542
543static CURLcode mqtt_doing(struct Curl_easy *data, bool *done)
544{
545 CURLcode result = CURLE_OK;
546 struct connectdata *conn = data->conn;
547 struct mqtt_conn *mqtt = &conn->proto.mqtt;
548 struct MQTT *mq = data->req.p.mqtt;
549 ssize_t nread;
550 curl_socket_t sockfd = conn->sock[FIRSTSOCKET];
551 unsigned char *pkt = (unsigned char *)data->state.buffer;
552 unsigned char byte;
553
554 *done = FALSE;
555
556 if(mq->nsend) {
C1: condition checks mq->nsend, not whether pointer is valid
557 /* send the remainder of an outgoing packet */
558 char *ptr = mq->sendleftovers;
C1: ptr assigned from mq->sendleftovers
559 result = mqtt_send(data, mq->sendleftovers, mq->nsend);
560 free(ptr);
C1: free(ptr) called without setting mq->sendleftovers = NULL
C2: free(ptr) in mqtt_doing without resetting mq->nsend or mq->sendleftovers
561 if(result)
562 return result;
563 }
564
565 infof(data, "mqtt_doing: state [%d]\n", (int) mqtt->state);
566 switch(mqtt->state) {
567 case MQTT_FIRST:
568 /* Read the initial byte only */
569 result = Curl_read(data, sockfd, (char *)&mq->firstbyte, 1, &nread);
570 if(!nread)
571 break;
572 Curl_debug(data, CURLINFO_HEADER_IN, (char *)&mq->firstbyte, 1);
573 /* remember the first byte */
574 mq->npacket = 0;
575 mqstate(data, MQTT_REMAINING_LENGTH, MQTT_NOSTATE);
576 /* FALLTHROUGH */
577 case MQTT_REMAINING_LENGTH:
578 do {
579 result = Curl_read(data, sockfd, (char *)&byte, 1, &nread);
580 if(!nread)
581 break;
582 Curl_debug(data, CURLINFO_HEADER_IN, (char *)&byte, 1);
583 pkt[mq->npacket++] = byte;
584 } while((byte & 0x80) && (mq->npacket < 4));
585 if(result)
586 break;
587 mq->remaining_length = mqtt_decode_len(&pkt[0], mq->npacket, NULL);
588 mq->npacket = 0;
589 if(mq->remaining_length) {
590 mqstate(data, mqtt->nextstate, MQTT_NOSTATE);
591 break;
592 }
593 mqstate(data, MQTT_FIRST, MQTT_FIRST);
594
595 if(mq->firstbyte == MQTT_MSG_DISCONNECT) {
596 infof(data, "Got DISCONNECT\n");
597 *done = TRUE;
598 }
599 break;
600 case MQTT_CONNACK:
601 result = mqtt_verify_connack(data);
602 if(result)
603 break;
604
605 if(data->state.httpreq == HTTPREQ_POST) {
606 result = mqtt_publish(data);
607 if(!result) {
608 result = mqtt_disconnect(data);
609 *done = TRUE;
610 }
611 mqtt->nextstate = MQTT_FIRST;
612 }
613 else {
614 result = mqtt_subscribe(data);
615 if(!result) {
616 mqstate(data, MQTT_FIRST, MQTT_SUBACK);
617 }
618 }
619 break;
620
621 case MQTT_SUBACK:
622 case MQTT_PUBWAIT:
623 case MQTT_PUB_REMAIN:
624 result = mqtt_read_publish(data, done);
625 break;
626
627 default:
628 failf(data, "State not handled yet");
629 *done = TRUE;
630 break;
631 }
632
633 if(result == CURLE_AGAIN)
634 result = CURLE_OK;
635 return result;
636}
lib/sendf.clines 297–345 · definition of Curl_write
297CURLcode Curl_write(struct Curl_easy *data,
298 curl_socket_t sockfd,
299 const void *mem,
300 size_t len,
301 ssize_t *written)
302{
303 ssize_t bytes_written;
304 CURLcode result = CURLE_OK;
305 struct connectdata *conn;
306 int num;
307 DEBUGASSERT(data);
308 DEBUGASSERT(data->conn);
309 conn = data->conn;
310 num = (sockfd == conn->sock[SECONDARYSOCKET]);
311
312#ifdef CURLDEBUG
313 {
314 /* Allow debug builds to override this logic to force short sends
315 */
316 char *p = getenv("CURL_SMALLSENDS");
317 if(p) {
318 size_t altsize = (size_t)strtoul(p, NULL, 10);
319 if(altsize)
320 len = CURLMIN(len, altsize);
321 }
322 }
323#endif
324 bytes_written = conn->send[num](data, num, mem, len, &result);
325
326 *written = bytes_written;
327 if(bytes_written >= 0)
328 /* we completely ignore the curlcode value when subzero is not returned */
329 return CURLE_OK;
330
331 /* handle CURLE_AGAIN or a send failure */
332 switch(result) {
C3: case CURLE_AGAIN handling
333 case CURLE_AGAIN:
334 *written = 0;
C3: *written = 0
335 return CURLE_OK;
C3: return CURLE_OK
336
337 case CURLE_OK:
338 /* general send failure */
339 return CURLE_SEND_ERROR;
340
341 default:
342 /* we got a specific curlcode, forward it */
343 return result;
344 }
345}
lib/mqtt.clines 112–132 · definition of mqtt_send
112static CURLcode mqtt_send(struct Curl_easy *data,
113 char *buf, size_t len)
114{
115 CURLcode result = CURLE_OK;
116 struct connectdata *conn = data->conn;
117 curl_socket_t sockfd = conn->sock[FIRSTSOCKET];
118 struct MQTT *mq = data->req.p.mqtt;
119 ssize_t n;
120 result = Curl_write(data, sockfd, buf, len, &n);
C3: Curl_write called, n receives bytes written
121 if(!result)
122 Curl_debug(data, CURLINFO_HEADER_OUT, buf, (size_t)n);
123 if(len != (size_t)n) {
C3: if(len != (size_t)n) triggers leftover logic when n=0
124 size_t nsend = len - n;
C2: nsend calculated as len - n when partial write occurs
125 char *sendleftovers = Curl_memdup(&buf[n], nsend);
C2: sendleftovers allocated via Curl_memdup for unsent portion
126 if(!sendleftovers)
127 return CURLE_OUT_OF_MEMORY;
128 mq->sendleftovers = sendleftovers;
C2: mq->sendleftovers = sendleftovers
129 mq->nsend = nsend;
C2: mq->nsend = nsend
130 }
131 return result;
132}
lib/curl_memory.hlines 130–130 · definition of free
Show 1 lines (not cited by the judge)
130#define free(ptr) Curl_cfree(ptr)
lib/config-win32ce.hlines 414–414 · definition of EAGAIN
Show 1 lines (not cited by the judge)
414#define EAGAIN 3
lib/curl_setup_once.hlines 396–396 · definition of EWOULDBLOCK
Show 1 lines (not cited by the judge)
396#define EWOULDBLOCK WSAEWOULDBLOCK