Skip to content

fix: 修复字符串分割生命周期与更新线程启动崩溃 - #598

Open
greenhat616 wants to merge 1 commit into
std-microblock:v2from
greenhat616:fix/bnstring-split-lifetime
Open

greenhat616 wants to merge 1 commit into
std-microblock:v2from
greenhat616:fix/bnstring-split-lifetime

Conversation

@greenhat616

Copy link
Copy Markdown

远程插件源返回非 JSON 内容时,BetterNCM 的备用源处理可能读到已释放的字符串;若重试或字符转换再次抛出异常,异常会逃出 detached 更新线程,使网易云启动时退出。这次修复让分割结果拥有自己的内存,并在更新线程入口兜底异常。

复现环境与原始异常

  • Windows x64,网易云音乐 3.1.42.205594,已安装 BetterNCM 1.3.4。
  • 系统启用了 Segment Heap;调试时核对主进程及相关子进程默认堆签名为 0xDDEEDDEE。
  • 本机请求内置旧插件源的 plugins.json 返回 HTTP 403;先捕获到 nlohmann::json::parse_error,首字符为 <,随后又捕获到 JSON 解析异常及字符转换相关的 std::system_error。
  • 最终子进程在 BetterNCM 的 msimg32.dll 路径中进入 std::terminate → abort,异常码为 0xC0000409,异常参数为 7。

以下是修复前捕获的原始致命调用栈,已移除本机文件路径及进程/线程标识,保留原始地址和帧顺序。原版 DLL 没有匹配的 PDB,因此 BetterNCM 帧中的 TransparentBlt + ... 是 LLDB 使用最近导出符号生成的标签,不能据此认定崩溃发生在该导出函数内部。

原始致命崩溃堆栈
Exception 0xc0000409 at 0x7ffa5492527e; ExceptionInformation[0] = 0x7
  * frame #0: 0x00007ffa5492527e ucrtbase.dll`abort + 78
    frame #1: 0x00007ffa5494218e ucrtbase.dll`terminate + 30
    frame #2: 0x00007ffa313b186b VCRUNTIME140_1.dll`
    frame #3: 0x00007ffa313b21a5 VCRUNTIME140_1.dll`
    frame #4: 0x00007ffa313b21f6 VCRUNTIME140_1.dll`
    frame #5: 0x00007ffa313b43f5 VCRUNTIME140_1.dll`__CxxFrameHandler4 + 165
    frame #6: 0x00007ff924a70e74 msimg32.dll`TransparentBlt + 243596
    frame #7: 0x00007ffa573e64af ntdll.dll`__chkstk + 159
    frame #8: 0x00007ffa57344065 ntdll.dll`RtlWow64GetCurrentMachine + 1621
    frame #9: 0x00007ffa573e5dee ntdll.dll`KiUserExceptionDispatcher + 46
    frame #10: 0x00007ffa54c1408a KERNELBASE.dll`RaiseException + 138
    frame #11: 0x00007ffa305955a9 VCRUNTIME140.dll`_CxxThrowException + 153
    frame #12: 0x00007ff9249c2431 msimg32.dll`
    frame #13: 0x00007ff9249d8655 msimg32.dll`
    frame #14: 0x00007ff9249ebc7d msimg32.dll`
    frame #15: 0x00007ff924a77c18 msimg32.dll`TransparentBlt + 271664
    frame #16: 0x00007ffa313b4560 VCRUNTIME140_1.dll`__CxxFrameHandler4 + 528
    frame #17: 0x00007ffa313b2552 VCRUNTIME140_1.dll`
    frame #18: 0x00007ffa573a4646 ntdll.dll`RtlCaptureContext2 + 1190
    frame #19: 0x00007ff924a137a0 msimg32.dll`
    frame #20: 0x00007ff924a1ebaf msimg32.dll`
    frame #21: 0x00007ffa548acd30 ucrtbase.dll`wcsrchr + 336
    frame #22: 0x00007ffa55ccdda7 kernel32.dll`BaseThreadInitThunk + 23
    frame #23: 0x00007ffa572fee4c ntdll.dll`RtlUserThreadStart + 44

修复思路

1. 修复 BNString::split() 的释放后读取

原实现返回 std::vector<std::wstring_view>,但将 this->substr(...) 生成的临时 std::wstring 放入容器。每个临时字符串在该语句结束时销毁,返回的视图随即悬空,即使原始 BNString 仍然存活也无法保证安全。

备用源处理会在 onlineConfig.split(L"\n")[0] 后复制该视图并调用 utf8(),因此会使用失效的数据。

  • 改为返回 std::vector<std::wstring>,让每个结果拥有数据。
  • 空分隔符抛出 std::invalid_argument,避免原实现无法前进的循环。
  • 当前调用处使用 auto,可直接接收拥有数据的字符串。

直接引用修复前的原始头文件,使用两行插件源 URL 做独立 AddressSanitizer 复现,得到:

ERROR: AddressSanitizer: heap-use-after-free
READ of size 128

ASan 将分配及释放位置都定位到原始 BNString.hpp:209 的 substr 临时字符串,读取发生在复制 split() 返回值时。该内存生命周期缺陷已独立证实;Segment Heap 是否是启动崩溃的必要触发条件,尚未通过切换堆配置验证。

2. 防止备用源异常逃出后台更新线程

performForceInstallAndUpdateSync() 的原有 catch 内仍可能在读取/转换备用配置或重试时抛出异常。这些异常不会被同一个 catch 再次捕获,最终会逃出 detached 线程。

在 performForceInstallAndUpdateAsync() 的线程函数外层增加 catch (const std::exception&) 和 catch (...),将失败记录到窄字符错误输出,避免网络或备用源处理失败终止整个客户端。

验证

  • 新增 tests/bnstring_split.cpp,覆盖长 URL、临时源销毁后的结果读取、空字段、空源、多字符分隔符及空分隔符拒绝。
  • 修复后的 ASan 回归测试通过,退出码 0。
  • 将生产异步方法体原样提取到隔离依赖桩测试,注入标准异常、未知异常并测试正常返回;ASan 下均正常完成。
  • 前端 npm run build 通过。
  • 完整 BetterNCMII.vcxproj 的 Release/x64 构建通过(MSVC v143 14.36.32532,仍有原项目既有警告)。
  • 在独立网易云副本和临时 BetterNCM 数据目录中进行 25 秒启动验证,Segment Heap 保持开启:主窗口正常响应,渲染进程存活,未捕获致命异常;仍出现的远程 JSON 解析错误未导致进程退出。

启动验证使用内置 PluginMarket,未穷尽第三方插件、播放和长时间运行场景。远程源的可用性仍取决于服务端。

本地完整构建的环境适配

使用项目 CI 固定的 vcpkg ports 提交 69efe9cc2df0015f0bb2d37d55acde4a75c9a25b、对应旧版工具和 CMake 3.25.1。由于部分旧 MSYS 归档已下线,本地构建复用了 pkgconf 2.3.0,并对 FLTK 1.3.8 的构建覆盖限制 Fontconfig 依赖及查找到非 Windows 平台(Windows 仍使用 GDI,OpenGL 保持启用)。这些构建环境适配没有包含在此 PR 中。项目自动部署步骤已关闭。

AI 辅助说明

本修复的分析、代码修改及验证使用 6.1-sol 辅助完成。

Return owning strings from BNString::split so callers do not read destroyed
substring temporaries. Reject empty delimiters to avoid an infinite loop.
Catch standard and unknown exceptions at the detached remote-update thread
boundary, including failures while handling a fallback source.

Add regression coverage for long URLs, source lifetime and delimiter cases.
Validated with AddressSanitizer, updater exception injection, an x64 Release
build, and an isolated CloudMusic startup test with Segment Heap enabled.
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