# Heap-Buffer-Overflow Read in ExecuTorch TensorLayout::create() via Forced dim_order/sizes Length Equality (.ptd) **Target:** ExecuTorch 1.3.1 (.ptd, huntr Model File Vulnerability program) **Severity:** Medium (memory corruption / out-of-bounds read via malformed model file) **CWE:** CWE-125 (Out-of-Bounds Read) **Component:** `extension/flat_tensor/flat_tensor_data_map.cpp` (caller) / `runtime/core/tensor_layout.cpp` (crash site) **Authentication Required:** No — only requires a victim application to load an attacker-supplied `.ptd` file and call a public API. ## Summary `create_tensor_layout()` constructs two `Span` objects — one for `TensorLayout.sizes`, one for `TensorLayout.dim_order` — using the **same** length value, taken only from `sizes()->size()`, regardless of the real length of the `dim_order` flatbuffer vector. This defeats `TensorLayout::create()`'s own internal safety check (`dim_order.size() != sizes.size()`), because both `Span`s were already forced to report the same length by the caller before that check ever runs. If the real `dim_order` vector is shorter than `sizes`, the subsequent per-element loop reads past the end of `dim_order`'s real heap allocation — a genuine, demonstrated out-of-bounds read, not merely a null-pointer check gap. **Confirmed against the pristine, unmodified ExecuTorch 1.3.1 source**, with a full symbolized crash stack. ## Vulnerability Details `extension/flat_tensor/flat_tensor_data_map.cpp`'s `create_tensor_layout()` (pristine 1.3.1 source): ```cpp Result create_tensor_layout( const flat_tensor_flatbuffer::TensorLayout* tensor_layout) { ScalarType scalar_type = static_cast(tensor_layout->scalar_type()); const int dim = tensor_layout->sizes()->size(); // <-- length taken ONLY from sizes() const auto serialized_sizes = tensor_layout->sizes()->data(); const auto serialized_dim_order = tensor_layout->dim_order()->data(); return TensorLayout::create( Span(serialized_sizes, dim), // sizes Span: length = dim Span(serialized_dim_order, dim), // dim_order Span: length = dim (WRONG - should be dim_order's own length) scalar_type); } ``` `runtime/core/tensor_layout.cpp`'s `TensorLayout::create()`: ```cpp Result TensorLayout::create( Span sizes, Span dim_order, executorch::aten::ScalarType scalar_type) { ... if (dim_order.size() != sizes.size()) { // <-- this check is DEFEATED by construction return Error::InvalidArgument; } for (const auto i : c10::irange(dim_order.size())) { if (dim_order[i] >= sizes.size()) { // <-- crash site: reads dim_order[i] out of bounds return Error::InvalidArgument; } } return TensorLayout(sizes, dim_order, scalar_type, nbytes.get()); } ``` The length-equality check `dim_order.size() != sizes.size()` is a reasonable defensive check **in isolation**, but both `Span` objects passed to it were already constructed by the caller using the *same* `dim` variable. The check is validating a fact the caller already forced to be true — it does not, and cannot, detect that the real underlying `dim_order` flatbuffer vector is shorter than `sizes`. The subsequent loop then reads `dim_order[i]` for `i` up to `sizes.size()`, walking past the end of the real (shorter) `dim_order` allocation. For comparison, the equivalent `.pte` `Tensor` parsing code (`tensor_parser_exec_aten.cpp`) does this correctly — it explicitly checks `s_tensor->dim_order()->size() == dim` (comparing the **real** length of `dim_order()` against `dim`) before ever constructing a `Span` from it, rather than forcing both `Span`s to report the same caller-chosen length. ## Steps to Reproduce ### Environment Linux x86-64, ExecuTorch 1.3.1 pristine source, clang-16, CMake, Ninja. No authentication, no host access. ### 1. Build and harness Identical to REPORT-01 (same build flags, same `poc/harness_flat_tensor_fuzzer.cpp`). ### 2. PoC file `poc/poc_dim_order_length_forgery.ptd` (192 bytes, **included in this report — sha256 `9e9b48e7f22cd3c023a5e78d224a73520cbb4cad0171c72ccf3f589121b0b40`**) contains a `NamedData` entry (`key = "weight0"`) whose `tensor_layout` has a `sizes` vector longer than its real `dim_order` vector. ### 3. 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_dim_order_length_forgery.ptd ``` ### Expected result (secure behavior) `get_tensor_layout("weight0")` should return a clean `Error::InvalidArgument`/`InvalidExternalData` when `sizes` and `dim_order` have genuinely mismatched lengths, since this indicates a malformed tensor description. ### Actual result — verified against the pristine, unmodified ExecuTorch 1.3.1 source ``` Running: poc/poc_dim_order_length_forgery.ptd ================================================================= ====ERROR: AddressSanitizer: heap-buffer-overflow on address 0x510000000200 at pc READ of size 1 at 0x510000000200 thread T0 #0 executorch::runtime::TensorLayout::create(...) runtime/core/tensor_layout.cpp:59 #1 executorch::extension::(anonymous namespace)::create_tensor_layout(...) extension/flat_tensor/flat_tensor_data_map.cpp:104 #2 executorch::extension::FlatTensorDataMap::get_tensor_layout(...) extension/flat_tensor/flat_tensor_data_map.cpp:122 #3 LLVMFuzzerTestOneInput SUMMARY: AddressSanitizer: heap-buffer-overflow ``` The full call chain above was recovered via `addr2line` against the built sanitized binary, confirming the exact crash location predicted by source review: line 59 of `tensor_layout.cpp` (the `dim_order[i] >= sizes.size()` bounds check itself, which reads `dim_order[i]` before the comparison completes). **Reproduced 3/3 identical runs against the pristine source** (re-verified live for this report): ``` run 1: heap-buffer-overflow on address 0x510000000200, READ of size 1 run 2: heap-buffer-overflow on address 0x510000000200, READ of size 1 run 3: heap-buffer-overflow on address 0x510000000200, READ of size 1 ``` ## Impact **Who is affected:** Any application calling `get_tensor_layout()`/`load_data_into()` on a `NamedData` entry whose `tensor_layout.dim_order` is shorter than its `sizes`. **What the attacker can do:** Cause a reliable, deterministic out-of-bounds heap read. Under ASan, this aborts the process (crash/DoS). In an unsanitized production build, this is undefined behavior — a read of adjacent heap memory whose value is compared against `sizes.size()`, potentially influencing subsequent control flow in a way that has not been separately demonstrated to leak information in this report (no secondary information-disclosure PoC was built beyond the OOB read itself — claiming more would be overclaiming). **What's at risk:** Availability, confirmed. Confidentiality is a theoretical secondary risk (the OOB-read byte feeds into a comparison whose outcome could in principle be observable), not demonstrated here. **Why Medium, not Critical:** This is a genuine memory-safety defect — the most severe class of finding identified in this component during this investigation — but only a crash was proven. No controlled write, no demonstrated information leak, no code execution. ## Suggested Remediation ```cpp Result create_tensor_layout( const flat_tensor_flatbuffer::TensorLayout* tensor_layout) { if (tensor_layout == nullptr) { return Error::InvalidExternalData; } ScalarType scalar_type = static_cast(tensor_layout->scalar_type()); if (tensor_layout->sizes() == nullptr || tensor_layout->dim_order() == nullptr) { return Error::InvalidExternalData; } if (tensor_layout->sizes()->size() != tensor_layout->dim_order()->size()) { return Error::InvalidExternalData; } return TensorLayout::create( Span(tensor_layout->sizes()->data(), tensor_layout->sizes()->size()), Span(tensor_layout->dim_order()->data(), tensor_layout->dim_order()->size()), scalar_type); } ``` The key correction: validate the **real** lengths of `sizes()` and `dim_order()` against each other *before* constructing either `Span`, and construct each `Span` using its own field's `->size()` — never a shared variable derived from only one of the two fields. **Design recommendation:** audit all other `Span`-construction call sites in this file for the same forced-length pattern, and consider mirroring `tensor_parser_exec_aten.cpp`'s validation order (explicit `dim_order()->size() == dim` check before any `Span` construction) as the canonical pattern for this file. A regression test should build a `.ptd` `TensorLayout` with `sizes` length 4 and `dim_order` length 1, asserting `get_tensor_layout()` returns a clean `Error` rather than reading out of bounds. ## Files Included in This Report - `poc/poc_dim_order_length_forgery.ptd` — the 192-byte PoC file (sha256 `9e9b48e7f22cd3c023a5e78d224a73520cbb4cad0171c72ccf3f589121b0b40`) - `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_dim_order_length_forgery.ptd` is ready for that upload.