| # 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<const executorch_flatbuffer::Program*>(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<flatbuffers::Offset<executorch_flatbuffer::BackendDelegateInlineData>>' |
| SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/program.cpp:541:3 in |
| ==<pid>== 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<const executorch_flatbuffer::Program*>(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. |
|
|