YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
OpenCV Caffe importer: ineffective integer-truncated blob size check
This repository contains a proof-of-concept malicious Caffe model that abuses an ineffective
size check in OpenCV's cv2.dnn.readNetFromCaffe. A 42-byte file declares a weight blob with
roughly 4.3 billion elements while carrying zero bytes of weight data, and the loader accepts it
because the length check is performed on a 32-bit-truncated element count.
It is a security PoC for a huntr Model File Format report. The model is intentionally malformed and the repo is gated so it cannot be downloaded casually.
Impact depends on the host allocator (read this first)
The observable result is not identical on every platform, because it depends on whether the host grants or refuses the oversized allocation that the full 64-bit element count implies (about 16 GiB here):
- On Windows (opencv-python 4.13.0), the allocation is granted via memory overcommit, the
subsequent blob copy runs out of bounds, and the process crashes with an access violation
(exit code
0xC0000005/3221225477). This is a memory-safety crash. - On Linux (Debian, opencv-python-headless 4.13.0), the allocator refuses the request and
OpenCV raises a catchable
cv2.error(Insufficient memory,OutOfMemoryError) insidedstBlob.create(), before any copy. There is no crash on this configuration.
So the honest impact ranges from an uncontrolled memory allocation reachable from a tiny untrusted file (denial of service, catchable on hosts that refuse the allocation) up to an out-of-bounds access and process crash on hosts where the allocation is granted. The underlying defect, the truncated size check, is the same in both cases.
Affected
opencv-python/ OpenCVcv2.dnn, confirmed on 4.13.0 (latest at time of writing).- The same code is present on
master. - Entry point:
cv2.dnn.readNetFromCaffe(prototxt, caffemodel).
Root cause
modules/dnn/src/caffe/caffe_importer.cpp, function blobFromProto:
dstBlob.create((int)shape.size(), &shape[0], CV_32F);
...
// FLOAT raw_data path
CV_Assert(raw_data.size() / 4 == (int)dstBlob.total());
Mat((int)shape.size(), &shape[0], CV_32FC1, (void*)raw_data.c_str()).copyTo(dstBlob);
dstBlob.total() is a 64-bit size_t element count. The assert that is supposed to reject an
undersized raw_data buffer casts it to int, truncating the count modulo 2^32. If the
BlobShape dims multiply to an exact multiple of 2^32, (int)total becomes 0, so a 0-byte
raw_data satisfies 0 / 4 == 0. Each individual dimension still fits in a positive int, so no
per-dimension range check fires. The loader then proceeds to allocate and copy a blob whose true
64-bit element count is enormous, which is where the platform-dependent behavior above kicks in.
Files
poc.caffemodel- the malicious weights file (42 bytes). OneInnerProductlayer with a single weight blob of shape[65536, 65536](product = 2^32) and zero bytes ofraw_data.poc.prototxt- a trivial network that just references the layer. The behavior does not depend on this architecture; any prototxt that causes the blob to be parsed works.make_poc.py- regenerates the two files from scratch (self-contained protobuf encoder).verify.py- loads the model in a child process and reports the exit code (crash vs catchable error).
Reproduce
pip install opencv-python
python make_poc.py # writes poc.prototxt + poc.caffemodel
python verify.py # loads in a child process, reports the outcome
Observed:
- Windows: the child process terminates via access violation (
3221225477=0xC0000005) duringreadNetFromCaffe, before any inference. - Linux: the child raises a catchable
cv2.error(Insufficient memory: Failed to allocate 17179869184 bytes) fromdstBlob.create()and exits without a crash.
Fix
Compute and validate the element count in 64-bit and reject blobs whose dimension product
overflows, instead of comparing against (int)dstBlob.total(). For example assert
raw_data.size() / elemSize == dstBlob.total() using the full size_t value, and bound the total
against the actual raw_data size before constructing the source Mat. The same truncation is
present on the FLOAT16 path (raw_data.size() / 2 == (int)dstBlob.total()) and should be fixed
together.