Skip to content

fix(http3): 帧回调里析构 WebSocket 会话后不再解下一帧(h3/h2) - #65

Merged
Antruly merged 1 commit into
masterfrom
fix/h3-ws-destroy-in-callback
Oct 28, 2026
Merged

Antruly merged 1 commit into
masterfrom
fix/h3-ws-destroy-in-callback

Conversation

@Antruly

@Antruly Antruly commented Oct 28, 2026

Copy link
Copy Markdown
Owner

问题

uvcpp_h3_ws_connection::on_data 会连续解同一批数据里的多帧。帧回调是应用代码,
在里面删掉这条会话是合法用法(收到一条错误帧就整条会话收摊)。旧实现在回调
返回后继续解下一帧,而这时对象已经析构:

  • uvcpp_ws_parser 的析构把 frame_arg_ 清成 nullptr,frame_fn_ 仍是
    trampoline_frame;
  • 下一帧进 trampoline_frame,第一句就是 cmpq $0x0,0x10(%rsi),rsi == 0
    → SIGSEGV。

现场栈(moLink 的 relay 客户端,两帧落在同一次 QUIC 流读取里):

#0 uvcpp::trampoline_frame(uvcpp_ws_frame const*, void*)        rsi=0
#1 uvcpp::uvcpp_ws_parser::parse_payload
#2 uvcpp::uvcpp_ws_parser::execute
#3 uvcpp::uvcpp_h3_ws_connection::on_data
#4 uvcpp::uvcpp_h3_session::cb_recv_data
#5 nghttp3_conn_read_bidi

修复

  • h3 / h2 的 ws 会话各持一枚自己的存活令牌(alive_),析构第一件事置位;
    on_data 每解完一帧只看令牌,回调里析构了就立刻返回,不再碰 this。
  • on_frame 先把消息 swap 出成员再回调(旧写法回调之后还有 message_.clear(),
    同样是在已释放对象上)。
  • fail() 先发 Close、置状态,最后才调 on_error(旧写法回调在最前面,之后还
    会碰 close_sent_ / h3_ / closed_)。
  • h3 与 h2 是同一套形状,两条腿一起改。

测试

两条 functional 用例,形状与生产崩溃一致:客户端把两条文本帧一次写进同一条
流
,服务端在第一帧的 on_text 里销毁会话。

用例 去掉守卫 加守卫
test_http3_ws_func destroy SIGSEGV @ trampoline_frame 通过,第二帧被丢弃
test_h2_ws_func destroy SIGSEGV @ trampoline_frame 通过,第二帧被丢弃

全量 ctest:163/163 通过(含 http3 / h2 / quic / webtransport)。

备注

  • 本 PR 基于 master(当前默认分支)。dev/1.9.0 领先 master 4 个提交
    (v1.9.0 标签那批),合并后那条线需要一次合并或 cherry-pick 才能拿到这个修复。
  • 应用侧不该在同一回调栈里同步析构传输,另一个修法是延迟释放;这里按库的既有
    约定(uvcpp_ws_connection::alive_token_)在库内兜住,两种写法不冲突。

`uvcpp_h3_ws_connection::on_data` 把一批数据里的每一帧依次交给解析器;帧回调
属于应用代码,在里面删掉这条会话是合法用法(应用收到一条错误帧就把整条会话
收摊)。旧实现在回调返回后继续解下一帧:此时对象已经析构,`uvcpp_ws_parser`
的析构把 `frame_arg_` 清成 nullptr,而 `frame_fn_` 仍是 `trampoline_frame`,
于是崩在 `trampoline_frame` 对空参数的第一次解引用上。

同一类问题在 h2 那条腿上也存在:`on_frame` 的 `message_.clear()`、`fail()` 里
的 `on_error` 都排在回调之后,一并调整了顺序。

- h3/h2 的 ws 会话各持一枚自己的存活令牌(`alive_`),析构第一件事置位;
  `on_data` 每解完一帧只看令牌,不再碰 `this`。
- `on_frame` 先把消息 swap 出成员再回调,回调之后不碰任何成员。
- `fail()` 先发 Close、置状态,最后才调 `on_error`。

测试:`test_http3_ws_func` 与 `test_h2_ws_func` 各加一条 destroy 场景 —— 两条
文本帧一次写进同一条流,服务端在第一帧的回调里销毁会话。去掉上面的守卫时两条
用例都在 `trampoline_frame` 处 SIGSEGV;加上后第二帧被丢弃、进程不崩。
全量 ctest 163/163。
@Antruly
Antruly merged commit c0b4e9e into master Oct 28, 2026
59 of 62 checks passed
@Antruly
Antruly deleted the fix/h3-ws-destroy-in-callback branch October 28, 2026 20:11
Antruly added a commit that referenced this pull request Oct 29, 2026
- 版本头 PATCH 1;两版 README 的徽章与 Version 行同步,"最新版本"改为 v1.9.1。
- CHANGELOG:三条主题条目(WebSocket 语义 / 横向扩展 / TLS 与网络)+ 新增
  v1.9.1 小节;当前源码树与 tag 清单同步。
- RELEASE.md:新增「v1.9.1 重点」,破坏性为**无**(只新增 API,C ABI 仍为 1)。

含自 v1.9.0 以来的三笔:#65(h3/h2 WS 帧回调里析构会话后的 UAF)、
#60(`uvcpp_udp_server(uvcpp_loop*)`,#59)、#57(五个 set_ssl_context 入口的
生命周期文档,issue #56),以及 `_local_deps` 误提交链接的清理。

本地:build-all 162/162、build-h3 120/120、build-default 60/60;文档门禁全绿;
release_notes.py --tag v1.9.1 能切出正文。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant