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