File size: 11,479 Bytes
3bfbf53 | 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 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | # Null Pointer Dereference in ExecuTorch MethodMeta::uses_backend() via Missing ExecutionPlan.delegates (.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_meta.cpp`
**Authentication Required:** No β the only requirement is that a victim application loads an attacker-supplied `.pte` file and calls a documented public API.
## Summary
`MethodMeta::uses_backend()` is a public API intended for applications to check whether a loaded method requires a specific backend before deciding how to dispatch it. It dereferences `ExecutionPlan.delegates` without checking it for null, even though this field is explicitly optional in the schema and the sibling function `num_backends()` three lines below correctly guards the exact same field. A `.pte` file with a well-formed `ExecutionPlan` that simply omits `delegates` passes both of ExecuTorch's verification levels (`InternalConsistency` and `Minimal`) and crashes any process that calls `uses_backend()` on it.
A second, independent null-deref exists in the same function on a different field: `BackendDelegate.id` is also optional, so a `.pte` whose `delegates` vector is present but contains an entry with no `id` triggers the same crash class one line later.
## Vulnerability Details
`schema/program.fbs` defines `ExecutionPlan.delegates` and `BackendDelegate.id` as ordinary (non-required) fields:
```
table ExecutionPlan {
...
delegates: [BackendDelegate];
...
}
table BackendDelegate {
id: string;
processed: BackendDelegateDataReference;
compile_specs: [CompileSpec];
}
```
`runtime/executor/method_meta.cpp`:
```cpp
bool MethodMeta::uses_backend(const char* backend_name) const {
ET_CHECK_MSG(backend_name, "backend name is null");
const auto delegates = s_plan_->delegates();
for (size_t i = 0; i < delegates->size(); i++) { // <-- delegates may be null
auto delegate = delegates->Get(i);
auto backend_name_len = std::strlen(backend_name);
auto delegate_id_len = delegate->id()->size(); // <-- delegate->id() may be null
if (backend_name_len == delegate_id_len &&
std::strncmp(delegate->id()->c_str(), backend_name, backend_name_len) == 0) {
return true;
}
}
return false;
}
size_t MethodMeta::num_backends() const {
const auto delegates = s_plan_->delegates();
return delegates ? delegates->size() : 0; // <-- correctly guarded, 3 lines below
}
```
The presence of the correct guard in `num_backends()` proves this is an inconsistency, not an intentional assumption β the maintainers clearly know `delegates` can be absent; the check simply was not applied to `uses_backend()`.
Neither `Program::load()`'s `flatbuffers::Verifier` structural check nor `validate_program()`'s semantic check (which validates `values`/tensor fields, not `ExecutionPlan.delegates`) nor `Program::method_meta()`'s own explicit field checks (`name`, `non_const_buffer_sizes`, `inputs`, `outputs` only) cover this field. A file omitting `delegates` is fully legal per the schema and loads cleanly under **both** verification levels.
## Steps to Reproduce
### Environment
- Linux x86-64, ExecuTorch 1.3.1 source, clang-16, CMake, Ninja
- No authentication, no host access β only the ability to supply a `.pte` file to a process that calls `MethodMeta::uses_backend()`
### 1. Build ExecuTorch with sanitizers
Same build as REPORT-01/02 Step 1. No additional flags needed β `executorch_core` (which contains `method_meta.cpp`) is part of the default target set.
### 2. Build the PoC harness (`poc/harness_program_fuzzer.cpp`, included in this report)
```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_program_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"
```
The harness calls `Program::load()` under both `InternalConsistency` and `Minimal` verification, then for every loaded method calls the full `MethodMeta` API surface including `uses_backend("XNNPACK")`, `num_backends()`, and `get_backend_name()` β matching how a real application inspects a loaded program before dispatch.
### 3. Generate the PoC .pte file
```bash
python3 poc/gen_poc.py "$ET_SRC/schema" "$ET_BUILD/third-party/flatc_ep/bin/flatc"
```
This produces `poc/poc_uses_backend_null_deref.pte` (200 bytes, **included in this report β sha256 `128498fe87ac5be3bfac96e4e1ddd359dba52e37c253da29372ab4d183e47b05`**), a well-formed `.pte` with:
- One `ExecutionPlan` named `"forward"`, with empty `values`/`inputs`/`outputs`/`chains`/`operators`, `non_const_buffer_sizes = [0]`
- A minimal `constant_segment` (required because this build disables the deprecated `constant_buffer` path via `-DET_ENABLE_DEPRECATED_CONSTANT_BUFFER=0` in `CMakeLists.txt`, even for a program with zero constants)
- **`delegates` field entirely omitted**
> **Note on why `constant_segment` is needed even for this trivial PoC:** ExecuTorch 1.3.1's build disables the legacy `constant_buffer` storage path. Without a `constant_segment`, `Program::load()` fails at an earlier, unrelated check (`Error::InvalidProgram`, "relies on the constant_buffer path, which is disabled") before ever reaching `method_meta()`. This is why the PoC includes a `constant_segment` with a single placeholder offset even though the program has no real constant data β this is a `.pte` file structure detail, not part of the vulnerability itself.
### 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_uses_backend_null_deref.pte
```
### Expected result (secure behavior)
`MethodMeta::uses_backend("XNNPACK")` should return `false` (no backend used), matching the correct behavior already implemented in the neighboring `num_backends()`.
### Actual result
```
Running: poc/poc_uses_backend_null_deref.pte
[dbg] IC program.ok()=1 num_methods=1
[dbg] Minimal program.ok()=1 num_methods=1
runtime/executor/method_meta.cpp:406:37: runtime error: member call on null pointer of type 'flatbuffers::Vector<flatbuffers::Offset<executorch_flatbuffer::BackendDelegate>>'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/method_meta.cpp:406:37 in
==<pid>== ERROR: libFuzzer: deadly signal
```
The `[dbg]` lines (from the harness's built-in debug output) confirm the file loads successfully under **both** `Program::Verification::InternalConsistency` and `Program::Verification::Minimal` β proving neither verification level catches this. The crash occurs only once `uses_backend()` is called.
**Reproduced 3/3 identical runs** (re-verified live for this report):
```
run 1: IC program.ok()=1 num_methods=1 / Minimal program.ok()=1 num_methods=1 / crash at method_meta.cpp:406:37
run 2: (identical)
run 3: (identical)
```
## Second Independent Crash β Same Function, Different Field
Continued fuzzing of the same harness found a second, distinct null-deref inside `uses_backend()` itself: `delegate->id()->size()` at line 409, triggered when `delegates` is non-null but contains a `BackendDelegate` entry whose own `id` field is omitted (also legal per schema β `id: string;` is not required). Confirmed via UBSan:
```
runtime/executor/method_meta.cpp:409:44: runtime error: member call on null pointer of type 'flatbuffers::Vector<char>'
```
This is filed as supporting evidence for the same finding (not a separate report) because both crashes are in the same function with the same missing-validation defect class and the same fix location β the recommended fix below covers both.
## Impact
**Who is affected:** Any application that calls `MethodMeta::uses_backend()` β a documented, intended-for-use public API β on a loaded `.pte` program. This is not a corner-case internal function; it exists specifically so applications can query backend requirements before dispatch.
**What the attacker can do:** Cause a deterministic, repeatable crash by supplying a `.pte` file whose `ExecutionPlan` simply omits the optional `delegates` field (or has a `delegates` entry with no `id`). The file loads successfully under both of ExecuTorch's verification levels β there is no existing defense that catches this before the crash.
**What's at risk:** Availability of the process calling `uses_backend()`. In practice, this is likely to be called during normal model-loading/dispatch logic in any application that branches on backend availability, making this a realistic crash path for ordinary usage, not just a corner case an attacker has to work hard to trigger.
**Exploitation complexity:** No interaction beyond the victim loading the file and calling a standard API. Matches huntr MFV's "Denial of Service (DoS) attacks through malformed model files" category.
**Why not Critical:** Controlled null-pointer dereference on a flatbuffers accessor β no memory corruption, no code execution, no data disclosure.
## Suggested Remediation
In `MethodMeta::uses_backend()` (method_meta.cpp lines 403β417):
```cpp
bool MethodMeta::uses_backend(const char* backend_name) const {
ET_CHECK_MSG(backend_name, "backend name is null");
const auto delegates = s_plan_->delegates();
if (delegates == nullptr) {
return false;
}
for (size_t i = 0; i < delegates->size(); i++) {
auto delegate = delegates->Get(i);
if (delegate == nullptr || delegate->id() == nullptr) {
continue;
}
auto backend_name_len = std::strlen(backend_name);
auto delegate_id_len = delegate->id()->size();
if (backend_name_len == delegate_id_len &&
std::strncmp(delegate->id()->c_str(), backend_name, backend_name_len) == 0) {
return true;
}
}
return false;
}
```
This mirrors the guard pattern already correctly implemented in `num_backends()` immediately below. A regression test should build an `ExecutionPlan` with `delegates` omitted, and separately one with a `BackendDelegate` entry lacking `id`, asserting `uses_backend()` returns `false` cleanly in both cases rather than crashing.
## Files Included in This Report
- `poc/poc_uses_backend_null_deref.pte` β the 200-byte PoC file (sha256 `128498fe87ac5be3bfac96e4e1ddd359dba52e37c253da29372ab4d183e47b05`)
- `poc/gen_poc.py` β deterministic script to regenerate the exact same PoC file from ExecuTorch's own `program.fbs` schema (verified byte-for-byte identical output)
- `poc/harness_program_fuzzer.cpp` β the harness used to trigger and reproduce the crash against the real public API, including the debug instrumentation that proves both verification levels accept the file
## 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_uses_backend_null_deref.pte` is ready for that upload.
|