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.

This repository contains malicious .tflite model files that intentionally trigger a heap out-of-bounds read/write in the OpenCV DNN TFLite importer. They are provided solely for coordinated vulnerability disclosure and vendor/triager verification. Do not load them on a system you care about.

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

OpenCV DNN TFLite importer β€” heap OOB write with attacker-chosen offset and values (CWE-787)

PoC files for a huntr Model File Vulnerability report against the TFLite (.tflite) format. Researcher: Cyfra Tech Solutions (Roman Arce BrΓ‘n) β€” coordinated disclosure, also reported to security@opencv.org.

THE HEADLINE: it does not have to crash

poc_write_silent.tflite declares a filter shape whose cv::Mat still lands inside the model allocation, so no allocation boundary is crossed: AddressSanitizer reports nothing, no exception is thrown, the process exits 0 β€” and the next tensor's payload has been replaced with values chosen in the attacker's file. Measured 5/5 in a plain Release build:

run 1..5: victim[0]=0xC0DE0022 exit=0     -> OK 5/5

The file declares those weights as 0x5EED0000, 0x5EED0004, …; the application reads back 0xC0DE0022, … through net.getLayer(id)->blobs β€” the exact value gen_poc.py predicted from the source, at the exact element it predicted. Controlled corruption with every detector silent, not a crash.

RCE is out of scope of this report: no heap grooming, no control-flow hijack. What is demonstrated is a write primitive with attacker-chosen offset and attacker-chosen 32-bit values, plus a heap read.

Not a duplicate of the recent TFLite hardening

#29321 (merged, quantization scale count) and #29371 (merged, parsePadding/parseResize parameter tensors) fix other layers of this same file. #29336 (closed unmerged, only over a Test_TFLite.max_unpooling regression) targets the custom_options struct size at :959 β€” the same function, a different field and data path; our PoCs pass that check with a full valid 12-byte TfLiteTransposeConvParams. None of them touches the tensor buffer length.

Mechanical proof this sink is unfixed: grep -c checkPayloadSize modules/dnn/src/tflite/tflite_importer.cpp = 0, while the sibling ONNX importer has exactly this validation (onnx_graph_simplifier.cpp:1723 / lambda at :1751, a dozen call sites) since PR #29314, merged 2026-06-23.

Summary

TFLiteImporter::parseTensor() (modules/dnn/src/tflite/tflite_importer.cpp:90-130) wraps a FlatBuffer tensor payload in a non-owning cv::Mat sized by the tensor's declared shape (:93, :125) and never compares prod(shape) * elemSize() against buffer->data()->size() (:102-106). The only validation is a NULL check at :103. Both quantities are independent, fully attacker-controlled fields of the .tflite file.

Consumers of that Mat then read/write the declared extent out of a shorter heap region:

  • write sink β€” parseDeconvolution() :996 (filter.clone(), OOB read), :998 (dstData points into the model buffer), :1009 (dstData[dst_i] = data[src_i], OOB write). The CV_CheckLT(dst_i, total) / CV_CheckLT(src_i, total) guards at :1007-1008 are tautologies: total at :1000 is derived from the same attacker-declared shape, so they pass on every iteration while the write walks past the allocation.
  • read sink β€” parseConvolution() :433 (transposeND) and addLayer() :327 (blob.clone()); out-of-allocation heap is handed back to the application as ordinary model weights.

Reachable from the public API with a single file: cv::dnn::readNetFromTFLite() (:1323, allocation at :1336, importer at :1342) and cv2.dnn.readNetFromTFLite() from Python.

Verified on opencv/opencv 4.x @ 70fc3465808d801c36eb6930591df5e362ec74ab (2026-07-26); the same code is present on 5.x (parseTensor at :95, the write at :1174).

Files

File Size What it demonstrates
poc_write_asan.tflite 1088 B OOB write crossing the allocation. ASAN: WRITE of size 4 ... 48 bytes after 1088-byte region at tflite_importer.cpp:1009. Plain Release build: SIGSEGV.
poc_write_silent.tflite 2304 B Controlled, silent corruption: the write stays inside the model allocation, ASAN reports nothing, process exits 0, and the next tensor's weights read back as the attacker's 0xC0DE0022… instead of the 0x5EED0000… declared in the file.
poc_read_asan.tflite 1584 B OOB read / heap disclosure: live heap pointers whose high half changes every run (0x6102/0x5636/0x6263/0x58C2 = ASLR bypass), glibc chunk headers (0x61/0x21/0x11) and neighbouring object contents ("CONV_2D", "DEPTHWISE_CONV_2D", "SQUARED_DIFFERENCE") returned as layer->blobs[0]. Must be run WITHOUT ASAN: under the sanitizer the redzone reads as zeros, so ASAN understates this leak.
poc_bufferidx.tflite 560 B Secondary: unbounded buffer index at parseTensor():100 (FLATBUFFERS_ASSERT is plain assert, removed by -DNDEBUG).
gen_poc.py β€” Deterministic generator; regenerates all four files byte-identically and predicts the OOB offsets from the source before running anything.
poc_harness.cpp β€” ~20-line driver: calls cv::dnn::readNetFromTFLite(argv[1]) and prints the layer blobs. Nothing else.
run_poc.sh β€” Runs all six scenarios (ASAN and plain Release).
ASAN-witness.txt β€” Full recorded output: 5 witnesses.

Evidence of control (not just "it crashes")

gen_poc.py predicts, from the source alone:

[1] poc_write_asan
  file=1088 B | payload@112 | tail_room=976 B | Mat spans 2048 B (=512 elems) | +1072 B past EOF
     value 0xC0DE0004 (payload elem   4) -> Mat elem 256 = +48 B past EOF

ASAN confirms it to the byte: 0x7ab0f78e04f0 is located 48 bytes after 1088-byte region.

[3] poc_write_silent
  victim payload starts 544 B into the filter Mat (elem 136)
     value 0xC0DE0022 (filter elem  34) -> victim elem 0

Observed, with no ASAN report and exit code 0:

[harness] layer 'victim' type=Const blobs=1
           blob[0] total=256 elemSize=4
             0xC0DE0022 0xC0DE0026 0xC0DE002A ...

Both the destination offset and the stored 32-bit values are chosen in the model file.

Reproduce

git clone https://github.com/opencv/opencv && cd opencv
git checkout 70fc3465808d801c36eb6930591df5e362ec74ab
cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_LIST=dnn -DBUILD_SHARED_LIBS=OFF \
      -DCMAKE_CXX_FLAGS='-fsanitize=address -fsanitize-recover=address -g'
cmake --build build -j && cmake --install build --prefix install

c++ poc_harness.cpp -I install/include/opencv4 -L install/lib -lopencv_dnn -lopencv_core \
    -fsanitize=address -o poc_harness

ASAN_OPTIONS=halt_on_error=0:abort_on_error=0:detect_leaks=0 ./poc_harness poc_write_asan.tflite
./poc_harness poc_write_silent.tflite dump     # no ASAN report, exit 0, victim blob[0][0] == 0xC0DE0022

Python, same path through the binding:

import cv2
net = cv2.dnn.readNetFromTFLite("poc_write_silent.tflite")   # returns normally

Suggested fix

The same validation OpenCV already merged for the ONNX importer in PR #29314 (validate onnx tensor payload size in getMatFromTensor), applied to parseTensor before :125: compute the declared element count with a saturating multiply and CV_CheckGE(buffer_data->size(), total_elems * CV_ELEM_SIZE(dtype), ...). Also bound the buffer index at :100.

Scope / ethics

Lab-only, self-hosted reproduction. No third-party system was touched. Files are gated and shared for triage and vendor verification under coordinated disclosure.

Downloads last month
4
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support