Skip to content

fix: non-contiguous array handling and memory safety in C++ bindings#1147

Merged
henryiii merged 4 commits into
developfrom
fix/cpp-noncontiguous-memsafety
Jun 11, 2026
Merged

fix: non-contiguous array handling and memory safety in C++ bindings#1147
henryiii merged 4 commits into
developfrom
fix/cpp-noncontiguous-memsafety

Conversation

@henryiii

Copy link
Copy Markdown
Member

🤖 AI text below 🤖

Part of #1143

Fixes a set of non-contiguous-array and memory-safety findings in the C++ bindings:

  • A1 (heap OOB / wrong results): array_like<T> no longer copies the input array's strides onto the output; callers fill the result linearly in C order, so Fortran-order input to IntCategory.index returned transposed results and negative-stride input caused out-of-bounds writes. The output is now plain C-contiguous.
  • A2 (silent wrong fills): the std::vector<std::string> caster ignored strides when iterating numpy S/U arrays, so sliced/reversed string arrays gave wrong StrCategory.index/fill results (and reversed views read OOB). Non-contiguous inputs now get a C-contiguous copy first.
  • A5 (heap overflow on malformed pickle): storage load() now validates that the serialized buffer size is a multiple of the accumulator width before resizing/copying; multi_cell::serialize validates the loaded buffer length; tuple_iarchive validates/converts array elements with py::array_t<T>::ensure instead of blindly reinterpreting, and the array_wrapper size check is a real runtime check instead of a release-mode no-op BOOST_ASSERT.
  • B4 (wrong-direction nudge): edges(..., numpy_upper=true) used std::nextafter(x, std::numeric_limits<double>::min()), which moves toward ~0 — wrong for negative/zero upper edges (Regular(4, -2, -1).to_numpy() reported a last edge above -1). Now nudges toward lowest().
  • B16: py::isinstance<int>(x) is always false, so the int fast path in axis_cast<int> was dead and large Python ints went through a lossy double round-trip. Now uses py::isinstance<py::int_> with a direct py::cast<int> (overflow raises).
  • B17 (leak): the __deepcopy__ lambdas for axes and histograms leaked the freshly new-ed object if copy.deepcopy(metadata) threw; they now construct via std::make_unique and return the holder.

Regression tests added for: Fortran-order / negative-stride / strided IntCategory.index and fill; strided / reversed (str and bytes) StrCategory.index and fill; the to_numpy upper-edge nudge direction for negative, zero, and positive edges; and corrupted pickle states (truncated buffer for weight and mean storages, non-array storage element).

🤖 Generated with Claude Code

@github-actions github-actions Bot added the needs changelog Might need a changelog entry label Jun 11, 2026
henryiii added 3 commits June 11, 2026 15:01
…udge

- array_like() copied the input array's strides onto the freshly
  allocated output array, but callers fill the result linearly in C
  order from a C-contiguous copy of the input. Fortran-order input gave
  transposed results and negative strides caused out-of-bounds writes
  (e.g. IntCategory.index on a reversed array). Allocate a plain
  C-contiguous output instead.
- The std::vector<std::string> caster iterated numpy 'S'/'U' arrays
  with p += itemsize, ignoring strides, so sliced or reversed string
  arrays gave silently wrong indices/fills (and reversed views read out
  of bounds). Take a C-contiguous copy when the input is not contiguous.
- edges(..., numpy_upper=true) nudged the last edge with
  std::nextafter(x, std::numeric_limits<double>::min()), which moves
  toward ~0: the wrong direction for negative or zero upper edges
  (Regular(4, -2, -1).to_numpy() reported a last edge above -1). Use
  std::numeric_limits<double>::lowest() to always nudge downward.

Part of #1143

Assisted-by: ClaudeCode:claude-fable-5
A malformed pickle state could write past heap buffers:

- weighted_sum/mean/weighted_mean storage load() resized to
  a.size() / N and then copied a.size() elements; if the serialized
  buffer size was not a multiple of N this wrote past the allocation.
  Now throws std::runtime_error on a size mismatch.
- multi_cell::serialize swap_ranges'd over size_ * nelem_ elements of
  the loaded vector without checking its size. Now validated.
- tuple_iarchive::operator>>(py::array_t<T>&) reinterpreted an
  arbitrary tuple element as a typed array without any validation. Now
  uses py::array_t<T>::ensure (C-contiguous, forcecast) and raises
  ValueError if the element is not convertible to an array.
- The array_wrapper load path only guarded the size match with
  BOOST_ASSERT, which is compiled out in release builds. Now a real
  runtime check.

Part of #1143

Assisted-by: ClaudeCode:claude-fable-5
- The __deepcopy__ lambdas for axes and histograms did a raw
  'new T(self)' and then called copy.deepcopy(metadata), which can
  throw, leaking the allocation. Construct via std::make_unique and
  return the unique_ptr (pybind11 accepts holder returns).
- axis_cast<int> used py::isinstance<int>(x), which is always false
  (int is not a registered C++ type), so the integer fast path was dead
  and every Python int went through a double round-trip. Use
  py::isinstance<py::int_> with a direct py::cast<int>, letting
  overflow raise instead of invoking undefined behavior via a
  double-to-int cast.

Part of #1143

Assisted-by: ClaudeCode:claude-fable-5
@henryiii henryiii force-pushed the fix/cpp-noncontiguous-memsafety branch from 7754f3e to 5ff7c02 Compare June 11, 2026 19:01
@henryiii henryiii marked this pull request as ready for review June 11, 2026 19:06
Pylint cannot statically resolve the .min attribute on np.finfo. Since
np.nextafter only uses the direction of its second argument, -np.inf
produces an identical result while reading more clearly.

Assisted-by: ClaudeCode:claude-opus-4.8
@henryiii henryiii merged commit fdbe1fa into develop Jun 11, 2026
21 checks passed
@henryiii henryiii deleted the fix/cpp-noncontiguous-memsafety branch June 11, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs changelog Might need a changelog entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant