romandaripper's picture
ExecuTorch .pte/.ptd external-constant OOB read PoC (Cyfra Tech Solutions)
c5e2f40 verified
|
Raw
History Blame Contribute Delete
3.36 kB
---
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.