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:

          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)

    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)

    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):

          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:

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:

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