executorch-freecall-oob-poc / 23-executorch-freecall-oob.md
ryansecuritytest-fanpierlabs's picture
Upload 23-executorch-freecall-oob.md with huggingface_hub
6964854 verified
|
Raw
History Blame Contribute Delete
7.66 kB
# Out-of-Bounds Array Access via Unvalidated FreeCall and MoveCall Indices in ExecuTorch PTE Loading
## Target
pytorch/executorch
## Vulnerability Type
Out-of-Bounds Read/Write (CWE-125, CWE-787)
## Severity
**HIGH** (CVSS 3.1: 8.6 -- AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
A crafted .pte model file can contain FreeCall or MoveCall instructions with attacker-controlled indices that are never validated, causing out-of-bounds access on the runtime values array during program execution.
## Summary
During ExecuTorch method initialization (`Method::init()`), each instruction in the program is validated at load time. KernelCall, DelegateCall, and JumpFalseCall instructions all have their index fields bounds-checked against the values array. However, FreeCall and MoveCall instructions fall through to the `default` case in the init switch statement, which performs **no validation at all**. At execution time, FreeCall directly indexes `values_[]` without bounds checking, and MoveCall passes its indices to `get_value()`/`mutable_value()` which use `ET_CHECK_MSG` (an assertion that aborts the process on debug builds but may be compiled out in release builds).
## Root Cause
### File: `runtime/executor/method.cpp`, lines 1063-1065 (init-time)
During `Method::init()`, the instruction setup loop validates indices for KernelCall (line 997-1027), DelegateCall (line 1028-1047), and JumpFalseCall (line 1048-1062). But MoveCall and FreeCall are handled by:
```cpp
default: {
chain_instruction_arg_lists[instr_idx] = InstructionArgs();
} break;
```
No validation of the indices (`move_from`, `move_to`, `value_index`) is performed.
### File: `runtime/executor/method.cpp`, line 1504 (execution-time, FreeCall)
```cpp
case executorch_flatbuffer::InstructionArguments::FreeCall: {
auto free_call = instruction->instr_args_as_FreeCall();
auto t = values_[free_call->value_index()].toTensor();
internal::reset_data_ptr(t);
} break;
```
`values_[free_call->value_index()]` directly indexes the `values_` array using an attacker-controlled `value_index` from the FlatBuffer with **no bounds check**. If `value_index` exceeds `n_value_`, this reads out-of-bounds heap memory, interprets it as an EValue, calls `.toTensor()` on it (type confusion), and then calls `reset_data_ptr()` which writes to whatever the corrupted Tensor pointer points to.
### File: `runtime/executor/method.cpp`, line 1495 (execution-time, MoveCall)
```cpp
case executorch_flatbuffer::InstructionArguments::MoveCall: {
auto move_call = instruction->instr_args_as_MoveCall();
mutable_value(move_call->move_to()) = get_value(move_call->move_from());
} break;
```
`mutable_value()` and `get_value()` do have `ET_CHECK_MSG` assertions, but these are:
1. Not present in production/release builds when `ET_CHECK_MSG` is compiled as a no-op
2. Even when present, they call `abort()` rather than returning an error, causing a denial-of-service
### Contrast with properly validated instructions
JumpFalseCall is properly validated at init time (lines 1048-1062):
```cpp
case executorch_flatbuffer::InstructionArguments::JumpFalseCall: {
auto index = ...->cond_value_index();
ET_CHECK_OR_RETURN_ERROR(
index >= 0 && static_cast<size_t>(index) < n_value_,
InvalidProgram,
"Index %zd negative or >= %" ET_PRIsize_t,
static_cast<ssize_t>(index),
n_value_);
```
Neither FreeCall's `value_index` nor MoveCall's `move_from`/`move_to` receive equivalent validation.
### Validation gap in program_validation.cpp
The `validate_program()` function in `runtime/executor/program_validation.cpp` validates Tensor values and TensorList indices, but does NOT validate any instruction indices (FreeCall, MoveCall, KernelCall op_index, DelegateCall delegate_index, etc.). The init-time validation in `method.cpp` is the only line of defense, and it has this gap for FreeCall and MoveCall.
## Exploitation Flow
1. **Attacker crafts a .pte file** containing a FreeCall instruction with `value_index` set to a large value (e.g., 0xFFFFFFFF or any value >= the values array size).
2. **The program loads successfully** -- FreeCall indices are never checked during init.
3. **During execution**, `values_[free_call->value_index()]` reads out of bounds:
- The read accesses heap memory beyond the `values_` array
- The bytes are reinterpreted as an `EValue` struct
- `.toTensor()` extracts a `TensorImpl*` pointer from the corrupted EValue
- `reset_data_ptr()` writes to the address pointed to by the corrupted TensorImpl's `data_` pointer
4. **Result**: An attacker who controls both the PTE file and can predict/influence heap layout achieves arbitrary memory write. Even without heap control, this is a reliable crash (denial of service).
### MoveCall variant
A MoveCall with `move_to` set to a large value would:
- In debug builds: trigger `ET_CHECK_MSG` -> `abort()` (DoS)
- In release builds (if ET_CHECK_MSG is compiled out): write an EValue to an out-of-bounds location in the values array -> heap corruption
## Impact
- **Heap Out-of-Bounds Read**: Reading arbitrary heap memory via crafted FreeCall/MoveCall indices
- **Heap Corruption / Arbitrary Write**: Writing to attacker-influenced memory locations
- **Denial of Service**: Process abort via assertion failure or segfault
- **Particularly dangerous on embedded targets**: ExecuTorch runs on mobile/embedded devices with limited memory protections
## Affected Code Path
```
Program::load() -> Method::load() -> Method::init()
-> [instruction loop, default case -- NO VALIDATION for FreeCall/MoveCall]
Method::execute() -> Method::step() -> Method::execute_instruction()
-> FreeCall: values_[free_call->value_index()] -- OUT-OF-BOUNDS
-> MoveCall: mutable_value(move_call->move_to()) -- ASSERT-ONLY CHECK
```
## Remediation
Add init-time bounds validation for FreeCall and MoveCall, matching the pattern used for JumpFalseCall:
```cpp
case executorch_flatbuffer::InstructionArguments::FreeCall: {
auto index = static_cast<const executorch_flatbuffer::FreeCall*>(
instr_args)->value_index();
ET_CHECK_OR_RETURN_ERROR(
index >= 0 && static_cast<size_t>(index) < n_value_,
InvalidProgram,
"FreeCall value_index %d out of range",
index);
chain_instruction_arg_lists[instr_idx] = InstructionArgs();
} break;
case executorch_flatbuffer::InstructionArguments::MoveCall: {
auto mc = static_cast<const executorch_flatbuffer::MoveCall*>(instr_args);
ET_CHECK_OR_RETURN_ERROR(
mc->move_from() >= 0 && static_cast<size_t>(mc->move_from()) < n_value_ &&
mc->move_to() >= 0 && static_cast<size_t>(mc->move_to()) < n_value_,
InvalidProgram,
"MoveCall indices out of range");
chain_instruction_arg_lists[instr_idx] = InstructionArgs();
} break;
```
Additionally, add a runtime bounds check at the FreeCall execution site (line 1504) as defense-in-depth:
```cpp
ET_CHECK_OR_RETURN_ERROR(
static_cast<size_t>(free_call->value_index()) < n_value_,
Internal,
"FreeCall value_index out of bounds");
```
## References
- `runtime/executor/method.cpp:1063-1065` -- Missing init-time validation (default case)
- `runtime/executor/method.cpp:1504` -- FreeCall unchecked array access
- `runtime/executor/method.cpp:1495` -- MoveCall assert-only check
- `runtime/executor/method.cpp:1048-1062` -- JumpFalseCall proper validation (for comparison)
- `runtime/executor/program_validation.cpp` -- No instruction index validation