YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
- Null Pointer Dereference in ExecuTorch BackendDelegate::GetProcessedData() via Missing BackendDelegate.processed (.pte)
- Summary
- Vulnerability Details
- Steps to Reproduce
- Environment
- 1. Build ExecuTorch with sanitizers
- 2. Build the execute-level harness (
poc/harness_execute_fuzzer.cpp, included in this report) - 3. PoC file
- 4. Trigger the crash
- Expected result (secure behavior)
- Actual result β verified against BOTH an independently rebuilt pristine build AND a separately-patched build
- Impact
- Suggested Remediation
- Files Included in This Report
- huntr Submission Note
- Summary
Null Pointer Dereference in ExecuTorch BackendDelegate::GetProcessedData() via Missing BackendDelegate.processed (.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 entry referencing a registered/available backend, while omitting the delegate's processed field (a routine deployment configuration, requiring nothing more than one compiled-in hardware-acceleration backend).
Summary
BackendDelegate::GetProcessedData() dereferences delegate.processed() β the whole optional BackendDelegateDataReference field on a BackendDelegate table β via ->location(), with no null guard. A .pte file with an ExecutionPlan.delegates entry whose id matches a backend the victim has registered and made available, but which omits the processed field entirely, crashes the moment Method::init() resolves that delegate β before any execution begins, and before compile specs are even examined.
This is the fifth distinct null-pointer-dereference bug found in this session's investigation of ExecuTorch's BackendDelegate::Init() delegate-resolution call chain (method.cpp), and the shallowest: unlike the sibling CompileSpec.key/CompileSpec.value findings (which require a populated compile_specs list) or the Operator.overload finding (which requires EXECUTORCH_ENABLE_LOGGING=ON), this crash needs only a bare BackendDelegate.id field with processed omitted β no compile specs, no logging flag, no kernel execution.
Confirmed twice independently: once against a freshly rebuilt, wholly unmodified pristine 1.3.1 source tree, and once against a separately-patched build (with unrelated compile-spec fixes applied), ruling out any local-patch artifact or coincidental interaction with prior fixes.
Vulnerability Details
runtime/executor/method.cpp's BackendDelegate::GetProcessedData():
static Result<FreeableBuffer> GetProcessedData(
const executorch_flatbuffer::BackendDelegate& delegate,
const Program* program) {
const executorch_flatbuffer::BackendDelegateDataReference* processed =
delegate.processed();
switch (processed->location()) { // <-- crash site: processed may be null
case executorch_flatbuffer::DataLocation::INLINE: {
...
BackendDelegate.processed is declared as an optional table field in schema/program.fbs:
table BackendDelegate {
id: string;
processed: BackendDelegateDataReference;
compile_specs: [CompileSpec];
}
None of the three fields are marked required, so a well-formed BackendDelegate entry can legally specify only id and omit processed entirely.
GetProcessedData() is called from BackendDelegate::Init():
static Error Init(
const executorch_flatbuffer::BackendDelegate& delegate,
const Program* program,
BackendInitContext& backend_init_context,
BackendDelegate* out) {
...
BackendInterface* backend = get_backend_class(backend_id);
...
ET_CHECK_OR_RETURN_ERROR(backend->is_available(), ...);
// Get the delegate data.
Result<FreeableBuffer> processed_data = GetProcessedData(delegate, program);
GetProcessedData() is the first accessor called in Init() after the backend-lookup checks β reached unconditionally for every delegate entry, unlike PopulateCompileSpecs() which is only invoked when delegate.compile_specs() != nullptr. This makes the bug the most directly reachable of the five delegate-resolution null-derefs found in this investigation.
BackendDelegate::Init() is in turn called from Method::init()'s delegate-resolution loop, reached during ordinary method loading (Program::load_method()), for any delegate whose registered backend is available (get_backend_class(id) != nullptr && backend->is_available()) β a standard condition for any deployment that has compiled in and registered at least one hardware-acceleration backend.
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
cmake -B build_asan -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all" \
-DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all" \
-DEXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON .
cmake --build build_asan -j
2. Build the execute-level harness (poc/harness_execute_fuzzer.cpp, included in this report)
This harness registers fake backends under common names (XnnpackBackend, QnnBackend, CoreMLBackend, etc.) that trivially succeed on init(), letting us exercise the real BackendDelegate::Init() 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_backend_delegate_processed_null.pte (248 bytes, included in this report β sha256 0377b7f4078dda503e5f2ae3e8b69f8332081f065964a45098fb0d95bba4b36) contains:
- One
ExecutionPlannamed"forward"with empty values/inputs/outputs/chains/operators - One
BackendDelegateentry withid = "XnnpackBackend"and noprocessedfield at all - The minimal
constant_segment/segmentsboilerplate required by this build'sET_ENABLE_DEPRECATED_CONSTANT_BUFFER=0configuration
Built directly from the project's own flatc + schema/program.fbs, no manual byte-patching required.
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_backend_delegate_processed_null.pte
Expected result (secure behavior)
Method::init() should return Error::InvalidProgram cleanly when a delegate entry omits the processed field, rather than crashing while reading its location.
Actual result β verified against BOTH an independently rebuilt pristine build AND a separately-patched build
runtime/executor/method.cpp:198:24: runtime error: member call on null pointer of type 'executorch_flatbuffer::BackendDelegateDataReference'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/method.cpp:198:24 in
==<pid>== ERROR: libFuzzer: deadly signal
BackendDelegate::GetProcessedData (method.cpp:198)
BackendDelegate::Init (method.cpp:97)
Method::init (method.cpp:971)
Method::load (method.cpp:862)
Reproduced 3/3 identical runs against both the pristine rebuild and the separately-patched build:
run 1: member call on null pointer of type 'executorch_flatbuffer::BackendDelegateDataReference'
run 2: (identical)
run 3: (identical)
Impact
Who is affected: Any application calling Program::load_method() on a method whose delegates include an entry that omits the processed field, where the victim has registered a backend under a name the attacker can predict (most deployments register at least one real hardware-acceleration backend). This is triggered during loading, not execution.
What the attacker can do: Cause a deterministic crash during method loading, unconditionally (no build-flag dependency, no compile_specs list required β the minimal possible malformed-delegate PoC).
What's at risk: Availability only.
Why not Critical: Controlled null-pointer dereference, no memory corruption or code execution demonstrated.
Suggested Remediation
static Result<FreeableBuffer> GetProcessedData(
const executorch_flatbuffer::BackendDelegate& delegate,
const Program* program) {
const executorch_flatbuffer::BackendDelegateDataReference* processed =
delegate.processed();
ET_CHECK_OR_RETURN_ERROR(
processed != nullptr,
InvalidProgram,
"Missing processed field for backend delegate %s",
delegate.id() != nullptr ? delegate.id()->c_str() : "<unknown>");
switch (processed->location()) {
...
Design recommendation: consider validating at Program::load() time (in validate_program(), runtime/executor/program_validation.cpp) that every ExecutionPlan.delegates[] entry has a non-null processed field β catching this, and potentially the sibling compile_specs null-key/value issues found in this same investigation, in one centralized pass rather than at each individual accessor deep in BackendDelegate::Init()'s call chain.
A regression test should build a .pte with one BackendDelegate entry (id matching a registered fake backend) that omits the processed field, asserting Method::load() returns a clean Error::InvalidProgram rather than crashing.
Files Included in This Report
poc/poc_backend_delegate_processed_null.pteβ the 248-byte PoC file (sha2560377b7f4078dda503e5f2ae3e8b69f8332081f065964a45098fb0d95bba4b36)poc/harness_execute_fuzzer.cppβ the harness used to trigger and reproduce the crash, including fake backend registration for exercisingBackendDelegate::Init()
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_backend_delegate_processed_null.pte is ready for that upload.
- Downloads last month
- 4