tsk-arasu's picture
Upload folder using huggingface_hub
a00553a verified
|
Raw
History Blame Contribute Delete
11.3 kB

Signed Integer Overflow in ExecuTorch Tensor Stride Computation via Unchecked Size Product (.pte)

Target: ExecuTorch 1.3.1 (.pte, huntr Model File Vulnerability program) Severity: Medium (integer overflow with plausible but unproven downstream memory-corruption chain; DoS confirmed) CWE: CWE-190 (Integer Overflow or Wraparound) Component: runtime/core/exec_aten/util/dim_order_util.h (overflow site) / runtime/executor/tensor_parser_portable.cpp (caller) Authentication Required: No β€” only requires a victim application to load an attacker-supplied .pte file via the standard, always-used Program::load_method() API.

Summary

ExecuTorch computes a tensor's memory strides from its sizes and dim_order fields during ordinary .pte tensor deserialization. The stride computation multiplies a running product of dimension sizes with no overflow check. A .pte file with a Tensor value whose sizes contains a large-but-individually-valid dimension (passing the existing "no negative sizes" check) causes this multiplication to overflow a 32-bit signed integer β€” Undefined Behavior in C++, caught deterministically by UBSan. This is the first finding across a broader security review of this codebase reached through the core Method::init() deserialization path (not a metadata-only accessor), and the first integer-overflow (rather than null-pointer-dereference) bug class identified.

huntr's own Model File Vulnerability program explicitly lists "integer overflows" as an example of "vulnerabilities in model file parsing leading to memory corruption" β€” this finding is a direct, verbatim match for that named category.

Vulnerability Details

runtime/executor/tensor_parser_portable.cpp's parseTensor() validates that no individual size is negative:

for (flatbuffers::uoffset_t i = 0; i < dim; i++) {
  ET_CHECK_OR_RETURN_ERROR(
      sizes[i] >= 0,
      InvalidProgram,
      "Negative size[%zu] %" PRId32,
      static_cast<size_t>(i),
      sizes[i]);
}

This check does not bound the product of sizes against the 32-bit signed integer type used for strides. The tensor's strides are subsequently computed via dim_order_to_stride() β†’ dim_order_to_stride_nocheck() in runtime/core/exec_aten/util/dim_order_util.h:

template <typename SizesType, typename DimOrderType, typename StridesType>
inline void dim_order_to_stride_nocheck(
    const SizesType* sizes,
    const DimOrderType* dim_order,
    const size_t dims,
    StridesType* strides) {
  if (dims == 0) return;
  strides[dim_order[dims - 1]] = 1;
  for (int32_t i = dims - 2; i >= 0; --i) {
    if (sizes[dim_order[i + 1]] == 0) {
      strides[dim_order[i]] = strides[dim_order[i + 1]];
    } else {
      strides[dim_order[i]] =
          strides[dim_order[i + 1]] * sizes[dim_order[i + 1]];   // <-- unchecked multiplication, line 147
    }
  }
}

StridesType/SizesType default to int32_t. This multiplication has no overflow check, unlike the analogous computation in runtime/core/tensor_layout.cpp's calculate_nbytes() (used by the parallel .ptd TensorLayout code path), which explicitly calls c10::mul_overflows() before trusting the product. The .pte Tensor stride-computation path has no equivalent protection.

Steps to Reproduce

Environment

Linux x86-64, ExecuTorch 1.3.1 pristine source, clang-16, CMake, Ninja. No authentication, no host access.

1. Build ExecuTorch with sanitizers

Same build as REPORT-01 Step 1.

2. Build the load_method-level harness (poc/harness_load_method_fuzzer.cpp, included in this report)

This is a new harness (not used in prior reports) that goes one layer deeper than metadata-only accessors β€” it calls Program::load_method(), exercising Method::init() β†’ parse_values() β†’ parseTensor(), the CORE tensor-deserialization path used every time a .pte is loaded:

export ET_PARENT=/path/to/parent-of-executorch
C10_INC="$ET_SRC/runtime/core/portable_type/c10"
INCLUDES="-I$ET_PARENT -I$ET_BUILD -I$ET_BUILD/schema/include -I$ET_BUILD/extension/flat_tensor/include -I$ET_BUILD/third-party/flatc_ep/include -I$C10_INC"

clang++-16 -std=c++17 -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all \
  $INCLUDES -DFLATBUFFERS_MAX_ALIGNMENT=1024 -DC10_USING_CUSTOM_GENERATED_MACROS \
  -c poc/harness_load_method_fuzzer.cpp -o harness.o

clang++-16 -fsanitize=fuzzer,address,undefined -o poc_harness harness.o \
  "$ET_BUILD/extension/data_loader/libextension_data_loader.a" \
  "$ET_BUILD/libexecutorch_core.a"

3. PoC file

poc/poc_stride_int_overflow.pte (24,054 bytes, included in this report β€” sha256 0767af28fc00e63e8ea665beaf5139f7d84332abc435bd8e0de3ebf8d0936e98) is a .pte file found via coverage-guided fuzzing (within ~10,000 executions) containing a Tensor value whose sizes array produces a stride-computation overflow. The libFuzzer crash minimizer was unable to reduce this file below its original size while preserving the crash, so the original fuzzer-found file is the canonical reproducer.

4. 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_stride_int_overflow.pte

Expected result (secure behavior)

Program::load_method() should return a clean Error::InvalidProgram when the size product would overflow the stride computation, matching the overflow-checked pattern already used in calculate_nbytes() for the parallel .ptd code path.

Actual result β€” verified against the pristine, unmodified ExecuTorch 1.3.1 source

Running: poc/poc_stride_int_overflow.pte
runtime/core/exec_aten/util/dim_order_util.h:147:37: runtime error: signed integer overflow: 8 * 2113929312 cannot be represented in type 'int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/core/exec_aten/util/dim_order_util.h:147:37 in
==<pid>== ERROR: libFuzzer: deadly signal
    #0 ... (abort machinery)
    ...
    dim_order_to_stride_nocheck (dim_order_util.h:147)
    dim_order_to_stride (dim_order_util.h:168)
    deserialization::parseTensor (tensor_parser_portable.cpp:144)

Full symbolized call chain (via addr2line against the built binary) confirms: dim_order_to_stride_nocheck (dim_order_util.h:147) ← dim_order_to_stride (dim_order_util.h:168) ← deserialization::parseTensor (tensor_parser_portable.cpp:144).

Reproduced 3/3 identical runs against the pristine source (re-verified live for this report):

run 1: signed integer overflow: 8 * 2113929312 cannot be represented in type 'int'
run 2: (identical)
run 3: (identical)

Chain Escalation Attempt (Reported Honestly)

Per standard practice of testing whether a confirmed bug can be escalated to a stronger primitive: a second sanitized build was made with UBSan disabled (ASan only), so the signed-integer-overflow multiplication would wrap silently instead of aborting β€” this lets us observe what actually happens to the corrupted stride value downstream, rather than only knowing it aborts under a sanitizer.

Result: the identical PoC ran cleanly with zero ASan errors when UBSan wasn't present to trap the overflow. This means:

  • The corrupted/wrapped stride value is computed and stored in the tensor's strides array.
  • Nothing within Program::load_method()'s own code path (parse_values/parseTensor/dim_order_to_stride) subsequently dereferences or uses that stride to compute a memory address.
  • The stride is only actually used for address computation when an operator executes and indexes into the tensor's data using it β€” a step that happens during Method::execute(), not load_method().

This report does not claim a proven memory-corruption chain. Proving one would require a substantially larger harness β€” kernel registration via register_kernels(), a full Method::execute() invocation, and a .pte whose instruction chain actually invokes an operator against the malformed tensor β€” none of which was built in this investigation. The finding is reported as a confirmed integer-overflow DoS (deterministic crash under sanitizers; silent, unobserved UB in production builds) with a plausible but unproven downstream out-of-bounds access risk during subsequent operator execution.

Impact

Who is affected: Any application calling Program::load_method() β€” the standard, always-used method-loading API β€” on a .pte containing a Tensor value with a crafted sizes array. This is the core deserialization path, not a rarely-exercised accessor.

What the attacker can do (proven): Cause a reliable, deterministic crash (SIGABRT under UBSan) purely by supplying a malformed model file.

What the attacker might additionally be able to do (unproven, flagged honestly): If the corrupted/wrapped stride value later feeds into address computation during operator execution, it could plausibly cause an out-of-bounds memory read or write β€” this was investigated but not demonstrated in this session; see Chain Escalation Attempt above.

What's at risk: Availability, confirmed. Integrity/memory-safety during operator execution, plausible but not demonstrated.

Why Medium, not Critical: Matches huntr's explicitly-named "integer overflow" example under the memory-corruption category, but only the DoS consequence was proven β€” claiming a full memory-corruption chain without a working PoC would be overclaiming.

Suggested Remediation

template <typename SizesType, typename DimOrderType, typename StridesType>
inline Error dim_order_to_stride_nocheck_safe(
    const SizesType* sizes,
    const DimOrderType* dim_order,
    const size_t dims,
    StridesType* strides) {
  if (dims == 0) return Error::Ok;
  strides[dim_order[dims - 1]] = 1;
  for (int32_t i = dims - 2; i >= 0; --i) {
    if (sizes[dim_order[i + 1]] == 0) {
      strides[dim_order[i]] = strides[dim_order[i + 1]];
    } else {
      StridesType product;
      if (c10::mul_overflows(strides[dim_order[i + 1]], sizes[dim_order[i + 1]], &product)) {
        return Error::InvalidArgument;
      }
      strides[dim_order[i]] = product;
    }
  }
  return Error::Ok;
}

This mirrors the overflow-checked pattern already correctly implemented in runtime/core/tensor_layout.cpp's calculate_nbytes() via c10::mul_overflows().

A regression test should build a .pte Tensor value whose sizes product overflows INT32_MAX while each individual size passes the existing non-negative check, asserting parseTensor() returns a clean Error rather than triggering UB.

Files Included in This Report

  • poc/poc_stride_int_overflow.pte β€” the 24,054-byte PoC file (sha256 0767af28fc00e63e8ea665beaf5139f7d84332abc435bd8e0de3ebf8d0936e98)
  • poc/harness_load_method_fuzzer.cpp β€” the harness used to trigger and reproduce the crash, calling Program::load_method() directly

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_stride_int_overflow.pte is ready for that upload.