dynamic_modules: make recreate_stream unsafe + add safe deferred wrapper#46367
dynamic_modules: make recreate_stream unsafe + add safe deferred wrapper#46367bonnetn wants to merge 2 commits into
Conversation
|
Hi @bonnetn, welcome and thank you for your contribution. We will try to review your Pull Request as quickly as possible. In the meantime, please take a look at the contribution guidelines if you have not done so already. |
| ``EnvoyHttpFilter::request_stream_recreation`` performs the recreation from the SDK event loop after | ||
| the filter hook returns, when no reference to the filter remains. Modules that call | ||
| ``recreate_stream`` directly must now wrap it in ``unsafe`` or switch to | ||
| ``request_stream_recreation``. |
There was a problem hiding this comment.
Note
Note to reviewers: I am following
Lines 154 to 157 in ca1d685
This is not truly behavioral change, but this is backward incompatible interface change (adding unsafe).
recreate_stream synchronously destroys the current filter chain — freeing the Box<dyn HttpFilter> that backs the running hook's self — before the FFI call returns. The borrow checker cannot see this cross-FFI free, so safe Rust could touch freed memory by using self after the call. Mark recreate_stream unsafe fn with a # Safety contract (matching the SDK's other caller-invariant FFI methods) so safe Rust cannot reach the free, and add a safe request_stream_recreation() that records the request and lets the SDK perform the freeing recreate from the request-path trampoline after the hook returns, when no reference to the filter remains. Tests: - rust_sdk_doc_test: compile_fail,E0133 doctest proves safe Rust cannot call recreate_stream (trait + generated mock). - rust_sdk_test: test_request_stream_recreation_deferred_to_trampoline asserts the recreate FFI fires once, after the hook, forcing StopIteration. - integration_test RecreateStreamNoUseAfterFree: drives the safe deferred path through real Envoy under AddressSanitizer with no heap-use-after-free. Signed-off-by: Nicolas Bonnet <mail@nicolasbon.net>
5dff7c8 to
2feb3c6
Compare
| &mut EnvoyHttpFilterImpl { | ||
| raw_ptr: std::ptr::null_mut(), | ||
| }, | ||
| &mut EnvoyHttpFilterImpl::new(std::ptr::null_mut()), |
There was a problem hiding this comment.
Note
Note to the reviewers: I added recreate_requested field which is private, so we cant use EnvoyHttpFilterImpl { syntax anymore. I added a thin new constructor and updated these tests.
|
For visibility: this is something we discussed with @agrawroh / @basundhara-c |
basundhara-c
left a comment
There was a problem hiding this comment.
Thanks for making this change! LGTM after resolving these two!
Addresses review feedback on the deferred recreate_stream wrapper: - take_and_perform_recreation now propagates recreate_stream()'s bool instead of unconditionally returning true. recreate_stream returns false when Envoy declines the recreation (e.g. request body not fully received), in which case the filter is NOT freed, so the trampoline must honor the hook's own status rather than forcing StopIteration. - Wire the deferred check into the response-path hooks (on_response_headers, on_response_body, on_response_trailers) and on_http_callout_done, not just the request path. recreate_stream is reachable on the response path (e.g. internal redirects driven from upstream headers via setupRedirect -> recreateStream in router.cc), so a filter may request recreation from those hooks too. Tests: - rust_sdk_test: extend test_request_stream_recreation_deferred_to_trampoline to cover the request path, response path, and callout hook; add test_request_stream_recreation_declined_keeps_hook_status for the false-return case (filter not freed, hook status preserved). The recreate_stream FFI stub gained a toggle to simulate Envoy declining the recreation. Signed-off-by: Nicolas Bonnet <mail@nicolasbon.net>
|
@wbpcode could you help review this? Thanks in advance! |
Commit Message
dynamic_modules: make recreate_stream unsafe + add safe deferred wrapper
EnvoyHttpFilter::recreate_stream()is a safe method today, but a successful callsynchronously destroys the current filter chain — freeing the
Box<dyn HttpFilter>that backs the running hook's
self— before the FFI call returns. Because thedeallocation happens across the FFI boundary in Envoy's C++, the Rust borrow
checker cannot see it, so any use of
selfafter the call is a use-after-freereachable from ordinary safe Rust.
This change makes
recreate_streamanunsafe fnwith a# Safetycontract(matching the SDK's other caller-invariant FFI methods such as
reset_http_stream,send_http_stream_data, andset_filter_state_object), so safe Rust can no longerreach the free. It also adds a safe
request_stream_recreation(): the filterrecords the intent, and the SDK performs the actual (freeing) recreation from the
event trampoline after the hook returns, when no reference to the filter remains.
Additional Description
The problem
Any access to
selfafter therecreate_streamcall reads/writes freedmemory. The borrow checker permits it because
&mut selfand&mut EnvoyHttpFilterare disjoint borrows and the free happens in opaque C++.
Assume a Rust HTTP filter (dynamic module) that calls
recreate_stream()uponreceiving request headers. The synchronous teardown runs entirely inside the call:
Minimal repro:
The fix
recreate_streambecomesunsafe fnwith a# Safetycontract documentingthe synchronous cross-FFI teardown ("return
StopIteration, never touchselfagain"). A
compile_fail,E0133doctest locks in that safe Rust cannot call it.request_stream_recreation()records the intent; the SDK performs thefreeing recreate from the request-path trampoline after the hook returns, when
the
&mut selfborrow is already gone.Risk Level
Low. Rust dynamic-module SDK only; no data-plane or ABI change. The single change
in behavior for module authors is that
recreate_streamnow requiresunsafe.Testing
//test/extensions/dynamic_modules:rust_sdk_doc_test— acompile_fail,E0133doctest on
recreate_streamproves safe Rust cannot call it (fails to compile onthe missing-
unsafe-block error, on both the trait and the generatedMockEnvoyHttpFilter).//test/extensions/dynamic_modules:rust_sdk_test— newtest_request_stream_recreation_deferred_to_trampolineasserts the recreate FFIfires exactly once, only after the hook returns, and that the trampoline
overrides the hook's
ContinuewithStopIteration.//test/extensions/dynamic_modules:rust_sdk_clippy— clean.//test/extensions/dynamic_modules/http:integration_test(
RecreateStreamNoUseAfterFree,--config=asan) — drives the safe deferred paththrough a real Envoy compiled with AddressSanitizer; passes for both
rustandrust_staticwith no heap-use-after-free.I mainly relied on AI to identify where these tests should go, so I would
appreciate guidance on whether they are written and located appropriately.
Docs Changes
N/A. The SDK is documented via rustdoc; the
# Safetycontract and therequest_stream_recreationdoc comment are updated inline.Release Notes
Added
changelogs/current/behavior_changes/dynamic_modules__recreate-stream-unsafe.rstnoting the backward-incompatible SDK interface change (
recreate_streamis nowunsafe) and the new saferequest_stream_recreation.Platform Specific Features
N/A.
Generative AI disclosure
Generative AI was used to help author this change and its description; the author
fully understands the code.