I found a memory-corruption bug in TensorFlow Lite that fires the moment a model loads - before it runs a single token. Here’s the bug, and why “the model is just data” is the most expensive assumption in AI right now.


I still hunt zero-days at night. Not despite running a security company - because of it. You cannot defend what you have never taken apart, and the fastest way to learn where the AI stack actually breaks is to break it yourself.

So on nights and weekends I point a fuzzer at the parts of the stack the industry has quietly decided to trust. One of them is the code that loads AI models. This is what it found.

The target was the model loading, not the model running

Every on-device AI runtime does the same unglamorous thing before it ever produces a token: it takes a binary file off disk or off the network, parses it, validates it, and lays it out in memory. That parser is written in C++. It runs on bytes you did not write. And almost nobody fuzzes it.

So I built a harness around TensorFlow Lite’s AllocateTensors() - the call that turns a parsed model into allocated, wired-up tensors - and fed it a corpus of malformed FlatBuffer models. It didn’t take long before one of them wrote outside its bounds.

The bug, in one sentence

A crafted .tflite model makes TensorFlow Lite copy 22 attacker-controlled bytes into a buffer that is only 20 bytes long.

It happens in the reshape operator’s preparation step. And it happens at load time - inside AllocateTensors(), before any image, audio, or prompt is ever fed in. No inference. No input. Opening the file is the entire attack.

The write lands during AllocateTensors(), before inference ever starts

Root cause: two allocations that disagree

When TFLite prepares a reshape, ResizeOutput() allocates the output tensor’s dimension array - a TfLiteIntArray - sized to the tensor’s initial rank. A TfLiteIntArray is a 4-byte length followed by one 32-bit integer per dimension. For an initial rank of 4, that’s 4 + (4 * 4) = 20 bytes.

Then Prepare() copies the new shape’s dimensions into that same array. But the new shape is read straight from the model - and the model gets to lie. When it declares a higher rank than the buffer was allocated for, the memcpy runs off the end:

// reshape.cc - output->dims was allocated for the initial rank (20 bytes).
// new_shape, read from the model, declares a higher rank.
memcpy(output->dims->data, new_shape_data, new_shape_bytes);
// new_shape_bytes is 22, so 22 bytes are written into a 20-byte allocation.

The bytes that land out of bounds are the shape values themselves - fully attacker-controlled, straight from the FlatBuffer.

Under AddressSanitizer, on a from-source build, it’s unambiguous:

==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 22 at 0x...f14 thread T0
    #0 __asan_memcpy
    #1 tflite::ops::builtin::reshape::Prepare()  reshape.cc:172
    #2 tflite::Subgraph::PrepareOpsAndTensors()  subgraph.cc
    #3 tflite::Subgraph::AllocateTensors()       subgraph.cc

0x...f14 is located 0 bytes after 20-byte region [0x...f00, 0x...f14)
SUMMARY: AddressSanitizer: heap-buffer-overflow in reshape::Prepare()

The detail I find most convincing: a number that shows up twice

On a stock Android build, the same model doesn’t silently corrupt anything. A size-consistency check in TFLite’s core catches the mismatch and aborts:

Internal error: Cannot create interpreter:
required_bytes != bytes (4 != 22)
Tensor 0 is invalidly specified in schema.

That 22 is the same 22 the sanitizer reports as the write size. The Android validator and the sanitizer are describing the same event from two directions: the file tries to write 22 bytes where 4 belong. One build turns that into a clean crash; the other lets the write land.

What I proved - and what I didn’t

I want to be precise about impact, because precision is the difference between a finding and a headline.

What I demonstrated: on hardened production builds, the size check turns this into a crash - a denial of service reachable by doing nothing but loading a file. Under sanitizer, a from-source build performs a 22-byte attacker-controlled heap write into the neighboring allocation, which in TFLite’s layout tends to hold another tensor’s metadata.

What I did not demonstrate: remote code execution. A 22-byte controlled write next to tensor metadata is the kind of primitive real exploits are built from - but I have no control-flow hijack to show you, and I won’t imply one. Call it what it is: a high-severity, load-time memory-safety bug. If it escalates, that’s a separate piece of work.

I reported it to Google through their OSS vulnerability program, and they cleared me to disclose it publicly.

Why this matters more than one bug

We have spent two years building a security industry around what AI says. Prompt injection. Jailbreaks. Output filters. Guardrails. Every one of those defenses has one thing in common: it runs after the model is already loaded into memory. They are all downstream of the moment this bug fires.

A model file is not a document. It is dense binary input to tens of thousands of lines of memory-unsafe C and C++ that execute before the model does anything at all. That is the exact shape of every image, font, and PDF vulnerability of the last twenty years - except this “file” is downloaded millions of times a day off public model hubs, and fetched automatically by apps over the network.

I reported my first memory-corruption bugs as a teenager, in Microsoft PowerPoint and Adobe Reader - software everyone treated as a harmless viewer until a “document” turned out to be a program in disguise. Same class of bug. New file type.

And it isn’t new

This isn’t the first time TFLite’s model loader has done this. There’s a long lineage of CVEs for exactly this pattern - a crafted .tflite triggering a heap overflow in one kernel or another - and maintainers are still patching it, operator by operator. In early 2026, a batch of fixes landed for integer-overflow bugs across roughly a dozen kernels. Reshape wasn’t in that batch, and this isn’t an integer overflow - it’s a rank mismatch - but it’s the same underlying story: the loader trusts a number the model was allowed to choose.

The bigger picture

It doesn’t stop at models, either. The entire AI stack runs on downloaded artifacts, and almost all of them are binary blobs parsed by memory-unsafe code before anything verifies them: weights, embeddings, vector indexes, tokenizers, LoRA adapters, checkpoints. Millions of downloads a day. Rarely signed. Almost never inspected.

Some of those formats are worse than this bug by design. Pickle-based weights execute arbitrary code as a feature - researchers have already found live malware on public hubs using exactly that. But the formats we call safe, like TFLite’s FlatBuffer, still get handed to a C++ parser first. That’s where this bug lives, and it’s why “it’s just data” is the assumption an attacker is counting on.

The AI supply chain today looks like npm did around 2015: enormous reach, near-zero verification, everyone assuming someone else is checking.

If you ship on-device ML

A few things are worth doing today:

  1. Treat an untrusted .tflite like untrusted native code - because that’s what it effectively becomes the moment you load it.
  2. Isolate model loading. Parse and allocate in a sandbox or a separate process, not in your main address space.
  3. Pin and verify sources. Sign your artifacts, check signatures before load, and don’t auto-fetch weights from anywhere you wouldn’t run a binary from.
  4. Fuzz your own loaders. AllocateTensors() and its equivalents are attack surface. Point a harness at them before someone else does.

We secured the conversation, and forgot the file

Every defense the AI industry shipped in the last two years assumes the model is already running. Prompt injection filters, jailbreak detection, output guardrails: this bug fires before any of them have anything to read.

We verify what a model says, and we take the file it came from on faith. That file is the part that executes first, against a C++ parser, and the part almost nobody signs. We built Ciphero to verify AI - what agents do, what models return. The artifact they came from belongs inside that same perimeter.

If you ship models you did not build, on devices you do not control, get in touch. I would rather hear about your loader before someone else points a fuzzer at it.


Reported to the TensorFlow / LiteRT maintainers via Google’s OSS vulnerability program and cleared for public disclosure. Technical details, proof-of-concept, and suggested fix: tensorflow/tensorflow#124982.