| # Null Pointer Dereference in ExecuTorch create_tensor_layout() via Missing TensorLayout.sizes (.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 API. |
|
|
| ## Summary |
|
|
| `NamedData.tensor_layout` (a `TensorLayout` table) can be present in a `.ptd` file while its own `sizes` field (and/or `dim_order`) is itself omitted β `sizes: [int32]` and `dim_order: [uint8]` are both ordinary, non-required fields in the schema. `create_tensor_layout()` dereferences `tensor_layout->sizes()->size()` without checking `sizes()` for null. This is a distinct, deeper defect than a simple "is `tensor_layout` itself present" check: a `NamedData` entry can have a fully non-null `tensor_layout` table and still crash the parser, because the crash is one level further down in the field hierarchy. |
|
|
| **Confirmed against the pristine, unmodified ExecuTorch 1.3.1 source** β this is a real defect in the shipped code, not an artifact of any local investigation tooling. |
|
|
| ## Vulnerability Details |
|
|
| `extension/flat_tensor/serialize/flat_tensor.fbs`: |
|
|
| ``` |
| table TensorLayout { |
| scalar_type: executorch_flatbuffer.ScalarType; |
| sizes: [int32]; // not required |
| dim_order: [uint8]; // not required |
| } |
| ``` |
|
|
| `extension/flat_tensor/flat_tensor_data_map.cpp` (pristine 1.3.1 source, lines 97β108): |
|
|
| ```cpp |
| Result<const TensorLayout> create_tensor_layout( |
| const flat_tensor_flatbuffer::TensorLayout* tensor_layout) { |
| ScalarType scalar_type = |
| static_cast<ScalarType>(tensor_layout->scalar_type()); |
| const int dim = tensor_layout->sizes()->size(); // <-- crash site: sizes() may be null |
| const auto serialized_sizes = tensor_layout->sizes()->data(); |
| const auto serialized_dim_order = tensor_layout->dim_order()->data(); |
| return TensorLayout::create( |
| Span<const int32_t>(serialized_sizes, dim), |
| Span<const uint8_t>(serialized_dim_order, dim), |
| scalar_type); |
| } |
| ``` |
|
|
| Note: this is the *unmodified upstream line numbering* (line 101 in the actual shipped 1.3.1 source tree; some of this report's supporting artifacts were produced against a locally-patched copy for unrelated reasons β see the Verification section below for the pristine-source confirmation). |
|
|
| `get_named_data()`'s validation (`segment_index` bounds, segment offset/size overflow checks) never inspects the internal fields of `tensor_layout` β it only validates the `NamedData` entry's own `segment_index`. No code path β not `get_named_data()`, not `create_tensor_layout()`, not `TensorLayout::create()` in `runtime/core/tensor_layout.cpp` β checks whether `tensor_layout->sizes()` or `tensor_layout->dim_order()` are non-null before they are used. |
|
|
| ## Steps to Reproduce |
|
|
| ### Environment |
| Same sanitized build and harness as REPORT-01 (identical `poc/harness_flat_tensor_fuzzer.cpp`, identical build commands against the **pristine, unmodified** ExecuTorch 1.3.1 source tree). |
|
|
| ### 1. PoC file |
|
|
| `poc/poc_tensorlayout_sizes_null.ptd` (272 bytes, **included in this report β sha256 `f9c20d5697f9d147938eb31b188a14f82767252af47f5075358b7bc98c802476`**) is a well-formed `.ptd` file with: |
| - One `NamedData` entry: `key = "weight1"`, `segment_index = 0` |
| - `tensor_layout` **present** (non-null), with `scalar_type` set to a valid value |
| - `tensor_layout.sizes` **omitted** |
|
|
| This exact file was found via coverage-guided fuzzing and is included verbatim β it is the precise reproducer verified below, not a re-derived approximation. |
|
|
| > **Note on `poc/gen_poc.py`:** a deterministic regeneration script is included for transparency and to demonstrate the minimal JSON shape that triggers this bug, but flatc's table-encoding is not guaranteed to be byte-identical across independently-constructed JSON inputs that describe the same logical structure. The regenerated file **triggers the identical crash** (`flat_tensor_data_map.cpp:101:43`, same `runtime error: member call on null pointer of type 'flatbuffers::Vector<int>'`) but is not byte-for-byte identical to the included PoC. **Use the included `poc_tensorlayout_sizes_null.ptd` file directly for exact reproduction**; use `gen_poc.py` only to understand or reconstruct the minimal triggering structure. |
| |
| ### 2. Build the harness and trigger the crash |
| |
| ```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" |
| |
| 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_tensorlayout_sizes_null.ptd |
| ``` |
| |
| ### Expected result (secure behavior) |
| `get_tensor_layout("weight1")` should return a clean `Error::InvalidExternalData`, since a `TensorLayout` with no `sizes` cannot describe a valid tensor shape. |
| |
| ### Actual result β verified against the pristine (unmodified) ExecuTorch 1.3.1 source |
| |
| ``` |
| Running: poc/poc_tensorlayout_sizes_null.ptd |
| extension/flat_tensor/flat_tensor_data_map.cpp:101:43: runtime error: member call on null pointer of type 'flatbuffers::Vector<int>' |
| SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior extension/flat_tensor/flat_tensor_data_map.cpp:101:43 in |
| ==<pid>== ERROR: libFuzzer: deadly signal |
| ``` |
| |
| **Reproduced 3/3 identical runs against the pristine, unmodified source tree** (re-verified live for this report, not against any locally-modified copy): |
| ``` |
| run 1: extension/flat_tensor/flat_tensor_data_map.cpp:101:43: runtime error: member call on null pointer of type 'flatbuffers::Vector<int>' |
| run 2: (identical) |
| run 3: (identical) |
| ``` |
| |
| ## Impact |
| |
| **Who is affected:** Any application calling `FlatTensorDataMap::get_tensor_layout()` or `load_data_into()` on a `NamedData` entry whose `tensor_layout` is present but incomplete. |
|
|
| **What the attacker can do:** Cause a deterministic crash with a `.ptd` file that appears more "valid" than the minimal ET-NEW-001 case (it has a non-null `tensor_layout` table with a set `scalar_type`) β meaning a shallow "is `tensor_layout` null" check alone is insufficient defense; the internal fields of that table must also be validated. |
|
|
| **What's at risk:** Availability only. No memory corruption or code execution. |
|
|
| **Why not Critical:** Same reasoning as the other null-pointer-dereference findings in this codebase β a controlled crash, not a memory-safety violation. |
|
|
| ## Suggested Remediation |
|
|
| In `create_tensor_layout()`, after checking `tensor_layout` itself for null, additionally check its `sizes` and `dim_order` fields: |
|
|
| ```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()); |
| if (tensor_layout->sizes() == nullptr || tensor_layout->dim_order() == nullptr) { |
| return Error::InvalidExternalData; |
| } |
| const int dim = tensor_layout->sizes()->size(); |
| const auto serialized_sizes = tensor_layout->sizes()->data(); |
| const auto serialized_dim_order = tensor_layout->dim_order()->data(); |
| return TensorLayout::create( |
| Span<const int32_t>(serialized_sizes, dim), |
| Span<const uint8_t>(serialized_dim_order, dim), |
| scalar_type); |
| } |
| ``` |
|
|
| A regression test should build a `.ptd` `NamedData.tensor_layout` with `sizes`/`dim_order` omitted and assert a clean `Error` return. |
|
|
| ## Files Included in This Report |
|
|
| - `poc/poc_tensorlayout_sizes_null.ptd` β the exact 272-byte PoC file used for all verification in this report (sha256 `f9c20d5697f9d147938eb31b188a14f82767252af47f5075358b7bc98c802476`) |
| - `poc/gen_poc.py` β illustrative regeneration script (produces an equivalent-but-not-byte-identical file that triggers the same crash line β see note above) |
| - `poc/harness_flat_tensor_fuzzer.cpp` β the harness used to trigger and reproduce the crash |
|
|
| ## 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_tensorlayout_sizes_null.ptd` is ready for that upload. |
|
|