Null Pointer Dereference in ExecuTorch BackendDelegate::PopulateCompileSpecs() via Missing CompileSpec.key (.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.cpp
Authentication Required: No β requires a victim application to load a .pte file whose method has a backend delegate with a compile_specs list, with at least one backend registered/available (a routine deployment configuration).
Summary
BackendDelegate::PopulateCompileSpecs() dereferences each CompileSpec entry's key field unconditionally via ->c_str(), without checking it for null. CompileSpec.key is declared as an optional string in the schema. A .pte file with a delegate whose compile_specs list contains an entry with a value but no key crashes the moment Method::init() resolves that delegate β before any execution begins.
This is the third distinct null-pointer-dereference finding reached through the same Method::init() delegate-resolution code path in this investigation (following the Operator.overload and Program.backend_delegate_data findings), reinforcing that this area of the codebase was written with less consistent defensive guarding than the tensor-parsing paths.
Vulnerability Details
runtime/executor/method.cpp's BackendDelegate::PopulateCompileSpecs():
static Error PopulateCompileSpecs(
const flatbuffers::Vector<flatbuffers::Offset<
executorch_flatbuffer::CompileSpec>>* compile_specs_in_program,
BackendInitContext& backend_init_context,
CompileSpec** out_spec) {
auto number_of_compile_specs = compile_specs_in_program->size();
CompileSpec* compile_specs_list = ...;
...
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(); // <-- crash site
compile_specs_list[j].value = {
static_cast<void*>(const_cast<uint8_t*>(compile_spec_in_program->value()->Data())),
compile_spec_in_program->value()->size(),
};
}
...
schema/program.fbs:
table CompileSpec {
// One compile spec. There are can be multiple specs for one method
key: string; // like max_value
value: [ubyte]; // like 4, or other types based on needs.
}
PopulateCompileSpecs() is called from BackendDelegate::Init() whenever delegate.compile_specs() != nullptr β a normal, documented way for a delegate to pass backend-specific compilation options.
Steps to Reproduce
Environment
Linux x86-64, ExecuTorch 1.3.1 pristine source, clang-16, CMake, Ninja. No authentication, no host access.
1. Build ExecuTorch with sanitizers
Same build as REPORT-11/12 Step 1.
2. Build the execute-level harness (poc/harness_execute_fuzzer.cpp, same harness as REPORT-11/12, included in this report)
This harness registers fake backends under common names (XnnpackBackend, QnnBackend, etc.) that trivially succeed on init(), exercising the real BackendDelegate::Init()/PopulateCompileSpecs() code path.
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_execute_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"
3. PoC file
poc/poc_compilespec_key_null.pte (44,800 bytes, included in this report β sha256 673c5e5f95788827bf24bef50f7a2de39aab9e63b17278a5415769746bd417bb). This PoC was originally discovered by coverage-guided fuzzing (a 1,355,528-byte input) and minimized via libFuzzer's built-in crash minimizer to this 44,800-byte reproducer while preserving the exact same crash.
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_compilespec_key_null.pte
Expected result (secure behavior)
Method::init() should return a clean Error (or substitute an empty string) when a CompileSpec entry has no key, rather than crashing.
Actual result β verified against BOTH a patched build AND an independently rebuilt, wholly unmodified pristine ExecuTorch 1.3.1 source tree
Running: poc/poc_compilespec_key_null.pte
runtime/executor/method.cpp:181:67: runtime error: member call on null pointer of type 'flatbuffers::String'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/method.cpp:181:67 in
==<pid>== ERROR: libFuzzer: deadly signal
#0 ... (abort machinery)
BackendDelegate::PopulateCompileSpecs (method.cpp:181)
BackendDelegate::Init (method.cpp)
Method::init (method.cpp:971)
Reproduced 3/3 identical runs against the independently rebuilt pristine source (re-verified live for this report):
run 1: member call on null pointer of type 'flatbuffers::String'
run 2: (identical)
run 3: (identical)
Impact
Who is affected: Any application calling Program::load_method() on a method whose delegates specify compile_specs with a null key, where the victim has registered/made available a backend matching the delegate's id β a routine configuration for any deployment with at least one hardware-acceleration backend.
What the attacker can do: Cause a deterministic crash during method loading, unconditionally (no build-flag dependency).
What's at risk: Availability only.
Why not Critical: Controlled null-pointer dereference, no memory corruption or code execution demonstrated.
Suggested Remediation
compile_specs_list[j].key = (compile_spec_in_program->key() != nullptr)
? compile_spec_in_program->key()->c_str()
: "";
Design recommendation: consider a single validation pass over all CompileSpec entries in a delegate's compile_specs list at the top of PopulateCompileSpecs(), checking both key and value for null in one place, consistent with the recommendation made in the companion Operator.overload (REPORT-11) and Program.backend_delegate_data (REPORT-12) findings β all three sit in the same delegate-resolution code area and would benefit from a shared, consistent validation approach.
A regression test should build a .pte with one BackendDelegate (using a registered fake backend) whose compile_specs contains an entry with a value but no key, asserting Method::load() returns a clean Error rather than crashing.
Files Included in This Report
poc/poc_compilespec_key_null.pteβ the 44,800-byte minimized PoC file (sha256673c5e5f95788827bf24bef50f7a2de39aab9e63b17278a5415769746bd417bb)poc/harness_execute_fuzzer.cppβ the harness used to trigger and reproduce the crash, including fake backend registration for exercisingBackendDelegate::Init()/PopulateCompileSpecs()
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_compilespec_key_null.pte is ready for that upload.