Downloads · 30 days
0
Talson/mfv-cntk-resize-overflow
mfv-cntk-resize-overflow is a machine learning model from Talson. Use it for the machine learning task on the model card, and read the license before you ship it in a product.
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 Ove…
Downloads · 30 days
0
Access
Public
Updated Jun 12, 2026
Repo size
—
Likes
0
Public
Click a slice to open those files.
.md12.2 KB · 40%
From the Hugging Face model README
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.
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 is CPUMatrix::operator>> which reads numRows and numCols as raw size_t values from the file.
Modern protobuf format (.model, CNTK v2 API): Uses NDShape with 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.
| # | 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.
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):
size_t: 18446744073709551618 mod 2^64 = 2So Resize allocates 2 elements (8 bytes for float), but stores m_numRows = 3 and m_numCols = 6148914691236517206.
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 = 3operator()(0, 1) = Data()[3] → heap-buffer-overflow WRITEThis happens in ALL matrix operations after model load: matmul, transpose, copy, print — they all index through LocateElement.
==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)
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)numRows * numCols (same small overflowed count) floats → temp array sized correctlySetValue(numRows, numCols, d_array) → calls Resize(numRows, numCols)m_numRows=3, m_numCols=hugeLocateElement → OOB WRITE==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)
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.
Microsoft/CNTK has zero integer overflow protection in its matrix dimension arithmetic. Key observations:
CPUMatrix::Resize (CPUMatrixImpl.h:1544): raw size_t multiply, 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 are assert() only (debug build no-ops)SetBuffer second overflow (CPUMatrixImpl.h:1557): numElements * sizeof(ElemType) also overflowsCPUSparseMatrix may have related issues in its Allocate pathLocateElement at arbitrary offsets past the heap buffer. The overflow content (model weight data) and overflow distance (via m_numRows * col) are both attacker-controlled.Function::Load + Evaluate) triggers matrix operations immediately.std::vector internals — all containing function pointers or size fields corruptible for RCE.10a8ffc, archived 2022-09-23): AFFECTED, all entry points.cntk pip package) that use the C++ backend are also affected.# 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
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.
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).