- BVLC/caffe β unchecked
input_dimindex in legacy-input upgrade path (UpgradeNetInput())
BVLC/caffe β unchecked input_dim index in legacy-input upgrade path (UpgradeNetInput())
Status: gated, manual-approval PoC repository. For authorized security research / bug-bounty triage only (huntr.com MFF).
Target
- Project: BVLC/caffe (branch
master) - Verified against commit:
9b891540183ddc834a02b2bd81b31afae71b2153(2020-02-13, HEAD ofmasterβ the project has had no further commits since) - Vulnerable code:
src/caffe/util/upgrade_proto.cpp,UpgradeNetInput() - Class: CWE-125 Out-of-bounds Read (protobuf
RepeatedFieldindex out of range), reachable via ordinary.caffemodel/.prototxtloading.
Root cause
Caffe's legacy (pre-input_shape) network-definition format lets a NetParameter declare
N string input names alongside a flat input_dim list that is documented/assumed to
contain exactly 4*N entries (batch, channels, height, width, once per input, concatenated).
UpgradeNetInput() β called on every model load to convert this legacy format into modern
Input layers β walks that assumption without ever checking it against the real size of the
input_dim field:
// src/caffe/util/upgrade_proto.cpp, UpgradeNetInput()
for (int i = 0; i < net_param->input_size(); ++i) {
layer_param->add_top(net_param->input(i));
if (has_shape) {
input_param->add_shape()->CopyFrom(net_param->input_shape(i));
} else {
// Turn legacy input dimensions into shape.
BlobShape* shape = input_param->add_shape();
int first_dim = i*4;
int last_dim = first_dim + 4;
for (int j = first_dim; j < last_dim; j++) {
shape->add_dim(net_param->input_dim(j)); // <-- no check that j < input_dim_size()
}
}
}
For i=0 alone, this reads input_dim(0), input_dim(1), input_dim(2), input_dim(3) β but
a NetParameter can declare any number of input_dim entries independent of input_size()
(they are two separate repeated fields in the wire format; nothing enforces input_dim_size() == 4*input_size()). oob_trigger.caffemodel declares input_size()=1 but only
input_dim_size()=1 β so the loop's very first iteration (j=1) already indexes past the end
of the real input_dim field.
NetNeedsInputUpgrade() gates this on nothing more than input_size() > 0, and
UpgradeNetInput() is invoked by UpgradeNetAsNeeded(), which both
ReadNetParamsFromTextFileOrDie() and ReadNetParamsFromBinaryFileOrDie() call unconditionally
β i.e. this runs on every normal .prototxt/.caffemodel load path.
Proof of concept
This harness avoids Caffe's full (heavy: Boost, HDF5, LMDB, LevelDB, OpenBLAS, optionally
OpenCV) build entirely β not feasible on this disk-constrained, non-root host β since
UpgradeNetInput()/NetNeedsInputUpgrade() only touch protobuf-generated message types and
have no other Caffe dependency. It compiles the real, unmodified function bodies
(upgrade_net_input_real.inc, copied verbatim from the real upgrade_proto.cpp at the pinned
commit) against the real, unmodified caffe.proto (compiled with protoc into real generated
NetParameter/BlobShape/etc. C++ classes) and a real C++ protobuf runtime, then feeds it the
actual oob_trigger.caffemodel/oob_trigger_wide.caffemodel bytes exactly as
ReadProtoFromBinaryFile() would (NetParameter::ParseFromString()).
oob_trigger.caffemodel (1 input, 1 input_dim entry) β triggers the bug
[parse] name="oob_trigger" input_size()=1 input_dim_size()=1 input_shape_size()=0
input(0) = "data"
input_dim(0) = 1
[call] UpgradeNetInput(¶m); <-- REAL function, real protobuf RepeatedField
F0000 ... repeated_ptr_field.h:140] Check failed: index < size (1 vs. 1)
@ ... google::protobuf::internal::RuntimeAssertInBounds()
@ ... google::protobuf::RepeatedField<>::Get()
@ ... caffe::NetParameter::input_dim()
@ ... UpgradeNetInput()
@ ... main
This proves the exact out-of-bounds access claimed: UpgradeNetInput() really does call
net_param->input_dim(1) against a RepeatedField whose real size is 1.
oob_trigger_wide.caffemodel (10 inputs, 0 input_dim entries) β does NOT trigger the bug
[parse] name="oob_trigger_wide" input_size()=10 input_dim_size()=0 input_shape_size()=0
[call] UpgradeNetInput(¶m);
[done] UpgradeNetInput returned without crashing this run
Source analysis explains why: UpgradeNetInput() only enters the vulnerable branch when
has_shape || has_dim is true; with input_dim_size()==0, has_dim is false, has_shape is
also false (no input_shape entries either), so the entire vulnerable loop is skipped. Only
oob_trigger.caffemodel (which has exactly one non-zero input_dim entry) actually reaches the
bug.
Important caveat: observed severity depends on the linked protobuf version
The protobuf build available for this verification (a modern build, protobuf ~5.x/Abseil-based,
reused from earlier work in this sweep) has google::protobuf::RepeatedField<>::Get() perform
an unconditional runtime bounds check (RuntimeAssertInBounds()) that is not compiled out
under NDEBUG β this is a relatively recent hardening added upstream to protobuf's
RepeatedField. With that protobuf, the out-of-bounds input_dim(j) access is caught and the
process aborts via a controlled LOG(FATAL) β still a crash/DoS on untrusted input, but not a
silent memory-safety violation.
BVLC/caffe (dead since 2020) would historically be linked against a much older protobuf (~3.x
era, contemporary with 2019-2020), whose RepeatedField::Get() implementation of that era used
only GOOGLE_DCHECK(index < current_size_) before return elements_[index] β a check that
is compiled out under NDEBUG/release builds, which would make this a genuine silent
out-of-bounds read into adjacent heap memory (matching the original CWE-125 claim) rather than
a controlled abort. This was not independently re-verified against an old protobuf build in this
sweep (out of scope/time for this pass); the underlying missing-bounds-check bug in Caffe's own
code is definitively real and dynamically confirmed either way β only the exact
crash-vs-silent-read manifestation is protobuf-version-dependent.
Files
upgrade_net_input_real.incβ realUpgradeNetInput()/NetNeedsInputUpgrade(), copied verbatim fromsrc/caffe/util/upgrade_proto.cpprepro_caffe.cppβ verification harnesscaffe.protoβ real, unmodified proto schema fetched at the pinned commitoob_trigger.caffemodel/oob_trigger.prototxt/oob_trigger_wide.caffemodelβ the original draft PoC's crafted filesrun_output_oob_trigger.txt/run_output_oob_trigger_wide.txtβ captured run output from the 2026-07-10 draft session (Abseil-based protobuf ~5.x build)final_run_trigger.txt/final_run_wide.txtβ captured run output from the 2026-07-12 independent re-verification below (real Kali/Debian-packagedlibprotobuf3.21.12)
Build & run
protoc --cpp_out=. caffe.proto
g++ -std=c++17 -O0 -g -fsanitize=address,undefined -I. -I<protobuf-src> \
-o repro_caffe_asan repro_caffe.cpp caffe.pb.cc <libprotobuf.a and deps>
./repro_caffe_asan oob_trigger.caffemodel
2026-07-12 independent re-verification (fresh rebuild, real Debian/Kali-packaged libprotobuf)
Re-verified independently from a completely fresh build tree, deliberately not reusing the 2026-07-10 session's protobuf build (which had since been repurposed/modified for an unrelated AFL-instrumented fuzzing effort). This time:
caffe.pb.{h,cc}were regenerated from scratch withprotoc(protobuf-compiler package) against the same real, unmodifiedcaffe.protoat the pinned commit.- Compiled/linked against the real, officially Kali/Debian-packaged
libprotobuf3.21.12 (libprotobuf32t64,libprotobuf-devβ fetched withapt-get download+ extracted withdpkg-deb -x, no root/system install needed), not a hand-built or previously-modified tree. - Built two variants (
repro_caffe_asan_debug,repro_caffe_asan_release) with-fsanitize=address,undefined;lddon the resulting binary confirmslibasan.so.8/libubsan.so.1are linked, andlibprotobuf.ais statically linked in (no separatelibprotobuf.sodependency). - Additionally probed whether compiling the harness with
-DNDEBUGchanges the outcome β it does not.nm -Con the packagedlibprotobuf.ashows:
i.e.U google::protobuf::RepeatedField<int>::Get(int) const 0000000000000000 W google::protobuf::RepeatedField<int>::Get(int) constRepeatedField<int32_t>is explicitly instantiated inside the prebuiltlibprotobufitself (extern template class RepeatedField<int32_t>upstream) β so the bounds-check behavior observed is fixed by however the linked protobuf library was built, independent of anyNDEBUG/optimization flags on Caffe's or the harness's own compilation units.
Fresh, live-rerun output (this session, 2026-07-12):
$ ./repro_caffe_asan_release oob_trigger.caffemodel
[load] read 21 bytes from oob_trigger.caffemodel
[parse] NetParameter::ParseFromString() => true
[parse] name="oob_trigger" input_size()=1 input_dim_size()=1 input_shape_size()=0
input(0) = "data"
input_dim(0) = 1
[call] NetNeedsInputUpgrade(param) = true
[call] UpgradeNetInput(¶m); <-- REAL function, real protobuf RepeatedField
[libprotobuf FATAL ./google/protobuf/repeated_field.h:654] CHECK failed: (index) < (current_size_):
terminate called after throwing an instance of 'google::protobuf::FatalException'
what(): CHECK failed: (index) < (current_size_):
exit=134 (SIGABRT)
Negative control, same binary:
$ ./repro_caffe_asan_release oob_trigger_wide.caffemodel
[parse] name="oob_trigger_wide" input_size()=10 input_dim_size()=0 input_shape_size()=0
[call] NetNeedsInputUpgrade(param) = true
[call] UpgradeNetInput(¶m); <-- REAL function, real protobuf RepeatedField
[done] UpgradeNetInput returned without crashing this run
[done] resulting layer_size()=0
exit=0
This confirms the same logical crash site as the 2026-07-10 draft session
(.../repeated_ptr_field.h:140] Check failed: index < size (1 vs. 1) there vs.
repeated_field.h:654] CHECK failed: (index) < (current_size_) here β same bug, different
protobuf build's exact check/message) via two independent, unrelated protobuf builds. The
underlying missing-bounds-check bug in Caffe's own UpgradeNetInput() is definitively real and
reproducible; only the exact abort message / crash-vs-silent-read manifestation depends on which
protobuf version is linked (see caveat above re: old ~3.x-era protobuf with NDEBUG).
Dedup note
No CVE or prior huntr report was found (as of 2026-07-10/2026-07-12 search) covering this specific
unchecked input_dim RepeatedField index in UpgradeNetInput(). BVLC/caffe has had no commits
since 2020-02-13 (this exact pinned commit), so no upstream fix exists and none is expected.