Conversation
eriknw
marked this pull request as ready for review
August 4, 2026 16:07
eriknw
force-pushed
the
09-array-udt-udf-views
branch
from
August 4, 2026 16:12
660faec to
8164778
Compare
eriknw
force-pushed
the
09-array-udt-udf-views
branch
from
August 5, 2026 00:06
8164778 to
8960e9b
Compare
eriknw
force-pushed
the
09-array-udt-udf-views
branch
from
August 5, 2026 03:18
8960e9b to
3a7d0be
Compare
eriknw
force-pushed
the
09-array-udt-udf-views
branch
2 times, most recently
from
August 5, 2026 18:03
af88351 to
e3b42d6
Compare
eriknw
force-pushed
the
09-array-udt-udf-views
branch
from
August 5, 2026 18:05
e3b42d6 to
e16fb44
Compare
eriknw
force-pushed
the
09-array-udt-udf-views
branch
from
August 6, 2026 07:59
e16fb44 to
4ba7ef5
Compare
eriknw
force-pushed
the
09-array-udt-udf-views
branch
from
August 6, 2026 15:39
4ba7ef5 to
0a2ca8a
Compare
eriknw
force-pushed
the
09-array-udt-udf-views
branch
2 times, most recently
from
August 6, 2026 20:36
4bce522 to
9a39af4
Compare
eriknw
force-pushed
the
09-array-udt-udf-views
branch
from
August 6, 2026 20:41
9a39af4 to
9cfc310
Compare
eriknw
force-pushed
the
09-array-udt-udf-views
branch
from
August 7, 2026 02:48
9cfc310 to
6c21f4a
Compare
eriknw
force-pushed
the
09-array-udt-udf-views
branch
from
August 7, 2026 05:09
6c21f4a to
fa7b6bc
Compare
A UDF over an array UDT wrote past the end of its output element. The cfunc
wrapper passed the raw element pointer and stored the return value with
`z_ptr[0] = ...`, so what landed in the buffer was Numba's NestedArray
descriptor (data pointer, shape, strides) rather than the element payload.
The descriptor is the wider of the two, so the store ran off the end of the
element into memory SuiteSparse owns. Measured against the code this
replaces, driving the wrapper's cfunc through ctypes over a guarded buffer:
(6,) np itemsize 48 | numba value type size 56 | overrun 8 bytes
(2, 3) np itemsize 48 | numba value type size 72 | overrun 24 bytes
z[:6] = [100.0, 101.0, 102.0, 103.0, 104.0, 105.0]
z[6:] = [5e-324, -1.0, -1.0, ...] # -1.0 is the guard byte pattern
indices written past the element: [6]
Both operands and the output are now bound with `numba.carray(ptr, shape)`
in the UDT's declared shape, so the UDF receives a numpy view it can index
(`x[i, j]`, `x.shape`) and the wrapper slice-assigns the result back with
`z[:] = ...`. Array-typed record leaves slice-assign for a second reason:
Numba's record-field setitem copies the destination's extent regardless of
the source's, so a short source was read past its end.
The return-type resolution has to move with the wrapper. Once the UDF
receives a view, a UDF that builds its result (`x + y`) instead of returning
an operand types as a plain Numba `Array`, which `lookup_dtype` does not
recognize; `_resolve_udt_return_type` gains an `Array` branch matching it
back to an input array UDT by base element type and rank. Splitting the two
apart would leave array-UDT UDFs that build a result broken outright.
An `Array` type carries `ndim` but not its extents, so a return whose shape
does not fit the UDT is not a type error. It is now memory-safe, but still
silent: the slice-assign's ValueError is raised inside a cfunc, which Numba
prints and swallows, leaving the element as SuiteSparse found it.
Binary ops outside _BUILTIN_UDT_BINARY_OPS (any, first, second) compile through the generic _numba_func branch of BinaryOp._compile_udt. The old wrapper there loaded and stored an array-UDT operand as a NestedArray value, which Numba models as its full array descriptor (meminfo, parent, nitems, itemsize, data, shape, strides): 56 bytes on 64-bit for a 1-D element, regardless of the 32-byte payload. SuiteSparse's generic reduce keeps a UDT accumulator in a stack array sized to the element, so each fold overflowed it by 24 bytes and clobbered a spilled GrB_Type pointer: segfault on some builds, SIGBUS or a silently wrong answer on others. The carray-based wrapper rework on this branch writes exactly itemsize bytes; this test keeps it that way. The test builds the wrapper for binary.any the same way _compile_udt does, compiles it with numba.cfunc, and calls it via ctypes on heap buffers with slack, so a regression trips an assert instead of corrupting a stack frame. The z guard sentinel (0xAB) must differ from the sentinel in y's trailing slack (0xCD): the descriptor load/store is a byte-preserving copy of the source element plus its trailing bytes, so with one shared sentinel the overflow would rewrite z's guard with identical values and go undetected. A public-path any-reduce smoke runs after the byte-level checks, so a regression fails the assert before reaching the code that can crash the process. Verified both directions: passes here (host numba 0.65.1 and a conda env with numba 0.66 plus python-suitesparse-graphblas 10.0.1.1), and on an export of main the assert trips with exactly 24 overflow bytes while the payload check still passes, demonstrating the blind spot a same-sentinel check would have.
eriknw
force-pushed
the
09-array-udt-udf-views
branch
from
August 26, 2026 17:30
fa7b6bc to
1dc9a8d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A UDF over an array UDT wrote past the end of its output element. The cfunc
wrapper passed the raw element pointer and stored the return value with
z_ptr[0] = ..., so what landed in the buffer was Numba's NestedArraydescriptor (data pointer, shape, strides) rather than the element payload.
The descriptor is the wider of the two, so the store ran off the end of the
element into memory SuiteSparse owns. Measured against the code this
replaces, driving the wrapper's cfunc through ctypes over a guarded buffer:
Both operands and the output are now bound with
numba.carray(ptr, shape)in the UDT's declared shape, so the UDF receives a numpy view it can index
(
x[i, j],x.shape) and the wrapper slice-assigns the result back withz[:] = .... Array-typed record leaves slice-assign for a second reason:Numba's record-field setitem copies the destination's extent regardless of
the source's, so a short source was read past its end.
The return-type resolution has to move with the wrapper. Once the UDF
receives a view, a UDF that builds its result (
x + y) instead of returningan operand types as a plain Numba
Array, whichlookup_dtypedoes notrecognize;
_resolve_udt_return_typegains anArraybranch matching itback to an input array UDT by base element type and rank. Splitting the two
apart would leave array-UDT UDFs that build a result broken outright.
An
Arraytype carriesndimbut not its extents, so a return whose shapedoes not fit the UDT is not a type error. It is now memory-safe, but still
silent: the slice-assign's ValueError is raised inside a cfunc, which Numba
prints and swallows, leaving the element as SuiteSparse found it.
Stack created with GitHub Stacks CLI • Give Feedback 💬