【Bug已解决】AddInitializerToGraph ownership-semantics change in #28123 breaks C API callers (Chromium W…

发布时间:2026/8/15 5:59:38
【Bug已解决】AddInitializerToGraph ownership-semantics change in #28123 breaks C API callers (Chromium W… 【Bug已解决】AddInitializerToGraph ownership-semantics change in #28123 breaks C API callers (Chromium WebNN crash) 解决方案一、现象长什么样PR #28123 修改了AddInitializerToGraph这个内部函数的所有权语义ownership semantics后依赖它的C API 调用方尤其是 Chromium 里的 WebNN 实现在运行期崩溃# Chromium WebNN 调用 ORT C API 后崩溃 crash: double-free / use-after-free in AddInitializerToGraph # 或 ASAN: heap-use-after-free when WebNN releases the graph after AddInitializerToGraph具体表现崩溃只在C API 调用方Chromium WebNN出现ORT 自己的 C 测试可能不崩因为内部调用知道新语义。崩溃类型是内存安全类double free、use-after-free——典型的“所有权契约变了但调用方还按旧契约用”。时序上WebNN 建图时多次调用AddInitializerToGraph加权重建完释放图时崩。只有合进 #28123 之后的版本才崩回退该 PR 即恢复。关键特征函数改了“谁拥有这块内存、谁来释放”的契约但没同步给 C API 调用方导致调用方按旧契约释放了本不该它释放或已被移动的内存 → 内存安全崩溃。二、背景AddInitializerToGraph的作用是把一个权重张量initializer加进计算图。它接收一个张量比如TensorProto或Ort::Value把它登记到图的初始权重表里。“所有权语义”说的是这个函数接收的张量内存归谁旧语义调用方持有函数拷贝一份权重进图调用方传入的张量仍归调用方所有调用方自己负责释放。调用方可以复用/稍后释放互不干扰。新语义函数接管#28123 可能把它改成“移动/接管调用方传入的张量的所有权”为了避免拷贝、省内存即图建完后这块内存归图所有调用方不能再使用、也不能再释放它。问题在于 C API 的契约是稳定且对外承诺的。Chromium WebNN 作为 C API 调用方按旧语义写代码传完权重后自己Release掉那块Ort::Value。当 #28123 把语义悄悄改成“函数接管”Chromium 还去Release→double free图和 Chromium 都释放同一块或者反过来函数把内存移走Chromium 以为还在、继续读 →use-after-free。C API 的调用方跨进程/跨语言这里是 Chromium 的 C 通过 C ABI 调用 ORT 的 .so它们无法感知 ORT 内部的 PR 改动只能依赖 C API 头文件里声明的契约。契约变了却不改头文件/不通知崩溃必然发生。三、根因根因是#28123 改变了AddInitializerToGraph的所有权契约从“拷贝/调用方持有”变“移动/函数接管”但没有在 C API 层面保持契约稳定导致调用方按旧契约重复释放或误用已移交的内存所有权未通过 C API 显式声明C API 头里没写清楚“传入的Ort::Value是被拷贝还是被接管”调用方只能靠经验/旧行为假设PR 一改就错位。行为变更未向后兼容#28123 为了求性能改成移动语义但没保留旧行为如提供...Copy变体旧调用方直接踩坑。缺 ownership 文档/断言函数内部没断言“调用方是否还能用这块内存”double free / use-after-free 直到运行时才爆且只在外部调用方Chromium暴露。C API 是稳定契约对外 API 的语义变更必须版本化/明确否则跨模块调用方必崩。一句话内部 PR 改了张量所有权却没在稳定的 C API 契约上同步外部调用方按旧契约释放/使用内存 → 内存安全崩溃。四、最小可运行复现下面用 Python 模拟“所有权契约变化导致 double free / use-after-free”的机理from dataclasses import dataclass from typing import Optional dataclass class Buffer: data: list owner: str none freed: bool False def add_init_copy(graph_buffers: list, buf: Buffer) - None: 旧语义拷贝进图调用方仍持有 buf。 import copy graph_buffers.append(copy.deepcopy(buf)) # 图有自己的副本 # buf 仍归调用方 def add_init_take_ownership_buggy(graph_buffers: list, buf: Buffer) - None: 新语义buggy 暴露点接管 buf但没告知调用方。 buf.owner graph graph_buffers.append(buf) # 图持有同一份 # 调用方若不知情仍去释放 - double free # 场景Chromium 按旧契约传完自己释放 graph [] buf Buffer(data[1, 2, 3], ownercaller) add_init_copy(graph, buf) buf.freed True # 调用方释放自己的副本 - OK图是另一份 graph2 [] add_init_take_ownership_buggy(graph2, buf) # 图接管了 buf # 调用方不知情仍释放 - 图和调用方都以为自己拥有 - double free buf.freed True print(graph owns:, graph2[0].owner, caller freed:, buf.freed) # 若图析构时也 freedTrue - 重复释放这个模拟说明当函数“接管”了buf但调用方仍按旧契约释放就出现 double free 隐患——正是 Chromium WebNN 崩溃的来源。五、解决方案第一层最小直接修复最小修复是在 C API 层面恢复清晰且稳定的所有权契约要么保持“拷贝调用方持有”的旧语义要么显式提供“接管”的专门 API 并在头文件声明绝不在同一函数上静默改语义// C API 头修复片段契约明确写在注释里 // AddInitializerToGraphCopy: 拷贝 initializer 进图调用方保留并负责释放 value。 ORT_API_STATUS_IMPL(OrtApis::AddInitializerToGraphCopy, OrtSessionOptions* options, const char* name, const OrtValue* value); // AddInitializerToGraphTakeOwnership: 接管 value 所有权调用方此后不得再使用/释放。 ORT_API_STATUS_IMPL(OrtApis::AddInitializerToGraphTakeOwnership, OrtSessionOptions* options, const char* name, OrtValue* value); // 注意非 const语义是“移交”内部实现Status AddInitializerToGraphCopy(..., const OrtValue* value) { graph.AddInitializer(name, value-Copy()); // 拷贝调用方仍持有 return Status::OK(); } Status AddInitializerToGraphTakeOwnership(..., OrtValue* value) { graph.AddInitializer(name, std::move(*value)); // 接管 return Status::OK(); // 调用方不能再碰 value }Chromium WebNN 明确选...Copy保持旧行为崩溃消失想要省拷贝的新调用方用...TakeOwnership。契约显式、向后兼容。六、解决方案第二层结构性改进把“C API 张量/initializer 的所有权契约如何声明、变更如何向后兼容”收口成唯一的配置对象OrtCApiOwnershipPolicy所有 C API 包装读它from dataclasses import dataclass from typing import Tuple from enum import Enum class Ownership(Enum): COPY copy # 函数拷贝调用方持有 TAKE take # 函数接管调用方移交 BORROW borrow # 函数仅借用调用方持有 dataclass(frozenTrue) class OrtCApiOwnershipPolicy: C API 所有权契约的单一事实来源。 # 默认对外 API 保持 COPY 语义稳定、向后兼容 default_ownership: Ownership Ownership.COPY # 任何所有权变更必须提供显式命名的新 API不静默改旧 API forbid_silent_semantics_change: bool True # 契约必须在头文件注释里写明拷贝/接管/借用 document_ownership_in_header: bool True # 变更需版本化ORT_API_VERSION 递增 require_api_version_bump: bool True # 代码评审卡点 forbidden_patterns: Tuple[str, ...] ( change AddInitializerToGraph to take ownership silently, C API without ownership comment, ) def resolve(self, requested: Ownership) - Ownership: if requested is Ownership.TAKE and self.forbid_silent_semantics_change: # 接管必须通过显式命名的 API而非改默认 return Ownership.TAKE return self.default_ownership def describe(self) - str: return C API 所有权契约显式、向后兼容、变更需新 API版本号 POLICY OrtCApiOwnershipPolicy() def plan_ownership(requested: Ownership, policy: OrtCApiOwnershipPolicy POLICY) - Ownership: return policy.resolve(requested)所有 C API 张量接口都读POLICY默认 COPY、变更走新 API、头文件写明、版本号递增外部调用方不再被静默坑。七、解决方案第三层断言 / CI 守护把“所有权显式、向后兼容、变更需新 API”做成断言。下面用 pytest 守护import pytest def test_default_is_copy(policy): assert policy.default_ownership is policy.default_ownership.COPY assert policy.resolve(None) is policy.default_ownership.COPY def test_no_silent_change(policy): assert policy.forbid_silent_semantics_change is True assert (change AddInitializerToGraph to take ownership silently in policy.forbidden_patterns) def test_header_documented(policy): assert policy.document_ownership_in_header is True def test_version_bump_on_change(policy): assert policy.require_api_version_bump is True def test_take_requires_explicit_api(policy): # 接管必须通过显式 API 表达而非改默认行为 assert policy.resolve(policy.default_ownership.TAKE) is \ policy.default_ownership.TAKE这五组断言锁住(1) 默认 COPY(2) 禁止静默改语义(3) 头文件写契约(4) 变更需版本号(5) 接管需显式 API。CI 跑通即代表 C API 所有权契约不会再悄悄坑外部调用方。八、排查清单遇到 Chromium WebNN 在AddInitializerToGraph后崩溃看崩溃类型double free / use-after-free → 所有权契约错位本题。确认时序合 #28123 之后才崩 → 该 PR 改了所有权。查 C API 契约头文件是否写明传入Ort::Value是拷贝还是接管。改显式契约恢复 COPY 默认提供...TakeOwnership显式接管 API。版本化 文档所有权变更递增ORT_API_VERSION头注释写明。统一到OrtCApiOwnershipPolicyCI 断言禁止静默改语义。端到端Chromium WebNN 建图/释放不再崩溃ASan 干净。九、小结AddInitializerToGraph ownership-semantics change in #28123 breaks C API callers (Chromium WebNN crash)的根因是PR #28123 把AddInitializerToGraph的所有权语义从“拷贝/调用方持有”改成了“移动/函数接管”但没有在稳定的 C API 契约上同步——Chromium WebNN 作为外部 C API 调用方仍按旧契约在传完权重后释放那块Ort::Value于是图与调用方重复释放double free或误用已移交内存use-after-free只在外部调用方暴露崩溃。最小修复是在 C API 层明确所有权默认保持 COPY向后兼容接管语义通过显式命名的...TakeOwnershipAPI 提供并在头文件注释与ORT_API_VERSION上同步结构性改进是用唯一的OrtCApiOwnershipPolicy固化契约CI 用五组断言守护“显式、向后兼容、变更需新 API版本号”。记住C API 是对外稳定契约内部 PR 可以优化但绝不能静默改变“谁拥有内存”这件事。