Skip to content

refactor(api): 原子构造 + 所有权进名字(v2.10 第一刀) - #470

Merged
adam-ikari merged 4 commits into
mainfrom
refactor/api-atomic
Oct 11, 2026
Merged

adam-ikari merged 4 commits into
mainfrom
refactor/api-atomic

Conversation

@adam-ikari

@adam-ikari adam-ikari commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

v2.10 第一刀:构造面。方向与兼容策略见 brain api-atomization-orthogonalization(破坏性改、不留兼容层)。

新增:uvhttp_server_listen_routes —— 原子的构造入口

此前"起一个带路由的监听服务器"要 5 步,且没有原子版本,调用方被迫为每一步手写回滚:

const uvhttp_route_t routes[] = {
    {"/",        UVHTTP_ANY, hello},
    {"/health",  UVHTTP_GET,  health},
};
uvhttp_server_listen_routes(loop, routes, 2, "0.0.0.0", 8080, &server);

要么返回完全就绪、路由齐全的监听中的 server,要么什么都没创建(所有失败路径上 *server 为 NULL)。无中间态,无回滚可写。

修复:官方嵌入示例的 double-free(ASan 可复现)

examples/embedding/main.c 的 listen 失败分支在 set_router 已转移所有权之后仍调 uvhttp_router_free(router),随后 uvhttp_server_free(server) 再释放一次。

重命名:所有权进函数名

  • uvhttp_server_set_router → uvhttp_server_take_router
  • uvhttp_server_set_context → uvhttp_server_take_context

"take" 明确表达所有权转移。同时这两个函数原先会静默覆盖已有指针(旧对象泄漏),现在返回 INVALID_PARAM 拒绝;重复 take 同一个对象仍为幂等,便于 teardown 路径复用。NULL 不是 detach:take 之后传 NULL 同样被拒 —— 头文件、代码注释、4 个新用例与 2 处旧测试注释都已说清这条语义。

删除:请求读取 API 的纯别名

uvhttp_get_header / uvhttp_get_param / uvhttp_get_body —— 三个函数体各只有一行转发,且声明在 server.h 里却不涉及 server;uvhttp_get_param 的 "param" 还有歧义(实为 query param)。

评审修复(第三个提交)

review 发现 5 项 P1,均为契约/文档诚实度问题(非内存安全问题,内存安全面经 ASan 探针逐条验证):

  • docs/spec/server-api.md 的 take_router Postconditions 曾写着"server 不持有 router,调用方应释放"——与头文件 OWNERSHIP TRANSFERS HERE 正相反,而它恰是引发 double-free 的那个误解。已改正,并补齐 take_context 与 listen_routes 两节
  • 两个根 README 未被触及,快速开始仍调用已改名的 set_router(合并后照抄编译不过)
  • ListenFailureLeavesNothingBehind 原用特权端口 1 制造失败,root 下 GTEST_SKIP → 本 PR 核心回归在该环境不被验证。改为先 uv_listen 占住端点端口再传同端口,必然 EADDRINUSE,与运行身份无关
  • 新 API 在 spec / API 参考面缺席;method 越值会登记"永不匹配"的死路由,现已在写点拒绝
  • 失败日志原先建议"free it first"——调用方无权单独释放 server 持有的对象(free 了就是 UAF)且 NULL 也卸不掉,改为给出可执行的出路

已按用户方案从本 PR 拆出:#472(14 个死配置字段与 4 个假旋钮 API)、#473(set_body 原子替换 + LRU setter 不变量)。

验证

ctest 120/120(含 ASan 全量);method 校验经变异测试验证有牙齿;cppcheck 零告警;test-validity 门禁 PASS;build-matrix 5 种裁剪组合全过。

adam added 2 commits October 9, 2026 09:25
按"原子性 = API 不可再分割,正交性 = API 没有重合"改造公共 API(v2.10
破坏性变更,不留兼容层)。本 PR 处理 N1(构造不可分割)、O1(读取 API
重合)、O3(构造入口三重叠)。

## 新增:uvhttp_server_listen_routes —— 原子的构造入口

此前"起一个带路由的监听服务器"需要 5 步,且没有原子版本:

    server_new -> router_new -> add_route xN -> set_router -> listen

调用方被迫为每一步手写回滚。现在一次调用完成:

    const uvhttp_route_t routes[] = {
        {"/",        UVHTTP_ANY, hello},
        {"/health", UVHTTP_GET, health},
    };
    uvhttp_server_listen_routes(loop, routes, 2, "0.0.0.0", 8080, &server);

要么返回一个完全就绪、路由齐全的监听中的 server,要么什么都没创建
(*server 在所有失败路径上为 NULL)。无中间态,调用方无回滚可写。

## 修复:官方嵌入示例的 double-free(ASan 可复现)

examples/embedding/main.c 的 listen 失败分支:

    uvhttp_router_free(router);   // router 已被 take_router 交给 server
    uvhttp_server_free(server);   // 内部再释放一次 -> 二次释放

根因是命名与语义不符:`set_router` 听起来是借用,实际是所有权转移,
而这个事实只写在 @note 里。调用方照着最直觉的写法就踩中。

## 重命名:所有权进函数名

- uvhttp_server_set_router  -> uvhttp_server_take_router
- uvhttp_server_set_context -> uvhttp_server_take_context

"take" 明确表达所有权转移。同时这两个函数原先会静默覆盖已有的指针
(旧 router 泄漏),现在返回 INVALID_PARAM 拒绝;重复 take 同一个对象
仍为幂等,便于 teardown 路径复用。

## 删除:请求读取 API 的纯别名(O1)

三个函数体各只有一行转发,且声明在 server.h 里却不涉及 server:

    uvhttp_get_header(req, name) -> uvhttp_request_get_header(req, name)
    uvhttp_get_param(req, name)  -> uvhttp_request_get_query_param(...)  // 名字有歧义
    uvhttp_get_body(req)         -> uvhttp_request_get_body(req)

同一能力两个入口,无依据可选。其中 uvhttp_get_param 实为 query param,
与路由参数 :id 混淆。保留 request.h 的正规名,测试改调之。

## 顺带:uvhttp_method_t 移到 uvhttp_common.h

server.h 需要它但不能 include request.h(循环)。原先靠前向声明不可行
(需要完整 enum 定义才能作为 struct 字段),故移到两个头文件共同依赖的
common.h。类型本身未变。

## 修复:另外 3 处同类 double-free

同一模式在 examples 与文档示例中重复出现,均已修正(router 交给 server
后不再单独释放):

- examples/custom_compression_strategy.c
- examples/06_advanced/sse_server.c(正常退出路径)
- examples/06_advanced/context_injection.c
- examples/08_e2e_tests/test_static_simple_e2e.c(2 处)
- docs/guide/EMBEDDING_GUIDE.md + docs/zh/(英中双语)

## 测试

新增 test/unit/test_server_atomic_construct.cpp(14 个):

- 构造成功路径(3 路由 / 0 路由)
- 6 条失败路径逐一断言 *server 保持 NULL(含路由表内 NULL path /
  NULL handler —— 旧 5 步版本要调用方自己处理的情况)
- 端口 1 绑定失败 = 官方示例崩溃的那条路径,ASan 下无二次释放
- 所有权:二次 take 被拒、同一对象重复 take 幂等、take_context 同理

验证:
- ctest 120/120 通过(新增 14 个)
- 新测试在 ASan 下 14/14 通过,无泄漏报告
- 文档示例(first-server.md / EMBEDDING_GUIDE.md)提取出来实编实跑:
  -Wall 无警告,实测返回 HTTP/1.1 200

## 未在本 PR 处理的

按 brain 审计的优先级,以下留待后续:

- P0 配置静默失效:uvhttp_config_t 26 字段中 14 个从不被执行路径读取
- P0 router 迁移致路由全丢:13+ 顶层路由 + 参数路由 -> 已注册路由全丢
- O2 两个 cache API 逐字同构,应合并为一个抽象
- O4 free/cleanup/simple_free 三组同义命名
- N2 发送响应 4~5 步且无原子版本
## api-atomization-orthogonalization(新)

方向:停止新增特性,v2.10 专做 API 原子化与正交化。
兼容策略 B(用户拍板):直接破坏性改,不留 deprecated 层。

第一性原理核实后修正了一版错误诊断。初版记的是"两条配置通道可能矛盾",
按 config->field 逐字段核实后发现真因更严重:uvhttp_config_t 26 个字段中
14 个从不被任何执行路径读取(3 个仅写不读 + 11 个零读点)。

- update_size_limits / set_max_body_size 返回 UVHTTP_OK 并打 INFO 日志,
  实际执行用编译期常量 UVHTTP_MAX_BODY_SIZE —— 零应用且报告成功
- set_timeout 写 request_timeout/keepalive_timeout(全库无读点),
  实际超时读 connection_timeout —— 第三个字段
- max_connections 有三个可写位置,生效值取决于用了哪个构造入口

## api-atomicity-audit(新)

判据按用户给定定义重写:**原子性 = API 不可再分割;正交性 = API 没有重合。**
(此前用的 C1-C4 提交性/幂等/跨对象半状态是事务原子性,与此无关,已作废。)

原子性违约:
- N1 构造一个可用 server 要 5 步且无原子版本 —— 实证来自官方嵌入示例
- N2 发送响应 4~5 步且无原子版本
- N3 设一个配置项要"选通道 + 装配 + 设置"三步

正交性违约:
- O1 请求读取双份并存,三对全是纯别名,其中 uvhttp_get_param 名字有歧义
- O2 lru_cache 与 gzip_cache 逐字同构,迫使调用方写两遍相同代码
- O3 构造入口三重叠,差异只是切分顺序
- O4 free/cleanup/simple_free 三组同义命名 + 4 种返回类型
- O5 压缩 5 个入口 + 2 个判定辅助,判定与设置未分离

两个实测确认的数据丢失 / 崩溃(探针程序,非代码推断):

- **N1 连带 double-free**:官方嵌入示例 listen 失败分支先 router_free
  再 server_free,而 server 已通过 set_router 接管 router。ASan 确认。
  同一模式在 4 个 examples + 英中双语嵌入指南中重复出现。
- **router 迁移致全丢**:13 个不同顶层路由 + 任意 :param 路由 ->
  migrate_to_trie 中途失败(child_indices[12] 定长上限撞顶)-> 已迁移
  条目因 use_trie 仍为 0 而永不可达,全部路由静默丢失。12 条安全,
  13 条全灭。现有测试零覆盖。

已做对、不要破坏的维度:无跨实例可变全局状态(TLS entropy 挂在 context
上,多 server 隔离天然成立);response 系 setter 先校验后写入。

## roadmap

v2.10.x 段重排,第一刀从"配置双通道合并"(基于错误诊断)改为
router 数据丢失修复 + 配置静默失效全线。
@adam-ikari
adam-ikari force-pushed the refactor/api-atomic branch from 2dbc6fe to 6681043 Compare October 9, 2026 12:07
adam added 2 commits October 10, 2026 16:27
针对 v2.10 第一刀(#470)的评审结论修复。无内存安全问题,全是契约与
文档的诚实度问题:

1. docs/spec/server-api.md 之前只改了函数名,Postconditions 仍写 "server
   does not own the router; the caller must free it" —— 与头文件
   "OWNERSHIP TRANSFERS HERE" 正相反,正是本 PR 要消灭的那个误解(它
   让官方示例 double-free)。同时补上:
   - take_context 一节(原 set_context 条目引用的函数已不存在)
   - listen_routes 一节(atomic 构造的契约:*server 失败恒 NULL、路由拷贝
     不引用调用方数组、构造后 router 句柄不可达)

2. README.md / README_CN.md 未被 #470 触及,快速开始仍调用已改名的
   uvhttp_server_set_router —— 合并后照抄编译不过。改为 take_router 并
   在注释里说明所有权转移。

3. ListenFailureLeavesNothingBehind 原来用特权端口 1 制造 listen 失败,
   root 下 GTEST_SKIP → 本 PR 的核心回归(listen 失败不二次释放)在该
   环境完全不被验证。改为先 uv_listen 占住一个端点端口再传同端口,必然
   EADDRINUSE,与运行身份无关。(GTEST_SKIP 属静默跳过,与 silent-skip
   治理同一类问题。)

4. take_router/take_context 的 "NULL 清空" 语义只在尚未持有时有效,已有
   对象时同样被拒 —— 头文件注释此前未说,两处测试注释还在宣称 "clears
   router"/"用于清除路由"。头文件补 "no detach operation" 说明,四处
   测试注释改为事实,新增 4 个用例锁定语义(NULL after take 拒绝且不动
   已持有对象 / 未持有时为空操作)。

5. 代码小修三处:
   - listen_routes 校验 method 范围:越界 method 会登记一条永不匹配的
     死路由(所有查找都是等值比较),原子构造不应建出一张说谎的路由表
     (变异测试:移除该校验 → OutOfRangeMethodIsRejected 变红)
   - take_router/take_context 的失败日志原建议 "free it first" —— 调用方
     无权单独释放 server 持有的对象(free 了就是 UAF),且 NULL 也卸不掉。
     改为给出可执行的出路(同对象幂等重传或用新 server)
   - uvhttp_server_free 释放 router 后未置 NULL(其余字段都置),补上,
     与 "字段要么权威要么不存在" 对齐

6. 测试补强:RoutesAreActuallyInstalledInTheRouter —— 此前 14 个新测试
   只断言 server != nullptr,"构造成功但路由没装进去"这类回归全绿(router
   迁移丢路由正是本仓同族 P0);现在按路径逐条断言 handler 与方法边界。

7. 注释纠错:uvhttp_server.h / uvhttp_request.h 声称两头文件 "include
   each other transitively"(实际互不 include,无环),uvhttp_server.c
   的自引用 "(was uvhttp_server_take_router)" 改为 set_router。

验证:默认构建 ctest 120/120;ASan 构建 test_server_atomic_construct
20/20 干净(含 listen 失败路径);变异测试确认 method 校验有牙齿。
release 门禁(scripts/ci/release_checklist.sh)对 main...HEAD 改动的
C/C++ 文件逐一跑 clang-format --dry-run --Werror。#470 引入的机械改名
带进了既有违规(test_server_atomic_construct.cpp 提交时即 23 处),
发布前必须清零——本 PR 不改,它们也是 "本次改动文件"。

全量清单与处数见提交说明:11 文件、469+/338-。无逻辑改动,ctest
120/120 复跑通过(纯空白重排)。与 #416 的存量全量重格式化同一处理方式,
区别是这里只覆盖本 PR 触达的文件。
@adam-ikari
adam-ikari merged commit d6cfb71 into main Oct 11, 2026
20 checks passed
@adam-ikari
adam-ikari deleted the refactor/api-atomic branch October 11, 2026 04:31
adam-ikari pushed a commit that referenced this pull request Oct 13, 2026
v2.10 roadmap d8(API 参考与示例同步)。对账发现两份文档对
uvhttp_server.h 的 45 个公共函数都有实质缺口,且 server.h 里还挂着声明在
别处的 request 函数。

## 1. builder 家族原本两份文档都没有条目

`uvhttp_server_create` / `uvhttp_get..uvhttp_any` /
`uvhttp_set_max_connections` / `uvhttp_server_run` / `stop_simple` /
`simple_free` / `uvhttp_serve` / `uvhttp_server_new_with_loop` —— 一个条目
都没有,而 `uvhttp_serve` 与 builder 链是 README 快速开始之外最常被抄的入口。
d5 刚改了这条路径的契约,文档还是空白。

补齐内容(spec 给完整契约、API 参考给精简版):
- **builder 是视图不是所有者** —— server/router/config 由 server_free 释放,
  拆卸只有 simple_free 一条路,没有 per-field teardown 的选项
- **错误码透传**(d5 新增):NULL host 是 INVALID_PARAM,端口占用是
  SERVER_LISTEN,不再一律塌缩成 SERVER_LISTEN
- **out-param 契约**:任何失败下 *server 为 NULL,调用方无回滚可写
- uvhttp_get..uvhttp_any 的失败**只记日志不返回** —— 链式调用无法从返回值
  发现注册失败,此前无任何文档说明
- uvhttp_set_max_connections 是**唯一有效**的 builder 配置旋钮(另两个假
  旋钮已在 v2.10 删除,指向 config-api.md 的归属表)
- uvhttp_serve 的 host=NULL 是错误而非默认(发警告后继续走 listen,然后失败)

## 2. uvhttp_request 的"内部函数"块从 server.h 拆出

server.h 尾部挂着 `/* internalFunction */` 的 uvhttp_request_init /
uvhttp_request_cleanup —— request API 出现在 server.h 里,正是本轮审计
O1「归属错误」的同款(#470 已删三个纯别名,这两个是漏网的)。cleanup 在
request.h 本就有一份声明(两份并存),init 则只在 server.h。

处理:init 移到 request.h(紧邻其余 request API),两块都从 server.h 移除。
uvhttp.h 同时包含两个头文件,标准入口不受影响;只 include server.h 的调用方
需自行补上 request.h —— 破坏性,但与 v2.10 "一次改干净"的口径一致。

## 3. 记录的文档债(本 PR 不补)

- WebSocket 13 个 ws_* 函数、限流 7 个 rate_limit_* 函数在两份文档仍无条目
  —— 分别属 docs/guide/websocket.md 与 RATE_LIMIT_API.md 的领域,建议各自
  单独 PR 补齐,不塞进本次
- port 范围校验在 listen / listen_routes / create 三条路径都缺(#470 评审
  已记),待 d6 一并处理

## 验证

ctest 120/120;test-validity 门禁 PASS;check-doc-sync(31 文件 0 过时 0 缺
中译);check-brain OK;核心 API 对账双向零缺口;门禁口径内文件
clang-format 合规。
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