File size: 7,652 Bytes
b009429 | 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 | # Null Pointer Dereference in ExecuTorch BackendDelegate::PopulateCompileSpecs() via Missing CompileSpec.key (.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 with a `compile_specs` list, with at least one backend registered/available (a routine deployment configuration).
## Summary
`BackendDelegate::PopulateCompileSpecs()` dereferences each `CompileSpec` entry's `key` field unconditionally via `->c_str()`, without checking it for null. `CompileSpec.key` is declared as an optional `string` in the schema. A `.pte` file with a delegate whose `compile_specs` list contains an entry with a `value` but no `key` crashes the moment `Method::init()` resolves that delegate β before any execution begins.
This is the third distinct null-pointer-dereference finding reached through the same `Method::init()` delegate-resolution code path in this investigation (following the `Operator.overload` and `Program.backend_delegate_data` findings), reinforcing that this area of the codebase was written with less consistent defensive guarding than the tensor-parsing paths.
## Vulnerability Details
`runtime/executor/method.cpp`'s `BackendDelegate::PopulateCompileSpecs()`:
```cpp
static Error PopulateCompileSpecs(
const flatbuffers::Vector<flatbuffers::Offset<
executorch_flatbuffer::CompileSpec>>* compile_specs_in_program,
BackendInitContext& backend_init_context,
CompileSpec** out_spec) {
auto number_of_compile_specs = compile_specs_in_program->size();
CompileSpec* compile_specs_list = ...;
...
for (size_t j = 0; j < number_of_compile_specs; j++) {
auto compile_spec_in_program = compile_specs_in_program->Get(j);
compile_specs_list[j].key = compile_spec_in_program->key()->c_str(); // <-- crash site
compile_specs_list[j].value = {
static_cast<void*>(const_cast<uint8_t*>(compile_spec_in_program->value()->Data())),
compile_spec_in_program->value()->size(),
};
}
...
```
`schema/program.fbs`:
```
table CompileSpec {
// One compile spec. There are can be multiple specs for one method
key: string; // like max_value
value: [ubyte]; // like 4, or other types based on needs.
}
```
`PopulateCompileSpecs()` is called from `BackendDelegate::Init()` whenever `delegate.compile_specs() != nullptr` β a normal, documented way for a delegate to pass backend-specific compilation options.
## 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
Same build as REPORT-11/12 Step 1.
### 2. Build the execute-level harness (`poc/harness_execute_fuzzer.cpp`, same harness as REPORT-11/12, included in this report)
This harness registers fake backends under common names (`XnnpackBackend`, `QnnBackend`, etc.) that trivially succeed on `init()`, exercising the real `BackendDelegate::Init()`/`PopulateCompileSpecs()` 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_compilespec_key_null.pte` (44,800 bytes, **included in this report β sha256 `673c5e5f95788827bf24bef50f7a2de39aab9e63b17278a5415769746bd417bb`**). This PoC was originally discovered by coverage-guided fuzzing (a 1,355,528-byte input) and minimized via libFuzzer's built-in crash minimizer to this 44,800-byte reproducer while preserving the exact same crash.
### 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_compilespec_key_null.pte
```
### Expected result (secure behavior)
`Method::init()` should return a clean `Error` (or substitute an empty string) when a `CompileSpec` entry has no `key`, rather than crashing.
### Actual result β verified against BOTH a patched build AND an independently rebuilt, wholly unmodified pristine ExecuTorch 1.3.1 source tree
```
Running: poc/poc_compilespec_key_null.pte
runtime/executor/method.cpp:181:67: runtime error: member call on null pointer of type 'flatbuffers::String'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/method.cpp:181:67 in
==<pid>== ERROR: libFuzzer: deadly signal
#0 ... (abort machinery)
BackendDelegate::PopulateCompileSpecs (method.cpp:181)
BackendDelegate::Init (method.cpp)
Method::init (method.cpp:971)
```
**Reproduced 3/3 identical runs against the independently rebuilt pristine source** (re-verified live for this report):
```
run 1: member call on null pointer of type 'flatbuffers::String'
run 2: (identical)
run 3: (identical)
```
## Impact
**Who is affected:** Any application calling `Program::load_method()` on a method whose delegates specify `compile_specs` with a null `key`, where the victim has registered/made available a backend matching the delegate's `id` β a routine configuration for any deployment with at least one 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
compile_specs_list[j].key = (compile_spec_in_program->key() != nullptr)
? compile_spec_in_program->key()->c_str()
: "";
```
**Design recommendation:** consider a single validation pass over all `CompileSpec` entries in a delegate's `compile_specs` list at the top of `PopulateCompileSpecs()`, checking both `key` and `value` for null in one place, consistent with the recommendation made in the companion `Operator.overload` (REPORT-11) and `Program.backend_delegate_data` (REPORT-12) findings β all three sit in the same delegate-resolution code area and would benefit from a shared, consistent validation approach.
A regression test should build a `.pte` with one `BackendDelegate` (using a registered fake backend) whose `compile_specs` contains an entry with a `value` but no `key`, asserting `Method::load()` returns a clean `Error` rather than crashing.
## Files Included in This Report
- `poc/poc_compilespec_key_null.pte` β the 44,800-byte minimized PoC file (sha256 `673c5e5f95788827bf24bef50f7a2de39aab9e63b17278a5415769746bd417bb`)
- `poc/harness_execute_fuzzer.cpp` β the harness used to trigger and reproduce the crash, including fake backend registration for exercising `BackendDelegate::Init()`/`PopulateCompileSpecs()`
## 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_compilespec_key_null.pte` is ready for that upload.
|