YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Second distinct OOB in Caffe upgrade path: unchecked input_shape(i) index in UpgradeNetInput() has_shape branch (CWE-125)
Target: BVLC/caffe β src/caffe/util/upgrade_proto.cpp, function UpgradeNetInput()
Pinned commit: 9b891540183ddc834a02b2bd81b31afae71b2153 (last commit on master, 2020-02-13)
Class: CWE-125 Out-of-bounds Read
Reachability: every model load β ReadNetParamsFromTextFileOrDie / ReadNetParamsFromBinaryFileOrDie β UpgradeNetAsNeeded β NetNeedsInputUpgrade (input_size() > 0) β UpgradeNetInput
Summary
UpgradeNetInput() converts the legacy top-level input / input_shape / input_dim
fields of a NetParameter into an Input layer. The conversion loop is indexed by
input_size(), but in the has_shape branch it indexes the separate input_shape
repeated field with that same index:
// src/caffe/util/upgrade_proto.cpp (verbatim, pinned commit)
bool has_shape = net_param->input_shape_size() > 0;
bool has_dim = net_param->input_dim_size() > 0;
if (has_shape || has_dim) {
LayerParameter* layer_param = net_param->add_layer();
layer_param->set_name("input");
layer_param->set_type("Input");
InputParameter* input_param = layer_param->mutable_input_param();
for (int i = 0; i < net_param->input_size(); ++i) { // loop bound = input_size()
layer_param->add_top(net_param->input(i));
if (has_shape) {
input_param->add_shape()->CopyFrom(net_param->input_shape(i)); // <-- line 983: OOB read
} 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)); // line 990: the *other*, known bug
}
}
}
...
input (proto field 3, repeated string) and input_shape (proto field 8,
repeated BlobShape) are two independent repeated fields on the wire. Nothing
enforces input_shape_size() == input_size(). When has_shape is true
(input_shape_size() > 0) but input_shape_size() < input_size(), the accessor
net_param->input_shape(i) for i >= input_shape_size() reads past the end of the
input_shape RepeatedPtrField. In a release build (no bounds assertion) this is an
out-of-bounds read of a BlobShape* slot that is then dereferenced by CopyFrom.
A trigger is a realistic malformed deploy prototxt / caffemodel that declares more
input: names than input_shape {} blocks β e.g. two inputs but only one shape.
Distinction from the already-known input_dim OOB (not a duplicate)
This is a different bug from the well-worn input_dim(j) OOB at line 990:
| This finding (line 983) | Known input_dim bug (line 990) |
|
|---|---|---|
| Branch | if (has_shape) |
else (has_dim && !has_shape) |
| Field read OOB | input_shape β RepeatedPtrField<BlobShape> (field 8) |
input_dim β RepeatedField<int32> (field 4) |
| Trigger condition | 0 < input_shape_size() < input_size() |
input_dim_size() < 4 * input_size() |
| Guarding predicate | has_shape true |
has_shape false, has_dim true |
The two branches are mutually exclusive (if (has_shape) {...} else {...}). Any PoC
for the input_dim bug has input_shape_size() == 0, so has_shape is false and it
never enters the branch that contains this defect. Reaching this code requires
input_shape_size() > 0, which by construction excludes the input_dim path. They are
distinct sinks reading distinct fields under distinct predicates.
PoC
The harness runs against the real toolchain:
caffe.pb.cc/caffe.pb.hgenerated byprotoc 36.0.0from the unmodifiedcaffe.protofetched from BVLC/caffe at the pinned commit β real generatedNetParameter/BlobShape/InputParameterclasses, not reimplemented.- Linked against the matching real
libprotobuf.aruntime, soRepeatedPtrField::Get()bounds behavior is the real protobuf implementation. - The verbatim
UpgradeNetInput()/NetNeedsInputUpgrade()bodies copied unmodified fromupgrade_proto.cpp(upgrade_net_input_real.inc). - Compiled with
-fsanitize=address,undefined.
The harness calls NetParameter::ParseFromString() on the PoC bytes and then the real
UpgradeNetInput() β exactly the real file-load path
(ReadProtoFromBinaryFile β ParseFromArray β UpgradeNetAsNeeded).
shape_oob.caffemodel (34 bytes): input = ["data", "extra"] (input_size = 2),
one input_shape { dim:1 dim:3 dim:224 dim:224 } (input_shape_size = 1), no
input_dim. At iteration i = 1, net_param->input_shape(1) is called against a
RepeatedPtrField of real size 1.
Negative control shape_ok.caffemodel (39 bytes): input_size = 2,
input_shape_size = 2 β runs UpgradeNetInput to completion, correctly emitting an
Input layer with both shapes, exit 0. This isolates the fault to the
input_size / input_shape count mismatch.
The build used here links a bounds-asserting protobuf, so the OOB access is caught
deterministically as a RuntimeAssertInBounds failure (SIGABRT, exit 134) instead of
a silent bad read β a clean, unambiguous witness that index i = 1 is out of bounds for
the size-1 input_shape field. In a stock release build the same access is an
unchecked out-of-bounds read/deref.
Captured evidence (verbatim)
### PoC (input_size=2, input_shape_size=1) -> CRASH
[load] read 34 bytes from shape_oob.caffemodel
[parse] NetParameter::ParseFromString() => true
[parse] name="shape_oob" input_size()=2 input_dim_size()=0 input_shape_size()=1
input(0) = "data"
input(1) = "extra"
[call] NetNeedsInputUpgrade(param) = true
[call] UpgradeNetInput(¶m); <-- REAL function, real protobuf RepeatedField
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
F0000 00:00:1784089412.559406 1305953 repeated_ptr_field.h:140] Check failed: index < size (1 vs. 1)
*** Check failure stack trace: ***
@ 0x5603a980a4b3 absl::lts_20250512::log_internal::LogMessage::SendToLog()
@ 0x5603a980a03c absl::lts_20250512::log_internal::LogMessage::Flush()
@ 0x5603a89dbfaa google::protobuf::internal::RuntimeAssertInBounds()
@ 0x5603a89d210b UpgradeNetInput()
@ 0x5603a89d6cd7 main
@ 0x7ffb17031f77 (unknown)
@ 0x7ffeb09cc75e (unknown)
exit=134
### Negative control (input_size=2, input_shape_size=2) -> OK
[load] read 39 bytes from shape_ok.caffemodel
[parse] NetParameter::ParseFromString() => true
[parse] name="shape_ok" input_size()=2 input_dim_size()=0 input_shape_size()=2
input(0) = "data"
input(1) = "extra"
[call] NetNeedsInputUpgrade(param) = true
[call] UpgradeNetInput(¶m); <-- REAL function, real protobuf RepeatedField
[done] UpgradeNetInput returned without crashing this run
[done] resulting layer_size()=1
layer[0] input_param.shape(0).dim = [1 3 8 8 ]
layer[0] input_param.shape(1).dim = [1 3 8 8 ]
exit=0
The Check failed: index < size (1 vs. 1) in repeated_ptr_field.h:140, framed by
RuntimeAssertInBounds() directly beneath UpgradeNetInput(), confirms the out-of-bounds
index into the input_shape RepeatedPtrField at loop iteration i = 1.
Impact
A crafted .caffemodel / deploy .prototxt with more input: entries than
input_shape {} blocks triggers an out-of-bounds read (and pointer dereference) during
the mandatory legacy-upgrade step of every model load. Denial of service on any
consumer that loads untrusted Caffe models; in a release build the OOB BlobShape* read
is unchecked and its consequences depend on adjacent heap contents.
Suggested fix
Bound the shape index by input_shape_size() (and reject / clamp the mismatch), e.g.
guard has_shape per-iteration with i < net_param->input_shape_size(), or validate
input_shape_size() == input_size() before entering the loop.
Files in this repo
repro_caffe.cppβ verification harness (parses bytes, calls realUpgradeNetInput)upgrade_net_input_real.incβ verbatimUpgradeNetInput()/NetNeedsInputUpgrade()bodiesgen_poc.cpp/gen_good.cppβ generators for the two model filesshape_oob.caffemodel(34 B) β PoC triggershape_ok.caffemodel(39 B) β negative controlrun_output_shape_oob.txtβ captured run output (verbatim above)caffe.protoβ unmodified proto at pinned commitbuild.shβ build steps
Dedup note
Distinct from the input_dim(j) OOB in the mutually-exclusive else branch of the same
function (tracked separately as huntr-poc-caffe-oob-read). No CVE is known to cover the
input_shape(i) has_shape-branch index specifically; existing public discussion of
UpgradeNetInput OOBs concerns the input_dim path. This report targets the independent
input_shape field-count mismatch.