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:
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:
/* 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:
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
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():
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 (sha2563d955721ff41ead7dc52eff62fcd3dc7fd0a355a197f263304385ca8fdee3dd3)poc/decode_poc.pyβ byte-level annotation script, shows exactly which field is corrupted and whypoc/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.