| --- |
| license: apache-2.0 |
| tags: |
| - security |
| - proof-of-concept |
| - executorch |
| - vulnerability-research |
| --- |
| |
| # ExecuTorch `.pte` + `.ptd` external-constant heap out-of-bounds read β PoC |
|
|
| Security research artifact. **Do not load these files with a vulnerable ExecuTorch build outside a sandbox.** |
|
|
| Reported through **huntr** (Model File Formats / AI-ML) under coordinated disclosure by |
| **Roman Arce Bran β Cyfra Tech Solutions (Costa Rica)**. |
|
|
| * Target: `pytorch/executorch`, `main` @ `55d693b1655314526d25436754d83b9554049540` (2026-07-27) |
| * CWE-125 out-of-bounds read → CWE-200 information disclosure, plus a remote worker crash |
| * Entry point: the documented public API `executorch::extension::Module(model_path, {data_files...})` |
|
|
| ## The defect in one line |
|
|
| The `.ptd` declares the tensor's size twice and independently β once as `NamedData.tensor_layout` |
| (`scalar_type`/`sizes`/`dim_order` → `nbytes()`) and once as `DataSegment.size` (the bytes |
| actually allocated and read). The runtime never compares them, so a 4 KiB tensor can be backed by a |
| 16-byte heap allocation. |
|
|
| `getTensorDataPtr()` (`runtime/executor/tensor_parser_exec_aten.cpp:194`) *receives* the correct byte |
| count as a parameter and uses it in every branch except the external-constant one |
| (lines 221-229), which returns `data->buffer.data()` unchecked. |
|
|
| ## Files |
|
|
| | file | what it is | |
| |---|---| |
| | `poc/poc_leak.{pte,ptd}` | 4 KiB declared over a 16-byte segment β silent 4080-byte over-read | |
| | `poc/poc_leak16k.*`, `poc/poc_leak32k.*`, `poc/poc_leak64k.*` | larger exfiltration windows | |
| | `poc/poc_big.{pte,ptd}` | 4 MiB declared β walks off the heap, SIGSEGV (denial of service) | |
| | `poc/poc_tiny.{pte,ptd}` | 256 B declared β minimal ASAN witness | |
| | `poc/poc_nokernel.{pte,ptd}` | **empty instruction list** β no kernel runs at all; the runtime still hands the caller a `Tensor` with `nbytes()=256` over a 16-byte allocation | |
| | `gen_poc.py` | builds every pair from the **upstream** schemas with upstream `flatc` β these are well-formed files, not fuzzer garbage | |
| | `poc_main.cpp`, `build.sh` | standalone driver (ASAN, `-O2 -DNDEBUG`) | |
| | `server_main.cpp`, `build_server.sh` | minimal model-serving service: loads an uploaded model over TCP and returns the output tensor | |
| | `attacker_client.py` | remote client β separate process, socket only, no access to the server's memory | |
| | `run_exfil_suite.sh`, `sweep_window.sh`, `aslr_analysis.py` | end-to-end exfiltration + measured ASLR-defeat harness | |
| | `ASAN-witness.txt`, `EXFIL-witness.txt` | captured evidence | |
|
|
| ## Measured impact |
|
|
| * 4080 → 65520 bytes of server heap delivered to the remote party that uploaded the model |
| (the attacker picks the size) |
| * 228 verbatim copies of another tenant's session token in a single 32 KiB response, 5/5 runs |
| * ASLR: across 5 fresh victim processes, **2148/2148** base predictions matched the victim's real |
| `/proc/self/maps` exactly β both the `[heap]` base and the PIE image base β from one 864-byte model |
| * 4 MiB window → `SIGSEGV` (exit 139), reproducible remote DoS of the worker |
|
|
| ## Reproduce |
|
|
| ```bash |
| python3 gen_poc.py ./poc |
| ./build_server.sh |
| ./sweep_window.sh |
| POC=leak32k ./run_exfil_suite.sh 5 |
| python3 aslr_analysis.py /tmp/cyfra_exfil 5 |
| ``` |
|
|
| Access to this repository is gated and granted to `protectai-bot` for huntr triage. |
|
|