YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
CWE-476 NULL Pointer Dereference in CNTK v2 Dictionary/DictionaryValue Checkpoint Deserializer
Status: Verified (independent adversarial re-verification complete, ASan repro captured).
Target
- Project: Microsoft Cognitive Toolkit (CNTK) v2
- Repository: https://github.com/microsoft/CNTK
- Affected files:
Source/CNTKv2LibraryDll/Serialization.cppSource/CNTKv2LibraryDll/Variable.cppSource/CNTKv2LibraryDll/Learner.cppSource/CNTKv2LibraryDll/proto/CNTK.protoSource/CNTKv2LibraryDll/API/CNTKLibrary.h
- Commit basis: current
masterat time of research (CNTK is an archived/deprecated Microsoft project; no version tags have shipped changes to this code path).
Dedup note: This is a distinct bug from two other findings already filed by this researcher against CNTK:
EnigmaConsultant/cntk-onnximport-unpackdouble-oobβ an out-of-bounds read in the ONNX-importUnpackDoublepath.EnigmaConsultant/huntr-poc-cntk-ndshape-bombβ anNDShape::TotalSizeinteger/allocation bomb.
This third bug is a NULL-pointer dereference in the native CNTK v2 checkpoint deserializer (Dictionary/DictionaryValue), unrelated to ONNX import and unrelated to shape/allocation arithmetic.
Root Cause
A three-step gap between proto validation, storage, and consumption:
1. Serializer::CreateFromProto(const proto::NDArrayView& src) (Serialization.cpp:528-534) returns nullptr when src.data_type() / src.storage_format() fail the generated proto::NDArrayView::DataType_IsValid() / StorageFormat_IsValid() check.
NDArrayView.DataType has a gap in its valid values: only 0(Unknown), 1(Float), 2(Double), 4(Float16), 5(Int8), 6(Int16) are valid; 3 is undefined. Confirmed directly from the generated protobuf code:
inline bool NDArrayView_DataType_IsValid(int value) {
return 0 <= value && value <= 6 && ((119u >> value) & 1) != 0;
}
// 119 = 0b1110111 -> bit 3 is 0 -> data_type == 3 is "invalid" but still parses off the wire
2. Serializer::Copy(const proto::DictionaryValue&, DictionaryValue&) (Serialization.cpp:732-734), case NDArrayView:
dst.m_data.m_ptr = CreateFromProto(src.nd_array_view_value());
stores that nullptr with no null check, while dst.m_valueType has already been set to DictionaryValue::Type::NDArrayView a few lines above (Serialization.cpp:697). The type tag and the payload pointer become inconsistent: tag says "valid NDArrayView", pointer is null.
3. DictionaryValue::Value<T>() for pointer-backed types (API/CNTKLibrary.h:1640-1662, dereference at lines 1647 and 1660):
template <typename T>
const T& DictionaryValue::Value() const
{
VerifyType<T>();
...
return *(reinterpret_cast<T*>(m_data.m_ptr)); // <-- unconditional deref
}
VerifyType<T>() (CNTKLibrary.h:1714-1718) only checks GetValueType<T>() != m_valueType (the tag) and throws a RuntimeError on a type mismatch β it never checks m_data.m_ptr for null. The function then unconditionally dereferences the pointer.
Reachability (real, unmodified call sites)
Variable::Deserialize(),Source/CNTKv2LibraryDll/Variable.cpp:547:followed immediately byauto& value = dict[valueKey].Value<NDArrayView>();value.DeepClone(device, value.IsReadOnly())(Variable.cpp:551) β invoked for every Parameter/Constant variable whenever any native CNTK v2.model/checkpoint file is loaded, viaDictionary::Load -> Function::Load -> ... -> Variable::Deserialize.- A second reachable site exists at
Learner.cpp:496for optimizer-state checkpoints.
A crafted checkpoint whose stored tensor value has data_type=3 (or any other undefined enum int) on the wire causes steps 1-3 above to hand back a dangling/null NDArrayView reference, and the very next member access on it (IsReadOnly()) dereferences address 0.
PoC Methodology
A full CNTK build is infeasible in this environment (archived/deprecated project requiring MKL/Boost/OpenCV/a pinned old protobuf toolchain; local disk headroom was ~2GB). Verification instead used the real generated protobuf code plus the exact vulnerable CNTK source lines, copied verbatim, rather than any approximation:
- Copied
CNTK.protoverbatim fromSource/CNTKv2LibraryDll/proto/CNTK.proto(current master) and compiled it with a real localprotoc v31.1, producing the actual generatedCNTK.pb.h/.ccthat CNTK's own build would produce from this identical schema. - Wrote a standalone harness (
harness.cpp) that:- parses a real raw-wire-format serialized
proto::Dictionarypayload with the genuine generated parser (dict.ParseFromArray, matchingSerializer::Read/ParseMessage's non-2GB-prefixed path,Serialization.cpp:869-877); - reproduces, byte-for-byte,
Serializer::CreateFromProto(proto::NDArrayView)'s IsValid-check-and-return-nullptr (Serialization.cpp:530-534); - reproduces
DictionaryValue::Value<T>()'s unconditional*(reinterpret_cast<T*>(m_data.m_ptr))(CNTKLibrary.h:1647/1660), confirmed against the realVerifyType<T>()(CNTKLibrary.h:1714-1718), which has no null check; - reproduces the exact real call sequence from
Variable.cpp:547/551(dict[valueKey].Value<NDArrayView>()then.IsReadOnly()).
- parses a real raw-wire-format serialized
- Hand-crafted two raw protobuf wire payloads with an independent Python byte-encoder (
make_payloads.py, not using the CNTK/protobuf libraries to build them β only raw varint/tag encoding per theCNTK.protowire layout) representingDictionary{"value": DictionaryValue{type=NDArrayView, nd_array_view_value=NDArrayView{...}}}:payload_good.binβdata_type=1/Float, valid.payload_bad.binβdata_type=3, an undefined enum gap value.
- Built the harness with AddressSanitizer against the real generated
CNTK.pb.ccand ran both payloads.
Result
payload_good.bin(control) parses and completes normally: exit 0,IsReadOnly=0, no crash. Confirms a clean negative control β the harness does not spuriously crash on well-formed input.payload_bad.bin(28 bytes) reproducibly crashes with a genuine SIGSEGV/NULL dereference caught by ASan, at the exact reinterpret_cast-dereferenced-then-member-accessed pattern thatVariable.cpp:547/551performs in the real CNTK binary.
Captured evidence (verbatim)
=== GOOD (control), well-formed data_type=1/Float ===
Parsed native CNTK Dictionary with 1 entries (version=1)
nd_array_view_value.data_type on the wire = 1 (IsValid=1)
CreateFromProto result pointer = 0x7b43bade0070
Calling dict["value"].Value<NDArrayView>() (Variable.cpp:547) ...
(unexpectedly reached past the deref: IsReadOnly=0)
Loaded parameter value, IsReadOnly=0 -- no crash.
exit=0
=== BAD (crash trigger), data_type=3 (undefined enum gap value) ===
Parsed native CNTK Dictionary with 1 entries (version=1)
nd_array_view_value.data_type on the wire = 3 (IsValid=0)
CreateFromProto result pointer = (nil)
Calling dict["value"].Value<NDArrayView>() (Variable.cpp:547) ...
AddressSanitizer:DEADLYSIGNAL
=================================================================
==354301==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x56310c5187b4 bp 0x7ffe5b3ec3f0 sp 0x7ffe5b3ec3e0 T0)
==354301==The signal is caused by a READ memory access.
==354301==Hint: address points to the zero page.
#0 0x56310c5187b4 in NDArrayView::IsReadOnly() const harness.cpp:79
#1 0x56310c51689f in DeserializeParameterValue harness.cpp:180
#2 0x56310c516f6e in main harness.cpp:243
#3 0x7f5f9f031f76 (/usr/lib/x86_64-linux-gnu/libc.so.6+0x29f76)
#4 0x7f5f9f032026 in __libc_start_main (/usr/lib/x86_64-linux-gnu/libc.so.6+0x2a026)
#5 0x56310c5165a0 in _start
SUMMARY: AddressSanitizer: SEGV harness.cpp:79 in NDArrayView::IsReadOnly() const
==354301==ABORTING
Impact
Any application that loads a CNTK v2 native checkpoint (.model file) from an untrusted source β a very common trust boundary for ML model files β can be crashed (denial of service) by supplying a checkpoint containing a single NDArrayView value with an out-of-range data_type (or storage_format) enum on the wire. The crash occurs deep inside the deserializer before any application-level validation of tensor contents can run, and the same pattern reaches any DictionaryValue::Value<T>() consumer downstream of Serializer::Copy, so the blast radius includes both model loading (Variable::Deserialize) and optimizer-state/checkpoint loading (Learner.cpp:496).
Suggested Fix
In Serializer::Copy(const proto::DictionaryValue&, DictionaryValue&) (Serialization.cpp), check the result of CreateFromProto(...) for nullptr before assigning it to dst.m_data.m_ptr, and either throw a well-formed deserialization error or leave dst in a state that will not be treated as containing a valid NDArrayView. Defense in depth: DictionaryValue::Value<T>() / VerifyType<T>() in API/CNTKLibrary.h should also assert/throw on a null m_data.m_ptr for pointer-backed types rather than unconditionally dereferencing.
Known CVEs / Prior Work
No CVE was found specifically covering this Dictionary/DictionaryValue null-propagation path in CNTK v2's native serializer at time of writing. This finding is unrelated to any known CVE in CNTK's ONNX import path.