CVE-2021-22945: UAF and double-free in MQTT sending
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
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.
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
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).
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.
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)
Source code
| 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 | |
| 543 | static 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 | } |
| 297 | CURLcode 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 | } |
| 112 | static 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 | } |
Show 1 lines (not cited by the judge)
| 130 | #define free(ptr) Curl_cfree(ptr) |
Show 1 lines (not cited by the judge)
| 414 | #define EAGAIN 3 |
Show 1 lines (not cited by the judge)
| 396 | #define EWOULDBLOCK WSAEWOULDBLOCK |