hank-ai/darknet β int32 overflow in make_connected_layer() weight-buffer allocation -> heap-buffer-overflow READ in gemm_nt()
Status: gated, manual-approval PoC repository. For authorized security research / bug-bounty triage only (huntr.com MFF).
Target
- Project: hank-ai/darknet (Darknet/YOLO, branch
master) - Verified against commit:
f684e1d75d4594298c8e73acc727ba6cf2e81c60(2026-06-27, HEAD ofmasterat verification time) - Vulnerable code:
src-lib/connected_layer.cpp,make_connected_layer()/forward_connected_layer() - Class: CWE-190 Integer Overflow -> CWE-122/CWE-125 Heap-buffer-overflow (out-of-bounds READ)
This is a second, independent finding on the same target/CWE family as this campaign's
darknet-cfg-int32-overflow claim (make_convolutional_layer()'s l.nweights computation β
see that repo's notes: as originally crafted, that PoC's specific parameter choice does not
reproduce a crash, because it also overflows the output-dimension computation into negative
territory, which short-circuits the vulnerable gemm loop to zero iterations). This finding is in
a completely different function (make_connected_layer, not make_convolutional_layer), and
does reproduce a real crash end-to-end.
Root cause
// src-lib/connected_layer.cpp, make_connected_layer()
l.inputs = inputs; // TRUE, unwrapped value stored
l.outputs = outputs; // TRUE, unwrapped value stored
...
l.weights = (float*)xcalloc(outputs * inputs, sizeof(float)); // plain int*int, silently wraps
// forward_connected_layer(), called on every forward pass:
int k = l.inputs; // the TRUE, unwrapped (large) value
int n = l.outputs; // the TRUE, unwrapped (large) value
float *b = l.weights; // the buffer allocated with the WRAPPED (small) size
gemm_cpu(0,1,m,n,k,1,a,k,b,k,1,c,n);
// -> dispatches to gemm_nt(1, n, k, ALPHA, A+t*lda, k, b, k, C+t*ldc, n) per batch row
outputs * inputs is a plain 32-bit int multiplication with no overflow check.
inputs=65537, outputs=65536 gives 65537*65536 = 4,295,032,832 = 2^32 + 65536, which wraps to
exactly 65536. l.weights is allocated with only 65536 real floats, but l.inputs (65537,
the TRUE unwrapped value) is retained separately and used as both the gemm loop bound and row
stride (ldb) in gemm_nt()'s real body:
// src-lib/gemm.cpp, gemm_nt()
for(k=0; k<K; ++k) sum += ALPHA*A[i*lda+k]*B[j*ldb + k];
// B == l.weights (65536-float buffer), ldb == l.inputs == 65537 (TRUE, large value)
Even row j=0 reads 65537 floats from a 65536-float allocation β an immediate heap
buffer over-read, growing with every subsequent row.
Methodology
Building the full hank-ai/darknet CMake project was not feasible on this disk-constrained
host: every real darknet-lib .cpp file unconditionally includes <opencv2/opencv.hpp> via
darknet_internal.hpp, and CM_dependencies.cmake marks FIND_PACKAGE(OpenCV REQUIRED) β a
full install pulls in several GB of transitive packages (ffmpeg, HDF5-MPI, ROCm/HIP, VTK, Java
bindings, ...).
Instead, this harness compiles the exact real vulnerable function bodies β copy-pasted
byte-for-byte unmodified from the fetched real sources (connected_layer_real.inc, taken
verbatim from make_connected_layer()/forward_connected_layer() in the real
connected_layer.cpp; real gemm_nt(), xcalloc_location(), fill_cpu(), axpy_cpu() are
also compiled verbatim into repro_connected.cpp) against a minimal, explicitly-documented set
of stand-ins for the surrounding (non-vulnerable) scaffolding (a trimmed Darknet::Layer/
Darknet::NetworkState struct with only the touched fields, a minimal ACTIVATION enum, and a
gemm_cpu() dispatcher stand-in that reproduces the real dispatcher's behavior for the specific
TA=0,TB=1 call forward_connected_layer() makes β verified against the real gemm.cpp that
for TB=1 the AVX2/FMA fast path is unreachable, so the real code unconditionally calls the
real gemm_nt() here too). None of the stand-ins touch the vulnerable arithmetic or the
vulnerable memory access. See the top-of-file comment in repro_connected.cpp for full detail.
Proof of concept (asan_connected_output.txt)
[craft] inputs=65537 outputs=65536
[craft] true 64-bit product = 4295032832, wrapped 32-bit product = 65536
[call] Darknet::Layer l = make_connected_layer(1, 1, 65537, 65536, LINEAR, 0); <-- REAL function
[alloc] l.weights allocated with 65536 floats (262144 bytes) -- l.inputs=65537, l.outputs=65536 (TRUE values retained)
[call] forward_connected_layer(l, state); <-- REAL function, dispatches to REAL gemm_nt()
==1194476==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7f7e873b3800
READ of size 4 at 0x7f7e873b3800 thread T0
#0 ... in gemm_nt(int, int, int, float, float*, int, float*, int, float*, int) repro_connected.cpp:184
#1 ... in gemm_cpu(...) repro_connected.cpp:206
#2 ... in forward_connected_layer(Darknet::Layer&, Darknet::NetworkState) connected_layer_real.inc:126
#3 ... in main repro_connected.cpp:307
0x7f7e873b3800 is located 0 bytes after 262144-byte region [0x7f7e87373800,0x7f7e873b3800)
allocated by thread T0 here:
#0 ... in calloc
#1 ... in xcalloc_location(...) repro_connected.cpp:151
#2 ... in make_connected_layer(...) connected_layer_real.inc:35
SUMMARY: AddressSanitizer: heap-buffer-overflow repro_connected.cpp:184 in gemm_nt(...)
Reachability
make_connected_layer()/forward_connected_layer() are invoked for every [connected] section
in a .cfg network definition and executed on the very first inference pass. inputs/outputs
are attacker-controlled via the .cfg's output= fields (this layer's own output=, and the
preceding layer's output=, which becomes this layer's inputs).
Files
repro_connected.cppβ harness (see file header for full methodology)connected_layer_real.incβ realmake_connected_layer()/forward_connected_layer()bodies, copied verbatim fromsrc-lib/connected_layer.cppasan_connected_output.txtβ captured AddressSanitizer heap-buffer-overflow reportpoc.cppβ the original draft PoC (kept for provenance; it hand-reimplemented the vulnerable arithmetic rather than calling real darknet functions β this repo'srepro_connected.cppsupersedes it with a real-function-based reproduction)
Build & run
g++ -std=c++17 -O0 -g -fsanitize=address,undefined -o repro_connected_asan repro_connected.cpp
./repro_connected_asan