File size: 9,113 Bytes
1ec1150 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | # 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.
|