Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/ethon/easy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ def reset
@on_progress = nil
@procs = nil
@mirror = nil
@active_action = nil
Curl.easy_reset(handle)
set_callbacks
end
Expand Down
3 changes: 2 additions & 1 deletion lib/ethon/easy/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ module Http
#
# @see Ethon::Easy::Options
def http_request(url, action_name, options = {})
fabricate(url, action_name, options).setup(self)
@active_action = fabricate(url, action_name, options)
@active_action.setup(self)
end

private
Expand Down
43 changes: 43 additions & 0 deletions spec/ethon/easy/http_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,48 @@
expect(easy.response_body).to include(%{"REQUEST_METHOD":"PURGE"})
end
end

context "when posting multipart form data" do
it "retains a reference to the action to prevent GC of form data" do
easy.http_request(url, :post, body: { text: "value" })
expect(easy.instance_variable_get(:@active_action)).to_not be_nil
end

it "clears the action reference on reset" do
easy.http_request(url, :post, body: { text: "value" })
easy.reset
expect(easy.instance_variable_get(:@active_action)).to be_nil
end

it "does not segfault when GC runs during multipart perform" do
# Reproducer for GC use-after-free: without the fix, the Form
# object (and its FFI::AutoPointer to curl_formfree) becomes
# unreachable after http_request returns. Since easy_perform
# releases the GVL (@blocking=true), GC can finalize the Form
# mid-request, freeing the curl_httppost chain that libcurl is
# reading → SIGSEGV.
#
# We force GC between http_request and perform to maximize the
# chance of triggering the bug. With the fix, @active_action
# prevents collection.
tmpfile = Tempfile.new(["ethon_test", ".bin"])
tmpfile.write("x" * 50_000)
tmpfile.rewind

10.times do
e = Ethon::Easy.new
e.http_request(url, :post, {
body: { file: [File.basename(tmpfile.path), "application/octet-stream", tmpfile.path] },
multipart: true
})
GC.start(full_mark: true, immediate_sweep: true)
e.perform
expect(e.return_code).to eq(:ok)
end

tmpfile.close
tmpfile.unlink
end
end
end
end