Envoy HTTP/3 崩溃修复深度拆解:HTTP/3 上游空指针与 CVE-2026-48521 完整指南
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
集群启用 auto_config 自动协商、上游又通过 Alt-Svc 通告 HTTP/3 时,Envoy 会因空指针解引用直接崩溃,即 CVE-2026-48521。这项 Envoy HTTP/3 崩溃修复的触发条件、协商机制、正确配置与回归验证,一次讲清。
崩溃现场:CVE-2026-48521 的三行 changelog
修复记录 http3__fixed-crash-due-to-null-deref.rst 只写了三行,但信息量足够完整:
- CVE 编号:CVE-2026-48521,对应安全公告 GHSA-5vff-j9p4-38j3;
- 受影响配置模式:集群
HttpProtocolOptions使用auto_config分支,由 ALPN(应用层协议协商,即 TLS 握手中双方互相报出可接受的协议列表)与上游决定协议; - 后果:abnormal process termination,进程级崩溃,不是优雅退出。
对代理而言,进程就是所有在途流的容器:一次崩溃意味着该实例上所有连接瞬间切断,下游看到成片的连接重置,上游看到成片的请求中断。对边缘网关来说这是请求级事故,恢复窗口等于编排系统重新拉起实例的时间——在这之前,流量要么丢弃要么绕行。
协议协商的条件门:auto_config ALPN 协商与 HTTP/3 的差异
auto_config对应http_protocol_options.proto中的AutoHttpConfig分支:HTTP/1 与 HTTP/2 是"默认选手",只要传输套接字支持 ALPN,用哪个由协商结果决定(默认列表"h2,http/1.1"),上游不支持 ALPN 时回退 HTTP/1。HTTP/3 不同,它有双重门:集群必须显式声明http3_protocol_options,并且上游必须通过 Alt-Svc 头(RFC 7838)或 HTTPS DNS 资源记录通告过自己支持 HTTP/3。没有任何通告,Envoy 就不会对那个主机发起 QUIC 连接。
| 协议 | 启用条件 | 回退行为 |
|---|---|---|
| HTTP/1 | 声明即可 | 上游不支持 ALPN 时启用 |
| HTTP/2 | ALPN 协商到h2 | 协商失败时让位给 HTTP/1 |
| HTTP/3 | 声明 + 上游 Alt-Svc/DNS RR 通告 | 无通告或 UDP 受阻时回退 TCP |
QUIC 跑在 UDP 上,而不少网络设备无差别拦截 UDP,这类网络里 HTTP/3 连接尝试会持续失败。于是上游链路必须"记住失败",避免发起注定失败的连接——架构文档source/docs/http3_upstream.md对这条要求有完整描述,它也正是本次崩溃的直接土壤。
双池竞速与失败记忆:ConnectivityGrid 双池里的三个角色
整套机制由连接池层三个协作对象支撑,空指针就藏在它们之间的接缝里。
ConnectivityGrid:300ms 双池竞速
ConnectivityGrid本身是一个 ConnectionPool,内部包装两个真实池:Http3ConnPoolImpl(QUIC)与HttpConnPoolImplMixed(TCP)。conn_pool_grid.cc 中newStream的分派核心:
if (shouldAttemptHttp3() && options.can_use_http3_) { pool = getOrCreateHttp3Pool(); // QUIC 池优先 } else { pool = getOrCreateHttp2Pool(); // 未通告或不支持时走 TCP }HTTP/3 已通告且可用时,请求先入 QUIC 池;kDefaultTimeoutMs(300ms)内未成功,同一请求再进入 TCP 混合池,谁先成功谁胜出。若状态追踪器判定 HTTP/3 已 broken 或最近失败,delay_tcp_attempt直接置 false,TCP 路径不再等待。http3_pool_、http2_pool_全部惰性创建——首次调用前它们都是 nullptr,任何绕过 getOrCreate 直接解引用的回调路径都是空指针。
HttpServerPropertiesCache:Alt-Svc 通告的账本
http_server_properties_cache_impl.cc 记录哪些主机通告了 HTTP/3;当前只存储与请求相同主机名和端口的通告,避免跨主机误用。行为由AlternateProtocolsCacheOptions控制:
name:缓存实例标识,不同组件引用同名缓存时各字段必须一致,否则配置加载失败;max_entries:默认 1024,淘汰为近似值且每个 worker 独立执行,实际条目数可略超配置;prepopulated_entries:以 7 天生命周期预填 HTTP/3 条目,让 Envoy 对未通告的上游也尝试 HTTP/3。
该缓存被网格与追踪器经由alternate_protocols_智能指针引用,名称对不上或 origin 未就绪时,这个引用就是空的。
Http3StatusTracker:Pending → Broken → FailedRecently → Confirmed
http3_status_tracker_impl.cc 是 HTTP/3 失败后的"记忆":markHttp3Broken()开启退避定时器DefaultExpirationTime * (1 << consecutive_broken_count_),即 1s → 2s → 4s 逐次翻倍;MaxConsecutiveBrokenCount = 17封顶,最长退避约 2^17 秒;定时器到期状态从Broken转入FailedRecently,markHttp3Confirmed()则把计数清零。
文档与代码存在一处已知出入:架构文档 http3_upstream.md 写的是"首次损坏 5 分钟、再次损坏翻倍、上限 1 天",而源码DefaultExpirationTime{1}是 1 秒起步、2^17 秒封顶——以源码为准。
追踪器经getHttp3StatusTracker()惰性获取,若状态查询先于其与网格的关联发生,检查的就是空指针。
⚠️ HTTP/3 上游空指针:风险点与修复推断
官方只披露了"修复异常终止"这一结果,未披露 patch 细节;以下是基于代码结构的推断:
- 惰性池成员。
newStream先经getOrCreateHttp3Pool()建池,随后WrapperCallbacks的异步回调会跨池触发tryAnotherConnection();这些回调路径中任何一处读取http3_pool_/http3_alternate_pool_时其仍为 nullptr,即触发崩溃。 - 追踪器与缓存未就绪。状态追踪器关联到
alternate_protocols_的HttpServerPropertiesCache::Origin;缓存未建立或 origin 解析失败时,后续状态查询同样可能空引用。 - 配置校验兜底。解析层已强制"auto_config + http3_protocol_options 必须伴随 alternate_protocols_cache_options",否则直接拒绝加载(config.cc 报错)。这堵住了最典型的空状态入口,但运行时时序仍靠代码自身保证。
此类修复的工程形态通常是双件:崩溃路径上加判空或提前返回,再补一个钉死该时序的回归测试。
完整配置:三处缺一不可
启用这条链路需要集群、过滤器、网络三处配合,缺任何一处都会静默退化或加载失败。
集群侧:auto_config 全量声明三种协议
static_resources: clusters: - name: upstream_with_h3 type: STRICT_DNS load_assignment: endpoints: - lb_endpoints: - endpoint: address: socket_address: address: upstream.example.com port_value: 443 transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions/upstreams/http/v3.HttpProtocolOptions auto_config: http_protocol_options: {} http2_protocol_options: {} http3_protocol_options: {} alternate_protocols_cache_options: name: default_alternate_protocols_cache- 三种协议选项必须全部出现,
auto_config依赖 ALPN,三个槽位缺一个都会被校验拒绝; http3_protocol_options只代表"允许" HTTP/3,是否真正启用仍取决于上游通告;alternate_protocols_cache_options.name是缓存实例标识,与过滤器侧引用不一致会加载失败;max_entries省略时用默认 1024;- 传输套接字必须支持 ALPN(TLS UpstreamTlsContext),使用不支持 ALPN 的传输套接字会导致整个配置加载失败。
过滤器侧:让 Alt-Svc 头真正入库
http_filters: - name: envoy.filters.http.alternate_protocols_cache typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.alternate_protocols_cache.v3.FilterConfig没有这个过滤器,alt-svc 响应头不会被解析,缓存永远为空。注意:过滤器配置里的alternate_protocols_cache_options字段已标记废弃(计划 3.0 移除),注释明确说明该字段被忽略——过滤器直接使用请求路由所对应集群声明的缓存。规范写法是集群级声明、名称保持一致。
回退行为:UDP 被拦截时的降级
网络拦截 UDP 时,HTTP/3 连接尝试全部失败,状态追踪器把该上游标记为 Broken 并进入指数退避,请求直接走 TCP 池(HTTP/2/1)——这正是本次修复所保护的路径。在 HTTP/3 正常的网络上,退避定时器到期后追踪器转回FailedRecently并重新探测,避免无谓的 TCP 尝试。
回归验证:两条测试路径
两个测试目标分别钉死"回退链不崩溃"与"退避值正确"两个性质:
| 测试文件 | 覆盖场景 | Bazel 命令 |
|---|---|---|
| test/common/http/conn_pool_grid_test.cc | Success:通告存在 → QUIC 池 → Confirmed;DoubleFailureThenSuccessSerial:h3 池失败 → 交替池失败 → h2 池成功且不向上抛错 | bazel test //test/common/http:conn_pool_grid_test |
| test/common/http/http3_status_tracker_impl_test.cc | MarkBrokenWithBackoff:1s→2s→4s→8s;MarkBrokenWithBackoffMax:2^17s 封顶;Confirmed 重置计数 | bazel test //test/common/http:http3_status_tracker_impl_test |
即 conn_pool_grid_test.cc 与 http3_status_tracker_impl_test.cc 两个目标。升级后运行它们作为验收基线:前者保证双池竞速的失败回退链完整可走,后者保证退避参数与源码实现一致。
运维行动清单
按优先级排序,第一条是唯一彻底的解:
- 升级到包含修复的版本:对
auto_config且上游可能通告 HTTP/3 的实例,升级前空指针始终存在; - 审视
auto_config配置完整性:确认三种协议选项与alternate_protocols_cache_options均在,避免加载失败或缓存引用错位; - 确认 UDP 可达性:上游网络拦截 UDP 时,退避机制会自动承担 HTTP/3→TCP 回退,不必手工调整超时;
- 跟踪回归测试:升级后运行上文两个目标,确认回退链与退避行为仍被测试覆盖。
对代理实例而言,配置只能让回退路径更短,崩溃本身要靠升级后的二进制来堵。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考