File size: 10,098 Bytes
3629a5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# Null Pointer Dereference in ExecuTorch BackendDelegate::GetProcessedData() via Missing BackendDelegate.processed (.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 entry referencing a registered/available backend, while omitting the delegate's `processed` field (a routine deployment configuration, requiring nothing more than one compiled-in hardware-acceleration backend).

## Summary

`BackendDelegate::GetProcessedData()` dereferences `delegate.processed()` β€” the whole optional `BackendDelegateDataReference` field on a `BackendDelegate` table β€” via `->location()`, with no null guard. A `.pte` file with an `ExecutionPlan.delegates` entry whose `id` matches a backend the victim has registered and made available, but which omits the `processed` field entirely, crashes the moment `Method::init()` resolves that delegate β€” before any execution begins, and before compile specs are even examined.

This is the fifth distinct null-pointer-dereference bug found in this session's investigation of ExecuTorch's `BackendDelegate::Init()` delegate-resolution call chain (`method.cpp`), and the *shallowest*: unlike the sibling `CompileSpec.key`/`CompileSpec.value` findings (which require a populated `compile_specs` list) or the `Operator.overload` finding (which requires `EXECUTORCH_ENABLE_LOGGING=ON`), this crash needs only a bare `BackendDelegate.id` field with `processed` omitted β€” no compile specs, no logging flag, no kernel execution.

Confirmed **twice independently**: once against a freshly rebuilt, wholly unmodified pristine 1.3.1 source tree, and once against a separately-patched build (with unrelated compile-spec fixes applied), ruling out any local-patch artifact or coincidental interaction with prior fixes.

## Vulnerability Details

`runtime/executor/method.cpp`'s `BackendDelegate::GetProcessedData()`:

```cpp
static Result<FreeableBuffer> GetProcessedData(
    const executorch_flatbuffer::BackendDelegate& delegate,
    const Program* program) {
  const executorch_flatbuffer::BackendDelegateDataReference* processed =
      delegate.processed();
  switch (processed->location()) {   // <-- crash site: processed may be null
    case executorch_flatbuffer::DataLocation::INLINE: {
      ...
```

`BackendDelegate.processed` is declared as an optional table field in `schema/program.fbs`:

```
table BackendDelegate {
  id: string;
  processed: BackendDelegateDataReference;
  compile_specs: [CompileSpec];
}
```

None of the three fields are marked `required`, so a well-formed `BackendDelegate` entry can legally specify only `id` and omit `processed` entirely.

`GetProcessedData()` is called from `BackendDelegate::Init()`:

```cpp
static Error Init(
    const executorch_flatbuffer::BackendDelegate& delegate,
    const Program* program,
    BackendInitContext& backend_init_context,
    BackendDelegate* out) {
  ...
  BackendInterface* backend = get_backend_class(backend_id);
  ...
  ET_CHECK_OR_RETURN_ERROR(backend->is_available(), ...);

  // Get the delegate data.
  Result<FreeableBuffer> processed_data = GetProcessedData(delegate, program);
```

`GetProcessedData()` is the **first accessor called** in `Init()` after the backend-lookup checks β€” reached unconditionally for every delegate entry, unlike `PopulateCompileSpecs()` which is only invoked when `delegate.compile_specs() != nullptr`. This makes the bug the most directly reachable of the five delegate-resolution null-derefs found in this investigation.

`BackendDelegate::Init()` is in turn called from `Method::init()`'s delegate-resolution loop, reached during ordinary method **loading** (`Program::load_method()`), for any delegate whose registered backend is available (`get_backend_class(id) != nullptr && backend->is_available()`) β€” a standard condition for any deployment that has compiled in and registered at least one hardware-acceleration backend.

## 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

```bash
cmake -B build_asan -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all" \
  -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all" \
  -DEXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON .
cmake --build build_asan -j
```

### 2. Build the execute-level harness (`poc/harness_execute_fuzzer.cpp`, 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_processed_null.pte` (248 bytes, **included in this report β€” sha256 `0377b7f4078dda503e5f2ae3e8b69f8332081f065964a45098fb0d95bba4b36`**) contains:
- One `ExecutionPlan` named `"forward"` with empty values/inputs/outputs/chains/operators
- One `BackendDelegate` entry with `id = "XnnpackBackend"` and **no `processed` field at all**
- The minimal `constant_segment`/`segments` boilerplate required by this build's `ET_ENABLE_DEPRECATED_CONSTANT_BUFFER=0` configuration

Built directly from the project's own `flatc` + `schema/program.fbs`, no manual byte-patching required.

### 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_processed_null.pte
```

### Expected result (secure behavior)
`Method::init()` should return `Error::InvalidProgram` cleanly when a delegate entry omits the `processed` field, rather than crashing while reading its `location`.

### Actual result β€” verified against BOTH an independently rebuilt pristine build AND a separately-patched build

```
runtime/executor/method.cpp:198:24: runtime error: member call on null pointer of type 'executorch_flatbuffer::BackendDelegateDataReference'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/method.cpp:198:24 in
==<pid>== ERROR: libFuzzer: deadly signal
    BackendDelegate::GetProcessedData (method.cpp:198)
    BackendDelegate::Init (method.cpp:97)
    Method::init (method.cpp:971)
    Method::load (method.cpp:862)
```

**Reproduced 3/3 identical runs** against both the pristine rebuild and the separately-patched build:
```
run 1: member call on null pointer of type 'executorch_flatbuffer::BackendDelegateDataReference'
run 2: (identical)
run 3: (identical)
```

## Impact

**Who is affected:** Any application calling `Program::load_method()` on a method whose delegates include an entry that omits the `processed` field, where the victim has registered a backend under a name the attacker can predict (most deployments register at least one real hardware-acceleration backend). This is triggered during **loading**, not execution.

**What the attacker can do:** Cause a deterministic crash during method loading, unconditionally (no build-flag dependency, no compile_specs list required β€” the minimal possible malformed-delegate PoC).

**What's at risk:** Availability only.

**Why not Critical:** Controlled null-pointer dereference, no memory corruption or code execution demonstrated.

## Suggested Remediation

```cpp
static Result<FreeableBuffer> GetProcessedData(
    const executorch_flatbuffer::BackendDelegate& delegate,
    const Program* program) {
  const executorch_flatbuffer::BackendDelegateDataReference* processed =
      delegate.processed();
  ET_CHECK_OR_RETURN_ERROR(
      processed != nullptr,
      InvalidProgram,
      "Missing processed field for backend delegate %s",
      delegate.id() != nullptr ? delegate.id()->c_str() : "<unknown>");
  switch (processed->location()) {
    ...
```

**Design recommendation:** consider validating at `Program::load()` time (in `validate_program()`, `runtime/executor/program_validation.cpp`) that every `ExecutionPlan.delegates[]` entry has a non-null `processed` field β€” catching this, and potentially the sibling `compile_specs` null-key/value issues found in this same investigation, in one centralized pass rather than at each individual accessor deep in `BackendDelegate::Init()`'s call chain.

A regression test should build a `.pte` with one `BackendDelegate` entry (`id` matching a registered fake backend) that omits the `processed` field, asserting `Method::load()` returns a clean `Error::InvalidProgram` rather than crashing.

## Files Included in This Report

- `poc/poc_backend_delegate_processed_null.pte` β€” the 248-byte PoC file (sha256 `0377b7f4078dda503e5f2ae3e8b69f8332081f065964a45098fb0d95bba4b36`)
- `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_processed_null.pte` is ready for that upload.