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

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:

  1. 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.

  2. 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.


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:

  1. d_array = new float[numRows * numCols] β†’ allocates based on overflowed product (small)
  2. Loop reads numRows * numCols (same small overflowed count) floats β†’ temp array sized correctly
  3. SetValue(numRows, numCols, d_array) β†’ calls Resize(numRows, numCols)
  4. Resize allocates small buffer, stores original m_numRows=3, m_numCols=huge
  5. 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): raw size_t multiply, no check
  • CPUMatrix::operator>> (CPUMatrix.h:536): new ElemType[numRows * numCols] with file-supplied values, no check
  • NDShape::TotalSize() (CNTKLibrary.h:359-363): unchecked product of attacker-controlled dims
  • GetNumElements() (CommonMatrix.h:593): return m_numRows * m_numCols; β€” same overflow
  • LocateColumn/LocateElement (CPUMatrixImpl.h:1615-1628): bounds checks are assert() only (debug build no-ops)
  • SetBuffer second overflow (CPUMatrixImpl.h:1557): numElements * sizeof(ElemType) also overflows
  • CPUSparseMatrix may have related issues in its Allocate path
  • 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 LocateElement at arbitrary offsets past the heap buffer. The overflow content (model weight data) and overflow distance (via m_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::vector internals β€” 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 (cntk pip 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

  1. 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;
    
  2. operator>>: Validate numRows/numCols against reasonable bounds before allocation:

    if (numRows > MAX_MATRIX_DIM || numCols > MAX_MATRIX_DIM)
        RuntimeError("Matrix dimensions exceed safe limits");
    
  3. NDShape::TotalSize: Use checked multiplication:

    for (auto dim : m_shapeDims) {
        if (dim > 0 && totalSize > SIZE_MAX / dim)
            RuntimeError("NDShape::TotalSize overflow");
        totalSize *= dim;
    }
    
  4. 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).

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support