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 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 (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.