# Null Pointer Dereference in ExecuTorch Program::get_backend_delegate_data() via Missing Program.backend_delegate_data (.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/program.cpp` **Authentication Required:** No — requires a victim application to load a `.pte` file whose method has a backend delegate referencing INLINE-located processed data, with at least one backend registered/available (a routine deployment configuration). ## Summary `Program::get_backend_delegate_data()` dereferences `Program.backend_delegate_data()` (an optional top-level flatbuffer field) directly inside its bounds-check condition, with no null guard. A `.pte` file with an `ExecutionPlan.delegates` entry whose `processed` field specifies `DataLocation::INLINE`, while the `Program`'s top-level `backend_delegate_data` list is entirely absent, crashes the moment `Method::init()` attempts to resolve that delegate — before any execution begins. Unlike a sibling finding in this same investigation (the `Operator.overload` null-deref), this crash sits directly in the bounds-check condition itself, not merely inside a log-message argument — making it **unconditionally reachable regardless of build configuration** (no dependency on `EXECUTORCH_ENABLE_LOGGING`). This bug was confirmed **twice independently**: once against this session's incrementally patched build, and once against a **freshly rebuilt, wholly unmodified pristine 1.3.1 source tree**, ruling out any possibility of a local-patch artifact. ## Vulnerability Details `runtime/executor/program.cpp`'s `Program::get_backend_delegate_data()`: ```cpp Error Program::get_backend_delegate_data( size_t index, const void** out_data, size_t* out_size) const { const auto* data_list = static_cast(internal_program_) ->backend_delegate_data(); ET_CHECK_OR_RETURN_ERROR( index < data_list->size(), // <-- crash site: data_list may be null NotFound, "index %zu >= list size %" PRIu32, index, data_list->size()); ... ``` `Program.backend_delegate_data` is declared as an optional `[BackendDelegateInlineData]` list in `schema/program.fbs`: ``` // List of delegate data. Pointed to by BackendDelegateDataReference. backend_delegate_data: [BackendDelegateInlineData]; ``` This function is called from `BackendDelegate::GetProcessedData()` (`method.cpp`) whenever a delegate's `processed` field specifies `DataLocation::INLINE`: ```cpp case executorch_flatbuffer::DataLocation::INLINE: { const void* data; size_t size; Error err = program->get_backend_delegate_data( processed->index(), &data, &size); ... ``` Which in turn is called from `BackendDelegate::Init()` during `Method::init()`'s delegate-resolution loop — reached during ordinary method **loading**, for any delegate whose registered backend is available (`get_backend_class(id) != nullptr && backend->is_available()`), a standard, expected condition for any deployment that has compiled in and registered at least one hardware-acceleration backend (which most real ExecuTorch deployments do). ## 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 (logging enabled or disabled — this bug reproduces either way) Same build as REPORT-11 Step 1 (or the simpler REPORT-01 build — `EXECUTORCH_ENABLE_LOGGING` does not affect this finding). ### 2. Build the execute-level harness (`poc/harness_execute_fuzzer.cpp`, same harness as REPORT-11, 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: ```bash 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_data_null.pte` (304 bytes, **included in this report — sha256 `2f3be1201b1a426b19d44d2b0886ba12ed872ec4e3f4b02d65457b6c4f441165`**) contains: - One `BackendDelegate` entry with `id = "XnnpackBackend"` and `processed = { location: INLINE, index: 0 }` - **No** `backend_delegate_data` field at the `Program` level at all ### 4. Trigger the crash ```bash 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_data_null.pte ``` ### Expected result (secure behavior) `Method::init()` should return `Error::NotFound` (or similar) cleanly when a delegate references INLINE data but the Program has no `backend_delegate_data` list, rather than crashing while checking bounds. ### Actual result — verified against BOTH a patched build AND an independently rebuilt, wholly unmodified pristine ExecuTorch 1.3.1 source tree ``` Running: poc/poc_backend_delegate_data_null.pte runtime/executor/program.cpp:541:3: runtime error: member call on null pointer of type 'flatbuffers::Vector>' SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/program.cpp:541:3 in ==== ERROR: libFuzzer: deadly signal #0 ... (abort machinery) Program::get_backend_delegate_data (program.cpp:541) BackendDelegate::GetProcessedData (method.cpp:202) BackendDelegate::Init (method.cpp:97) Method::init (method.cpp:971) Method::load (method.cpp:862) ``` **Reproduced 3/3 identical runs against the independently rebuilt pristine source** (re-verified live for this report, using a fresh build with zero local modifications): ``` run 1: member call on null pointer of type 'flatbuffers::Vector<...>' run 2: (identical) run 3: (identical) ``` ## Impact **Who is affected:** Any application calling `Program::load_method()` on a method whose delegates reference INLINE processed data, where the Program's `backend_delegate_data` list is absent — this is triggered during **loading**, not execution, and requires only that the victim has registered a backend under a name the attacker can predict (most deployments register at least one real 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 ```cpp Error Program::get_backend_delegate_data( size_t index, const void** out_data, size_t* out_size) const { const auto* data_list = static_cast(internal_program_) ->backend_delegate_data(); if (data_list == nullptr) { ET_LOG(Error, "Program has no backend_delegate_data list"); return Error::NotFound; } ET_CHECK_OR_RETURN_ERROR( index < data_list->size(), NotFound, "index %zu >= list size %" PRIu32, index, data_list->size()); ... ``` **Design recommendation:** consider validating at `Program::load()` time (in `validate_program()`, `runtime/executor/program_validation.cpp`) that if any `ExecutionPlan.delegates[].processed.location == INLINE` exists anywhere in the program, `Program.backend_delegate_data` must also be present — catching this cross-field consistency issue once, at load time, rather than at each individual accessor. A regression test should build a `.pte` with one `BackendDelegate` referencing INLINE processed data at index 0, and no `backend_delegate_data` field on the `Program`, asserting `Method::load()` returns a clean `Error::NotFound` rather than crashing. ## Files Included in This Report - `poc/poc_backend_delegate_data_null.pte` — the 304-byte PoC file (sha256 `2f3be1201b1a426b19d44d2b0886ba12ed872ec4e3f4b02d65457b6c4f441165`) - `poc/harness_execute_fuzzer.cpp` — the harness used to trigger and reproduce the crash, including fake backend registration for exercising `BackendDelegate::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_data_null.pte` is ready for that upload.