| # Null Pointer Dereference in ExecuTorch get_named_data() Search Loop via Missing NamedData.key (.ptd) |
|
|
| **Target:** ExecuTorch 1.3.1 (.ptd, huntr Model File Vulnerability program) |
| **Severity:** Low-Medium (Denial of Service) |
| **CWE:** CWE-476 (NULL Pointer Dereference) |
| **Component:** `extension/flat_tensor/flat_tensor_data_map.cpp` |
| **Authentication Required:** No β only requires a victim application to load an attacker-supplied `.ptd` file and call any of three public lookup APIs. |
|
|
| ## Summary |
|
|
| `get_named_data()` β the anonymous-namespace linear-search helper backing `FlatTensorDataMap::get_tensor_layout()`, `get_data()`, and `load_data_into()` β dereferences each `NamedData` entry's `key` field without checking it for null while comparing entries against the requested lookup key. A `.ptd` file containing **any** `NamedData` entry with an omitted `key` crashes the search loop as soon as it reaches that entry, **regardless of which key the caller was actually looking for**. This means a single malformed entry anywhere in the file breaks every subsequent lookup by name, not just lookups for that specific (missing) key. |
|
|
| This is a distinct bug from a previously identified null-deref in `get_key()` (the direct index-based lookup) β confirmed independently below using a harness that never calls `get_key()` at all, ruling out any possibility that this report is merely re-describing that other function's crash. |
|
|
| ## Vulnerability Details |
|
|
| `extension/flat_tensor/flat_tensor_data_map.cpp`'s `get_named_data()` (pristine 1.3.1 source, anonymous namespace, lines 49β66): |
|
|
| ```cpp |
| Result<const flat_tensor_flatbuffer::NamedData*> get_named_data( |
| executorch::aten::string_view key, |
| const flatbuffers::Vector<flatbuffers::Offset<flat_tensor_flatbuffer::NamedData>>* named_data, |
| const flatbuffers::Vector<flatbuffers::Offset<flat_tensor_flatbuffer::DataSegment>>* segments, |
| size_t segment_end_offset) { |
| if (named_data == nullptr) { |
| return Error::NotFound; |
| } |
| for (flatbuffers::uoffset_t i = 0; i < named_data->size(); ++i) { |
| if (key.size() == named_data->Get(i)->key()->size() && // <-- crash site, line 61 |
| std::strncmp( |
| named_data->Get(i)->key()->c_str(), |
| key.data(), |
| named_data->Get(i)->key()->size()) == 0) { |
| const auto* found = named_data->Get(i); |
| ... |
| ``` |
|
|
| The loop iterates **every** element of `named_data` looking for a match against the caller-supplied `key`. If any element β not necessarily the one the caller wants β has a null `key()`, the comparison on line 61 crashes before the loop can move past that element to check the rest. |
|
|
| ## 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-01 Step 1. |
|
|
| ### 2. Build the isolated PoC harness (`poc/harness_isolated_get_named_data.cpp`, included in this report) |
|
|
| This harness deliberately calls **only** `get_tensor_layout()` and `get_data()` for a fixed set of candidate key strings β it never calls `get_key()` or `get_num_keys()` β to prove this bug is independent of the `get_key()` accessor: |
|
|
| ```cpp |
| extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) { |
| // ... load FlatTensorDataMap ... |
| const char* candidates[] = {"weight0", "weight1", "a", "x", ""}; |
| for (const char* k : candidates) { |
| (void)m.get_tensor_layout(executorch::aten::string_view(k, strlen(k))); |
| (void)m.get_data(executorch::aten::string_view(k, strlen(k))); |
| } |
| } |
| ``` |
|
|
| ```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_isolated_get_named_data.cpp -o harness.o |
| |
| clang++-16 -fsanitize=fuzzer,address,undefined -o poc_harness harness.o \ |
| "$ET_BUILD/extension/flat_tensor/libextension_flat_tensor.a" \ |
| "$ET_BUILD/extension/data_loader/libextension_data_loader.a" \ |
| "$ET_BUILD/libexecutorch_core.a" |
| ``` |
|
|
| ### 3. PoC file |
|
|
| `poc/poc_get_named_data_search_null.ptd` (182 bytes, **included in this report β sha256 `9fcfd7bc5d389d0c86e93fc5a847bb5ee537be8ffaa664059d1afbf3e8f97f9c`**) contains a `named_data` vector with at least one `NamedData` entry whose `key` field is omitted. |
|
|
| ### 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_get_named_data_search_null.ptd |
| ``` |
|
|
| ### Expected result (secure behavior) |
| The search loop should skip past a `NamedData` entry with no `key` (or return a clean error), regardless of which key the caller is actually searching for. |
|
|
| ### Actual result β verified in isolation against the pristine, unmodified ExecuTorch 1.3.1 source |
|
|
| ``` |
| Running: poc/poc_get_named_data_search_null.ptd |
| extension/flat_tensor/flat_tensor_data_map.cpp:61:50: runtime error: member call on null pointer of type 'flatbuffers::Vector<char>' |
| SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior extension/flat_tensor/flat_tensor_data_map.cpp:61:50 in |
| ==<pid>== ERROR: libFuzzer: deadly signal |
| ``` |
|
|
| **Reproduced 3/3 identical runs, in isolation** (harness never calls `get_key()`; re-verified live for this report against the pristine source): |
| ``` |
| run 1: extension/flat_tensor/flat_tensor_data_map.cpp:61:50: runtime error: member call on null pointer of type 'flatbuffers::Vector<char>' |
| run 2: (identical) |
| run 3: (identical) |
| ``` |
|
|
| This isolation test is the key evidence distinguishing this finding from a previously-identified crash in `get_key()`'s direct-index lookup (a different function reading the same `NamedData.key` field): the crash reproduces here even when `get_key()` is never called at all. |
|
|
| ## Impact |
|
|
| **Who is affected:** Any application calling `get_tensor_layout()`, `get_data()`, or `load_data_into()` β three separate public lookup APIs, all backed by this one search function. |
|
|
| **What the attacker can do:** Cause a deterministic crash on **any** name lookup by placing a single malformed entry anywhere in the `named_data` vector. This is broader in effect than a bug that only affects a specific key β a victim application would see every subsequent tensor/blob lookup fail with a crash, not just a lookup for the missing entry, since the search must linearly scan past it for any query. |
|
|
| **What's at risk:** Availability only. |
|
|
| **Why not Critical:** Controlled null-pointer dereference, no memory corruption or code execution. |
|
|
| ## Suggested Remediation |
|
|
| ```cpp |
| for (flatbuffers::uoffset_t i = 0; i < named_data->size(); ++i) { |
| const auto* entry = named_data->Get(i); |
| if (entry == nullptr || entry->key() == nullptr) { |
| continue; // skip malformed entries rather than crashing |
| } |
| if (key.size() == entry->key()->size() && |
| std::strncmp(entry->key()->c_str(), key.data(), entry->key()->size()) == 0) { |
| const auto* found = entry; |
| ... |
| ``` |
|
|
| **Design recommendation:** this is the third function in this file found to independently need this exact `NamedData.key` null check (alongside `get_key()`). A single shared helper (e.g. `is_valid_named_data_entry(const NamedData*)`) used by every accessor that reads `NamedData` elements would close this class of bugs at the source rather than requiring per-function patches as new accessors are added or fuzzed. |
|
|
| A regression test should build a `.ptd` with one `NamedData` entry having `key` omitted and a second entry with a real key, then call `get_tensor_layout()`/`get_data()`/`load_data_into()` for the second (valid) key, asserting the lookup succeeds (or fails cleanly) rather than crashing while scanning past the first, invalid entry. |
|
|
| ## Files Included in This Report |
|
|
| - `poc/poc_get_named_data_search_null.ptd` β the 182-byte PoC file (sha256 `9fcfd7bc5d389d0c86e93fc5a847bb5ee537be8ffaa664059d1afbf3e8f97f9c`) |
| - `poc/harness_isolated_get_named_data.cpp` β the isolated harness proving this bug independent of `get_key()` |
|
|
| ## 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_get_named_data_search_null.ptd` is ready for that upload. |
|
|