Null Pointer Dereference in ExecuTorch MethodMeta::uses_backend() via Missing ExecutionPlan.delegates (.pte)
Target: ExecuTorch 1.3.1 (.pte, huntr Model File Vulnerability program)
Severity: Low-Medium (Denial of Service)
CWE: CWE-476 (NULL Pointer Dereference)
Component: runtime/executor/method_meta.cpp
Authentication Required: No β the only requirement is that a victim application loads an attacker-supplied .pte file and calls a documented public API.
Summary
MethodMeta::uses_backend() is a public API intended for applications to check whether a loaded method requires a specific backend before deciding how to dispatch it. It dereferences ExecutionPlan.delegates without checking it for null, even though this field is explicitly optional in the schema and the sibling function num_backends() three lines below correctly guards the exact same field. A .pte file with a well-formed ExecutionPlan that simply omits delegates passes both of ExecuTorch's verification levels (InternalConsistency and Minimal) and crashes any process that calls uses_backend() on it.
A second, independent null-deref exists in the same function on a different field: BackendDelegate.id is also optional, so a .pte whose delegates vector is present but contains an entry with no id triggers the same crash class one line later.
Vulnerability Details
schema/program.fbs defines ExecutionPlan.delegates and BackendDelegate.id as ordinary (non-required) fields:
table ExecutionPlan {
...
delegates: [BackendDelegate];
...
}
table BackendDelegate {
id: string;
processed: BackendDelegateDataReference;
compile_specs: [CompileSpec];
}
runtime/executor/method_meta.cpp:
bool MethodMeta::uses_backend(const char* backend_name) const {
ET_CHECK_MSG(backend_name, "backend name is null");
const auto delegates = s_plan_->delegates();
for (size_t i = 0; i < delegates->size(); i++) { // <-- delegates may be null
auto delegate = delegates->Get(i);
auto backend_name_len = std::strlen(backend_name);
auto delegate_id_len = delegate->id()->size(); // <-- delegate->id() may be null
if (backend_name_len == delegate_id_len &&
std::strncmp(delegate->id()->c_str(), backend_name, backend_name_len) == 0) {
return true;
}
}
return false;
}
size_t MethodMeta::num_backends() const {
const auto delegates = s_plan_->delegates();
return delegates ? delegates->size() : 0; // <-- correctly guarded, 3 lines below
}
The presence of the correct guard in num_backends() proves this is an inconsistency, not an intentional assumption β the maintainers clearly know delegates can be absent; the check simply was not applied to uses_backend().
Neither Program::load()'s flatbuffers::Verifier structural check nor validate_program()'s semantic check (which validates values/tensor fields, not ExecutionPlan.delegates) nor Program::method_meta()'s own explicit field checks (name, non_const_buffer_sizes, inputs, outputs only) cover this field. A file omitting delegates is fully legal per the schema and loads cleanly under both verification levels.
Steps to Reproduce
Environment
- Linux x86-64, ExecuTorch 1.3.1 source, clang-16, CMake, Ninja
- No authentication, no host access β only the ability to supply a
.ptefile to a process that callsMethodMeta::uses_backend()
1. Build ExecuTorch with sanitizers
Same build as REPORT-01/02 Step 1. No additional flags needed β executorch_core (which contains method_meta.cpp) is part of the default target set.
2. Build the PoC harness (poc/harness_program_fuzzer.cpp, included in this report)
export ET_PARENT=/path/to/parent-of-executorch
C10_INC="$ET_SRC/runtime/core/portable_type/c10"
INCLUDES="-I$ET_PARENT -I$ET_BUILD -I$ET_BUILD/schema/include -I$ET_BUILD/extension/flat_tensor/include -I$ET_BUILD/third-party/flatc_ep/include -I$C10_INC"
clang++-16 -std=c++17 -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all \
$INCLUDES -DFLATBUFFERS_MAX_ALIGNMENT=1024 -DC10_USING_CUSTOM_GENERATED_MACROS \
-c poc/harness_program_fuzzer.cpp -o harness.o
clang++-16 -fsanitize=fuzzer,address,undefined -o poc_harness harness.o \
"$ET_BUILD/extension/data_loader/libextension_data_loader.a" \
"$ET_BUILD/libexecutorch_core.a"
The harness calls Program::load() under both InternalConsistency and Minimal verification, then for every loaded method calls the full MethodMeta API surface including uses_backend("XNNPACK"), num_backends(), and get_backend_name() β matching how a real application inspects a loaded program before dispatch.
3. Generate the PoC .pte file
python3 poc/gen_poc.py "$ET_SRC/schema" "$ET_BUILD/third-party/flatc_ep/bin/flatc"
This produces poc/poc_uses_backend_null_deref.pte (200 bytes, included in this report β sha256 128498fe87ac5be3bfac96e4e1ddd359dba52e37c253da29372ab4d183e47b05), a well-formed .pte with:
- One
ExecutionPlannamed"forward", with emptyvalues/inputs/outputs/chains/operators,non_const_buffer_sizes = [0] - A minimal
constant_segment(required because this build disables the deprecatedconstant_bufferpath via-DET_ENABLE_DEPRECATED_CONSTANT_BUFFER=0inCMakeLists.txt, even for a program with zero constants) delegatesfield entirely omitted
Note on why
constant_segmentis needed even for this trivial PoC: ExecuTorch 1.3.1's build disables the legacyconstant_bufferstorage path. Without aconstant_segment,Program::load()fails at an earlier, unrelated check (Error::InvalidProgram, "relies on the constant_buffer path, which is disabled") before ever reachingmethod_meta(). This is why the PoC includes aconstant_segmentwith a single placeholder offset even though the program has no real constant data β this is a.ptefile structure detail, not part of the vulnerability itself.
4. Trigger the crash
export ASAN_OPTIONS="abort_on_error=1:symbolize=0"
export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=0"
./poc_harness -timeout=5 -runs=0 poc/poc_uses_backend_null_deref.pte
Expected result (secure behavior)
MethodMeta::uses_backend("XNNPACK") should return false (no backend used), matching the correct behavior already implemented in the neighboring num_backends().
Actual result
Running: poc/poc_uses_backend_null_deref.pte
[dbg] IC program.ok()=1 num_methods=1
[dbg] Minimal program.ok()=1 num_methods=1
runtime/executor/method_meta.cpp:406:37: runtime error: member call on null pointer of type 'flatbuffers::Vector<flatbuffers::Offset<executorch_flatbuffer::BackendDelegate>>'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/method_meta.cpp:406:37 in
==<pid>== ERROR: libFuzzer: deadly signal
The [dbg] lines (from the harness's built-in debug output) confirm the file loads successfully under both Program::Verification::InternalConsistency and Program::Verification::Minimal β proving neither verification level catches this. The crash occurs only once uses_backend() is called.
Reproduced 3/3 identical runs (re-verified live for this report):
run 1: IC program.ok()=1 num_methods=1 / Minimal program.ok()=1 num_methods=1 / crash at method_meta.cpp:406:37
run 2: (identical)
run 3: (identical)
Second Independent Crash β Same Function, Different Field
Continued fuzzing of the same harness found a second, distinct null-deref inside uses_backend() itself: delegate->id()->size() at line 409, triggered when delegates is non-null but contains a BackendDelegate entry whose own id field is omitted (also legal per schema β id: string; is not required). Confirmed via UBSan:
runtime/executor/method_meta.cpp:409:44: runtime error: member call on null pointer of type 'flatbuffers::Vector<char>'
This is filed as supporting evidence for the same finding (not a separate report) because both crashes are in the same function with the same missing-validation defect class and the same fix location β the recommended fix below covers both.
Impact
Who is affected: Any application that calls MethodMeta::uses_backend() β a documented, intended-for-use public API β on a loaded .pte program. This is not a corner-case internal function; it exists specifically so applications can query backend requirements before dispatch.
What the attacker can do: Cause a deterministic, repeatable crash by supplying a .pte file whose ExecutionPlan simply omits the optional delegates field (or has a delegates entry with no id). The file loads successfully under both of ExecuTorch's verification levels β there is no existing defense that catches this before the crash.
What's at risk: Availability of the process calling uses_backend(). In practice, this is likely to be called during normal model-loading/dispatch logic in any application that branches on backend availability, making this a realistic crash path for ordinary usage, not just a corner case an attacker has to work hard to trigger.
Exploitation complexity: No interaction beyond the victim loading the file and calling a standard API. Matches huntr MFV's "Denial of Service (DoS) attacks through malformed model files" category.
Why not Critical: Controlled null-pointer dereference on a flatbuffers accessor β no memory corruption, no code execution, no data disclosure.
Suggested Remediation
In MethodMeta::uses_backend() (method_meta.cpp lines 403β417):
bool MethodMeta::uses_backend(const char* backend_name) const {
ET_CHECK_MSG(backend_name, "backend name is null");
const auto delegates = s_plan_->delegates();
if (delegates == nullptr) {
return false;
}
for (size_t i = 0; i < delegates->size(); i++) {
auto delegate = delegates->Get(i);
if (delegate == nullptr || delegate->id() == nullptr) {
continue;
}
auto backend_name_len = std::strlen(backend_name);
auto delegate_id_len = delegate->id()->size();
if (backend_name_len == delegate_id_len &&
std::strncmp(delegate->id()->c_str(), backend_name, backend_name_len) == 0) {
return true;
}
}
return false;
}
This mirrors the guard pattern already correctly implemented in num_backends() immediately below. A regression test should build an ExecutionPlan with delegates omitted, and separately one with a BackendDelegate entry lacking id, asserting uses_backend() returns false cleanly in both cases rather than crashing.
Files Included in This Report
poc/poc_uses_backend_null_deref.pteβ the 200-byte PoC file (sha256128498fe87ac5be3bfac96e4e1ddd359dba52e37c253da29372ab4d183e47b05)poc/gen_poc.pyβ deterministic script to regenerate the exact same PoC file from ExecuTorch's ownprogram.fbsschema (verified byte-for-byte identical output)poc/harness_program_fuzzer.cppβ the harness used to trigger and reproduce the crash against the real public API, including the debug instrumentation that proves both verification levels accept the file
huntr Submission Note
Per the huntr MFV program's submission requirements, this PoC needs to be uploaded to a public HuggingFace repository before filing. poc/poc_uses_backend_null_deref.pte is ready for that upload.