Skip to content

Raise OverflowError on out-of-range c_int/c_byte kernel arguments#2402

Open
rparolin wants to merge 2 commits into
NVIDIA:mainfrom
rparolin:bindings/raise-on-kernel-arg-overflow
Open

Raise OverflowError on out-of-range c_int/c_byte kernel arguments#2402
rparolin wants to merge 2 commits into
NVIDIA:mainfrom
rparolin:bindings/raise-on-kernel-arg-overflow

Conversation

@rparolin

Copy link
Copy Markdown
Collaborator

Prepared with the assistance of an automated agent (Claude Code / Claude Opus 4.8). Please review carefully.

Summary

An out-of-range Python int passed as a c_int / c_byte kernel argument was silently narrowed — e.g. 2**32 + 5 was stored as 5, so the kernel ran with a value the caller never intended, with no error or warning.

The c_int/c_byte param feeders now range-check the value and raise OverflowError instead. Implementation notes:

  • A single code path across all supported Pythons via PyLong_AsLongAndOverflow (flags out-of-long values through its overflow out-param) plus an explicit 32-bit / 8-bit bounds check.
  • feed() is declared except? -1 in the .pxd so Cython actually propagates the exception — without that, the raise would be silently swallowed (the same class of bug).

Behavior change (please confirm)

This is intentionally stricter than ctypes' own behavior, which silently wraps out-of-range ints. Raising was chosen deliberately over matching ctypes.

Tests

test_kernelParams_c_int_out_of_range_raises (GPU): an in-range value still launches; 2**32+5 (c_int) and 200 (c_byte) now raise OverflowError. Verified on Python 3.14.

Context

Addresses Glasswing finding V9.1. Behavior and implementation reviewed/shaped by @leofang (the single-code-path PyLong_AsLongAndOverflow form was his call). Split out from the hardening batch in #2399 so it can be reviewed on its own.

An out-of-range Python int passed as a c_int/c_byte kernel argument was
silently narrowed (e.g. 2**32+5 -> 5). The feeders now range-check via
PyLong_AsLongAndOverflow + an explicit 32-bit/8-bit bounds check and raise
OverflowError; feed() is declared 'except? -1' so Cython propagates the
exception instead of swallowing it. Adds a GPU regression test.

Addresses Glasswing finding V9.1; behavior reviewed and shaped by @leofang
(single code path via PyLong_AsLongAndOverflow).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rparolin rparolin added this to the cuda.bindings next milestone Jul 21, 2026
@rparolin rparolin added bug Something isn't working cuda.bindings Everything related to the cuda.bindings module labels Jul 21, 2026
rparolin added a commit to rparolin/cuda-python that referenced this pull request Jul 21, 2026
…#2402)

The out-of-range c_int/c_byte -> OverflowError change is a distinct behavior
change; moved to a standalone PR so it can be reviewed separately from this
hardening batch. No functional change to the remaining hardening work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rparolin
rparolin requested a review from leofang July 21, 2026 23:02
@rparolin rparolin self-assigned this Jul 21, 2026
A reviewer asked why the manual check uses INT_MIN/INT_MAX rather than the
function's long-based overflow flag. Add a comment: the target slot is 32-bit
(8-bit for c_byte), and long is 64-bit on LP64, so overflow alone misses
2**31..2**63 -- the explicit int-range check catches those. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Comment on lines +74 to +91
// Bound to the 32-bit int slot, NOT to `long`. AsLongAndOverflow's
// `overflow` only flags values outside `long`, which is 64-bit on
// LP64 (Linux/macOS) -- so a value in 2**31..2**63 comes back with
// overflow==0 and would be silently truncated by (int)v. The
// explicit INT_MIN/INT_MAX check rejects it. When overflow!=0, v is
// the -1 sentinel (not the real value), so that case must be caught
// first, before trusting v.
int overflow = 0;
long v = PyLong_AsLongAndOverflow(value, &overflow);
if (overflow == 0 && v == -1 && PyErr_Occurred())
return -1; // non-overflow conversion error; exception already set
if (overflow != 0 || v < INT_MIN || v > INT_MAX)
{
PyErr_SetString(PyExc_OverflowError,
"Python int is out of range for a c_int (32-bit) kernel argument");
return -1;
}
*((int*)ptr) = (int)v;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python 3.13 and later have PyLong_AsInt, which already does exactly what we want. The usual way to handle this is to put a backport copy of the function here, and then gate on the Python version.

Below is the implementation directly from CPython, wrapped in a version guard. This should be added to the top of the file.

#if PY_VERSION_HEX < 0x030D0000
int
PyLong_AsInt(PyObject *obj)
{
    int overflow;
    long result = PyLong_AsLongAndOverflow(obj, &overflow);
    if (overflow || result > INT_MAX || result < INT_MIN) {
        PyErr_SetString(PyExc_OverflowError,
                        "Python int too large to convert to C int");
        return -1;
    }
    return (int)result;
}
#endif
Suggested change
// Bound to the 32-bit int slot, NOT to `long`. AsLongAndOverflow's
// `overflow` only flags values outside `long`, which is 64-bit on
// LP64 (Linux/macOS) -- so a value in 2**31..2**63 comes back with
// overflow==0 and would be silently truncated by (int)v. The
// explicit INT_MIN/INT_MAX check rejects it. When overflow!=0, v is
// the -1 sentinel (not the real value), so that case must be caught
// first, before trusting v.
int overflow = 0;
long v = PyLong_AsLongAndOverflow(value, &overflow);
if (overflow == 0 && v == -1 && PyErr_Occurred())
return -1; // non-overflow conversion error; exception already set
if (overflow != 0 || v < INT_MIN || v > INT_MAX)
{
PyErr_SetString(PyExc_OverflowError,
"Python int is out of range for a c_int (32-bit) kernel argument");
return -1;
}
*((int*)ptr) = (int)v;
v = PyLong_AsInt(value);
if (v == -1 && PyErr_Occurred())
return -1;
*((int*)ptr) = (int)v;

Comment on lines +111 to +123
// Same rationale as the c_int feeder above; here the slot is an
// 8-bit c_byte, so bound to INT8_MIN/INT8_MAX.
int overflow = 0;
long v = PyLong_AsLongAndOverflow(value, &overflow);
if (overflow == 0 && v == -1 && PyErr_Occurred())
return -1; // non-overflow conversion error; exception already set
if (overflow != 0 || v < INT8_MIN || v > INT8_MAX)
{
PyErr_SetString(PyExc_OverflowError,
"Python int is out of range for a c_byte (8-bit) kernel argument");
return -1;
}
*((int8_t*)ptr) = (int8_t)v;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one we do need to keep because it doesn't have an equivalent in CPython.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cuda.bindings Everything related to the cuda.bindings module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants