Upload 23-executorch-freecall-oob.md with huggingface_hub
Browse files- 23-executorch-freecall-oob.md +156 -0
23-executorch-freecall-oob.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Out-of-Bounds Array Access via Unvalidated FreeCall and MoveCall Indices in ExecuTorch PTE Loading
|
| 2 |
+
|
| 3 |
+
## Target
|
| 4 |
+
pytorch/executorch
|
| 5 |
+
|
| 6 |
+
## Vulnerability Type
|
| 7 |
+
Out-of-Bounds Read/Write (CWE-125, CWE-787)
|
| 8 |
+
|
| 9 |
+
## Severity
|
| 10 |
+
**HIGH** (CVSS 3.1: 8.6 -- AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
|
| 11 |
+
|
| 12 |
+
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.
|
| 13 |
+
|
| 14 |
+
## Summary
|
| 15 |
+
|
| 16 |
+
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).
|
| 17 |
+
|
| 18 |
+
## Root Cause
|
| 19 |
+
|
| 20 |
+
### File: `runtime/executor/method.cpp`, lines 1063-1065 (init-time)
|
| 21 |
+
|
| 22 |
+
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:
|
| 23 |
+
|
| 24 |
+
```cpp
|
| 25 |
+
default: {
|
| 26 |
+
chain_instruction_arg_lists[instr_idx] = InstructionArgs();
|
| 27 |
+
} break;
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
No validation of the indices (`move_from`, `move_to`, `value_index`) is performed.
|
| 31 |
+
|
| 32 |
+
### File: `runtime/executor/method.cpp`, line 1504 (execution-time, FreeCall)
|
| 33 |
+
|
| 34 |
+
```cpp
|
| 35 |
+
case executorch_flatbuffer::InstructionArguments::FreeCall: {
|
| 36 |
+
auto free_call = instruction->instr_args_as_FreeCall();
|
| 37 |
+
auto t = values_[free_call->value_index()].toTensor();
|
| 38 |
+
internal::reset_data_ptr(t);
|
| 39 |
+
} break;
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
`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.
|
| 43 |
+
|
| 44 |
+
### File: `runtime/executor/method.cpp`, line 1495 (execution-time, MoveCall)
|
| 45 |
+
|
| 46 |
+
```cpp
|
| 47 |
+
case executorch_flatbuffer::InstructionArguments::MoveCall: {
|
| 48 |
+
auto move_call = instruction->instr_args_as_MoveCall();
|
| 49 |
+
mutable_value(move_call->move_to()) = get_value(move_call->move_from());
|
| 50 |
+
} break;
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
`mutable_value()` and `get_value()` do have `ET_CHECK_MSG` assertions, but these are:
|
| 54 |
+
1. Not present in production/release builds when `ET_CHECK_MSG` is compiled as a no-op
|
| 55 |
+
2. Even when present, they call `abort()` rather than returning an error, causing a denial-of-service
|
| 56 |
+
|
| 57 |
+
### Contrast with properly validated instructions
|
| 58 |
+
|
| 59 |
+
JumpFalseCall is properly validated at init time (lines 1048-1062):
|
| 60 |
+
```cpp
|
| 61 |
+
case executorch_flatbuffer::InstructionArguments::JumpFalseCall: {
|
| 62 |
+
auto index = ...->cond_value_index();
|
| 63 |
+
ET_CHECK_OR_RETURN_ERROR(
|
| 64 |
+
index >= 0 && static_cast<size_t>(index) < n_value_,
|
| 65 |
+
InvalidProgram,
|
| 66 |
+
"Index %zd negative or >= %" ET_PRIsize_t,
|
| 67 |
+
static_cast<ssize_t>(index),
|
| 68 |
+
n_value_);
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
Neither FreeCall's `value_index` nor MoveCall's `move_from`/`move_to` receive equivalent validation.
|
| 72 |
+
|
| 73 |
+
### Validation gap in program_validation.cpp
|
| 74 |
+
|
| 75 |
+
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.
|
| 76 |
+
|
| 77 |
+
## Exploitation Flow
|
| 78 |
+
|
| 79 |
+
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).
|
| 80 |
+
|
| 81 |
+
2. **The program loads successfully** -- FreeCall indices are never checked during init.
|
| 82 |
+
|
| 83 |
+
3. **During execution**, `values_[free_call->value_index()]` reads out of bounds:
|
| 84 |
+
- The read accesses heap memory beyond the `values_` array
|
| 85 |
+
- The bytes are reinterpreted as an `EValue` struct
|
| 86 |
+
- `.toTensor()` extracts a `TensorImpl*` pointer from the corrupted EValue
|
| 87 |
+
- `reset_data_ptr()` writes to the address pointed to by the corrupted TensorImpl's `data_` pointer
|
| 88 |
+
|
| 89 |
+
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).
|
| 90 |
+
|
| 91 |
+
### MoveCall variant
|
| 92 |
+
|
| 93 |
+
A MoveCall with `move_to` set to a large value would:
|
| 94 |
+
- In debug builds: trigger `ET_CHECK_MSG` -> `abort()` (DoS)
|
| 95 |
+
- 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
|
| 96 |
+
|
| 97 |
+
## Impact
|
| 98 |
+
|
| 99 |
+
- **Heap Out-of-Bounds Read**: Reading arbitrary heap memory via crafted FreeCall/MoveCall indices
|
| 100 |
+
- **Heap Corruption / Arbitrary Write**: Writing to attacker-influenced memory locations
|
| 101 |
+
- **Denial of Service**: Process abort via assertion failure or segfault
|
| 102 |
+
- **Particularly dangerous on embedded targets**: ExecuTorch runs on mobile/embedded devices with limited memory protections
|
| 103 |
+
|
| 104 |
+
## Affected Code Path
|
| 105 |
+
|
| 106 |
+
```
|
| 107 |
+
Program::load() -> Method::load() -> Method::init()
|
| 108 |
+
-> [instruction loop, default case -- NO VALIDATION for FreeCall/MoveCall]
|
| 109 |
+
|
| 110 |
+
Method::execute() -> Method::step() -> Method::execute_instruction()
|
| 111 |
+
-> FreeCall: values_[free_call->value_index()] -- OUT-OF-BOUNDS
|
| 112 |
+
-> MoveCall: mutable_value(move_call->move_to()) -- ASSERT-ONLY CHECK
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
## Remediation
|
| 116 |
+
|
| 117 |
+
Add init-time bounds validation for FreeCall and MoveCall, matching the pattern used for JumpFalseCall:
|
| 118 |
+
|
| 119 |
+
```cpp
|
| 120 |
+
case executorch_flatbuffer::InstructionArguments::FreeCall: {
|
| 121 |
+
auto index = static_cast<const executorch_flatbuffer::FreeCall*>(
|
| 122 |
+
instr_args)->value_index();
|
| 123 |
+
ET_CHECK_OR_RETURN_ERROR(
|
| 124 |
+
index >= 0 && static_cast<size_t>(index) < n_value_,
|
| 125 |
+
InvalidProgram,
|
| 126 |
+
"FreeCall value_index %d out of range",
|
| 127 |
+
index);
|
| 128 |
+
chain_instruction_arg_lists[instr_idx] = InstructionArgs();
|
| 129 |
+
} break;
|
| 130 |
+
|
| 131 |
+
case executorch_flatbuffer::InstructionArguments::MoveCall: {
|
| 132 |
+
auto mc = static_cast<const executorch_flatbuffer::MoveCall*>(instr_args);
|
| 133 |
+
ET_CHECK_OR_RETURN_ERROR(
|
| 134 |
+
mc->move_from() >= 0 && static_cast<size_t>(mc->move_from()) < n_value_ &&
|
| 135 |
+
mc->move_to() >= 0 && static_cast<size_t>(mc->move_to()) < n_value_,
|
| 136 |
+
InvalidProgram,
|
| 137 |
+
"MoveCall indices out of range");
|
| 138 |
+
chain_instruction_arg_lists[instr_idx] = InstructionArgs();
|
| 139 |
+
} break;
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
Additionally, add a runtime bounds check at the FreeCall execution site (line 1504) as defense-in-depth:
|
| 143 |
+
```cpp
|
| 144 |
+
ET_CHECK_OR_RETURN_ERROR(
|
| 145 |
+
static_cast<size_t>(free_call->value_index()) < n_value_,
|
| 146 |
+
Internal,
|
| 147 |
+
"FreeCall value_index out of bounds");
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
## References
|
| 151 |
+
|
| 152 |
+
- `runtime/executor/method.cpp:1063-1065` -- Missing init-time validation (default case)
|
| 153 |
+
- `runtime/executor/method.cpp:1504` -- FreeCall unchecked array access
|
| 154 |
+
- `runtime/executor/method.cpp:1495` -- MoveCall assert-only check
|
| 155 |
+
- `runtime/executor/method.cpp:1048-1062` -- JumpFalseCall proper validation (for comparison)
|
| 156 |
+
- `runtime/executor/program_validation.cpp` -- No instruction index validation
|