Fix GC use-after-free of multipart form data during perform#272
Open
faoiseamh wants to merge 1 commit intotyphoeus:masterfrom
Open
Fix GC use-after-free of multipart form data during perform#272faoiseamh wants to merge 1 commit intotyphoeus:masterfrom
faoiseamh wants to merge 1 commit intotyphoeus:masterfrom
Conversation
`http_request` creates a Post action containing a Form object, calls `.setup(self)` to set CURLOPT_HTTPPOST on the curl handle, then discards the action — no Ruby reference is kept. The Form holds an FFI::AutoPointer that calls `curl_formfree()` when garbage collected. `Curl.easy_perform` is declared with `@blocking = true`, so FFI releases the GVL during the call. While libcurl is sending multipart data, Ruby's GC can finalize the now-unreachable Form, calling `curl_formfree()` and freeing the `curl_httppost` chain that libcurl is actively reading. This causes a segmentation fault (SIGSEGV). The bug is non-deterministic — it depends on GC timing and is more likely under memory pressure or with repeated multipart uploads. It only affects multipart form uploads (requests using `curl_formadd`/`CURLOPT_HTTPPOST`); regular POST requests using `CURLOPT_POSTFIELDS` are unaffected because libcurl copies that data internally. Fix: store the action as `@active_action` on the Easy instance so the Form (and its native `curl_httppost` data) remains reachable during `perform`. Clear the reference in `reset` so pooled handles don't hold stale form data.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
http_requestcreates aPostaction containing aFormobject, calls.setup(self)to setCURLOPT_HTTPPOSTon the curl handle, then discards the action — no Ruby reference is stored on theEasyinstance. TheFormholds anFFI::AutoPointerthat callscurl_formfree()when garbage collected.Curl.easy_performis declared with@blocking = true, so FFI releases the GVL during the call. While libcurl is sending multipart data, Ruby's GC can finalize the now-unreachableForm, callingcurl_formfree()and freeing thecurl_httppostchain that libcurl is actively reading → SIGSEGV.Why this is hard to reproduce
curl_formadd/CURLOPT_HTTPPOST). Regular POST with string bodies useCURLOPT_POSTFIELDS, which libcurl copies internally.curl_easy_perform= wider GC window).0x0000000019751858(stale heap pointer after realloc) and0x0000000000000045(NULL + struct field offset after free) — both duringeasy_performon multipart POSTs.The fix
lib/ethon/easy/http.rb— Store the action as@active_actionon the Easy instance:lib/ethon/easy.rb— Clear the reference inresetso pooled handles don't hold stale form data:How we found this
We run a Rails app on RHEL9 (libcurl 7.76.1) that uploads medical images via multipart POST through Typhoeus/Ethon. After upgrading to ethon 0.18.0, sequential multipart uploads would SIGSEGV after several requests. The crash always occurred inside
curl_easy_performafter a successful TLS handshake, only on multipart POSTs, and only when enough prior requests had run to trigger GC pressure.The C-level backtrace confirmed the crash originated inside libcurl's form data reading code, and
rb_nogvlin the call stack confirmed the GVL was released. ForcingGC.startbetweenhttp_requestandperformreliably triggers the bug without the fix.Test plan
retains a reference to the action to prevent GC of form dataclears the action reference on resetdoes not segfault when GC runs during multipart perform(forcesGC.startbetweenhttp_requestandperformin a loop with 50KB file uploads)30 examples, 0 failures)