tsk-arasu's picture
Upload folder using huggingface_hub
0e2a53c verified
|
Raw
History Blame Contribute Delete
9.55 kB
# 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.