【Bug已解决】C API memory leak Asan 解决方案

📅 2026/8/13 20:21:44
【Bug已解决】C API memory leak Asan 解决方案
【Bug已解决】C API memory leak Asan 解决方案一、现象长什么样用 ONNX Runtime 的 C API不是 Python/Java 高层封装写程序并用 AddressSanitizer (ASAN) 编译运行ASAN 在退出时报告内存泄漏指向 C API 分配的某块内存没有被释放12345ERROR: LeakSanitizer: detected memory leak Direct leak of 1024 byte(s) in 1 object(s) allocated from: #0 ort::... Allocator::Alloc #1 OrtGetApiBase / Ort::...::... (C API 内部)最小触发C API#include onnxruntime/core/session/onnxruntime_c_api.h int main() { OrtEnv* env NULL; OrtCreateEnv(ORT_LOGGING_LEVEL_WARNING, demo, env); // ... 用 C API 创建 session、跑推理 ... OrtReleaseEnv(env); // 必须配对的 Release 调用 return 0; } // ASAN 报泄漏某块 C API 内部分配没被对应 Release 释放注意程序能跑出正确结果只是 ASAN 抓到泄漏。这是C API 的资源释放契约问题——分配的每块内存都要有对应的OrtReleaseXxx调用漏一个就泄漏。二、背景ONNX Runtime 的 C API 遵循“谁分配、谁释放、用显式 Release 函数”的契约。C API 里几乎所有不透明句柄OrtEnv、OrtSession、OrtValue、OrtRunOptions、OrtMemoryInfo、OrtAllocator等都是内部在堆上分配的调用方拿到指针后必须在不用时调用对应的OrtReleaseXxx归还。ASAN 会在程序退出时扫描所有未释放的堆块报告泄漏。常见的泄漏来源忘记配对 Release创建了OrtValue/OrtRunOptions/OrtMemoryInfo却没OrtReleaseValue/OrtReleaseRunOptions/OrtReleaseMemoryInfo。API 本身漏释放某些 C API 函数在内部Alloc了一块比如返回字符串、返回数组但没有提供对应的 Release 函数或文档没说要释放调用方无从释放 - 泄漏在 API 侧。异常路径提前返回C 封装C API用了 RAII但裸 C 调用里中间出错goto/提前 return跳过了后面的 Release。长期持有不释放循环里反复创建OrtValue但不释放累积泄漏。三、根因根因是C API 分配的某些资源没有对应的 Release 调用被发起要么调用方漏写要么 API 没提供 ReleaseASAN 退出时抓到未释放堆块调用方漏配对 Release最常见的泄漏——OrtCreateXxx了但忘了OrtReleaseXxx或错误/提前返回路径跳过了 Release。API 侧缺 Release 入口某些函数返回的堆内存如OrtGetErrorMessage返回的字符串、Ort::...某些数组需要特定 Release 释放但 API 没暴露或文档不清调用方释放不了 - 泄漏在 API 实现里。C 封装与裸 C 混用用了 C API 的 RAII 对象但中间又手动new/取了裸指针没管。不是推理错结果正确只是资源释放契约被破坏ASAN 抓泄漏。所以这不是数值错而是C API 资源释放契约没被完整履行漏 Release 或 API 不给 Release。四、最小可运行复现下面用 C 标准库模拟“Alloc 了但没 Release - ASAN 报泄漏”的精简模型#include stdlib.h #include stdio.h // 模拟 ORT C APIAlloc 必须配对 Release void* OrtAlloc(size_t n) { return malloc(n); } void OrtRelease(void* p) { free(p); } int main() { void* a OrtAlloc(1024); // 分配 // 忘记 OrtRelease(a); // -- 漏了配对 Release - 泄漏 (void)a; return 0; } // 用 clang -fsanitizeaddress 编译运行 - LeakSanitizer 报 1024 字节泄漏clang -fsanitizeaddress -g demo.c -o demo ./demo会报Direct leak of 1024 byte(s)正是“Alloc 不 Release”的精简复现。把OrtRelease(a)加回去泄漏就消失。五、解决方案第一层最小直接修复最小修复给每个OrtCreateXxx/OrtAlloc配对的OrtReleaseXxx覆盖所有返回路径包括错误/提前返回。推荐用 C RAII 封装避免手漏#include onnxruntime/core/session/onnxruntime_cxx_api.h int main() { Ort::Env env(ORT_LOGGING_LEVEL_WARNING, demo); // RAII析构自动 Release Ort::SessionOptions so; Ort::Session session(env, model.onnx, so); // RAII // 用 C API 的 Ort::ValueRAII管理输入输出析构自动释放 Ort::MemoryInfo mi(Cpu, OrtDeviceAllocator, 0, OrtMemTypeDefault); // 所有 Ort:: 对象离开作用域自动 Release杜绝漏配 return 0; }如果必须用裸 C API用goto cleanup/defer模式确保每条路径都 ReleaseOrtValue* val NULL; OrtStatus* st OrtCreateValue(..., val); if (st) { /* 处理错误并 goto cleanup */ } // ... 使用 val ... cleanup: if (val) OrtReleaseValue(val); // 所有路径都释放 if (st) OrtReleaseStatus(st);这一层立刻消除大多数调用方泄漏。六、解决方案第二层结构性改进把“每个 C API 句柄的配对 Release 契约”收口成唯一的配置对象OrtCApiAsanLeakPolicy代码评审与静态检查读它from dataclasses import dataclass, field from typing import Tuple, Dict dataclass(frozenTrue) class OrtCApiAsanLeakPolicy: C API 资源释放契约的单一事实来源。 # 句柄 - 必须配对的 Release 函数全量映射 release_map: Tuple[Tuple[str, str], ...] ( (OrtEnv, OrtReleaseEnv), (OrtSession, OrtReleaseSession), (OrtValue, OrtReleaseValue), (OrtRunOptions, OrtReleaseRunOptions), (OrtMemoryInfo, OrtReleaseMemoryInfo), (OrtAllocator, OrtReleaseAllocator), (OrtStatus, OrtReleaseStatus), ) # 是否要求优先用 C RAII 封装避免手漏 prefer_raii: bool True # ASAN 构建下必须零泄漏 require_zero_leak_under_asan: bool True def paired_release(self, handle: str) - str: m dict(self.release_map) return m.get(handle, UNKNOWN) def describe(self) - str: return 每个 OrtCreateXxx 必须配对 OrtReleaseXxx优先 RAIIASAN 零泄漏 POLICY OrtCApiAsanLeakPolicy() def check_release(handle: str, policy: OrtCApiAsanLeakPolicy POLICY) - str: return policy.paired_release(handle)所有 C API 使用与评审读同一份POLICY释放契约被固化漏配一眼可见。七、解决方案第三层断言 / CI 守护把“C API 无泄漏ASAN 通过”做成断言。下面用 pytest 风格守护用模拟对象验证配对import pytest def test_every_handle_has_release(policy): for handle, rel in policy.release_map: assert rel.startswith(OrtRelease) assert rel ! UNKNOWN def test_prefer_raii(policy): assert policy.prefer_raii is True def test_asan_zero_leak_required(policy): assert policy.require_zero_leak_under_asan is True def test_release_pairing_complete(): handles [OrtEnv, OrtSession, OrtValue, OrtRunOptions] for h in handles: assert check_release(h) fOrtRelease{h[3:]} # OrtEnv-OrtReleaseEnv这四组断言锁住(1) 每个句柄都有 Release(2) 优先 RAII(3) ASAN 零泄漏要求(4) 配对完整。CI 跑通即代表释放契约被守护。八、排查清单遇到 C API 程序 ASAN 报泄漏看泄漏栈指向哪个 Alloc定位是哪个 Ort 句柄没释放。查配对 Release有没有OrtReleaseXxx对应每个OrtCreateXxx/OrtAlloc。查所有返回路径错误/提前 return 路径是不是也 Release 了。优先改 C RAII用Ort::封装析构自动释放避免手漏。API 侧泄漏若泄漏在 ORT 内部没提供 Release向 ORT 提 issue 补 Release。统一策略对象用OrtCApiAsanLeakPolicy固化释放映射。CI 守护ASAN 构建必须零泄漏断言配对完整。九、小结C API memory leak Asan的根因是ONNX Runtime 的 C API 遵循“分配必须配对 Release”的契约但使用方漏写OrtReleaseXxx尤其错误/提前返回路径或某些 API 内部分配的内存没有对应的 Release 入口ASAN 在退出时抓到未释放堆块。最小修复是给每个OrtCreateXxx配对的OrtReleaseXxx、覆盖所有返回路径优先用 C RAII 封装Ort::自动释放结构性改进是用唯一的OrtCApiAsanLeakPolicy固化句柄-Release 映射CI 用四组断言守护“每句柄有 Release、优先 RAII、ASAN 零泄漏、配对完整”。记住C API 里每个 Alloc 都要有 Release缺一个 ASAN 就报泄漏RAII 是最省心的防线。