Skip to content

Commit 0d22cb4

Browse files
rparolinclaude
andauthored
Raise OverflowError on out-of-range c_int/c_byte kernel arguments (#2402)
* Raise OverflowError on out-of-range c_int/c_byte kernel arguments 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> * param_packer: clarify why c_int/c_byte bound to int range, not long 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> * param_packer: use PyLong_AsInt for c_int range check (backport for <3.13) Address review feedback on #2402: replace the hand-rolled c_int bounds check with CPython's PyLong_AsInt, which does exactly the INT_MIN/INT_MAX range check we want and raises OverflowError itself. PyLong_AsInt only became public in Python 3.13, so add a file-local, version-gated backport (a copy of the CPython implementation, made `static` because this header is compiled into every extension module that includes it). The c_byte feeder has no CPython equivalent, so it keeps the explicit PyLong_AsLongAndOverflow bounds check; its comment is now self-contained rather than referencing the (now-changed) c_int feeder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e51fb7f commit 0d22cb4

3 files changed

Lines changed: 78 additions & 3 deletions

File tree

cuda_bindings/cuda/bindings/_lib/param_packer.h

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@
77
#include <functional>
88
#include <stdexcept>
99
#include <string>
10+
#include <climits>
11+
#include <cstdint>
12+
13+
// PyLong_AsInt entered the public/stable CPython API in 3.13. cuda.bindings
14+
// supports Python 3.10+, so provide a file-local backport for older builds.
15+
// This is a copy of the CPython implementation; it is `static` (unlike the
16+
// original) because this header is compiled into every extension module that
17+
// includes it, mirroring the other helpers below.
18+
#if PY_VERSION_HEX < 0x030D0000
19+
static int
20+
PyLong_AsInt(PyObject *obj)
21+
{
22+
int overflow;
23+
long result = PyLong_AsLongAndOverflow(obj, &overflow);
24+
if (overflow || result > INT_MAX || result < INT_MIN) {
25+
PyErr_SetString(PyExc_OverflowError,
26+
"Python int too large to convert to C int");
27+
return -1;
28+
}
29+
return (int)result;
30+
}
31+
#endif
1032

1133
static PyObject* ctypes_module = nullptr;
1234

@@ -69,7 +91,13 @@ static void populate_feeders(PyTypeObject* target_t, PyTypeObject* source_t)
6991
{
7092
m_feeders[{target_t,source_t}] = [](void* ptr, PyObject* value) -> int
7193
{
72-
*((int*)ptr) = (int)PyLong_AsLong(value);
94+
// PyLong_AsInt range-checks against the 32-bit int slot and raises
95+
// OverflowError itself, so an out-of-range value is rejected rather
96+
// than silently truncated.
97+
int v = PyLong_AsInt(value);
98+
if (v == -1 && PyErr_Occurred())
99+
return -1;
100+
*((int*)ptr) = v;
73101
return sizeof(int);
74102
};
75103
return;
@@ -89,7 +117,23 @@ static void populate_feeders(PyTypeObject* target_t, PyTypeObject* source_t)
89117
{
90118
m_feeders[{target_t,source_t}] = [](void* ptr, PyObject* value) -> int
91119
{
92-
*((int8_t*)ptr) = (int8_t)PyLong_AsLong(value);
120+
// c_byte is an 8-bit slot with no dedicated CPython converter, so
121+
// range-check explicitly against INT8_MIN/INT8_MAX. AsLongAndOverflow's
122+
// `overflow` only flags values outside `long` (64-bit on LP64), so a
123+
// value in that range would be silently truncated by (int8_t)v without
124+
// the explicit bounds check. When overflow!=0, v is the -1 sentinel
125+
// (not the real value), so that case must be caught before trusting v.
126+
int overflow = 0;
127+
long v = PyLong_AsLongAndOverflow(value, &overflow);
128+
if (overflow == 0 && v == -1 && PyErr_Occurred())
129+
return -1; // non-overflow conversion error; exception already set
130+
if (overflow != 0 || v < INT8_MIN || v > INT8_MAX)
131+
{
132+
PyErr_SetString(PyExc_OverflowError,
133+
"Python int is out of range for a c_byte (8-bit) kernel argument");
134+
return -1;
135+
}
136+
*((int8_t*)ptr) = (int8_t)v;
93137
return sizeof(int8_t);
94138
};
95139
return;

cuda_bindings/cuda/bindings/_lib/param_packer.pxd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
# Include "param_packer.h" so its contents get compiled into every
55
# Cython extension module that depends on param_packer.pxd.
66
cdef extern from "param_packer.h":
7-
int feed(void* ptr, object o, object ct)
7+
int feed(void* ptr, object o, object ct) except? -1

cuda_bindings/tests/test_kernelParams.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,3 +782,34 @@ def __init__(self, address, typestr):
782782
ASSERT_DRV(err)
783783
(err,) = cuda.cuModuleUnload(module)
784784
ASSERT_DRV(err)
785+
786+
787+
def test_kernelParams_c_int_out_of_range_raises(device):
788+
# #363: an out-of-range Python int for a c_int / c_byte kernel argument must
789+
# raise instead of being silently truncated to fit the declared width.
790+
kernelString = """\
791+
extern "C" __global__ void take_int(int i) {}
792+
"""
793+
module = common_nvrtc(kernelString, device)
794+
err, kernel = cuda.cuModuleGetFunction(module, b"take_int")
795+
ASSERT_DRV(err)
796+
err, stream = cuda.cuStreamCreate(0)
797+
ASSERT_DRV(err)
798+
799+
# An in-range value still packs and launches fine.
800+
(err,) = cuda.cuLaunchKernel(kernel, 1, 1, 1, 1, 1, 1, 0, stream, ((5,), (ctypes.c_int,)), 0)
801+
ASSERT_DRV(err)
802+
803+
# Out-of-range values now raise OverflowError during packing (previously the
804+
# high bits were silently dropped, so the kernel saw a different value).
805+
with pytest.raises(OverflowError):
806+
cuda.cuLaunchKernel(kernel, 1, 1, 1, 1, 1, 1, 0, stream, ((2**32 + 5,), (ctypes.c_int,)), 0)
807+
with pytest.raises(OverflowError):
808+
cuda.cuLaunchKernel(kernel, 1, 1, 1, 1, 1, 1, 0, stream, ((200,), (ctypes.c_byte,)), 0)
809+
810+
(err,) = cuda.cuStreamSynchronize(stream)
811+
ASSERT_DRV(err)
812+
(err,) = cuda.cuStreamDestroy(stream)
813+
ASSERT_DRV(err)
814+
(err,) = cuda.cuModuleUnload(module)
815+
ASSERT_DRV(err)

0 commit comments

Comments
 (0)