Skip to content

multi/easy: reject internal handles in the public API - #22917

Closed
MegaManSec wants to merge 1 commit into
curl:masterfrom
MegaManSec:multi-ntfy-reject-internal-handles
Closed

MegaManSec wants to merge 1 commit into
curl:masterfrom
MegaManSec:multi-ntfy-reject-internal-handles

Conversation

@MegaManSec

@MegaManSec MegaManSec commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Curl_eapi_enter() now refuses easy handles with state.internal set, as do curl_multi_add_handle() and curl_multi_remove_handle().

A CURLMNOTIFY_INFO_READ callback that applies the documented remove-then-cleanup pattern to the handle it receives frees multi->admin, causing a use-after-free and a double-free in curl_multi_cleanup().

Putting the check in Curl_eapi_enter() covers every easy API at once and keeps the call-stack guard balanced: making curl_easy_cleanup() a no-op instead would leave the entry unpopped, since easy_cleanup is marked as killing its handle.

Test 3107 covers it.

@MegaManSec

Copy link
Copy Markdown
Contributor Author
Proof of concept (ASan reproducer)

Build an ASan-instrumented libcurl, then compile and run the driver below. exploit triggers the double-free; control exits cleanly.

/* drv.c - build: cc -fsanitize=address -g -O1 drv.c -lcurl -o drv
   run:  ./drv exploit   (ASan fires)
         ./drv control   (clean exit) */
#include <curl/curl.h>
#include <stdio.h>
#include <string.h>

static int g_exploit = 0;

static void ntfy_cb(CURLM *m, unsigned int type, CURL *easy, void *ud)
{
  (void)ud;
  if(type == CURLMNOTIFY_INFO_READ && g_exploit) {
    curl_multi_remove_handle(m, easy); /* detaches admin, leaves multi->admin dangling */
    curl_easy_cleanup(easy);           /* frees the admin allocation */
  }
}

int main(int argc, char **argv)
{
  g_exploit = (argc > 1 && strcmp(argv[1], "exploit") == 0);

  curl_global_init(CURL_GLOBAL_DEFAULT);
  CURLM *m = curl_multi_init();
  curl_multi_setopt(m, CURLMOPT_NOTIFYFUNCTION, ntfy_cb);
  curl_multi_notify_enable(m, CURLMNOTIFY_INFO_READ);

  CURL *e = curl_easy_init();
  curl_easy_setopt(e, CURLOPT_URL, "http://127.0.0.1:1/");
  curl_easy_setopt(e, CURLOPT_CONNECTTIMEOUT_MS, 2000L);
  curl_multi_add_handle(m, e);

  int running = 1, loops = 0;
  while(running && loops++ < 100000) {
    if(curl_multi_perform(m, &running) != CURLM_OK) break;
    if(running) { int n = 0; curl_multi_poll(m, NULL, 0, 50, &n); }
  }

  CURLMsg *msg; int q;
  while((msg = curl_multi_info_read(m, &q))) {}

  curl_multi_cleanup(m);   /* second Curl_close(&multi->admin): double-free */
  curl_global_cleanup();
  return 0;
}

The bug fires unconditionally — no specific server response required; any completing transfer (success, failure, timeout) queues the notification.

@MegaManSec
MegaManSec force-pushed the multi-ntfy-reject-internal-handles branch from ac993fe to 8d8ae85 Compare September 11, 2026 18:20
@MegaManSec
MegaManSec force-pushed the multi-ntfy-reject-internal-handles branch from 8d8ae85 to fb44e16 Compare September 11, 2026 18:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The cleanup no-op leaves the API call stack unbalanced, and the reported regression lacks automated coverage.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Prevents applications from freeing libcurl-owned internal easy handles.

Changes:

  • Rejects removal of internal easy handles.
  • Makes their cleanup a no-op.
  • Documents internal-handle restrictions.
File summaries
File Description
lib/multi.c Rejects internal handles during removal.
lib/easy.c Skips cleanup for internal handles.
docs/libcurl/opts/CURLMOPT_NOTIFYFUNCTION.md Documents notification handle ownership.
docs/libcurl/curl_multi_remove_handle.md Documents the new error behavior.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/easy.c Outdated
Comment thread lib/multi.c
@icing

icing commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

I think we should reject internal handles in all API calls. The multi calls that act on easy handles need an extra check, for sure.

A notify callback that removed and cleaned up the handle it gets for
CURLMNOTIFY_INFO_READ freed multi->admin, leading to a use-after-free
and a double-free in curl_multi_cleanup().
@MegaManSec
MegaManSec force-pushed the multi-ntfy-reject-internal-handles branch from fb44e16 to 187bb0d Compare September 14, 2026 09:02
@github-actions github-actions Bot added the tests label Sep 14, 2026
@MegaManSec MegaManSec changed the title multi/easy: reject internal handles in public remove and cleanup APIs multi/easy: reject internal handles in the public API Sep 14, 2026
@MegaManSec
MegaManSec force-pushed the multi-ntfy-reject-internal-handles branch from 187bb0d to 3effdfe Compare September 14, 2026 09:22
@icing

icing commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Stole it and cleaned it for CI in #22966 😈

@MegaManSec

Copy link
Copy Markdown
Contributor Author

A travesty of justice

@MegaManSec MegaManSec closed this Sep 16, 2026
@icing

icing commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

I kept your name on it, just in case we need someone to blame.

@MegaManSec
MegaManSec deleted the multi-ntfy-reject-internal-handles branch September 16, 2026 11:14
@MegaManSec

MegaManSec commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Passing blame? I see you're working on becoming a bit of a "security professional" yourself 😏

jay pushed a commit that referenced this pull request Sep 16, 2026
- Change curl_multi_add_handle(), curl_multi_remove_handle() and
  Curl_eapi_enter() to refuse easy handles with state.internal set.

Prior to this change, a notify callback that removed and cleaned up the
handle it gets for CURLMNOTIFY_INFO_READ freed multi->admin, leading to
a use-after-free and a double-free in curl_multi_cleanup().

Co-authored-by: Stefan Eissing

Closes #22917
Closes #22966
@jay

jay commented Sep 16, 2026

Copy link
Copy Markdown
Member

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

5 participants