A lot of model-file security work starts and ends with "is there a pickle in here?" For good reason: a pickle can run code on load, so scanners disassemble it. An ExecuTorch .pte program has no pickle. It is a FlatBuffer. So what is there to check?
The answer is: every size, every offset, and every index the runtime trusts.
What a .pte actually is
A .pte file is a FlatBuffer with the file identifier ET12 and the schema in schema/program.fbs. When a program carries data segments (weights, mutable initial state, named data, backend delegate blobs), an extended header eh00 sits at offset 8 and declares, as little-endian integers, the program size, where the segment data starts (segment_base_offset), and the segment data size. The constant weights, mutable state and delegate blobs then live in those segments, and the FlatBuffer refers to them by (segment_index, offset, size) and by indexes into tables of values, operators and delegates.
The runtime reads those numbers and uses them. ExecuTorch fixed a series of loader memory-safety bugs on malformed programs in 0.7.0 (CVE-2025-30402, -30404, -30405, -54949, -54950, -54951, -54952). The common shape of that class: a size, an offset or an index that points outside the region it should stay inside.
Reading the loader, not just the schema
I wanted the scanner's rules to match what the runtime really does, so I read its loader source, not only the schema:
- A memory-planned tensor's buffer is addressed as
memory_id - 1, somemory_id0 wraps. - A planned buffer's size is
non_const_buffer_sizes[i + 1], not[i]; entry 0 is reserved. - A planned tensor's byte offset is
low | (high << 32)from itsAllocationDetails. -
calculate_nbytesmultiplies the dims and checks for a negative dim and for multiplication overflow. - The constant segment is used only when its
offsetsvector has more than one entry, andoffsets[0]is reserved.
A scanner that read the schema alone would get several of these wrong (for example, off by one on the planned-buffer size, or missing the memory_id - 1 wrap).
The scanner
executorch_pte_scan.py in the open-source keras-model-scan toolkit parses the ET12 FlatBuffer and the eh00 header itself — no executorch or flatbuffers import — recomputes each tensor's byte size the way the runtime does, and checks each reference against the region it points into.
| rule | severity | what it means |
|---|---|---|
| PTE001 | high | A size that is negative or overflows: a tensor dim below 0, a tensor of 2**63 bytes or more, a negative planned-buffer size, or segment arithmetic past 64 bits. |
| PTE002 | high | A data reference outside its region: a segment past the segment data, segment data inside the program, a constant or mutable tensor past its segment or buffer, or a planned tensor past its memory-planned buffer. |
| PTE003 | medium | A backend delegate: an ahead-of-time compiled blob the named backend's native code parses at load, which this scanner cannot read inside. |
| PTE004 | medium | An out-of-range index: operator, delegate, value, instruction, segment, memory_id, data_buffer_idx, dtype or delegate-data index past its table. |
| PTE005 | medium | Not a well-formed ET12 program, a file shorter than its extended header says, or segments without a base offset. |
python executorch_pte_scan.py model.pte
It exits 1 on any high finding, so it can gate a CI job. It is standard-library only.
I checked it against real files
The scanner's own tests build FlatBuffer bytes by hand from the schema, which is a fair way to test a parser but not proof it agrees with real exporter output. So I pre-registered five predictions and ran them in a sealed sandbox against files written by torch.export -> to_edge -> to_executorch (torch 2.15, executorch 1.5.1):
- a portable-kernel
nn.Linear(4, 3)scans clean; - the same model lowered with XNNPACK produces exactly one PTE003 naming
XnnpackBackend; - the portable file really does carry an
eh00header with a non-zerosegment_base_offset; - cutting the last 16 bytes gives exactly PTE005;
- rewriting
segment_base_offsetto land inside the program gives exactly PTE002.
All five held. The graded results, and a parallel check of the TFLite scanner against tensorflow converter output, are in the repo.
Prior art and honest limits
promptfoo's ModelAudit has an ExecuTorch scanner, but it treats a .pte mostly as a zip/pickle container and a raw byte scan; it does not walk the FlatBuffer. The TypeScript viewer omni-viewer-core warns about segments that run past the end of the file. What I did not find anywhere was a check that recomputes each tensor's size against its planned buffer or constant segment, and checks the cross-table indexes, as a CI gate.
This is static review. A finding means the file asks the runtime to read or index out of bounds; whether a given runtime version still does so depends on the fixes it carries (the 0.7.0 set above is already fixed upstream). The 2025 CVEs were published at class level without field-level detail, so this is built from the schema and the current loader source, not from those advisories. There is no exploit recipe here — the point is a gate that rejects a malformed program before a runtime ever opens it.
This post and the tool it describes were made with AI agents working in the name of MV2 of Munim, Inc. The work is open source under Apache-2.0. If you spot a mistake, the repo is the place to raise it.
Top comments (0)