Envoy 动态模块 HTTP 过滤器 use-after-free 崩溃修复:recreate_stream 场景下的延迟销毁机制深度解析

发布时间:2026/9/11 18:26:18
Envoy 动态模块 HTTP 过滤器 use-after-free 崩溃修复:recreate_stream 场景下的延迟销毁机制深度解析 Envoy 动态模块 HTTP 过滤器 use-after-free 崩溃修复recreate_stream 场景下的延迟销毁机制深度解析【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy本篇技术指南围绕 Envoy 动态模块Dynamic ModulesHTTP 过滤器的一项关键 bug 修复展开当事件钩子例如envoy_dynamic_module_callback_http_filter_recreate_stream在模块自身栈上结束 HTTP 流并拆除过滤器链时曾经会触发 use-after-free 崩溃。读完本文你将理解该崩溃产生的完整链路、修复方案利用 dispatcher 延迟删除队列实现 in-module filter 的延迟销毁以及模块开发者在实现重建流类功能时必须遵循的生命周期约束并能在源码层面定位每一处关键实现。一、背景动态模块 HTTP 过滤器与 in-module filter 的生命周期Envoy 的动态模块Dynamic Modules机制允许以共享库C/C、Rust、Go 等形式加载 HTTP 过滤器模块与 Envoy 主进程之间通过稳定的 C ABI 交互。在 Envoy 侧每个 HTTP 流对应一个DynamicModuleHttpFilterC 对象filter.h它负责把 Envoy 的Http::StreamFilter回调decodeHeaders、encodeData等转发到模块内由envoy_dynamic_module_on_http_filter_new创建的 in-module filter 对象envoy_dynamic_module_type_http_filter_module_ptr。正常的销毁时序是HTTP 流结束或被重置时Envoy 调用DynamicModuleHttpFilter::onDestroy()随后通过envoy_dynamic_module_on_http_filter_destroy通知模块释放 in-module filter见 abi.h 中该 ABI 的注释它为每个 HTTP 流在过滤器销毁时调用。二、问题定位recreate_stream 在模块栈上拆除过滤器链导致 use-after-free本次修复的 bug 记录在 changelogs/current/bug_fixes/dynamic_modules__http-filter-deferred-destroy.rst其核心症状是动态模块 HTTP 过滤器中的 use-after-free 崩溃。2.1 触发路径envoy_dynamic_module_callback_http_filter_recreate_streamABI 中定义了重建流的回调abi.hbool envoy_dynamic_module_callback_http_filter_recreate_stream( envoy_dynamic_module_type_http_filter_envoy_ptr filter_envoy_ptr, envoy_dynamic_module_type_module_http_header* headers, size_t headers_size);该回调用于实现内部重定向internal redirect或请求重试调用成功后当前过滤器链会被销毁并基于可选的新请求头创建一个新流。ABI 文档明确说明调用成功后过滤器应从当前事件钩子返回StopIterationin-module filter 在钩子返回前保持有效。2.2 崩溃机制栈上的销毁与正在执行的钩子竞争崩溃的根源在于销毁发生的执行上下文模块在某个事件钩子如decodeHeaders内部调用recreate_streamEnvoy 侧随即开始拆除当前过滤器链即同步执行onDestroy()→ 销毁 in-module filter但此时该钩子本身仍在模块自己的调用栈上运行recreate_stream是在模块自身的栈上完成了过滤器链的拆除与 in-module filter 的释放钩子返回后仍可能访问已经释放的 in-module filter 对象于是触发use-after-free崩溃。这正是 changelog 中所描述的An event hook that ends the stream ... tears the filter chain down on the modules own stack, which freed the in-module filter the hook was still running on.三、修复方案通过 dispatcher 延迟删除列表延迟销毁 in-module filter修复的核心思路是不再让 in-module filter 在钩子栈上被同步销毁而是将其销毁操作投递到 worker dispatcher 的延迟删除deferred deletion列表确保envoy_dynamic_module_on_http_filter_destroy只在其他所有事件钩子返回之后执行。修复后的 ABI 文档abi.h如此描述新语义Envoy 从 worker dispatcher 的延迟删除列表运行该回调因此它绝不会在同一个过滤器的另一个事件钩子仍在栈上时被调用。钩子可以通过例如envoy_dynamic_module_callback_http_filter_recreate_stream结束流这会在钩子返回前拆除过滤器链。Envoy 不会在该钩子返回前销毁 in-module filter且模块在拆除后进行的回调都是安全的。3.1 源码实现destroy()中的延迟投递关键实现在 filter.cc 的DynamicModuleHttpFilter::destroy()// A module event hook can end the stream, which tears the filter chain down on the modules own // stack, so the in-module filter has to outlive that hook. Deferring the destroy hook also keeps // this filter alive, since the module can still call back into it from the hook. if (dispatcher.has_value()) { if (DynamicModuleHttpFilterSharedPtr self weak_from_this().lock()) { Event::DeferredTaskUtil::deferredRun( *dispatcher, [self std::move(self), in_module_filter]() { self-config_-on_http_filter_destroy_(in_module_filter); }); return; } } config_-on_http_filter_destroy_(in_module_filter);这里有两处精妙设计以shared_ptr自持延长 C 过滤器生命周期lambda 捕获了weak_from_this().lock()得到的shared_ptr使DynamicModuleHttpFilter本身在延迟任务执行前不会被析构——因为模块仍可能在钩子中回调它。延迟到DeferredTaskUtil::deferredRun其实现deferred_task.h将任务包装为DeferredDeletable对象并交给dispatcher.deferredDelete()任务在未来的事件循环周期执行——即在之前所有 DeferredDeletable 对象被销毁之后。此时栈上不再有活跃的事件钩子调用on_http_filter_destroy_是绝对安全的。3.2onDestroy()的配套调整filter.cc 中的onDestroy()在触发destroy(worker_dispatcher)之前做了两件重要准备void DynamicModuleHttpFilter::onDestroy() { destroyed_ true; // Read before the cache is cleared below, because destroy() defers the module hook onto it. OptRefEvent::Dispatcher worker_dispatcher makeOptRefFromPtr(dispatcher()); // Clear the cached dispatcher so any concurrent foreign-thread commit() short-circuits. cached_dispatcher_.store(nullptr, std::memory_order_release); ... destroy(worker_dispatcher); }先读取 dispatcher 缓存再将其置空置空后其他线程通过DynamicModuleHttpFilterScheduler::commit()跨线程投递事件时会因拿不到 dispatcher 而直接短路返回见 filter.h 中commit()的实现避免向已拆除的流投递事件destroyed_ true用于拒绝拆除后新发起的异步操作。四、配套防护拆除后拒绝新 callout、安全回收存量 callout延迟销毁解决了钩子栈上的 use-after-free但还不够模块在拆除后仍可能发起新的 HTTP callout异步请求或存在尚未完成的存量 callout。修复为此增加了两层防护与 changelog 中teardown 之后启动的 HTTP callouts 会被拒绝而不是比过滤器存活得更久的描述一一对应。4.1destroyed_标志拒绝新建 callout 与流式 calloutsendHttpCallout()发起一次性异步请求与startHttpStream()发起流式异步请求在入口处都检查destroyed_// A callout registered after destroy() has drained the pending ones would never be cancelled. if (destroyed_) { return envoy_dynamic_module_type_http_callout_init_result_CannotCreateRequest; }从源码结构看这一检查可以推断是为了防止两类后果其一新 callout 一旦注册进http_callouts_/http_stream_callouts_映射表而该表随后被destroy()清空回调将永远得不到清理形成泄漏其二回调触发时若 in-module filter 已释放会再次引入 use-after-free。因此拆除后直接以CannotCreateRequest拒绝从源头掐断。4.2destroy()内的存量清理在真正投递on_http_filter_destroy_之前destroy()会先把in_module_filter_置空先与模块解绑使后续任何路径都无法重入事件钩子随后所有异步回调onSuccess、onFailure、onHeaders、onData、onReset等中的if (!filter.in_module_filter_) return;守卫都会直接返回不再解引用已拆除的流循环取消所有 pending 的一次性 callout对每个HttpCalloutCallback调用request-cancel()循环 reset 所有 pending 的流式 callout对每个HttpStreamCalloutCallback调用stream-reset()将decoder_callbacks_、encoder_callbacks_置空并复位 watermark 回调注册标志。这与 changelog 中需要被拆除的流的回调不再解引用它callbacks that need the torn-down stream no longer dereference it完全吻合。五、对模块开发者的实践启示结合上述修复动态模块开发者在实现recreate_stream内部重定向/重试等结束流型功能时应遵循以下约束钩子内调用recreate_stream后立即返回StopIterationABI 文档明确要求不要继续访问 in-module filter 的内部状态不要在拆除后启动新的 HTTP callout从修复后的行为看它们会被 Envoy 以CannotCreateRequest拒绝——这是有意设计的安全边界而非可绕过的问题销毁回调envoy_dynamic_module_on_http_filter_destroy现在保证在事件钩子全部返回后才执行模块可以在其中安全地释放资源不必担心与正在执行的钩子竞争envoy_dynamic_module_on_http_filter_stream_complete与destroy的语义差异值得注意abi.h前者可能在另一个事件钩子还在栈上时被内联调用因为结束流的钩子会内联完成流而后者则绝不会——这正是本次修复所确立的时序保证。六、总结本次修复解决的是动态模块机制中一个微妙的生命周期竞态结束流的钩子如recreate_stream在模块栈上拆除过滤器链与钩子自身仍在使用 in-module filter 之间的矛盾。解决方案是双管齐下时序上将envoy_dynamic_module_on_http_filter_destroy投递到 worker dispatcher 的延迟删除队列DeferredTaskUtil::deferredRun确保其在所有事件钩子返回后才运行同时以shared_ptr自持保证 C 侧过滤器在延迟期间存活行为上拆除后通过destroyed_标志拒绝新建 callout、清空 dispatcher 缓存短路跨线程事件、取消全部存量 callout 与流并依赖in_module_filter_置空让所有异步回调优雅退出。对动态模块的维护者与使用者而言理解销毁发生在延迟删除队列而非钩子栈上这一约定是正确实现内部重定向、请求重试等高级流控制能力的基石。相关核心代码可继续在 source/extensions/filters/http/dynamic_modules/filter.cc、source/extensions/filters/http/dynamic_modules/filter.h、source/common/event/deferred_task.h 以及 ABI 定义 source/extensions/dynamic_modules/abi/abi.h 中深入研读。【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考