| # Null Pointer Dereference in ExecuTorch FlatTensorDataMap via Missing tensor_layout Field (.ptd) |
| |
| **Target:** ExecuTorch 1.3.1 (.pte / .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 β the only requirement is that a victim application loads an attacker-supplied `.ptd` file. |
| |
| ## Summary |
| |
| ExecuTorch's `.ptd` (FlatTensor) format lets a `NamedData` entry optionally carry a `tensor_layout` field describing the shape of a tensor blob. The schema comment explicitly documents this field as optional ("if the underlying data is a tensor, store layout information"), but `create_tensor_layout()` in `extension/flat_tensor/flat_tensor_data_map.cpp` dereferences this pointer unconditionally. A `.ptd` file with a well-formed `NamedData` entry that simply omits `tensor_layout` causes a null-pointer dereference and crashes any process that calls `FlatTensorDataMap::get_tensor_layout()` or `FlatTensorDataMap::load_data_into()` on that entry's key. |
| |
| This is reachable both directly (an application using the `extension/flat_tensor` public API) and through the ordinary `.pte` + `.ptd` loading flow, since `runtime/executor/tensor_parser_exec_aten.cpp`'s `getTensorDataPtr()` calls `get_tensor_layout()` when resolving a mutable external tensor referenced by fully-qualified name. |
| |
| ## Vulnerability Details |
| |
| `extension/flat_tensor/serialize/flat_tensor.fbs` defines: |
| |
| ``` |
| table NamedData { |
| key: string; |
| segment_index: uint32; |
| // Optional: if the underlying data is a tensor, store layout information. |
| tensor_layout: TensorLayout; |
| } |
| ``` |
| |
| `extension/flat_tensor/flat_tensor_data_map.cpp` then does: |
| |
| ```cpp |
| Result<const TensorLayout> create_tensor_layout( |
| const flat_tensor_flatbuffer::TensorLayout* tensor_layout) { |
| ScalarType scalar_type = |
| static_cast<ScalarType>(tensor_layout->scalar_type()); // <-- no null check |
| const int dim = tensor_layout->sizes()->size(); |
| ... |
| } |
| ``` |
| |
| Both public callers pass `named_data.get()->tensor_layout()` straight into this function with no null check: |
| |
| ```cpp |
| Result<const TensorLayout> FlatTensorDataMap::get_tensor_layout(string_view key) const { |
| ... |
| return create_tensor_layout(named_data.get()->tensor_layout()); |
| } |
|
|
| Error FlatTensorDataMap::load_data_into(string_view key, void* buffer, size_t size) const { |
| ... |
| Result<const TensorLayout> tensor_layout = |
| create_tensor_layout(named_data.get()->tensor_layout()); |
| ... |
| } |
| ``` |
| |
| flatbuffers returns `nullptr` from a table accessor when the corresponding field was not serialized. Since `tensor_layout` is legitimately optional, a well-formed `.ptd` can have a `NamedData` entry with `key` and `segment_index` set but `tensor_layout` entirely absent. `FlatTensorDataMap::load()`'s own validation (magic check, size check, alignment check, top-level `named_data() != nullptr` / `segments() != nullptr` checks) never inspects the internal fields of individual `NamedData` entries, so this file loads successfully. The crash only occurs on the subsequent `get_tensor_layout()` / `load_data_into()` call for that specific key. |
| |
| `get_data()` is **not** affected β it never touches `tensor_layout`. |
| |
| ## Steps to Reproduce |
| |
| ### Environment |
| - Linux x86-64, ExecuTorch 1.3.1 source (as distributed) |
| - clang-16 / clang++-16, CMake, Ninja |
| - No authentication, no host access, no prior state needed β only the ability to supply a `.ptd` file to a process that loads it |
| |
| ### 1. Build ExecuTorch with sanitizers (proves memory-safety class, not required to observe the crash in a release build β the underlying dereference is unconditional in all build types) |
| |
| ```bash |
| export ET_SRC=/path/to/executorch-1.3.1 |
| export ET_BUILD=/path/to/build-asan |
| |
| cmake -S "$ET_SRC" -B "$ET_BUILD" \ |
| -DCMAKE_BUILD_TYPE=RelWithDebInfo \ |
| -DCMAKE_C_COMPILER=clang-16 -DCMAKE_CXX_COMPILER=clang++-16 \ |
| -DEXECUTORCH_ENABLE_PROGRAM_VERIFICATION=ON \ |
| -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all" \ |
| -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all" \ |
| -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" \ |
| -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address,undefined" \ |
| -DEXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON \ |
| -DEXECUTORCH_BUILD_EXTENSION_DATA_LOADER=ON \ |
| -DEXECUTORCH_BUILD_TESTS=OFF -DEXECUTORCH_BUILD_PYBIND=OFF \ |
| -DEXECUTORCH_BUILD_XNNPACK=OFF -DEXECUTORCH_BUILD_CPUINFO=OFF \ |
| -DEXECUTORCH_BUILD_PTHREADPOOL=OFF -DEXECUTORCH_BUILD_EXECUTOR_RUNNER=OFF \ |
| -G Ninja |
| |
| ninja -C "$ET_BUILD" -j8 executorch_core extension_flat_tensor extension_data_loader |
| ``` |
| |
| > Note: the repo must be checked out into a directory literally named `executorch` (a known upstream constraint β see https://github.com/pytorch/executorch/issues/6475). |
| |
| ### 2. Build the PoC harness (`poc/harness_flat_tensor_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_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" |
| ``` |
|
|
| The harness calls exactly the real public API surface: `FlatTensorDataMap::load()` β `get_num_keys()` β `get_key()` β `get_tensor_layout()` β `get_data()`, matching how a real application would enumerate and inspect a `.ptd` file. |
|
|
| ### 3. Generate the PoC .ptd file |
|
|
| ```bash |
| python3 poc/gen_poc.py "$ET_SRC/extension/flat_tensor/serialize" "$ET_BUILD/third-party/flatc_ep/bin/flatc" |
| ``` |
|
|
| This produces `poc/poc_null_tensor_layout.ptd` (272 bytes, **included in this report β sha256 `9205a89b772c395a93ea8cbb2c364d5eac8941b5b3ccf1a4341a4cddb3956d29`**), a well-formed FlatTensor file with: |
| - Valid `FH01` extended header (magic, offsets, sizes all self-consistent) |
| - One `NamedData` entry: `key = "weight_no_layout"`, `segment_index = 0`, **`tensor_layout` omitted** |
| - One `DataSegment` of 16 bytes, backed by 16 bytes of real segment data |
| |
| ### 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_null_tensor_layout.ptd |
| ``` |
| |
| ### Expected result (secure behavior) |
| `FlatTensorDataMap::get_tensor_layout("weight_no_layout")` should return a clean `Error` (e.g. `InvalidExternalData`), since the file is malformed for the purpose of retrieving a layout. |
| |
| ### Actual result |
| |
| ``` |
| Running: poc/poc_null_tensor_layout.ptd |
| extension/flat_tensor/flat_tensor_data_map.cpp:100:46: runtime error: member call on null pointer of type 'flat_tensor_flatbuffer::TensorLayout' |
| SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior extension/flat_tensor/flat_tensor_data_map.cpp:100:46 in |
| ==<pid>== ERROR: libFuzzer: deadly signal |
| #0 ... (abort) |
| #1 ... TensorLayout* accessor returning null, dereferenced |
| #2 ... create_tensor_layout(...) |
| #3 ... FlatTensorDataMap::get_tensor_layout(...) |
| #4 ... LLVMFuzzerTestOneInput |
| ``` |
| |
| The process terminates via `SIGABRT` (UBSan trap under sanitizers; a debug/release build without sanitizers dereferences a near-null pointer through the flatbuffers vtable mechanism, producing undefined behavior β typically `SIGSEGV`). |
| |
| **Reproduced 3/3 identical runs** with the exact same PoC file and command. |
| |
| ## Real-World Reachability (beyond the direct API) |
| |
| This bug is not limited to applications calling the `extension/flat_tensor` API directly. `runtime/executor/tensor_parser_exec_aten.cpp`'s `getTensorDataPtr()` β part of the ordinary tensor-deserialization path used every time a `.pte` program is loaded β calls: |
| |
| ```cpp |
| Result<const TensorLayout> tensor_layout_res = named_data_map->get_tensor_layout(fqn); |
| ``` |
| |
| when resolving a **mutable external tensor** referenced by fully-qualified name. This means the bug is also triggerable through the standard `.pte` + `.ptd` loading flow: any time a `.pte` program references an external mutable tensor by name, and the paired `.ptd`'s `NamedData` entry for that name omits `tensor_layout`, the crash occurs during normal method loading β no direct call to the `extension/flat_tensor` API is required by the victim application at all. |
| |
| ## Impact |
| |
| **Who is affected:** Any application or deployment pipeline that embeds ExecuTorch's `extension/flat_tensor` component (built with `EXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON`, the standard configuration for external-weights / merged-data-map deployments) and loads externally-supplied `.ptd` files β either directly, or indirectly via a `.pte` program that references external mutable tensors by name. |
| |
| **What the attacker can do:** Cause a deterministic, repeatable crash (denial of service) in the victim process simply by supplying a malformed-but-schema-legal `.ptd` file. No data is disclosed, no memory is corrupted, and no code executes β the process cleanly terminates via an unhandled null-pointer dereference. |
|
|
| **What's at risk:** Availability of the process loading the file. In an on-device inference pipeline (the primary ExecuTorch deployment model β mobile/edge inference), this means an attacker who can supply a model's external weight file (e.g. via a compromised CDN, a malicious app update, or any untrusted-file-intake pipeline) can reliably crash the inference process on demand. |
|
|
| **Exploitation complexity:** No interaction required beyond the victim loading the file β no click, no auth, no prior session. This is a pure file-format parsing bug, matching huntr's Model File Vulnerability program's explicitly listed "Denial of Service (DoS) attacks through malformed model files" category. |
|
|
| **Why this is NOT rated Critical:** The vulnerability is a controlled null-pointer table-accessor dereference, not an out-of-bounds read/write, integer overflow, or type confusion. No arbitrary code execution, memory disclosure, or persistent state corruption is demonstrated or plausible from this specific defect in isolation. This report deliberately does not inherit any higher severity label from prior automated static-analysis passes over this codebase β those labels were not independently verified and are not a reliable signal of actual impact. |
|
|
| ## Suggested Remediation |
|
|
| Add a null check on `tensor_layout` before use. Either: |
|
|
| 1. **At the point of use** (minimal, localized fix) β in `create_tensor_layout()`: |
| ```cpp |
| Result<const TensorLayout> create_tensor_layout( |
| const flat_tensor_flatbuffer::TensorLayout* tensor_layout) { |
| if (tensor_layout == nullptr) { |
| return Error::InvalidExternalData; |
| } |
| ScalarType scalar_type = static_cast<ScalarType>(tensor_layout->scalar_type()); |
| ... |
| ``` |
|
|
| 2. **Centralized** (more robust) β extend `get_named_data()`'s existing validation block to reject (or flag) `NamedData` entries whose `tensor_layout` is null when the calling context requires layout information, so future callers of `tensor_layout()` inherit the protection automatically. |
|
|
| A regression test should build a `.ptd` with a `NamedData` entry lacking `tensor_layout` and assert that `get_tensor_layout()` / `load_data_into()` return a clean `Error` rather than crashing. |
|
|
| ## Files Included in This Report |
|
|
| - `poc/poc_null_tensor_layout.ptd` β the 272-byte PoC file (sha256 `9205a89b772c395a93ea8cbb2c364d5eac8941b5b3ccf1a4341a4cddb3956d29`) |
| - `poc/gen_poc.py` β deterministic script to regenerate the exact same PoC file from ExecuTorch's own `flat_tensor.fbs` schema |
| - `poc/harness_flat_tensor_fuzzer.cpp` β the exact harness used to trigger and reproduce the crash against the real public API |
|
|
| ## huntr Submission Note |
|
|
| Per the huntr MFV program's submission requirements, this PoC needs to be uploaded to a public HuggingFace repository before filing (huntr requires a HuggingFace-hosted PoC link, not a local file attachment). `poc/poc_null_tensor_layout.ptd` in this folder is ready for that upload. |
|
|