tsk-arasu's picture
Upload folder using huggingface_hub
3bfbf53 verified
|
Raw
History Blame Contribute Delete
11.5 kB
# 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.