HexBrain

curl · Report #4085412

Rustls backend discloses out-of-bounds heap data during legal IMAP upload cancellation

N/Astatus
mediumseverity
6upvotes
5 Oct 2026disclosed

How it was found

Affected: Rustls-backed libcurl, curl 8.8.0 through 8.22.0 and current 8.23 development

Summary

A malicious IMAP server can apply TLS socket backpressure during an APPEND upload. If the application then legally removes the active easy handle from its multi handle, curl's IMAP cleanup sends a two-byte APPEND terminator while the Rustls backend still accounts for plaintext accepted from a longer upload call.

After flushing that retained record, cr_send() reports the old plaintext length as progress for the new two-byte send. Curl_pp_flushsend() trusts the oversized count, underflows its remaining length, and calls Rustls again with a pointer beyond curl's command allocation and a near-SIZE_MAX length. Rustls copies, encrypts, and transmits out-of-bounds heap data, or the client crashes while reading it.

The application uses ordinary IMAP upload and multi APIs. It does not call curl_easy_send(), curl_ws_send(), connect-only mode, or Rustls directly. The only application action at the trigger is curl_multi_remove_handle() outside a callback, which curl documents as legal during an active transfer.

Minimal reproduction

The attachment contains:

  • imap_cancel_client.c
  • imap_server.py
  • run.sh

Build a release libcurl with Rustls, point PKG_CONFIG_PATH at that installation, and run:

export PKG_CONFIG_PATH=/path/to/rustls-libcurl/lib/pkgconfig
./run.sh

For a static or nonstandard build, set CURL_CFLAGS and CURL_LIBS directly. The script creates a temporary self-signed certificate, starts the local IMAP TLS peer, compiles the client, and performs one transfer.

The client performs a real IMAP CAPABILITY, STARTTLS, TLS handshake, LOGIN, and 65,536-byte APPEND. Its upload callback returns 1,024 bytes and arms a one-shot send() interposer that returns EAGAIN on the next client socket write. This is a deterministic substitute for a peer filling the nonblocking socket. The event loop then calls curl_multi_remove_handle() outside the callback.

A current uninstrumented release run produced:

libcurl=8.23.0-DEV TLS=rustls-ffi/0.15.4/rustls/0.23.45/aws-lc-rs
PRE_REMOVE callback_bytes=1024 injected_eagain=1
Segmentation fault
PEER_RESULT captured=132096 expected_callback=1024 surplus=131072 prefix_ok=1
CLIENT_EXIT=139 SERVER_EXIT=0

prefix_ok=1 confirms that the first 1,024 bytes are the callback data. The remaining 131,072 decrypted bytes were never returned by the callback.

With ASan, deterministic-asan.log contains:

ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 16384
  memcpy
  OutboundChunks::copy_to_vec
  GcmMessageEncrypter::encrypt
  rustls_connection_write
  cr_send
  Curl_pp_flushsend
  imap_done
  Curl_multi_remove_handle
  curl_multi_remove_handle

Attacker-controlled reachability

The deterministic reproducer changes one client send() result to EAGAIN. It does not modify Rustls state, curl state, the upload buffer, the cleanup buffer, or any returned byte count.

The independent network-paced harness uses a real TLS 1.3 peer, a 1 KiB socket receive buffer, bounded peer reads, and a 16 KiB upload callback. It reproduced the same ASan read in 9/10 fresh processes without interposing any client I/O. See network-peer-asan.log and network-peer-repeat-summary.tsv.

This route does not rely on an application replacing a pending raw send with a shorter buffer. Curl itself changes from the upload buffer to its internal two-byte IMAP terminator during documented transfer cancellation.

Root cause

When the upload write blocks, Rustls has already accepted plaintext and curl stores its length in backend->plain_out_buffered. On the later two-byte IMAP cleanup send, current cr_send() flushes the old record and executes:

if(blen > backend->plain_out_buffered) {
  blen -= backend->plain_out_buffered;
  buf += backend->plain_out_buffered;
}
else
  blen = 0;
*pnwritten += backend->plain_out_buffered;
backend->plain_out_buffered = 0;

For a retained count of 1,024 and a current length of two, it reports 1,024 bytes written for the two-byte command.

Curl_pp_flushsend() then subtracts without validating the backend count:

if(written != pp->sendleft)
  pp->sendleft -= written;

The remaining length underflows. The next iteration derives an out-of-range pointer from the corrupted progress values and Rustls attempts to copy a full 16 KiB record from it.

The internal shorter send occurs because imap_done() ignores the premature argument and terminates the APPEND even during cancellation:

(void)premature;
...
result = Curl_pp_sendf(data, &imapc->pp, "%s", "");

Affected range and current status

Commit 6eee810db42ec097760e7ac57b95a80abe2e4aaa (rustls: fix partial send handling) introduced plain_out_buffered and the oversized old-count return. It first shipped in curl 8.8.0. Curl 8.7.1 lacks this retained-plaintext accounting shape.

Validated:

  • curl 8.22.0: affected
  • current development commit 39c8f9dc: affected
  • rustls-ffi 0.15.4 / Rustls 0.23.45 / aws-lc-rs

Proposed fix

proposed-bounds-guard.patch rejects a current send shorter than the retained Rustls plaintext before flushing or changing progress state:

if(backend->plain_out_buffered > plainlen) {
  failf(data, "re-send data shorter than previously buffered");
  return CURLE_BAD_FUNCTION_ARGUMENT;
}

With that guard, the identical injected EAGAIN and removal schedule exits without an ASan finding (guard-control.log). The IMAP cleanup path should also avoid sending the APPEND terminator when premature is true. The backend guard remains useful defense in depth for other protocol transitions.

AI assistance disclosure

AI models assisted with harness development, source review, and drafting. I validated the report with source inspection, an uninstrumented release build, peer-decrypted byte counts, ASan, repeated fresh processes, a stable-release run, a bounds-guard control, and an independent peer-backpressure route. This report and reproducer have not been published.

Impact

Security impact

The malicious TLS endpoint receives client-process heap bytes that were never provided as upload data. Because the endpoint terminates TLS, it decrypts those bytes normally. The invalid read also crashes the client.

Confirmed on current development commit 39c8f9dcde7d7fe68dedda6ba7c68cbd305168a6:

  • An uninstrumented release build crashed with SIGSEGV after the peer received 132,096 decrypted literal bytes. The upload callback had supplied only 1,024 bytes, leaving 131,072 bytes of surplus process memory.
  • The release disclosure and crash reproduced identically in 10/10 fresh processes.
  • ASan reports a 16,384-byte heap-buffer-overflow read in Rustls encryption, reached through cr_send -> Curl_pp_flushsend -> imap_done -> curl_multi_remove_handle. The deterministic schedule reproduced in 10/10.
  • A separate harness using only peer receive-buffer pressure and read pacing, without a send() interposer, reproduced the same ASan fault in 9/10 runs.
  • curl 8.22.0 reproduces the same 16,384-byte read with the ordinary IMAP route.

Preconditions are a Rustls-backed libcurl, an IMAP APPEND, TLS backpressure, and cancellation of the stalled transfer. The server controls backpressure and can keep the cancellation window open. No write primitive was found.

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