Upload folder using huggingface_hub
Browse files- REPORT.md +181 -0
- poc/harness_load_method_fuzzer.cpp +67 -0
- poc/poc_stride_int_overflow.pte +0 -0
REPORT.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Signed Integer Overflow in ExecuTorch Tensor Stride Computation via Unchecked Size Product (.pte)
|
| 2 |
+
|
| 3 |
+
**Target:** ExecuTorch 1.3.1 (.pte, huntr Model File Vulnerability program)
|
| 4 |
+
**Severity:** Medium (integer overflow with plausible but unproven downstream memory-corruption chain; DoS confirmed)
|
| 5 |
+
**CWE:** CWE-190 (Integer Overflow or Wraparound)
|
| 6 |
+
**Component:** `runtime/core/exec_aten/util/dim_order_util.h` (overflow site) / `runtime/executor/tensor_parser_portable.cpp` (caller)
|
| 7 |
+
**Authentication Required:** No — only requires a victim application to load an attacker-supplied `.pte` file via the standard, always-used `Program::load_method()` API.
|
| 8 |
+
|
| 9 |
+
## Summary
|
| 10 |
+
|
| 11 |
+
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.
|
| 12 |
+
|
| 13 |
+
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.
|
| 14 |
+
|
| 15 |
+
## Vulnerability Details
|
| 16 |
+
|
| 17 |
+
`runtime/executor/tensor_parser_portable.cpp`'s `parseTensor()` validates that no individual size is negative:
|
| 18 |
+
|
| 19 |
+
```cpp
|
| 20 |
+
for (flatbuffers::uoffset_t i = 0; i < dim; i++) {
|
| 21 |
+
ET_CHECK_OR_RETURN_ERROR(
|
| 22 |
+
sizes[i] >= 0,
|
| 23 |
+
InvalidProgram,
|
| 24 |
+
"Negative size[%zu] %" PRId32,
|
| 25 |
+
static_cast<size_t>(i),
|
| 26 |
+
sizes[i]);
|
| 27 |
+
}
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
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`:
|
| 31 |
+
|
| 32 |
+
```cpp
|
| 33 |
+
template <typename SizesType, typename DimOrderType, typename StridesType>
|
| 34 |
+
inline void dim_order_to_stride_nocheck(
|
| 35 |
+
const SizesType* sizes,
|
| 36 |
+
const DimOrderType* dim_order,
|
| 37 |
+
const size_t dims,
|
| 38 |
+
StridesType* strides) {
|
| 39 |
+
if (dims == 0) return;
|
| 40 |
+
strides[dim_order[dims - 1]] = 1;
|
| 41 |
+
for (int32_t i = dims - 2; i >= 0; --i) {
|
| 42 |
+
if (sizes[dim_order[i + 1]] == 0) {
|
| 43 |
+
strides[dim_order[i]] = strides[dim_order[i + 1]];
|
| 44 |
+
} else {
|
| 45 |
+
strides[dim_order[i]] =
|
| 46 |
+
strides[dim_order[i + 1]] * sizes[dim_order[i + 1]]; // <-- unchecked multiplication, line 147
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
`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.
|
| 53 |
+
|
| 54 |
+
## Steps to Reproduce
|
| 55 |
+
|
| 56 |
+
### Environment
|
| 57 |
+
Linux x86-64, ExecuTorch 1.3.1 pristine source, clang-16, CMake, Ninja. No authentication, no host access.
|
| 58 |
+
|
| 59 |
+
### 1. Build ExecuTorch with sanitizers
|
| 60 |
+
|
| 61 |
+
Same build as REPORT-01 Step 1.
|
| 62 |
+
|
| 63 |
+
### 2. Build the load_method-level harness (`poc/harness_load_method_fuzzer.cpp`, included in this report)
|
| 64 |
+
|
| 65 |
+
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:
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
export ET_PARENT=/path/to/parent-of-executorch
|
| 69 |
+
C10_INC="$ET_SRC/runtime/core/portable_type/c10"
|
| 70 |
+
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"
|
| 71 |
+
|
| 72 |
+
clang++-16 -std=c++17 -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all \
|
| 73 |
+
$INCLUDES -DFLATBUFFERS_MAX_ALIGNMENT=1024 -DC10_USING_CUSTOM_GENERATED_MACROS \
|
| 74 |
+
-c poc/harness_load_method_fuzzer.cpp -o harness.o
|
| 75 |
+
|
| 76 |
+
clang++-16 -fsanitize=fuzzer,address,undefined -o poc_harness harness.o \
|
| 77 |
+
"$ET_BUILD/extension/data_loader/libextension_data_loader.a" \
|
| 78 |
+
"$ET_BUILD/libexecutorch_core.a"
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
### 3. PoC file
|
| 82 |
+
|
| 83 |
+
`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.
|
| 84 |
+
|
| 85 |
+
### 4. Trigger the crash
|
| 86 |
+
|
| 87 |
+
```bash
|
| 88 |
+
export ASAN_OPTIONS="abort_on_error=1:symbolize=0"
|
| 89 |
+
export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=0"
|
| 90 |
+
./poc_harness -timeout=5 -runs=0 poc/poc_stride_int_overflow.pte
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### Expected result (secure behavior)
|
| 94 |
+
`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.
|
| 95 |
+
|
| 96 |
+
### Actual result — verified against the pristine, unmodified ExecuTorch 1.3.1 source
|
| 97 |
+
|
| 98 |
+
```
|
| 99 |
+
Running: poc/poc_stride_int_overflow.pte
|
| 100 |
+
runtime/core/exec_aten/util/dim_order_util.h:147:37: runtime error: signed integer overflow: 8 * 2113929312 cannot be represented in type 'int'
|
| 101 |
+
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/core/exec_aten/util/dim_order_util.h:147:37 in
|
| 102 |
+
==<pid>== ERROR: libFuzzer: deadly signal
|
| 103 |
+
#0 ... (abort machinery)
|
| 104 |
+
...
|
| 105 |
+
dim_order_to_stride_nocheck (dim_order_util.h:147)
|
| 106 |
+
dim_order_to_stride (dim_order_util.h:168)
|
| 107 |
+
deserialization::parseTensor (tensor_parser_portable.cpp:144)
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
Full symbolized call chain (via `addr2line` against the built binary) confirms:
|
| 111 |
+
`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).
|
| 112 |
+
|
| 113 |
+
**Reproduced 3/3 identical runs against the pristine source** (re-verified live for this report):
|
| 114 |
+
```
|
| 115 |
+
run 1: signed integer overflow: 8 * 2113929312 cannot be represented in type 'int'
|
| 116 |
+
run 2: (identical)
|
| 117 |
+
run 3: (identical)
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
## Chain Escalation Attempt (Reported Honestly)
|
| 121 |
+
|
| 122 |
+
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.
|
| 123 |
+
|
| 124 |
+
**Result: the identical PoC ran cleanly with zero ASan errors** when UBSan wasn't present to trap the overflow. This means:
|
| 125 |
+
|
| 126 |
+
- The corrupted/wrapped stride value **is** computed and stored in the tensor's `strides` array.
|
| 127 |
+
- **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.
|
| 128 |
+
- 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()`.
|
| 129 |
+
|
| 130 |
+
**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.
|
| 131 |
+
|
| 132 |
+
## Impact
|
| 133 |
+
|
| 134 |
+
**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.
|
| 135 |
+
|
| 136 |
+
**What the attacker can do (proven):** Cause a reliable, deterministic crash (SIGABRT under UBSan) purely by supplying a malformed model file.
|
| 137 |
+
|
| 138 |
+
**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.
|
| 139 |
+
|
| 140 |
+
**What's at risk:** Availability, confirmed. Integrity/memory-safety during operator execution, plausible but not demonstrated.
|
| 141 |
+
|
| 142 |
+
**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.
|
| 143 |
+
|
| 144 |
+
## Suggested Remediation
|
| 145 |
+
|
| 146 |
+
```cpp
|
| 147 |
+
template <typename SizesType, typename DimOrderType, typename StridesType>
|
| 148 |
+
inline Error dim_order_to_stride_nocheck_safe(
|
| 149 |
+
const SizesType* sizes,
|
| 150 |
+
const DimOrderType* dim_order,
|
| 151 |
+
const size_t dims,
|
| 152 |
+
StridesType* strides) {
|
| 153 |
+
if (dims == 0) return Error::Ok;
|
| 154 |
+
strides[dim_order[dims - 1]] = 1;
|
| 155 |
+
for (int32_t i = dims - 2; i >= 0; --i) {
|
| 156 |
+
if (sizes[dim_order[i + 1]] == 0) {
|
| 157 |
+
strides[dim_order[i]] = strides[dim_order[i + 1]];
|
| 158 |
+
} else {
|
| 159 |
+
StridesType product;
|
| 160 |
+
if (c10::mul_overflows(strides[dim_order[i + 1]], sizes[dim_order[i + 1]], &product)) {
|
| 161 |
+
return Error::InvalidArgument;
|
| 162 |
+
}
|
| 163 |
+
strides[dim_order[i]] = product;
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
return Error::Ok;
|
| 167 |
+
}
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
This mirrors the overflow-checked pattern already correctly implemented in `runtime/core/tensor_layout.cpp`'s `calculate_nbytes()` via `c10::mul_overflows()`.
|
| 171 |
+
|
| 172 |
+
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.
|
| 173 |
+
|
| 174 |
+
## Files Included in This Report
|
| 175 |
+
|
| 176 |
+
- `poc/poc_stride_int_overflow.pte` — the 24,054-byte PoC file (sha256 `0767af28fc00e63e8ea665beaf5139f7d84332abc435bd8e0de3ebf8d0936e98`)
|
| 177 |
+
- `poc/harness_load_method_fuzzer.cpp` — the harness used to trigger and reproduce the crash, calling `Program::load_method()` directly
|
| 178 |
+
|
| 179 |
+
## huntr Submission Note
|
| 180 |
+
|
| 181 |
+
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.
|
poc/harness_load_method_fuzzer.cpp
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// libFuzzer harness for Program::load_method() -> Method::init()
|
| 2 |
+
// Targets the ExecutionPlan.delegates iteration in Method::init() specifically -
|
| 3 |
+
// this loop runs BEFORE any kernel/backend resolution, so no kernel or backend
|
| 4 |
+
// registration is required to reach it.
|
| 5 |
+
#include <cstddef>
|
| 6 |
+
#include <cstdint>
|
| 7 |
+
#include <memory>
|
| 8 |
+
#include <vector>
|
| 9 |
+
|
| 10 |
+
#include <executorch/extension/data_loader/buffer_data_loader.h>
|
| 11 |
+
#include <executorch/runtime/executor/method.h>
|
| 12 |
+
#include <executorch/runtime/executor/program.h>
|
| 13 |
+
#include <executorch/runtime/core/memory_allocator.h>
|
| 14 |
+
#include <executorch/runtime/core/hierarchical_allocator.h>
|
| 15 |
+
#include <executorch/runtime/executor/memory_manager.h>
|
| 16 |
+
#include <executorch/runtime/platform/runtime.h>
|
| 17 |
+
|
| 18 |
+
using executorch::extension::BufferDataLoader;
|
| 19 |
+
using executorch::runtime::Program;
|
| 20 |
+
using executorch::runtime::Method;
|
| 21 |
+
using executorch::runtime::MemoryAllocator;
|
| 22 |
+
using executorch::runtime::HierarchicalAllocator;
|
| 23 |
+
using executorch::runtime::MemoryManager;
|
| 24 |
+
using executorch::runtime::Span;
|
| 25 |
+
|
| 26 |
+
static bool g_initialized = false;
|
| 27 |
+
|
| 28 |
+
extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) {
|
| 29 |
+
if (!g_initialized) {
|
| 30 |
+
executorch::runtime::runtime_init();
|
| 31 |
+
g_initialized = true;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
constexpr std::size_t kMaxInput = 8U * 1024U * 1024U;
|
| 35 |
+
if (data == nullptr || size == 0 || size > kMaxInput) {
|
| 36 |
+
return 0;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
BufferDataLoader loader(data, size);
|
| 40 |
+
auto program = Program::load(&loader, Program::Verification::InternalConsistency);
|
| 41 |
+
if (!program.ok()) {
|
| 42 |
+
return 0;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Minimal allocators - large enough for a tiny ExecutionPlan with no real
|
| 46 |
+
// tensors/kernels; we only need to reach Method::init()'s delegates loop.
|
| 47 |
+
static std::vector<uint8_t> method_pool(256 * 1024);
|
| 48 |
+
static std::vector<uint8_t> planned_pool(64 * 1024);
|
| 49 |
+
MemoryAllocator method_allocator(method_pool.size(), method_pool.data());
|
| 50 |
+
static Span<uint8_t> planned_span(planned_pool.data(), planned_pool.size());
|
| 51 |
+
HierarchicalAllocator planned_memory({&planned_span, 1});
|
| 52 |
+
MemoryManager mm(&method_allocator, &planned_memory, nullptr);
|
| 53 |
+
|
| 54 |
+
auto& p = program.get();
|
| 55 |
+
auto n = p.num_methods();
|
| 56 |
+
for (size_t i = 0; i < n; ++i) {
|
| 57 |
+
auto name = p.get_method_name(i);
|
| 58 |
+
if (name.ok()) {
|
| 59 |
+
// load_method() calls Method::init() internally. We expect this to
|
| 60 |
+
// fail gracefully for most fuzzer inputs (missing kernels, etc) -
|
| 61 |
+
// we're specifically hunting for a crash, not a successful load.
|
| 62 |
+
auto method = p.load_method(name.get(), &mm);
|
| 63 |
+
(void)method;
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
return 0;
|
| 67 |
+
}
|
poc/poc_stride_int_overflow.pte
ADDED
|
Binary file (24.1 kB). View file
|
|
|