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 Spans 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):
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():
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 Spans 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
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
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 (sha2569e9b48e7f22cd3c023a5e78d224a73520cbb4cad0171c72ccf3f589121b0b40)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.