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(dstDatapoints into the model buffer),:1009(dstData[dst_i] = data[src_i], OOB write). TheCV_CheckLT(dst_i, total)/CV_CheckLT(src_i, total)guards at:1007-1008are tautologies:totalat:1000is 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) andaddLayer():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