# 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(index) < n_value_, InvalidProgram, "Index %zd negative or >= %" ET_PRIsize_t, static_cast(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( instr_args)->value_index(); ET_CHECK_OR_RETURN_ERROR( index >= 0 && static_cast(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(instr_args); ET_CHECK_OR_RETURN_ERROR( mc->move_from() >= 0 && static_cast(mc->move_from()) < n_value_ && mc->move_to() >= 0 && static_cast(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(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