EnigmaConsultant's picture
Upload README.md with huggingface_hub
9455b58 verified
|
Raw
History Blame Contribute Delete
7.06 kB

CWE-825 / CWE-476: Invalid-iterator dereference in CNTK v2 CompositeFunction::DeserializeBlockComposite

Unchecked *std::find_if(... end()) on the root-function lookup crashes on any crafted native .model file.

Target

  • Project: Microsoft Cognitive Toolkit (CNTK)
  • Component: Source/CNTKv2LibraryDll/CompositeFunction.cpp — native CNTK v2 model (checkpoint) deserializer
  • Version: current master (archived project; the vulnerable code is present in the released 2.7 line through HEAD)
  • Entry point: CNTK::Function::Load(filepath, DeviceDescriptor, ...) with format ModelFormat::CNTKv2
  • Class: CWE-825 (Expired Pointer / invalid iterator dereference), CWE-476 (NULL Pointer Dereference)

Root cause

CompositeFunction::DeserializeBlockComposite resolves the composite graph's root function by dereferencing the result of std::find_if without ever comparing it to end() (CompositeFunction.cpp, ~lines 203-206, current master):

FunctionPtr root = *std::find_if(allPrimitiveFunctions.begin(), allPrimitiveFunctions.end(),
    [&rootUid](const FunctionPtr& func) {
        return func->Uid() == rootUid;
    });
  • rootUid is taken from the model-controlled rootKey of the composite Dictionary.
  • allPrimitiveFunctions is a std::unordered_set<FunctionPtr> (FunctionPtr == std::shared_ptr<Function>) built from the model-controlled functionsKey vector in CompositeFunction::Deserialize (CompositeFunction.cpp ~lines 251-289).

If rootUid matches no deserialized function, std::find_if returns end(). Dereferencing that past-the-end iterator of the unordered_set reads through the container's null terminal hash node and copy-constructs a std::shared_ptr<Function> from it — a read of near-null memory (offset 0x8).

This is trivially reachable:

  • an empty functions vectorallPrimitiveFunctions is empty → find_if immediately returns end(), or
  • any rootKey uid string that does not correspond to some function's Uid().

ValidateDictionary (CompositeFunction.cpp ~196/233) only checks required-key presence, the type tag, and the version. It performs no cross-reference that the declared root actually exists among the functions, so nothing guards the dereference.

Reachability (source trace)

Function::Load(filepath, CNTKv2)          Function.cpp:508-519
  -> operator>>(std::istream&, Dictionary&)
  -> Serializer::Read
  -> Function::Deserialize                Function.cpp:1093-1095
  -> CompositeFunction::Deserialize       CompositeFunction.cpp:229
  -> DeserializeBlockComposite            CompositeFunction.cpp:296 -> 190
  -> *std::find_if(...)  <-- crash        CompositeFunction.cpp:203

A malicious native CNTK v2 .model whose top-level composite Dictionary has type=CompositeFunction, an empty inputs vector, an empty functions vector, a uid, and a rootKey set to any string reaches the dereference of end() on the empty allPrimitiveFunctions set.

Proof of concept

A full CNTK build is infeasible in this environment (archived project requiring MKL / Boost / OpenCV / pinned protobuf; only ~7 GB free on /), matching the approach used for the three prior verified CNTK findings by this researcher. Instead, a faithful harness reproduces the exact vulnerable lines verbatim: the same std::find_if lambda over the same container type std::unordered_set<std::shared_ptr<Function>> (CNTK's FunctionPtr), with Function exposing the same Uid() surface. Only Function/FunctionPtr are reduced to a minimal stand-in; the crashing statement is copied character-for-character from CompositeFunction.cpp:203-206.

  • Harness: harness.cpp (in this repo)
  • Build: g++ 15.2 -fsanitize=address -O0 -std=c++14 harness.cpp -o harness
  • TRIGGER (empty set, no matching rootUid): AddressSanitizer SEGV.
  • NEGATIVE CONTROL (insert a Function whose Uid() == rootUid, so find_if returns a valid iterator): returns cleanly, exit 0, use_count=2 — proving the crash is specifically the unchecked end() dereference and not an artifact of the harness.

Captured evidence (verbatim)

=== NEGATIVE CONTROL (matching root uid) ===
[control] set size=1
returned root use_count=2 ptr=0x7b7823be0020
control exit=0
=== TRIGGER (empty set / no matching root uid) ===
[trigger] set size=0 (empty) -> find_if returns end()
AddressSanitizer:DEADLYSIGNAL
==1100204==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000008 (pc 0x5604be983bdd bp 0x7fffe078c2f0 sp 0x7fffe078c2e0 T0)
==1100204==The signal is caused by a READ memory access.
==1100204==Hint: address points to the zero page.
    #0 ... std::__shared_ptr<Function, ...>::__shared_ptr(... const&) /usr/include/c++/15/bits/shared_ptr_base.h:1529
    #1 ... std::shared_ptr<Function>::shared_ptr(std::shared_ptr<Function> const&) /usr/include/c++/15/bits/shared_ptr.h:203
    #2 ... DeserializeBlockComposite_vuln harness.cpp:46   (== CompositeFunction.cpp:203 verbatim)
    #3 ... main harness.cpp:74
SUMMARY: AddressSanitizer: SEGV harness.cpp:46 in DeserializeBlockComposite_vuln
==1100204==ABORTING
Registers: rax=0x0000000000000008 rsi=0x0000000000000008  (read of past-the-end null hash-node value at offset 0x8)

The crash was re-reproduced during packaging (fresh build, g++ 15.2, ASan): SEGV on address 0x000000000008, READ access, same std::__shared_ptr copy-constructor frame reached from DeserializeBlockComposite_vuln; the control path returns exit 0 with use_count=2.

Impact

Denial of service (guaranteed crash) on loading an untrusted native CNTK v2 .model file via the public Function::Load API. The dereference reads attacker-influenceable near-null memory during a shared_ptr copy; the reference-count manipulation on a fabricated control block is a memory-safety violation with potential for worse-than-DoS behavior depending on heap layout.

Suggested fix

Compare the iterator against end() before dereferencing and raise a deserialization error if the declared root uid is not found:

auto it = std::find_if(allPrimitiveFunctions.begin(), allPrimitiveFunctions.end(),
    [&rootUid](const FunctionPtr& func) { return func->Uid() == rootUid; });
if (it == allPrimitiveFunctions.end())
    RuntimeError("DeserializeBlockComposite: root function uid '%S' not found among deserialized functions", rootUid.c_str());
FunctionPtr root = *it;

Deduplication

Distinct from the three prior CNTK findings by this researcher:

  1. DictionaryValue::Value<T>() null-pointer dereference (DictionaryValue type-tag handling),
  2. ONNX-import UnpackDouble out-of-bounds read (ONNX import path),
  3. NDShape::TotalSize allocation bomb (shape arithmetic).

This is an unchecked-iterator (find_if == end()) invalid dereference in the composite-function graph deserializer — unrelated to DictionaryValue tagging, ONNX import, or shape arithmetic. No known CVE covers CompositeFunction::DeserializeBlockComposite.