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
(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:
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:
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
- Attacker supplies a
.tflitefile (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> }.
- Victim code calls
tflite::FlatBufferModel::VerifyAndBuildFromBuffer(...)(orBuildFromBuffer/BuildFromFile, all funnel to the same internal logic) β the model is accepted. - 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:
- Builds a malicious
.tfliteflatbuffer exactly as described above (evil_metadata_buffer_oob.tflite, included,Metadata.buffer = 100,Model.bufferssize1). - Loads it through the real, public, "safe" entry point:
tflite::FlatBufferModel::VerifyAndBuildFromBuffer(...)β confirmed accepted. - 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
.tflitefile and callsGetMinimumRuntime()(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/tensorflowGitHub Security Advisories and GitHub issues forGetMinimumRuntime,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 identicalMetadata.bufferfield, andInterpreterBuilder::ParseTensorsbounds-checks the analogousTensor.bufferfield β confirming this specific accessor is a genuine, isolated gap rather than a previously-identified/fixed class of bug. - Verified directly against
tensorflow/tensorflowHEAD @ commitdb3bb0b77151263d709e33d8264da24a6c23b373(2026-07-06).
Suggested fix
Add the same guard ReadAllMetadata() already uses:
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)
# 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
- Downloads last month
- 6