| # ExecuTorch `.pte` β default Minimal verification bypass PoC model files |
|
|
| Malicious ExecuTorch (`.pte`) model files that cause an out-of-bounds read and a controllable |
| adjacent-heap information disclosure when loaded via the default C++ path (`Module::load` / |
| `executor_runner` / `Program::load(loader)`), which uses `Verification::Minimal`. Tested on |
| pytorch/executorch HEAD `ab45eb6` (`runtime/executor/program.cpp`). |
|
|
| | File | Result | Sink | |
| |---|---|---| |
| | `et_oob_read.pte` | out-of-bounds READ during load (CWE-125) | `Program::load`, `program.cpp:251` | |
| | `et_leak.pte` | controllable adjacent-heap info disclosure (CWE-200) | `get_method_name`, `program.cpp:366` | |
| | `et_valid.pte` | loads cleanly (negative control) | β | |
|
|
| ## Root cause |
|
|
| The default load mode is `Verification::Minimal`, in which `VerifyProgramBuffer` is not called β only a |
| root-offset bounds check runs. All subsequent FlatBuffers vtable/vector/string offsets from the `.pte` |
| are dereferenced unvalidated: |
|
|
| - `constant_segment->offsets()` (`program.cpp:251`) β OOB read during `Program::load`. |
| - `get_method_name()` returns `name->c_str()` (`program.cpp:366`), an unvalidated FlatBuffers string |
| offset; `c_str()` ignores the length prefix and reads to a NUL, so an attacker-chosen offset leaks |
| arbitrary-location/length adjacent process memory through a public, loggable API. |
|
|
| ## Reproduce |
|
|
| Official tool `executor_runner` built with AddressSanitizer (uses `Module::load` = |
| `Verification::Minimal` by default): |
|
|
| ``` |
| ./executor_runner --model_path et_oob_read.pte # heap-buffer-overflow READ at program.cpp:251 |
| ./executor_runner --model_path et_valid.pte # loads cleanly |
| ``` |
|
|
| `leak_demo.py` shows attacker control over the disclosed memory location for `et_leak.pte`. |
|
|
| Verifier-bypass proof: the same files loaded with `Verification::InternalConsistency` are cleanly |
| rejected ("Verification failed", InvalidProgram, no crash) β the verifier would have caught them; the |
| default Minimal path skips it. |
|
|
| ## Suggested fix |
|
|
| Make `InternalConsistency` the default for untrusted input, or bounds-check the string/vector offsets |
| in Minimal mode, or document Minimal as trusted-input-only and have `Module`/`executor_runner` validate. |
|
|
| Crash/leak-only proof of concept. |
|
|