在 Windows/MinGW(`UVCPP_BUILD_EXPAND=ON` 全量树,master `548b84d`)上独立复核了 `81fe1107`。 做法不是读代码,而是**把每条修复分别改回缺陷、重跑用例,看它红不红**。 ## 先说结论 | 变异 | 改回什么 | 读数 | 判定 | |---|---|---|---| | M1 | `thread_local_cache::push()` 的 `return false` → `true` | 红在 `memory_pool_test.cpp:310`:`SUPER 块释放后额度必须还回来 —— 第二次分配被拒说明块被丢弃了` | **有效** ✓ | | M2 | `push_to_global_pool()` 的 `default:` 里撤掉「先取 size」 | **Segmentation fault**(0 PASS,探针连打印都没来得及 —— 死在读 freed 块头) | **有效**(靠崩)✓ | | M3 | `shutdown_` 那两条分支里撤掉「先取 size」 | **23/23 全绿** | **无判据** ✗ | | M4 | `static_release_callback` 还原 `::operator delete` 那条分支 | **`0xC0000374` STATUS_HEAP_CORRUPTION** | **有效** ✓,「堆损坏」逐字复现 | M2 与 M4 逐字证实了你提交信息里写的两个现象。M1 的判据也是真的。 **M3 那两处没有判据**,下面单独说。 ## M3:探针证明分支走到了,但用例看不见 不是"分支没跑到"——我在那条分支里加了打印,`test_pool_release_cache_after_shutdown` 跑了多次都进了: ``` [M3PROBE] 走了 shutdown_ 非SUPER 分支: free 前=96 free 后读到=96 (还是原值) ``` 两点原因,任一条都够: 1. 那条用例用的是默认 `memory_pool_config`,**`max_total_memory == 0`**,而 `release_bytes()` 第一句就是 `if (config_.max_total_memory == 0) return;` —— 传什么值进去都没人看。 2. 更直接的是:**这次读到的值就是对的**(96,未被覆写),所以即便有上限, `release_bytes(96)` 也和修复后的行为一模一样 —— 我在这个分配器上量不到"值错"。 也就是说:**第 2 条那三处里,只有 `default:` 那处会崩,`shutdown_` 那两处是纯形式 UB** (消掉是对的,只是它的"错"在我这套装置上不可观测)。你写"每一条都先用失败用例或实测崩溃证出来" ——对第 1、3 条成立,对第 2 条里的 `default:` 成立,**对 `shutdown_` 那两处不成立**, 你当时的证据应该来自页面堆(那台机器上我这里没有,所以我复现不出「必崩」,只能复现"读到的值没变")。 **要不要一条判据?** 值不当的回读不可靠(同一个 `block->size` 在 free 后可能被堆的连接指针覆写、 也可能原样留着,取决于尺寸与 CRT),所以它**不适合**当断言的对象。我的建议是二选一: - 在 `test_pool_release_cache_after_shutdown` 的注释里写明它证的是第 3 条(释放器配对), 别让后来的人以为它同时覆盖了第 2 条;或者 - 那条用例改用非零 `max_total_memory`,至少让 `release_bytes()` 不是空调用(虽然仍不保证能红)。 我不主张把那两行删掉:`shutdown_ == true` 时额度确实可能非零(`shutdown()` 之后 `allocate()` 并不被拒,它走 `allocate_block()` 拿新内存并重新记账),所以那两行是防御性的, 只是当前装置看不到差别。 ## 顺带两条装置上的坑(不是仓的缺陷,但会骗人) 1. **单目标构建不刷新测试目录的 DLL。** 我头一轮的 M2/M3 读数是废的:敲的是 `cmake --build . --target test_memory_pool`,它只重链了根目录的 `libuvcpp.dll`, 而运行时加载的是 `tests/expand/libuvcpp.dll` 那份旧副本 —— 变异根本没生效,"全绿"什么都没说明。 是全量 `cmake --build .` 才会跑 `copy_test_dlls`(它 `DEPENDS ${_dll_deps}`,规则本身是对的)。 M1 侥幸有效是因为它改的是头里的 inline 代码,测试 exe 自己重编了。 **判据:改完先确认变异真的进了运行时那份二进制**(我是打印探针确认的)。 2. `UVCPP_USE_SYSTEM_LIBUV` 默认 ON,配置时会挑 MSYS2 那份 `libuv.dll.a`, 于是跑 ctest 前得把 `mingw64/bin` 放进 `PATH`,否则全表以 `0xc0000135` 在 0.01 秒内退出。 ## 另外 master `548b84d` 上这套配置:构建 `rc=0`、ctest **43/43**、`test_memory_pool` **23/23**(含你新加的两条)。
Activity
perf(web): 响应头表预留位置;补上 SUPER 块 shutdown 后释放的用例
Antruly commentedon Sep 21, 2026
谢谢做这套变异复核 —— 逐条对着源码核了一遍,下面按你的四条分别回。
机制 #1 逐字属实
release_bytes()的第一句就是if (config_.max_total_memory == 0) return;(
src/expand/uvcpp_memory_pool.cpp:319),而那条用例用的是默认 config。你说的完全对。「
shutdown_那两处没有判据」—— 对,而且比你说到的更彻底比「用例看不见」还要空一层:
shutdown_分支里 SUPER 那半边全仓零覆盖。test_pool_release_cache_after_shutdown用的是 64 字节 TINY 块,只走非 SUPER 那一支;thread_local_cache::push()对idx >= 7返回false,块当场交给
push_to_global_pool()),所以它只能由「shutdown()之后直接deallocate()一个大块」走到 —— 全仓此前没有任何用例走过这条路。
我按你的建议 (a) 在旧用例的注释里写明了它证的是释放器配对、不是那处 UAF,(b) 没有采纳
(改用非零
max_total_memory只是让release_bytes()不是空调用,仍然不产生判据,还可能把那条用例原本证的东西搅浑)。代替它的是新加一条用例,见下。
「纯形式 UB」这一点我不同意 —— 在这台机器上是硬崩
你那边读到「值仍是 96」,我这边也一样;但读数不可观测不等于没有判据,它是分配器相关的。
MSVC + 全量页堆下它是可观测的,而且很干脆。实测(
build-h2,shutdown_两处一起还原成release_bytes(block->size)写在 free 之后):0xC0000005cdb把崩点分别定位到push_to_global_pool+0x7e(SUPER 支)与+0x3b(非 SUPER 支),调用点分别在
test_memory_pool+0x2a48与+0x265d—— 两条分支各崩各的,模型自洽:旧的先崩掉约 6/10,剩下的 4/10 全被新的兜住,合起来 10/10。
新用例就是你建议的那条路的补集,
shutdown()之后直接放一个 512 KiB 的块:大块比小块更容易把这条读变成硬崩:512 KiB free 之后 CRT 往往真的把页还给系统,同一个读当场
变成访问违例,不像 64 字节块那样约一半的次数能蒙混过去。
你指出的那条,我认
81fe110的提交信息写「每一条都先用失败用例或实测崩溃证出来才动代码」。对第 1、3 条成立,对第 2 条里的
default:成立,对shutdown_那两处不成立 —— 那条提交记录的验收写的是「页面堆门禁 94/94 绿、抓 0 个」,那是修复后的绿读数,不是对着变异体跑出来的对照读数。
判据必须有对照组,这处我当时没做。已经在新用例的注释里把这段写清楚了。
两条装置坑
第 1 条(单目标构建不刷新测试目录那份 DLL)我这边也踩过,结论一样:
copy_test_dlls是ALL 目标,只有默认全量构建才会刷新那四份副本,
--target xxx不会 —— 判据要拿PE
.text段的哈希比,不能只看产物存在。第 2 条(MSYS2PATH)记下了,谢谢。修复在
64fbe08(1.2.11-dev)。Antruly commentedon Oct 18, 2026
这条可以关了 —— M3 缺判据那一格已经补上。
tests/expand/memory_pool_test.cpp:375test_pool_super_release_after_shutdown()(64fbe08)。它走的就是你指出的那半边:init()→allocate(512 KiB)(> 256 KiB ⇒ SUPER)→shutdown()→deallocate(),于是进shutdown_为真时 SUPER 那一支;上面那条老用例只有 64 字节 TINY 块,够不到这里。free之后 CRT 往往真的把页还给系统,release_bytes(block->size)写在free之后那一读当场就是访问违例;64 字节块约有一半次数能蒙混过去(你那次 23/23 全绿就是这个原因)。test_memory_pool(ctest -R test_memory_pool),本机现在绿。按你原来的做法把 M3 改回去,这条应当红 —— 如果你还想复核一次,现在有判据可用了。谢谢这套变异复核:M3 那两处此前确实全仓零覆盖,光读代码是看不出来的。