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):
```cpp
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` vector** → `allPrimitiveFunctions` 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:
```cpp
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`.