EnigmaConsultant's picture
TFLite GetMinimumRuntime unchecked Metadata.buffer index OOB read PoC
24a9d12 verified
|
Raw
History Blame Contribute Delete
10.7 kB
metadata
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 (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

  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:

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