Skip to content

curl_easy_ssls_import: memory leak #22323

Description

@xmoezzz

I did this

I built the current curl master (87704c4) with AddressSanitizer and LeakSanitizer enabled, then imported serialized SSL session data containing two CURL_SPACK_TICKET fields.

The second ticket replaces the pointer stored for the first ticket without freeing the first allocation.

Minimal reproducer:

#include <curl/curl.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define IMPORT_COUNT 128
#define FIRST_TICKET_SIZE 4096

static unsigned char *make_packet(size_t *packet_len)
{
  const size_t total =
    1 +                              /* format version */
    1 + 2 + FIRST_TICKET_SIZE +      /* first ticket */
    1 + 2 + 1;                       /* second ticket */

  unsigned char *packet = malloc(total);
  unsigned char *p;

  if(!packet)
    return NULL;

  p = packet;

  /* CURL_SPACK_VERSION */
  *p++ = 0x01;

  /* First CURL_SPACK_TICKET: 4096 bytes. */
  *p++ = 0x04;
  *p++ = (unsigned char)(FIRST_TICKET_SIZE >> 8);
  *p++ = (unsigned char)FIRST_TICKET_SIZE;
  memset(p, 'A', FIRST_TICKET_SIZE);
  p += FIRST_TICKET_SIZE;

  /* Second CURL_SPACK_TICKET: one byte. */
  *p++ = 0x04;
  *p++ = 0x00;
  *p++ = 0x01;
  *p++ = 'B';

  *packet_len = total;
  return packet;
}

int main(void)
{
  unsigned char *packet;
  size_t packet_len;
  CURLSH *share;
  CURL *easy;
  CURLSHcode shrc;
  CURLcode result = CURLE_OK;
  int i;

  packet = make_packet(&packet_len);
  if(!packet)
    return 1;

  result = curl_global_init(CURL_GLOBAL_DEFAULT);
  if(result != CURLE_OK)
    return 1;

  share = curl_share_init();
  easy = curl_easy_init();

  if(!share || !easy)
    return 1;

  shrc = curl_share_setopt(share,
                           CURLSHOPT_SHARE,
                           CURL_LOCK_DATA_SSL_SESSION);
  if(shrc != CURLSHE_OK)
    return 1;

  result = curl_easy_setopt(easy, CURLOPT_SHARE, share);
  if(result != CURLE_OK)
    return 1;

  for(i = 0; i < IMPORT_COUNT; ++i) {
    result = curl_easy_ssls_import(easy,
                                   "example.test:443",
                                   NULL,
                                   0,
                                   packet,
                                   packet_len);
    if(result != CURLE_OK) {
      fprintf(stderr,
              "import %d failed: %d (%s)\n",
              i,
              (int)result,
              curl_easy_strerror(result));
      break;
    }
  }

  free(packet);
  curl_easy_cleanup(easy);
  curl_share_cleanup(share);
  curl_global_cleanup();

  return result == CURLE_OK ? 0 : 1;
}

I configured curl on Fedora Linux with:

cmake -S . -B build-asan-linux -G Ninja \
  -DCMAKE_C_COMPILER=clang \
  -DCMAKE_BUILD_TYPE=Debug \
  -DBUILD_SHARED_LIBS=ON \
  -DBUILD_STATIC_LIBS=OFF \
  -DBUILD_CURL_EXE=OFF \
  -DBUILD_TESTING=OFF \
  -DBUILD_EXAMPLES=OFF \
  -DUSE_SSLS_EXPORT=ON \
  -DCURL_USE_OPENSSL=ON \
  -DCURL_ZLIB=OFF \
  -DCURL_BROTLI=OFF \
  -DCURL_ZSTD=OFF \
  -DUSE_NGHTTP2=OFF \
  -DUSE_LIBIDN2=OFF \
  -DCURL_USE_LIBPSL=OFF \
  -DCURL_USE_LIBSSH2=OFF \
  -DCURL_USE_LIBSSH=OFF \
  -DCURL_DISABLE_LDAP=ON \
  -DCURL_DISABLE_LDAPS=ON \
  -DCMAKE_C_FLAGS="-O1 -g -fsanitize=address -fno-omit-frame-pointer -fno-optimize-sibling-calls" \
  -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address"

I then built the local shared libcurl:

cmake --build build-asan-linux \
  --target libcurl_shared \
  -j"$(nproc)"

I compiled the reproducer against the locally built libcurl:

LIBCURL_SO="$(
  find "$PWD/build-asan-linux/lib" \
    -maxdepth 1 \
    -name 'libcurl*.so*' \
    -print |
  head -1
)"

clang \
  -O1 \
  -g \
  -fsanitize=address \
  -fno-omit-frame-pointer \
  -fno-optimize-sibling-calls \
  -I"$PWD/include" \
  ssls_import_leak.c \
  "$LIBCURL_SO" \
  -Wl,-rpath,"$PWD/build-asan-linux/lib" \
  -o ssls_import_leak

Run:

ASAN_OPTIONS=detect_leaks=1:halt_on_error=0 \
LSAN_OPTIONS=report_objects=1 \
./ssls_import_leak

LeakSanitizer reports:

==1383615==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 524416 byte(s) in 128 object(s) allocated from:
    #0 0x0000004a6f98 in malloc (/omitted/curl-master/ssls_import_leak+0x4a6f98) (BuildId: d8f7c4dc12985f17896846f2707c00052f903097)
    #1 0x7f526dd85a9f in curlx_memdup0 /omitted/curl-master/lib/curlx/strdup.c:87:37
    #2 0x7f526dd80bfc in spack_decdata16 /omitted/curl-master/lib/vtls/vtls_spack.c:179:10
    #3 0x7f526dd80bfc in Curl_ssl_session_unpack /omitted/curl-master/lib/vtls/vtls_spack.c:298:16
    #4 0x7f526dd7ccd0 in Curl_ssl_session_import /omitted/curl-master/lib/vtls/vtls_scache.c:1193:12
    #5 0x7f526dc61fe1 in curl_easy_ssls_import /omitted/curl-master/lib/easy.c:1350:10
    #6 0x000000400a96 in main /omitted/curl-master/ssls_import_leak.c:177:14
    #7 0x7f526d8f3680 in __libc_start_call_main (/lib64/libc.so.6+0x3680) (BuildId: 18472003bbf1c5f098a09b5016b9b8bd4c7c59f0)
    #8 0x7f526d8f3797 in __libc_start_main@GLIBC_2.2.5 (/lib64/libc.so.6+0x3797) (BuildId: 18472003bbf1c5f098a09b5016b9b8bd4c7c59f0)
    #9 0x000000400ee4 in _start (/omitted/curl-master/ssls_import_leak+0x400ee4) (BuildId: d8f7c4dc12985f17896846f2707c00052f903097)

524416 is 128 * 4097. Each import leaks the first 4096-byte ticket allocation plus the extra terminating byte allocated by curlx_memdup0().

The relevant code is:

case CURL_SPACK_TICKET:
  result = spack_decdata16(&pval8, &s->sdata_len, &buf, end);
  s->sdata = pval8;
  break;

When another CURL_SPACK_TICKET field is encountered, s->sdata is overwritten without freeing the allocation associated with the previous field.

I expected the following

Malformed serialized session data containing duplicate ticket fields should either be rejected or handled without losing the previous allocation.

curl/libcurl version

curl master (87704c4)

operating system

Fedora Linux 44
7.0.12-201.fc44.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Jun 11 01:30:16 UTC 2026 x86_64 GNU/Linux

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions