Envoy CVE-2026-48521 修复解读:ALPN 自动协商叠加 HTTP/3 上游为何引发空指针崩溃,以及修复验证与配置要点
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
本文针对 Envoy 的一次安全修复——CVE-2026-48521(对应安全公告 GHSA-5vff-j9p4-38j3):当集群通过 ALPN(应用层协议协商,TLS 握手中交换双方支持的协议列表)自动选择上游协议、且上游服务器实际使用 HTTP/3 时,进程可能因空指针解引用而直接崩溃。读完本文,你能定位这条"自动协商 + HTTP/3"链路上的状态对象与异步回调、掌握完整可用的 auto_config 配置写法,并用仓库内既有测试验证修复后的回退行为。
一、影响面速览:一张表定位故障半径
| 项目 | 内容 |
|---|---|
| 编号 | CVE-2026-48521 / GHSA-5vff-j9p4-38j3 |
| 触发场景 | 集群启用auto_config(基于 ALPN 自动选择上游协议),且上游经 HTTP/3 应答 |
| 后果 | 异常进程终止(abnormal process termination),非优雅退出 |
| 根因 | 空指针解引用(null dereference) |
| 修复载体 | 收录于changelogs/current/未发布变更集的bug_fixes条目:http3__fixed-crash-due-to-null-deref.rst |
| 修复版本 | current变更集(未随已发布小版本流出),以包含该条目的构建为准 |
changelog 原文只有两行:声明该编号,并说明"当 Envoy 配置为基于 ALPN 与上游自动选择协议、而服务器使用 HTTP/3 时,修复异常进程终止"。没有堆栈、没有补丁摘要——这也是为什么下文把"推断"与"事实"分开标注。
为什么进程级崩溃值得单独对待:边缘/中间/服务代理是长连接持有者,一次段错误不只是单请求失败,而是该实例上所有在途 QUIC 连接、TCP 连接与排队请求被同时丢弃,对端表现为连接闪断或 5xx 风暴。相比"某协议路径行为异常",进程消失意味着没有任何重试语义可以兜底,因此这类缺陷在代理项目中通常按高危处理。
二、触发链路:从配置到崩溃
2.1 ALPN 自动协商的硬约束
崩溃场景对应的不是固定协议(explicit_http_config),也不是跟随下游(use_downstream_protocol_config),而是HttpProtocolOptions.auto_config,即 AutoHttpConfig 消息。把 proto 注释翻译成一组可核查的约束:
- 协议二选一,由 ALPN 裁决:该模式下集群只会用 HTTP/1 或 HTTP/2,最终取 ALPN 协商结果中较高的协议;
- 无 ALPN 即降级:上游不支持 ALPN 时,回落到 HTTP/1;
- 传输套接字准入:只允许使用支持 ALPN 的 transport socket,配错会导致配置加载失败而不是运行期报错;
- ALPN 列表:传输层可自定义 ALPN,但集群默认值(或自定义 ALPN 失败时)为
"h2,http/1.1"; - HTTP/3 是条件项:与 HTTP/1、HTTP/2 不同,HTTP/3 不会因为出现在
auto_config里就无条件启用,必须同时满足"配置了http3_protocol_options"与"存在服务器端 HTTP/3 支持的通告"两个前提。
第 5 条是本条目的关键:自动协商链路里混入了一个取决于运行期外部输入(上游通告)的协议,协商结果从"配置决定"变成了"配置 × 运行期状态"。
2.2 为什么"自动协商 + HTTP/3"是最脆弱的路径
上游 HTTP/3 架构说明给出了这条路径额外的复杂度来源:
- 通告驱动:
auto_config中配置了 HTTP/3 后,Envoy 只向通告过HTTP/3 支持的服务器发起 QUIC 连接,通告来源是 Alt-Svc 头(RFC 7838)或 HTTPS DNS 资源记录;没有通告则退回 HTTP/2 或 HTTP/1; - 传输介质分裂:HTTP/3 跑在 QUIC/UDP 上,HTTP/1/2 跑在 TCP 上。网络设备拦截 UDP 很常见,导致 QUIC 连接尝试被阻断,必须回退 TCP——但反过来,在 QUIC 畅通的网段又应避免无谓地发起 HTTP/2 连接;
- 需要"记忆":上述双向要求意味着连接代码必须记住HTTP/3 连接尝试是否持续失败,据此决定是继续探测还是直接走 TCP。
把这三点合起来看:一条请求从进入连接池到拿到响应,要经过"查通告缓存 → 查 HTTP/3 健康状态 → 创建/复用惰性池 → 300ms 竞速 → 失败回调 → 标记状态"这样一串跨越多个状态对象、多个回调层的动作。任何一个状态对象在特定回调时序下"还没被创建就被访问",就是一次空指针解引用。固定协议配置下这些对象根本不会被引入,这也是为什么只有 auto_config + HTTP/3 组合会触发该 CVE。
三、崩溃路径上的关键成员
3.1 双池竞速:ConnectivityGrid 如何路由一个请求
核心类 ConnectivityGrid 本身实现ConnectionPool接口,内部同时持有两类池:
- QUIC 池:
Http3ConnPoolImpl,由createHttp3Pool()(conn_pool_grid.cc 第 421-427 行,经Http3::allocateConnPool创建)构建; - TCP 混合池:
HttpConnPoolImplMixed,由createHttp2Pool()(第 415-419 行)构建,承担 HTTP/1 与 HTTP/2。
两者都是惰性成员:http3_pool_、http3_alternate_pool_、http2_pool_初始为 null,首次用到时才通过getOrCreateHttp3Pool()/getOrCreateHttp2Pool()创建(后者见 第 403-413 行)。
newStream()(第 441-501 行)的分派逻辑可拆成四条分支:
shouldAttemptHttp3()为真且请求允许can_use_http3_时,首选用 HTTP/3 池;- 若 HTTP/3 处于
Pending或FailedRecently状态(受 runtime flagenvoy.reloadable_features.quic_no_tcp_delay影响),delay_tcp_attempt置为 false——不再等 300ms,立即并行发起 TCP 尝试,同时禁止 early data; - 通告缓存中没有HTTP/3 记录时,请求直接进 TCP 混合池;
- HTTP/3 已通告且健康时,请求先进 HTTP/3 池;300ms(
kDefaultTimeoutMs = 300,第 28 行)内未成功则同时向混合池补发,谁先就绪谁胜出。
回调侧由WrapperCallbacks/ConnectionAttemptCallbacks承接:onPoolFailure(第 100-105 行)会把失败上抛给父回调,父回调再决定是切池还是终止。注意这些回调执行时,"当前池"成员可能刚被创建、也可能尚未创建——惰性成员 + 回调密集,正是空指针的典型温床(该判断基于代码结构推断,见第四节)。
3.2 通告缓存:HttpServerPropertiesCache 里装的是什么
HttpServerPropertiesCache 实现负责回答"哪些服务器通告了 HTTP/3"。两个要点:
- 通告来源目前是Alt-Svc 响应头(HTTPS DNS RR 在路线图上);
- 存储范围是精确匹配的:只保存与请求相同主机名、相同端口的通告,不做域名级泛化。
对应配置结构是 AlternateProtocolsCacheOptions,字段与默认值如下:
| 字段 | 语义 | 默认 / 限制 |
|---|---|---|
name | 缓存实例名;同一名称在不同配置组件被引用时各字段必须完全一致,否则加载失败 | 必填 |
max_entries | 缓存最大条目数 | 1024;近似值,按 worker 线程独立执行,实际可因时序略超 |
key_value_store_config | 可选持久化 KV 存储,把条目刷盘 | 仅并发度为 1 时支持 |
prepopulated_entries | 预填充条目,以 7 天生命周期让 Envoy 对未通告的上游也尝试 HTTP/3;会被 Alt-Svc 头或缓存值覆盖 | 无 |
canonical_suffixes | 可共享 Alt-Svc 条目的主机名后缀列表,每项必须以.开头;命中多个时取列表中第一个 | 无 |
3.3 失败记忆:损坏期退避状态机
"记住哪些上游的 HTTP/3 不可用"的职责由 Http3StatusTrackerImpl 承担,是一个四态状态机:
Pending:待定(初始态);Broken:已标记损坏,损坏期计时中;FailedRecently:损坏期刚结束、尚未被验证的过渡态;Confirmed:已确认可用。
文档与源码的退避参数不一致,以源码为准:架构文档写的是"首次损坏 5 分钟、再次损坏翻倍、上限 1 天";而 http3_status_tracker_impl.cc 的实际实现是——初始损坏期DefaultExpirationTime{1}(1 秒),每次markHttp3Broken()时按1 << consecutive_broken_count_指数翻倍,MaxConsecutiveBrokenCount = 17封顶(源码注释称"约一天半",即 2^17 秒 ≈ 36 小时),markHttp3Confirmed()把计数清零并关闭计时器。这套参数差了一个数量级,排障时若按文档推算"上游为何一直不走 QUIC"会得出错误结论。
四、空指针从哪来:修复逻辑与测试闭环
changelog 只声明了"修复空指针解引用导致的异常进程终止",未披露补丁内容,因此以下来源分析基于代码结构推断,不是官方修复细节:
- 惰性池成员在回调中被解引用:
ConnectivityGrid的三个池成员都是惰性创建。onPoolFailure→onConnectionAttemptFailed→ 切池/重试的链路上,若某一步在getOrCreateXxxPool()执行前就解引用了尚为 null 的成员(例如交替池http3_alternate_pool_的创建判断路径),即为直接崩溃点; - 状态追踪器未就绪:
Http3StatusTracker挂在通告缓存的Origin上,若alternate_protocols_关联尚未建立或 origin 解析失败,追踪器访问同样可能落空; - 配置校验堵住了一个入口:source/extensions/upstreams/http/config.cc 第 92-98 行 在解析期强制校验——
auto_config启用 HTTP/3 而未配alternate_protocols_cache_options时直接拒绝加载,报错"alternate protocols cache must be configured when HTTP/3 is enabled with auto_config"。这至少保证"HTTP/3 已启用但通告缓存缺失"的非法状态进不了运行期。
工程上此类修复通常包含两部分:崩溃路径上加判空/提前返回,以及补一条复现该时序的回归测试。仓库内已有两条测试链路可以直接当"验收标准"用:
1. 双池回退链——ConnectivityGridTest(test/common/http/conn_pool_grid_test.cc)
Success(第 286-304 行):注入 Alt-Svc 通告后发起newStream,断言http3Pool()已创建而http2Pool()仍为 null(请求确实走了 QUIC 池);握手完成后isHttp3Confirmed()为真,onPoolReady透传给原始调用方;DoubleFailureThenSuccessSerial(第 323-357 行):开启envoy.reloadable_features.http3_happy_eyeballs后验证完整降级链——HTTP/3 池失败时首次失败不向上抛(pool_failure_期望调用 0 次),转而尝试交替池;交替池也失败后落到 HTTP/2 池并成功;最终断言isHttp3Broken()为真。这条用例精确覆盖了"失败回调密集、池成员多次切换"的空指针高危窗口。
2. 退避状态机——Http3StatusTrackerImplTest(test/common/http/http3_status_tracker_impl_test.cc)
MarkBrokenWithBackoff(第 67-99 行):依次断言损坏期1s → 2s → 4s → 8s,每次到期后进入FailedRecently;MarkBrokenWithBackoffMax(第 101-124 行):循环 17 次验证2^0 … 2^16秒递增,之后连续两次markHttp3Broken()均停在2^17 秒,封顶生效;MarkBrokenThenExpiresThenConfirmedThenBroken:Broken → 到期 → Confirmed(计数清零)→ 再 Broken 时回到初始 1 秒。
有 Bazel 环境时,对应目标为bazel test //test/common/http:conn_pool_grid_test //test/common/http:http3_status_tracker_impl_test;通告缓存层另有 http_server_properties_cache_impl_test.cc 可一并纳入回归基线。
五、配置蓝图:三处缺一不可
5.1 集群侧:auto_config 完整声明
static_resources: clusters: - name: upstream_with_h3 connect_timeout: 5s type: STRICT_DNS load_assignment: cluster_name: upstream_with_h3 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 sni: upstream.example.com 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 max_entries: 1024三个容易漏的点:auto_config下三种协议选项要写全(架构文档明确要求同时指定 HTTP/1、HTTP/2、HTTP/3);http3_protocol_options只是"允许",是否真正启用仍取决于上游通告;由于 ALPN 是硬性依赖,transport_socket必须换成支持 ALPN 的 TLS 套接字(如上述UpstreamTlsContext,默认 ALPN"h2,http/1.1")。
5.2 过滤器侧:让 Alt-Svc 头进入缓存
通告不会凭空出现在缓存里,需要在 HttpConnectionManager 过滤器链中启用 Alternate Protocols Cache Filter 来解析并写入 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注意该过滤器的 FilterConfig 里原有的alternate_protocols_cache_options字段已标记废弃(deprecated_at_minor_version = "3.0"),注释明确写着"此字段被忽略:过滤器将使用请求路由到的集群所配置的缓存"。也就是说:缓存声明只写在集群auto_config.alternate_protocols_cache_options一处(5.1 所示),过滤器侧留空即可;若旧配置仍在过滤器里写选项,需要清理并保证name与集群引用一致。
5.3 常见坑
- 漏配
alternate_protocols_cache_options:加载期直接失败,报错即第四节引用的"alternate protocols cache must be configured when HTTP/3 is enabled with auto_config"; - 同名缓存字段不一致:同一
name在不同组件被引用但字段值不等,同样加载失败; - 配了
key_value_store_config但并发度 > 1:解析期拒绝,报错会带出实际并发度; - TLS 套接字不支持 ALPN:
auto_config直接配置失败,不会带病运行; - 只配了
http3_protocol_options却没有上游通告、也没配prepopulated_entries:配置合法但不走 QUIC——这不是 bug,是设计行为。
六、上线后四步检查
- 升级到包含该修复的构建:以 changelog 条目 http3__fixed-crash-due-to-null-deref.rst 进入的版本为下限,优先覆盖"auto_config + 上游可能通告 HTTP/3"的集群;
- 核对配置完整性:
auto_config三种协议选项齐全、alternate_protocols_cache_options已声明且各组件name一致、TLS 套接字支持 ALPN; - 评估 UDP 可达性:若上游所在网络拦截 UDP,QUIC 尝试会持续失败,此时系统依赖第四节状态机的指数退避回退 TCP——确认该行为(
isHttp3Broken置位、请求落到混合池)与监控指标一致; - 锁定回归基线:以 conn_pool_grid_test.cc(
Success、DoubleFailureThenSuccessSerial)与 http3_status_tracker_impl_test.cc(退避序列 1s→2s→4s→8s 及 2^17 秒封顶)作为升级前后对比的验收点,确认失败回退与空指针防护行为未被回归。
附录:仓库内证据索引
| 类别 | 文件 |
|---|---|
| 修复条目 | changelogs/current/bug_fixes/http3__fixed-crash-due-to-null-deref.rst |
| 架构说明 | source/docs/http3_upstream.md |
| 协议定义 | api/envoy/extensions/upstreams/http/v3/http_protocol_options.proto、api/envoy/config/core/v3/protocol.proto |
| 过滤器定义 | api/envoy/extensions/filters/http/alternate_protocols_cache/v3/alternate_protocols_cache.proto |
| 核心实现 | source/common/http/conn_pool_grid.cc、source/common/http/http3_status_tracker_impl.cc、source/common/http/http_server_properties_cache_impl.cc、source/extensions/upstreams/http/config.cc |
| 测试用例 | test/common/http/conn_pool_grid_test.cc、test/common/http/http3_status_tracker_impl_test.cc、test/common/http/http_server_properties_cache_impl_test.cc |
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考