YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
CWE-825/CWE-476: Unchecked std::find_if iterator dereference in CNTK v2 PrimitiveFunction::Deserialize block argument-map resolution (OOB read / near-null shared_ptr copy) on model load
Target
- Project: Microsoft Cognitive Toolkit (CNTK) v2 β
CNTKv2LibraryDll - Repo: https://github.com/microsoft/CNTK
- Version/commit: current
master(10a8ffc); CNTK v2 model deserializer - Vulnerable file:
Source/CNTKv2LibraryDll/PrimitiveFunction.cpp(PrimitiveFunction::Deserialize,op == PrimitiveOpType::Blockbranch, lines 1399β1416) - Entry point: public
CNTK::Function::Load(const std::wstring& filepath, DeviceDescriptor, ...)with CNTKv2 model format - Attack surface: untrusted CNTK-v2 model file loaded by a victim
Vulnerability
In the Block-op branch of PrimitiveFunction::Deserialize, the lambda
findCompositeArgumentByUid resolves each block argument-map key uid against the
inner composite's Arguments() by dereferencing the result of std::find_if
without ever comparing the returned iterator against end():
// Source/CNTKv2LibraryDll/PrimitiveFunction.cpp:1399-1404
auto compositeArguments = composite->Arguments();
auto findCompositeArgumentByUid = [&compositeArguments](const std::wstring& uid) {
return *std::find_if(compositeArguments.begin(), compositeArguments.end(),
[&uid](const Variable& argument) {
return (argument.Uid() == uid);
});
};
...
// later, over fully model-controlled keys:
for (size_t i = 0; i < blockArgumentsMapKeys.size(); ++i)
argumentsMap.push_back({ findCompositeArgumentByUid(blockArgumentsMapKeys[i]), // <-- OOB deref
uidToVariableMap.at(blockArgumentsMapValues[i]) });
blockArgumentsMapKeysis fully model-controlled β it is deserialized from theblockFunctionCompositeArgumentsMapKeysKeyvector of theBlockFunctiondictionary.compositeArgumentsiscomposite->Arguments()of the deserialized inner composite.- If a key uid matches no argument β in particular when the inner composite
has zero arguments β
std::find_ifreturnsend(), and*end()copy-constructs aVariableout of past-the-end / null memory. The copy reads the nullVariable'sstd::shared_ptr<VariableFields> m_dataFieldscontrol block:
// Source/CNTKv2LibraryDll/Variable.cpp:45-47
const std::wstring& Variable::Uid() const { return m_dataFields->m_uid; }
// Source/CNTKv2LibraryDll/API/CNTKLibrary.h:2117
VariableFieldsPtr m_dataFields; // == std::shared_ptr<VariableFields>
ValidateDictionary only checks required-key presence, type-tag, and version;
it never cross-references the block argument-map keys against the composite's
actual arguments, so nothing guards the dereference.
CWE-825 (dereference of a past-the-end iterator) / CWE-476 (copy of a
default-constructed null shared_ptr).
Reachability
Function::Load(path, CNTKv2)
-> operator>>(istream&, Dictionary&) -> Serializer::Read
-> Function::Deserialize -> CompositeFunction::Deserialize
for each function dict:
PrimitiveFunction::Deserialize(...) // op == Block
-> composite = DeserializeBlockComposite(...) // inner composite w/ 0 args
-> compositeArguments = composite->Arguments() // EMPTY
-> findCompositeArgumentByUid(blockArgumentsMapKeys[0])
-> *std::find_if(begin, end, pred) // pred never matches -> end()
-> copy-construct Variable from *end() // <-- OOB read + shared_ptr copy -> SIGSEGV
PoC / Verification method
A full CNTK build is infeasible in this environment (archived project; requires MKL / Boost / OpenCV / pinned protobuf toolchain) β matching the approach used for the five prior verified CNTK findings. Verification uses a faithful compiled harness that reproduces the crashing statement verbatim:
harness.cppcopiesPrimitiveFunction.cpp:1401-1403character-for-character β the same*std::find_if(compositeArguments.begin(), compositeArguments.end(), [uid]{ argument.Uid()==uid })over the exact container typestd::vector<Variable>.Variableis reduced to its crash-relevant layout only: a singlestd::shared_ptr<VariableFields> m_dataFields, withUid()copied verbatim (return m_dataFields->m_uid;).- TRIGGER: empty
compositeArgumentswith a key matching nothing (models a block whose inner composite has no arguments). - NEGATIVE CONTROL:
compositeArgumentscontains aVariablewhoseUid()equals the key.
Built three ways: production g++ -O2 -g -std=c++14 (matches CNTK Release), ASan
g++ -fsanitize=address -O0, and run under gdb. The trigger crashes reliably
(SIGSEGV, read of address 0x0); the control exits 0 in every build, proving
the crash is specifically the unchecked end() dereference and not a harness
artifact.
Captured evidence (verbatim)
PRODUCTION build (g++ -O2 -g -std=c++14 harness.cpp -o harness_prod)
[control] compositeArguments.size()=1, key present
control ok: resolved argument uid rank=38
control exit=0
[trigger] compositeArguments.size()=0 (empty) -> find_if returns end()
trigger exit=139 (139 = 128 + SIGSEGV)
gdb backtrace (prod)
Program received signal SIGSEGV, Segmentation fault.
#0 operator() (uid=L"x_placeholder_uid_that_matches_nothing", ...) at harness.cpp:93 (== PrimitiveFunction.cpp:1401-1403, *find_if)
#1 ResolveBlockArgument (compositeArguments=std::vector of length 0, capacity 0, ...) at harness.cpp:96
#2 main (...) at harness.cpp:122
$1 = (void *) 0x0 (si_addr β read of null address)
AddressSanitizer (g++ -fsanitize=address -O0)
[trigger] compositeArguments.size()=0 (empty) -> find_if returns end()
AddressSanitizer:DEADLYSIGNAL
==280525==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x55a134584be3 ...)
==280525==The signal is caused by a READ memory access.
==280525==Hint: address points to the zero page.
#0 std::__shared_ptr<VariableFields, (__gnu_cxx::_Lock_policy)2>::__shared_ptr(... const&) shared_ptr_base.h:1529
#1 std::shared_ptr<VariableFields>::shared_ptr(... const&) shared_ptr.h:203
#2 Variable::Variable(Variable const&) harness.cpp:67
#3 operator() harness.cpp:93 (== PrimitiveFunction.cpp:1401-1403)
#4 ResolveBlockArgument harness.cpp:96
#5 main harness.cpp:122
SUMMARY: AddressSanitizer: SEGV harness.cpp:67 in Variable::Variable(Variable const&)
==280525==ABORTING
(ASan negative control: exit 0)
Impact
Loading an untrusted CNTK v2 model file triggers an out-of-bounds / near-null
pointer dereference during deserialization, before any inference runs. Minimum
impact is a reliable loader-side denial of service (SIGSEGV). Because the
dereferenced address is *end() of a heap std::vector<Variable> β memory
immediately past a controlled allocation β under a suitable heap layout the read
target and the subsequently-copied shared_ptr control block are influenced by
adjacent heap contents, raising the ceiling above pure DoS to an
attacker-influenced invalid read on model load.
Suggested fix
Compare the std::find_if result against compositeArguments.end() before
dereferencing, and raise a deserialization error (as other CNTK deserializer
paths do) when a block argument-map key uid resolves to no composite argument:
auto it = std::find_if(compositeArguments.begin(), compositeArguments.end(),
[&uid](const Variable& argument) { return (argument.Uid() == uid); });
if (it == compositeArguments.end())
RuntimeError("Deserialize: block argument-map key '%S' does not match any composite argument.", uid.c_str());
return *it;
Additionally, ValidateDictionary for BlockFunction should cross-reference the
argument-map keys against the inner composite's arguments.
Dedup note
- No CVE is known for this specific unchecked-
find_if-dereference path in CNTK'sPrimitiveFunction::Deserializeblock argument-map resolution. - Distinct from the reporter's prior verified CNTK findings (different functions /
root causes):
cntk-ndshape-bomb,cntk-dictionaryvalue-nullderef,cntk-compositefunction-invalid-iterator,cntk-inferoutputs-arity-oob,cntk-onnximport-unpackdouble-oob. - CNTK is archived/in maintenance mode but the repository is public and the
vulnerable code is in current
master.
Files
harness.cppβ faithful minimal reproducer (crashing statement copied verbatim fromPrimitiveFunction.cpp:1401-1403)