YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
- MFV β CNTK v2 model file: CPUMatrix::Resize integer overflow β heap-buffer-overflow WRITE
- Threat model (why this is in MFV scope)
- Summary of findings
- Root Cause β CPUMatrix::Resize integer overflow
- Entry Point 1: Legacy binary format β operator>>
- Entry Point 2: Modern protobuf format β NDShape::TotalSize
- General hardening assessment
- Exploit Primitives
- Affected versions
- Reproduction
- Remediation
- Novelty / dedup
- Suggested CVSS
- Threat model (why this is in MFV scope)
MFV β CNTK v2 model file: CPUMatrix::Resize integer overflow β heap-buffer-overflow WRITE
Target format (huntr): CNTK v2 ($1500 tier, "Model File Formats")
Affected project: microsoft/CNTK β Microsoft Cognitive Toolkit (~17.5k stars, archived)
Class: CWE-190 Integer Overflow β CWE-122 Heap-based Buffer Overflow β attacker-controlled matrix dimensions from a CNTK model file cause integer overflow in size_t multiplication, producing an undersized allocation. Subsequent element access via column-major indexing writes past the heap buffer.
Status: TOOL-VERIFIED under AddressSanitizer (heap-buffer-overflow WRITE). 2026-06-12.
Threat model (why this is in MFV scope)
CNTK models are loaded via two formats:
Legacy binary format (
.dnn,.cntk, custom extensions): marker-delimited (BMAT/EMAT,BCN/ECN) sections containing raw matrix data. The standard load path isCPUMatrix::operator>>which readsnumRowsandnumColsas rawsize_tvalues from the file.Modern protobuf format (
.model, CNTK v2 API): UsesNDShapewith attacker-controlled dimension arrays.NDShape::TotalSize()multiplies all dimensions with no overflow check.
Both paths feed into CPUMatrix::Resize() which performs the vulnerable size_t multiplication. Loading an untrusted CNTK model file causes memory corruption during the first matrix operation after load.
Summary of findings
| # | Bug | Primitive | Function / line | Trigger | Verified |
|---|---|---|---|---|---|
| 1 | CPUMatrix::Resize numRows*numCols overflow |
heap-buffer-overflow WRITE | CPUMatrixImpl.h:1544 |
Matrix dims from model file | β ASAN |
| 2 | NDShape::TotalSize unchecked product |
same overflow, alternate entry | CNTKLibrary.h:359-363 |
Shape dims from protobuf model | β (same root cause) |
| 3 | operator>> deserialization |
same overflow, file load path | CPUMatrix.h:536 |
numRows/numCols from BMAT section |
β ASAN |
All three share one root cause: unchecked size_t multiplication of attacker-controlled dimensions, where the 64-bit overflow produces a value much smaller than the true product. This causes an undersized heap allocation, while the original (non-overflowed) dimensions are stored in m_numRows/m_numCols and used for element indexing β creating a massive heap-buffer-overflow WRITE on any subsequent matrix access.
Root Cause β CPUMatrix::Resize integer overflow
CPUMatrixImpl.h:1537-1565 (CNTK @ 10a8ffc, 2022-09-23):
template <class ElemType>
void CPUMatrix<ElemType>::Resize(const size_t numRows, const size_t numCols, bool growOnly)
{
if (GetNumRows() == numRows && GetNumCols() == numCols)
return;
VerifyResizable(__func__);
size_t numElements = numRows * numCols; // *** BUG: size_t overflow, no check ***
if (numElements > GetSizeAllocated() || ...)
{
ElemType* pArray = nullptr;
if (numElements > 0)
pArray = NewArray<ElemType>(numElements); // undersized allocation
delete[] Buffer();
SetBuffer(pArray, numElements * sizeof(ElemType)); // second overflow
SetSizeAllocated(numElements);
}
m_sliceViewOffset = 0;
m_numRows = numRows; // stores ORIGINAL large value
m_numCols = numCols; // stores ORIGINAL large value
}
With numRows = 3, numCols = 0x5555555555555556 (6148914691236517206):
- True product: 3 Γ 6148914691236517206 = 18446744073709551618 (2^64 + 2)
- Overflowed
size_t: 18446744073709551618 mod 2^64 = 2
So Resize allocates 2 elements (8 bytes for float), but stores m_numRows = 3 and m_numCols = 6148914691236517206.
The OOB access: LocateColumn
CPUMatrixImpl.h:1615-1619:
inline size_t CPUMatrix<ElemType>::LocateColumn(const size_t col) const
{
// For performance reason avoid extra validation in release.
assert(col == 0 || col < GetNumCols()); // debug-only, no-op in release
return col * m_numRows; // column-major storage
}
LocateElement(row, col) = col * m_numRows + row (CPUMatrixImpl.h:1623-1628).
After the overflowed Resize, accessing element (0, 1):
LocateColumn(1) = 1 * 3 = 3- Buffer has 2 elements β index 3 is 1 element (4 bytes) past the allocation
operator()(0, 1) = Data()[3]β heap-buffer-overflow WRITE
This happens in ALL matrix operations after model load: matmul, transpose, copy, print β they all index through LocateElement.
ASAN Evidence (direct Resize path)
==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b96063e001c
at pc 0x55758de70704 bp 0x7ffd1892d0f0 sp 0x7ffd1892d0e8
WRITE of size 4 at 0x7b96063e001c thread T0
#0 test_resize_overflow() harness_resize.cpp:175
#1 main harness_resize.cpp:247
0x7b96063e001c is located 4 bytes after 8-byte region [0x7b96063e0010,0x7b96063e0018)
allocated by thread T0 here:
#0 operator new[](unsigned long)
#1 CPUMatrixFloat::Resize(unsigned long, unsigned long, bool) harness_resize.cpp:98
#2 test_resize_overflow() harness_resize.cpp:143
(full log: findings/cntk_evidence/resize_heap_overflow.txt)
Entry Point 1: Legacy binary format β operator>>
CPUMatrix.h:525-544:
friend File& operator>>(File& stream, CPUMatrix<ElemType>& us)
{
stream.GetMarker(fileMarkerBeginSection, std::wstring(L"BMAT"));
size_t elsize;
stream >> elsize;
// ...
size_t numRows, numCols;
stream >> matrixName >> format >> numRows >> numCols; // attacker-controlled
ElemType* d_array = new ElemType[numRows * numCols]; // overflowed allocation
for (size_t i = 0; i < numRows * numCols; ++i) // reads overflowed count
stream >> d_array[i];
stream.GetMarker(fileMarkerEndSection, std::wstring(L"EMAT"));
us.SetValue(numRows, numCols, d_array, matrixFlagNormal); // β Resize with originals
delete[] d_array;
return stream;
}
The attacker supplies arbitrary numRows and numCols as raw size_t values in the BMAT section of a CNTK legacy binary model file. The operator>> code performs no validation whatsoever on these values.
Flow:
d_array = new float[numRows * numCols]β allocates based on overflowed product (small)- Loop reads
numRows * numCols(same small overflowed count) floats β temp array sized correctly SetValue(numRows, numCols, d_array)β callsResize(numRows, numCols)- Resize allocates small buffer, stores original
m_numRows=3,m_numCols=huge - First matrix operation β
LocateElementβ OOB WRITE
ASAN Evidence (operator>> / SetValue path)
==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7ba090be003c
WRITE of size 4 at 0x7ba090be003c thread T0
#0 test_operator_stream_path() harness_resize.cpp:212
0x7ba090be003c is located 4 bytes after 8-byte region [0x7ba090be0030,0x7ba090be0038)
allocated by thread T0 here:
#0 operator new[](unsigned long)
#1 CPUMatrixFloat::Resize() harness_resize.cpp:98
#2 CPUMatrixFloat::SetValue() harness_resize.cpp:119
#3 test_operator_stream_path() harness_resize.cpp:204
(full log: findings/cntk_evidence/stream_deserialization_overflow.txt)
Entry Point 2: Modern protobuf format β NDShape::TotalSize
CNTKLibrary.h:354-364:
size_t TotalSize() const
{
if (HasUnboundDimension())
RuntimeError(...);
size_t totalSize = 1;
for (auto dim : m_shapeDims)
totalSize *= dim; // unchecked size_t overflow
return totalSize;
}
In the modern CNTK v2 protobuf model format, tensor shapes are stored as repeated dimension values. An attacker crafts dimensions like [3, 0x5555555555555556] whose product overflows. TotalSize() returns the overflowed value, which is then used in Serialization.cpp:237-238:
auto size = src.Shape().TotalSize(); // overflowed
dst->Resize((int)size, DstT()); // truncated to int, then Resize
The (int)size cast adds a SECOND truncation (64-bit β 32-bit) before reaching Resize, further reducing the allocation size.
General hardening assessment
Microsoft/CNTK has zero integer overflow protection in its matrix dimension arithmetic. Key observations:
CPUMatrix::Resize(CPUMatrixImpl.h:1544): rawsize_tmultiply, no checkCPUMatrix::operator>>(CPUMatrix.h:536):new ElemType[numRows * numCols]with file-supplied values, no checkNDShape::TotalSize()(CNTKLibrary.h:359-363): unchecked product of attacker-controlled dimsGetNumElements()(CommonMatrix.h:593):return m_numRows * m_numCols;β same overflowLocateColumn/LocateElement(CPUMatrixImpl.h:1615-1628): bounds checks areassert()only (debug build no-ops)SetBuffersecond overflow (CPUMatrixImpl.h:1557):numElements * sizeof(ElemType)also overflowsCPUSparseMatrixmay have related issues in itsAllocatepath- Project archived since 2023 β no security patches forthcoming
Exploit Primitives
- Write-what-where: After the overflowed Resize, the matrix data (attacker-controlled via model weights) is accessible through
LocateElementat arbitrary offsets past the heap buffer. The overflow content (model weight data) and overflow distance (viam_numRows * col) are both attacker-controlled. - Heap grooming: The attacker controls the full model file structure β number of layers, matrix sizes, load order β giving deterministic control over heap layout at the time of the OOB write.
- Trigger timing: The overflow occurs during model load or the first forward pass. CNTK's standard usage pattern (
Function::Load+Evaluate) triggers matrix operations immediately. - Adjacent objects: The heap region containing the undersized matrix buffer will have adjacent allocations for other model matrices, layer parameters, and
std::vectorinternals β all containing function pointers or size fields corruptible for RCE.
Affected versions
- microsoft/CNTK @ HEAD (
10a8ffc, archived 2022-09-23): AFFECTED, all entry points. - All downstream users of CNTK's CPUMatrix or the CNTK C++ library.
- The CNTK Python bindings (
cntkpip package) that use the C++ backend are also affected.
Reproduction
# Build harness with ASAN:
g++ -std=c++17 -fsanitize=address -g -O0 -o harness_resize harness_resize.cpp
# Test 1: Direct Resize overflow (core vulnerability)
./harness_resize
# Test 2: operator>> deserialization path (file load entry point)
./harness_resize stream
# Test 3: NDShape::TotalSize path (protobuf format entry point)
./harness_resize ndshape
Build: poc/mfv_cntk_resize.cpp
Build flags: g++ -std=c++17 -fsanitize=address -g -O0
Remediation
CPUMatrix::Resize: Add overflow check before multiplication:
if (numCols > 0 && numRows > SIZE_MAX / numCols) RuntimeError("CPUMatrix::Resize: numRows * numCols overflows size_t"); size_t numElements = numRows * numCols;operator>>: Validate numRows/numCols against reasonable bounds before allocation:
if (numRows > MAX_MATRIX_DIM || numCols > MAX_MATRIX_DIM) RuntimeError("Matrix dimensions exceed safe limits");NDShape::TotalSize: Use checked multiplication:
for (auto dim : m_shapeDims) { if (dim > 0 && totalSize > SIZE_MAX / dim) RuntimeError("NDShape::TotalSize overflow"); totalSize *= dim; }LocateColumn/LocateElement: Replace debug-only
assert()with runtime bounds checks.
Novelty / dedup
- No CVE found for integer overflow in CNTK's CPUMatrix::Resize or NDShape::TotalSize.
- No known huntr disclosure for CNTK v2 format.
- Known CNTK security issues are limited to general archived-project warnings.
- Dup risk: low. CNTK is archived and rarely audited for memory-safety bugs.
Suggested CVSS
AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H β 7.8 (local file, user loads model; heap overflow WRITE with attacker-controlled content and offset β RCE candidate).