Sitemap
Press enter or click to view image in full size

I Found a 9.8 Critical RCE in Google’s TensorFlow. Google Refused to Pay Me.

8 min read2 days ago

--

Introduction

In August 2026, I found a critical integer overflow vulnerability in TensorFlow Lite Micro (tflite-micro), Google’s framework for running machine learning models on microcontrollers and embedded devices. The bug could lead to heap buffer overflows, denial of service, and potentially remote code execution on millions of IoT devices.

I reported it through Google’s Vulnerability Reward Program (VRP), created a proof of concept, wrote a patch, and submitted a Pull Request.

The bug was real. The fix was correct. But I never got a reward.

This is the full story — the technical details, the process, and the brutal lessons I learned about how bug bounty programs actually work.

Part 1: The Discovery

What is tflite-micro?
TensorFlow Lite Micro is a lightweight version of TensorFlow designed to run machine learning models on devices with limited memory and processing power — think smart home devices, industrial sensors, medical equipment, and automotive systems. It’s used by millions of devices worldwide.

The Vulnerable Code
I was auditing the `micro_utils.cc` file when I found this function:

int ElementCount(const TfLiteIntArray& dims) {
int result = 1;
for (int i = 0; i < dims.size; ++i) {
result *= dims.data[i]; // OVERFLOW HERE - no overflow check
}
return result;
}
Press enter or click to view image in full size

This function computes the total number of elements in a tensor by multiplying its dimensions. The problem? It uses a 32-bit signed integer (int) without any overflow checks.

The Overflow
When a tensor has dimensions like [65536, 65536]:
65536 × 65536 = 4,294,967,296

Press enter or click to view image in full size
Press enter or click to view image in full size

But a 32-bit signed integer can only hold values up to 2,147,483,647. The result wraps around to 0.

The Impact
The overflowed value is then used by EvalTensorBytes():

size_t EvalTensorBytes(const TfLiteEvalTensor* tensor) {
size_t bytes_per_element;
TFLITE_DCHECK(kTfLiteOk == TfLiteTypeSizeOf(tensor->type, &bytes_per_element));
return ElementCount(*tensor->dims) * bytes_per_element;
}

When ElementCount() returns 0, the byte size becomes 0. The interpreter allocates a zero-byte buffer, and when it tries to write data into it, a heap buffer overflow occurs.

| Aspect | Details |
| : — — | : — — |
| Vulnerability | Integer Overflow → Heap Buffer Overflow |
| CWE | CWE-190 |
| CVSS Score | 9.8 (Critical) |
| Attack Vector | Remote (malicious model file) |
| Authentication | None required |
| Impact | DoS, Information Disclosure, potential RCE |

Part 2: Confirming the Bug

I wrote a minimal C++ program to confirm the overflow and compiled it with UBSan:

arith_overflow.cc:7:16: runtime error: signed integer overflow: 
65536 * 65536 cannot be represented in type 'int'
Shape: [65536, 65536]
Mathematical product: 4,294,967,296
Actual result (32-bit int): 0
OVERFLOW confirmed!
Press enter or click to view image in full size

I then built a test harness that loads a malicious .tflite model and triggers the crash with AddressSanitizer (ASan):

Bytes reported: 262144
AddressSanitizer:DEADLYSIGNAL
=================================================================
==29539==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000004
==29539==The signal is caused by a READ memory access.
==29539==Hint: address points to the zero page.
Press enter or click to view image in full size

The bug was real, reproducible, and devastating.

Part 3: The Report and the Patch

I submitted a detailed report to Google’s OSS Vulnerability Reward Program, including full technical analysis, PoC code, UBSan/ASan output, and my recommended fix.

My fix changed the return type to size_t and added overflow checks:

size_t ElementCount(const TfLiteIntArray& dims) {
size_t result = 1;
for (int i = 0; i < dims.size; ++i) {
if (dims.data[i] > 0 && result > SIZE_MAX / dims.data[i]) {
return 0; // overflow detected
}
result *= dims.data[i];
}
return result;
}

I submitted PR #3658 to the tflite-micro repository. All automated checks passed. I verified the fix and even updated the IssueTracker with the link.

Press enter or click to view image in full size

Part 4: The Bureaucracy (The Timeline)

This is where the story turns from a technical victory into a masterclass in open-source bureaucratic hell. Here is the exact timeline of what happened next, pulled directly from the Google IssueTracker and GitHub.

Get Aser Ahmed’s stories in your inbox

Join Medium for free to get updates from this writer.

August 11: Google VRP responds to my initial report. They mark the status as Won’t Fix (Intended Behavior) and explain that for OT1/OT0 projects, they require either exact OSS-Fuzz reproduction steps or a committed and merged patch.

Press enter or click to view image in full size

August 12: I ask the maintainers to review my PR. Maintainer ddavis-2015 closes the PR immediately with this comment:

”This type change will not be implemented prior to the upstream LiteRT adopting such changes in the common/shared code.”

Press enter or click to view image in full size

August 12: I go back to Google VRP, explaining that my PR was closed but the bug is real. Google VRP responds:

”Since the maintainers have closed your pull request, we won’t be taking any further action on this report.”

Press enter or click to view image in full size
Press enter or click to view image in full size

I reply, asking if there is any way to get credit or a smaller reward. Their response:

”As per the program rules, we only reward vulnerabilities that result in a fix or an internal security bug.”

Press enter or click to view image in full size

August 20: I ask the maintainers for guidance. If the fix needs to go upstream to LiteRT, where in the LiteRT codebase should I submit the patch?

Press enter or click to view image in full size

August 30: I audit the google-ai-edge/LiteRT repository from top to bottom. I discover that the vulnerable ElementCount() function does not exist upstream. The production kernel utilities in LiteRT use safe, checked versions (e.g., CheckedElementCount). The vulnerability is entirely specific to tflite-micro.

I update the GitHub PR and the Google IssueTracker with this proof, asking them to reconsider the PR since the upstream dependency is a myth.

Press enter or click to view image in full size
Press enter or click to view image in full size

September 18: Google VRP replies:

”Hi Aser, Please report this directly upstream. If the maintainers recognize it as a security vulnerability, feel free to reopen this report.”

Press enter or click to view image in full size

September 22: Trapped in a loop, I open Issue #3761 on the tflite-micro GitHub repo. I explicitly ask the maintainers:
1. Is this a security vulnerability in tflite-micro?
2. If yes, can you provide a statement so I can reopen my report with Google’s VRP?
3. If no, please explain why it is not considered a vulnerability.

Press enter or click to view image in full size

October 2: I ping the maintainers again, asking for an acknowledgment.

November 1: Maintainer veblush closes Issue #3761 with a single tag: Not planned. No explanation. No acknowledgment.

Press enter or click to view image in full size

The Catch-22

Google VRP says: Get the fix merged and we will pay you.
Maintainers say: We won’t merge until it goes upstream to LiteRT.
The Reality: The vulnerable code doesn’t exist upstream in LiteRT.
The Result: I am caught in a loop with no exit. The ticket is closed. The bug remains in the codebase.

Part 5: What I Learned

1. Open Source Governance is Complicated (and sometimes hostile)
Maintainers are often volunteers or busy engineers. They have their own priorities, and external contributions — even correct ones that fix 9.8 Critical memory corruptions — may not align with their immediate roadmap.

2. VRP Rules Have Strict Requirements
For memory corruption bugs in high-tier projects, Google requires either exact OSS-Fuzz reproduction steps or a merged patch. Without one of these, the report won’t be rewarded, no matter how valid the bug is.

3. Don’t Submit Public PRs for VRP Bugs
Because I submitted a public PR, the vulnerability details became public. It was no longer a “zero-day.” Google may consider public bugs less valuable, and the maintainers felt comfortable ignoring it because the pressure of a private disclosure was gone.

4. The Real Reward is the Experience
I learned more from this process than any single bounty could have taught me:
How to audit C++ code for memory safety bugs.
How to build PoCs with UBSan and ASan.
How to navigate the politics of open-source governance.
Why you should always double-check if a program requires a merged patch before you invest weeks of your life.

Conclusion

Finding a critical vulnerability in a major open-source project is a significant achievement. But the path from discovery to reward is not always straightforward.

If you’re starting in security research, here’s my advice:
1. Read the VRP guidelines carefully. If they require a merged patch, be prepared for a fight.
2. Choose your targets wisely. Some projects have clearer reward paths than others.
3. Document everything. Your report, your PoC, and your IssueTracker screenshots are your only evidence.
4. Focus on the skills, not just the bounty.

The bug was real. The fix was correct. The system didn’t reward me. But I’m a better security researcher because of it.

And that’s worth more than any bounty.

--

--

Aser Ahmed
Aser Ahmed

Written by Aser Ahmed

Ex.Web Developer | Security Researcher | OSINT | Content Writer | Digital Forensics | CTF Player | Penetration Tester | Bug Bounty Hunter