| # Null Pointer Dereference in ExecuTorch FlatTensorDataMap::get_key() 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 a public enumeration API. |
| |
| ## Summary |
| |
| `FlatTensorDataMap::get_key()` is a public API for enumerating the names of tensors/blobs stored in a `.ptd` file by index β the standard pattern for an application to discover what data a file contains before requesting it by name. It dereferences `NamedData.key` without checking it for null, even though `key` is an ordinary (non-required) field in the schema. A `.ptd` file with a `NamedData` entry present in the `named_data` vector (so it is counted by `get_num_keys()`) but whose own `key` field is omitted crashes any process that enumerates keys by index β a completely standard, expected usage pattern, not an edge case. |
| |
| **Confirmed against the pristine, unmodified ExecuTorch 1.3.1 source.** |
| |
| ## Vulnerability Details |
| |
| `extension/flat_tensor/serialize/flat_tensor.fbs`: |
| |
| ``` |
| table NamedData { |
| key: string; // not required |
| segment_index: uint32; |
| tensor_layout: TensorLayout; |
| } |
| ``` |
| |
| `extension/flat_tensor/flat_tensor_data_map.cpp` (pristine 1.3.1 source, lines 197β211): |
| |
| ```cpp |
| ET_NODISCARD Result<uint32_t> FlatTensorDataMap::get_num_keys() const { |
| return flat_tensor_->named_data()->size(); |
| } |
| |
| ET_NODISCARD Result<const char*> FlatTensorDataMap::get_key( |
| uint32_t index) const { |
| uint32_t num_keys = get_num_keys().get(); |
| ET_CHECK_OR_RETURN_ERROR( |
| index >= 0 && index < num_keys, |
| InvalidArgument, |
| "Index %u out of range of size %u", |
| index, |
| num_keys); |
| return flat_tensor_->named_data()->Get(index)->key()->c_str(); // <-- crash site, line 201 (offset within function) |
| } |
| ``` |
| |
| `FlatTensorDataMap::load()`'s validation only checks that the `named_data` **vector itself** is non-null at the top level β it never inspects the fields of individual `NamedData` elements. `get_named_data()` (the linear-search helper backing `get_tensor_layout()`/`get_data()`/`load_data_into()`) happens to implicitly avoid crashing on a null `key` in some cases due to how its comparison is structured, but `get_key()` is a **completely separate, simpler code path** that enumerates by index directly β it does not go through `get_named_data()` at all, so it inherits none of that incidental behavior. |
|
|
| ## 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 (`EXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON`). |
|
|
| ### 2. Build the harness (`poc/harness_flat_tensor_fuzzer.cpp`, included β identical to REPORT-01's harness, which already calls `get_key()` for every index up to `get_num_keys()`) |
|
|
| ```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_flat_tensor_fuzzer.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_key_null.ptd` (272 bytes, **included in this report β sha256 `427bc13020be820b41aafec2c6e00de360fe97185d3879d5ec770594c21ac39`**) 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_key_null.ptd |
| ``` |
|
|
| ### Expected result (secure behavior) |
| `get_key()` should return a clean `Error` (e.g. `InvalidExternalData`) for an entry with no `key`, rather than crashing. |
|
|
| ### Actual result β verified against the pristine, unmodified ExecuTorch 1.3.1 source |
|
|
| ``` |
| Running: poc/poc_get_key_null.ptd |
| extension/flat_tensor/flat_tensor_data_map.cpp:201:57: runtime error: member call on null pointer of type 'flatbuffers::String' |
| SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior extension/flat_tensor/flat_tensor_data_map.cpp:201:57 in |
| ==<pid>== ERROR: libFuzzer: deadly signal |
| ``` |
|
|
| **Reproduced 3/3 identical runs against the pristine source** (re-verified live for this report): |
| ``` |
| run 1: extension/flat_tensor/flat_tensor_data_map.cpp:201:57: runtime error: member call on null pointer of type 'flatbuffers::String' |
| run 2: (identical) |
| run 3: (identical) |
| ``` |
|
|
| ## Impact |
|
|
| **Who is affected:** Any application enumerating tensor/blob names in a `.ptd` file via `get_num_keys()` + `get_key()` β the standard discovery pattern for this format. |
|
|
| **What the attacker can do:** Cause a deterministic crash simply by having one entry in the `named_data` vector with no `key` β the file still loads successfully (the vector itself is present), so the crash only surfaces during ordinary enumeration. |
|
|
| **What's at risk:** Availability only. |
|
|
| **Why not Critical:** Controlled null-pointer dereference, no memory corruption or code execution. |
|
|
| ## Suggested Remediation |
|
|
| ```cpp |
| ET_NODISCARD Result<const char*> FlatTensorDataMap::get_key( |
| uint32_t index) const { |
| uint32_t num_keys = get_num_keys().get(); |
| ET_CHECK_OR_RETURN_ERROR( |
| index >= 0 && index < num_keys, |
| InvalidArgument, |
| "Index %u out of range of size %u", |
| index, |
| num_keys); |
| auto* named_data = flat_tensor_->named_data()->Get(index); |
| ET_CHECK_OR_RETURN_ERROR( |
| named_data != nullptr && named_data->key() != nullptr, |
| InvalidExternalData, |
| "NamedData entry %u or its key is null", |
| index); |
| return named_data->key()->c_str(); |
| } |
| ``` |
|
|
| **Design recommendation:** this is one of several findings in this file (see companion reports on `get_tensor_layout()`/`create_tensor_layout()`) where the file validates the top-level presence of `named_data`/`segments` vectors at `load()` time but never validates the fields within each element. A single validation pass over `named_data()` at load time β or a shared per-entry validator called from every accessor β would close this entire class of findings at once rather than requiring a separate patch per accessor as each is independently discovered. |
|
|
| A regression test should build a `.ptd` with a `NamedData` entry that has `key` omitted, asserting `get_key()` returns a clean `Error` rather than crashing. |
|
|
| ## Files Included in This Report |
|
|
| - `poc/poc_get_key_null.ptd` β the 272-byte PoC file (sha256 `427bc13020be820b41aafec2c6e00de360fe97185d3879d5ec770594c21ac39`) |
| - `poc/harness_flat_tensor_fuzzer.cpp` β the harness used to trigger and reproduce the crash (same binary as REPORT-01/02/04, which all target `extension/flat_tensor/flat_tensor_data_map.cpp`) |
|
|
| ## 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_key_null.ptd` is ready for that upload. |
|
|