Skip to content

内存安全 ​

UVHTTP 最突出的差异化优势并非单机峰值吞吐(这方面 nginx 与 h2o 更胜一筹)——而是在一个轻量、可嵌入、支持 32 位的 C 库中提供经过验证的内存安全。对于无法容忍长期运行中出现缓慢内存泄漏、或在持续运行数周后出现堆内存释放后使用(use-after-free)的生产服务与嵌入式设备而言,这才是真正关键的属性,也是大多数同类轻量级 C HTTP 库所不具备的。

本文档即为这一主张的工程证据。

承诺 ​

完整的 101 项测试套件在以下两种配置下均验证通过:

消毒器检测内容结果
AddressSanitizer(含内存泄漏检测)堆内存释放后使用、堆缓冲区溢出、栈缓冲区溢出、内存泄漏101/101 pass,零发现
UndefinedBehaviorSanitizer有符号整数溢出、非法移位、空指针解引用、对齐错误、越界访问101/101 pass,零发现

ASan 与 UBSan 无法在同一个构建中共存,因此以两个独立配置分别运行。两者必须全部为绿色(通过)。

自行复现(一条命令) ​

bash
make verify-memory-safety

该命令会在 ASan 与 UBSan 下分别配置并构建测试套件,在每种消毒器下运行完整的 101 项测试,若任一测试失败或任一消毒器报告发现,则以非零状态码退出。一次干净的运行会输出:

==> PASS: full suite clean under ASan and UBSan (101/101 each).

等价的手动命令如下:

bash
# AddressSanitizer(内存泄漏、堆内存释放后使用、缓冲区溢出)
cmake -B build_asan -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON
cmake --build build_asan -j$(nproc)
(cd build_asan && ctest --output-on-failure)

# UndefinedBehaviorSanitizer
cmake -B build_ubsan -DCMAKE_BUILD_TYPE=Debug -DENABLE_UBSAN=ON
cmake --build build_ubsan -j$(nproc)
(cd build_ubsan && ctest --output-on-failure)

消毒器构建保留调试符号(不执行 -s strip),因此任何发现都会产生可解析的、源码级别的栈回溯。

持续验证 ​

内存安全是一项不回退的不变量(non-regressing invariant),而非一次性的检查:

覆盖率:项目代码(不含 deps/)行覆盖率 86%、函数覆盖率 99%。

  • PR CI 门禁(.github/workflows/ci-pr.yml):asan-gate 作业在每个 PR 上以 ASan 运行测试套件——内存安全不可回退进 main。
  • 每夜 CI(.github/workflows/ci-nightly.yml):test-memory(含内存泄漏检测的 ASan)与 test-ubsan 作业每夜在各自消毒器下运行完整测试套件。
  • 每夜模糊测试(.github/workflows/ci-fuzz.yml):libFuzzer 测试桩(test/fuzz/fuzz_router.c)在 ASan 下以数百万任意输入驱动路由器——发现固定测试输入无法覆盖的 bug(已发现一个,见下)。
  • 构建系统护栏:消毒器 / Debug 构建从不被 strip,因此 CI 中的发现始终可调试。

模糊测试 ​

消毒器覆盖测试套件所执行的代码路径。模糊测试覆盖它未触及的路径。UVHTTP 为路由器提供了一个 libFuzzer 测试桩(test/fuzz/fuzz_router.c),在 CI 中每夜运行。

bash
# Build (clang + libFuzzer + ASan)
cmake -B build_fuzz -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON \
  -DBUILD_TESTING=OFF -DBUILD_EXAMPLES=OFF
cmake --build build_fuzz -j$(nproc) --target uvhttp
clang -g -O1 -fsanitize=fuzzer,address -fno-omit-frame-pointer \
  -Iinclude -Ideps/llhttp/include -Ideps/uthash/src \
  -Ideps/mbedtls/include -Ideps/cjson -Ideps/libuv/include \
  test/fuzz/fuzz_router.c build_fuzz/dist/lib/libuvhttp.a \
  -Wl,--start-group \
  deps/llhttp/build/libllhttp.a deps/cjson/build/libcjson.a \
  deps/mbedtls/build/library/libmbedtls.a deps/mbedtls/build/library/libmbedx509.a \
  deps/mbedtls/build/library/libmbedcrypto.a deps/libuv/build/libuv.a \
  -Wl,--end-group -lpthread -lm -ldl -o fuzz_router

# Run
./fuzz_router -max_total_time=60 -max_len=256

一次干净的运行以 0 退出且不含 ERROR: 行。该测试桩已发现下表所列的 add_route_method 堆缓冲区溢出(一种迁移后注册非参数路由的方式,单元测试从未以该顺序执行过)——这恰恰正是模糊测试的意义所在。

为达成此承诺而修复的缺陷类别 ​

测试套件最初并不干净。要做到零消毒器发现,需要在生产级库代码中修复真实的内存安全缺陷——这些缺陷能通过常规测试套件,却在消毒器下破坏内存。代表性修复如下(完整列表见更新日志):

缺陷类别位置详情
堆缓冲区溢出uvhttp_router.c add_route_method由 libFuzzer 发现。 在一条参数路由触发 trie 迁移(将 array_capacity 重置为 0)后,后续的一条非参数路由落入 add_array_route,其 0*2 容量计算出一个零大小的 realloc,strncpy 随之溢出。常规测试套件从未触及这一路由注册顺序。
堆内存释放后使用uvhttp_router.c find_or_create_child指向节点池的缓存指针在 realloc 扩展池后悬空
堆内存释放后使用uvhttp_server.c uvhttp_server_free双重释放时从已释放内存读取 freed 标志
堆内存释放后使用uvhttp_connection.c关闭后读取;close_pending 计数在重入时出错
堆缓冲区溢出uvhttp_response.c build_response_headerssnprintf 返回值累加使 pos 超出缓冲区,下溢突破边界
栈缓冲区下溢uvhttp_websocket.c uvhttp_ws_close写入已超出作用域的栈上连接
内存泄漏uvhttp_connection.c restart_read请求体指针被置空却未释放
内存泄漏uvhttp_server.c on_connection 503 路径temp_client(uv_tcp_t)从未关闭 / 释放
内存泄漏uvhttp_server.c ws_disable_connection_management管理结构体(含内嵌 uv_timer_t)在关闭回调触发前被释放
内存泄漏uvhttp_error_helpers.c对非 libuv errno 调用 uv_strerror 触发了无法释放的 libuv uv__strdup
未定义行为uvhttp_connection.c switch_to_websocket重入破坏 CLOSING 状态,导致关闭计数错乱(表现为 UBSan 下的挂起)

每一项均经过根因分析(而非掩盖),并在源码层面修复。

为何重要(以及为何多数轻量级 C 库缺失此项) ​

一个“通过测试”的 C HTTP 库仍可能在每次连接中泄漏数 KB 内存、在罕见的关闭竞态中解引用已释放内存,或把一段未以 null 结尾的字符串拼进响应。这些都不会令功能测试失败。它们表现为:

  • RSS 缓慢增长,持续运行数天 / 数周(嵌入式设备无法重启)。
  • 间歇性崩溃,发生在测试套件从未触及的生产负载模式下。
  • 可达安全性的内存破坏(头部 / 主体解析中的缓冲区溢出)。

消毒器正是在部署之前发现这些问题的手段。多数轻量级 C HTTP 库之所以不宣传“消毒器干净”的状态,并非因为它们干净——而是因为它们根本没有运行这一检查。UVHTTP 在每个每夜构建中运行它,并将任何发现视为发布阻断项。

纵深防御 ​

除消毒器外,代码库还应用了:

  • -Werror 配合 -Wall -Wextra -Wformat=2 -Wformat-security:零警告的编译门槛。
  • -fstack-protector-strong、-fno-common、完整 RELRO(-Wl,-z,relro,now)。
  • HTTP 响应拆分防护(contains_control_chars),对所有头部值在发出前执行检查。
  • 输入校验:URL 长度 / 路径穿越、头部大小 / 格式、主体大小限制。
  • 幂等资源清理:*_cleanup / *_free_resources 路径在释放后将指针置空,并依据 freed 标志做防护,以在 API 边界处对双重释放保持健壮。

当报告了消毒器发现时 ​

  1. 用 make verify-memory-safety(或特定的消毒器构建)复现。
  2. 依据源码级栈回溯定位根因(消毒器构建未被 strip)。
  3. 在源码层面修复——不要抑制。
  4. 合并前确认 ASan 与 UBSan 均为绿色。

与安全相关的发现,请按安全指南中的流程报告。

基于 MIT 许可证发布