File size: 9,545 Bytes
0e2a53c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | # 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<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(); // <-- 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<const int32_t>(serialized_sizes, dim), // sizes Span: length = dim
Span<const uint8_t>(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<const TensorLayout> TensorLayout::create(
Span<const int32_t> sizes,
Span<const uint8_t> 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
=================================================================
==<pid>==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x510000000200 at pc <addr>
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<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;
}
if (tensor_layout->sizes()->size() != tensor_layout->dim_order()->size()) {
return Error::InvalidExternalData;
}
return TensorLayout::create(
Span<const int32_t>(tensor_layout->sizes()->data(), tensor_layout->sizes()->size()),
Span<const uint8_t>(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.
|