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.

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

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 of master β€” 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 RepeatedField index out of range), reachable via ordinary .caffemodel/.prototxt loading.

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(&param);  <-- 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(&param);
[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 β€” real UpgradeNetInput()/NetNeedsInputUpgrade(), copied verbatim from src/caffe/util/upgrade_proto.cpp
  • repro_caffe.cpp β€” verification harness
  • caffe.proto β€” real, unmodified proto schema fetched at the pinned commit
  • oob_trigger.caffemodel / oob_trigger.prototxt / oob_trigger_wide.caffemodel β€” the original draft PoC's crafted files
  • run_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-packaged libprotobuf 3.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 with protoc (protobuf-compiler package) against the same real, unmodified caffe.proto at the pinned commit.
  • Compiled/linked against the real, officially Kali/Debian-packaged libprotobuf 3.21.12 (libprotobuf32t64, libprotobuf-dev β€” fetched with apt-get download + extracted with dpkg-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; ldd on the resulting binary confirms libasan.so.8 / libubsan.so.1 are linked, and libprotobuf.a is statically linked in (no separate libprotobuf.so dependency).
  • Additionally probed whether compiling the harness with -DNDEBUG changes the outcome β€” it does not. nm -C on the packaged libprotobuf.a shows:
                     U google::protobuf::RepeatedField<int>::Get(int) const
    0000000000000000 W google::protobuf::RepeatedField<int>::Get(int) const
    
    i.e. RepeatedField<int32_t> is explicitly instantiated inside the prebuilt libprotobuf itself (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 any NDEBUG/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(&param);  <-- 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(&param);  <-- 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.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support