File size: 11,264 Bytes
a00553a | 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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | # 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:
```cpp
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`:
```cpp
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:
```bash
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
```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_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
```cpp
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.
|