# 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>' SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/method_meta.cpp:406:37 in ==== 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' ``` 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.