File size: 7,949 Bytes
c809321 | 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 | # Null Pointer Dereference in ExecuTorch BackendDelegate::PopulateCompileSpecs() via Missing CompileSpec.value (.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 `value` field unconditionally via `->Data()` and `->size()`, without checking it for null. `CompileSpec.value` is declared as an optional `[ubyte]` in the schema β the direct sibling of the `CompileSpec.key` field covered in REPORT-13. This is the fourth distinct null-pointer-dereference finding reached through the `Method::init()` delegate-resolution code path in this investigation, and was discovered as an immediate consequence of fixing REPORT-13: within the same fuzzing run started right after that patch, the fuzzer found this second, independent bug in the very next field of the very same loop.
## Vulnerability Details
`runtime/executor/method.cpp`'s `BackendDelegate::PopulateCompileSpecs()` (pristine source):
```cpp
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();
compile_specs_list[j].value = {
/*buffer*/ static_cast<void*>(
const_cast<uint8_t*>(compile_spec_in_program->value()->Data())), // <-- crash site
/*nbytes*/ compile_spec_in_program->value()->size(), // <-- crash site
};
}
```
`schema/program.fbs`:
```
table CompileSpec {
key: string; // like max_value
value: [ubyte]; // like 4, or other types based on needs.
}
```
Neither `key` nor `value` is marked `required`, so both are legitimately optional. `PopulateCompileSpecs()` handled **neither** safely in the pristine source.
## An Important Investigative Note: How This Was Isolated As a Genuinely Separate Bug
Since the pristine source hits the `key` null-deref first (it's dereferenced before `value` in source order), the initial crash always reported the `key` issue (REPORT-13). To confirm `value` was an *independent* second bug rather than the same crash re-appearing, this PoC was tested three ways:
1. Against the pristine source: crashes on `key` (as expected, since `key` is checked first).
2. Against the REPORT-13-patched build (where `key` is now null-safe): the *same* input proceeds past the fixed `key` check and crashes on `value` instead β proving `value` has its own, separate missing guard.
3. Against a build with both fixes applied: the input runs cleanly.
## 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/13.
### 2. Build the execute-level harness (`poc/harness_execute_fuzzer.cpp`, same harness as REPORT-11/12/13, 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_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_value_null.pte` (44,801 bytes, **included in this report β sha256 `eab0858e7b4583e6c94ab96eb652989738d132e4631b07f16c9d0a6d4b1faf3d`**). Found by continued coverage-guided fuzzing immediately after the REPORT-13 fix was applied.
### 4. Trigger the crash
Against the pristine source, this PoC crashes on the `key` field (REPORT-13's bug) β you must apply REPORT-13's fix first to observe this report's `value` crash in isolation, OR simply trust the differential analysis above. To observe both independently:
```bash
export ASAN_OPTIONS="abort_on_error=1:symbolize=0"
export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=0"
# Against pristine source (crashes on key - see REPORT-13):
./poc_harness_pristine -timeout=5 -runs=0 poc/poc_compilespec_value_null.pte
# Against a build with ONLY the key fix applied (crashes on value - this report):
./poc_harness_key_fixed -timeout=5 -runs=0 poc/poc_compilespec_value_null.pte
```
### Actual result β verified on the REPORT-13-patched build (isolating this bug), 3/3
```
Running: poc/poc_compilespec_value_null.pte
runtime/executor/method.cpp:184:XX: runtime error: member call on null pointer of type 'flatbuffers::Vector<unsigned char>'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/method.cpp:184 in
==<pid>== ERROR: libFuzzer: deadly signal
#0 ... (abort machinery)
BackendDelegate::PopulateCompileSpecs (method.cpp)
BackendDelegate::Init (method.cpp)
Method::init (method.cpp:971)
```
**Reproduced 3/3 identical runs.**
## Impact
**Who is affected:** Any application calling `Program::load_method()` on a method whose delegates specify `compile_specs` with a null `value`, where the victim has registered/made available a backend matching the delegate's `id`.
**What the attacker can do:** Cause a deterministic crash during method loading, unconditionally.
**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()
: "";
const auto* spec_value = compile_spec_in_program->value();
compile_specs_list[j].value = {
(spec_value != nullptr)
? static_cast<void*>(const_cast<uint8_t*>(spec_value->Data()))
: nullptr,
(spec_value != nullptr) ? spec_value->size() : 0,
};
```
**Design recommendation:** This is the second of two optional fields in the same loop found unguarded (see REPORT-13 for the first). Both should be validated together at the top of the loop body rather than patched incrementally field-by-field, since ad-hoc patching already missed a sibling field once.
A regression test should build a `.pte` with one `BackendDelegate` (using a registered fake backend) whose `compile_specs` contains an entry with a `key` but no `value`, asserting `Method::load()` returns a clean `Error` rather than crashing. Combine with the REPORT-13 test to cover both fields (key-null, value-null, both-null) in one test matrix.
## Files Included in This Report
- `poc/poc_compilespec_value_null.pte` β the 44,801-byte PoC file (sha256 `eab0858e7b4583e6c94ab96eb652989738d132e4631b07f16c9d0a6d4b1faf3d`)
- `poc/harness_execute_fuzzer.cpp` β the harness used to trigger and reproduce the crash
## 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_value_null.pte` is ready for that upload. Given the close relationship to REPORT-13 (same function, same loop, sibling field), consider whether huntr's triage prefers these filed as one combined report or two separate ones β both are provided as separate, fully-evidenced reports here per this session's one-bug-one-report convention, but the underlying fix is naturally combined.
|