memoryview: count exports, hash the exporter, and index through __index__ - #8553
Conversation
A tuple key was taken apart only when every item was already an `int`, so `m[I(), 2]` with an `__index__` on `I` was reported as an invalid slice key rather than indexed. What kind of key it is now follows from the types in it, the way `is_multiindex` decides, and each item is converted where it sits; an item that answers `__index__` but raises on the way reports that rather than making the whole key invalid. That conversion runs Python, which can release the view, so the multi-dimensional read goes through `unpack_single` and its re-check rather than reading the bytes itself. `IndexError` named the index that was out of bounds where `lookup_dimension` names the dimension it was out of bounds on, counted from one, and the one-dimensional path had a message of its own. Assisted-by: Claude
The view kept no count of the buffers taken from it, so `release()` always succeeded, even while something was still reading through an export. It now answers `BufferError` the way `_memory_release` does, and `__exit__` reports the same refusal. `memory_hash` asks the exporter for its hash and throws the answer away, so a view is no more hashable than what it looks at; a read-only view over a `bytearray` hashed here where CPython refuses it. Asking runs Python, so the view counts as exported for the duration and a release attempted from inside that hash is refused rather than obeyed. Assisted-by: Claude
`hex()` took the separator's length from its bytes rather than from the
object, so a `bytes` subclass that answers `__len__` was measured by what it
holds instead of what it says: `b'abcd'.hex(S(b'::'))` with `__len__`
returning 1 was refused where `_Py_strhex_impl` accepts it, and a two-byte
answer was accepted where it refuses. The check also came after the
`bytes_per_sep == 0` and empty-data shortcuts, so `b'abcd'.hex('::', 0)`
never reached it at all.
Measuring runs Python, and the bytes to be written out must not be borrowed
while it does, so the separator is now resolved from the arguments before
the buffer is reached. For a memoryview that also means the view counts as
exported for the duration, so a release attempted from inside `__len__` is
refused.
Assisted-by: Claude
`array.array` is mutable but inherited `object.__hash__`, so an array could be put in a set or used as a dict key and then changed underneath it. It is unhashable, as `tp_hash = PyObject_HashNotImplemented` makes it. A read-only memoryview over an array now reports the exporter as unhashable too, which is what `memory_hash` asks it for. Assisted-by: Claude
test_hash_use_after_free, test_hex_use_after_free and test_use_released_memory pass. Assisted-by: Claude
`cast()` accepted a shape of any length, so `mv.cast('B', (1,)*64 + (8,))`
built a 65-dimensional view where `memoryview_cast_impl` refuses anything
past `PyBUF_MAX_NDIM`. The limit is answered before the shape is looked at
any further, as it is there.
Assisted-by: Claude
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThe PR marks ChangesMemoryview and hex behavior
Array hashability
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant PythonCaller
participant PyMemoryView
participant Exporter
PythonCaller->>PyMemoryView: hash(memoryview)
PyMemoryView->>Exporter: hash underlying exporter while exported
Exporter-->>PyMemoryView: hash result or error
PyMemoryView-->>PythonCaller: return hash result
Possibly related PRs
Suggested labels: Suggested reviewers: ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [ ] test: cpython/Lib/test/test_memoryview.py (TODO: 4) dependencies: dependent tests: (no tests depend on memoryview) Legend:
|
…dex__` (#8553) * memoryview: index through `__index__` and name the dimension A tuple key was taken apart only when every item was already an `int`, so `m[I(), 2]` with an `__index__` on `I` was reported as an invalid slice key rather than indexed. What kind of key it is now follows from the types in it, the way `is_multiindex` decides, and each item is converted where it sits; an item that answers `__index__` but raises on the way reports that rather than making the whole key invalid. That conversion runs Python, which can release the view, so the multi-dimensional read goes through `unpack_single` and its re-check rather than reading the bytes itself. `IndexError` named the index that was out of bounds where `lookup_dimension` names the dimension it was out of bounds on, counted from one, and the one-dimensional path had a message of its own. Assisted-by: Claude * memoryview: count what a view has exported, and hash the exporter The view kept no count of the buffers taken from it, so `release()` always succeeded, even while something was still reading through an export. It now answers `BufferError` the way `_memory_release` does, and `__exit__` reports the same refusal. `memory_hash` asks the exporter for its hash and throws the answer away, so a view is no more hashable than what it looks at; a read-only view over a `bytearray` hashed here where CPython refuses it. Asking runs Python, so the view counts as exported for the duration and a release attempted from inside that hash is refused rather than obeyed. Assisted-by: Claude * bytes, bytearray, memoryview: measure the hex separator as `len()` does `hex()` took the separator's length from its bytes rather than from the object, so a `bytes` subclass that answers `__len__` was measured by what it holds instead of what it says: `b'abcd'.hex(S(b'::'))` with `__len__` returning 1 was refused where `_Py_strhex_impl` accepts it, and a two-byte answer was accepted where it refuses. The check also came after the `bytes_per_sep == 0` and empty-data shortcuts, so `b'abcd'.hex('::', 0)` never reached it at all. Measuring runs Python, and the bytes to be written out must not be borrowed while it does, so the separator is now resolved from the arguments before the buffer is reached. For a memoryview that also means the view counts as exported for the duration, so a release attempted from inside `__len__` is refused. Assisted-by: Claude * array: refuse to hash an array `array.array` is mutable but inherited `object.__hash__`, so an array could be put in a set or used as a dict key and then changed underneath it. It is unhashable, as `tp_hash = PyObject_HashNotImplemented` makes it. A read-only memoryview over an array now reports the exporter as unhashable too, which is what `memory_hash` asks it for. Assisted-by: Claude * Remove expectedFailure from three memoryview tests test_hash_use_after_free, test_hex_use_after_free and test_use_released_memory pass. Assisted-by: Claude * memoryview: bound the dimensions a cast can name `cast()` accepted a shape of any length, so `mv.cast('B', (1,)*64 + (8,))` built a 65-dimensional view where `memoryview_cast_impl` refuses anything past `PyBUF_MAX_NDIM`. The limit is answered before the shape is looked at any further, as it is there. Assisted-by: Claude
Three
memoryviewtests were markedexpectedFailurefor "re-entrant buffer release not detected". Chasing them turned up four separate defects underneath, each visible from Python on its own.All comparisons below are against CPython 3.14.6.
A view kept no count of what it had exported
release()always succeeded, even while something was still reading through an export:PyMemoryViewnow carries the count_memory_releaseconsults, kept by thebf_getbuffer/bf_releasebufferslots, and__exit__reports the same refusal.memory_hashnever asked the exporterA view is no more hashable than what it looks at.
memory_hashhashesview->objand throws the answer away for exactly that reason:Asking runs Python, so the view counts as exported for the duration and a release attempted from inside that hash is refused rather than obeyed. That is
test_hash_use_after_free(gh-142664).While fixing this it turned out
array.arraywas hashable here despite being mutable, so an array could go into a set and then be changed underneath it.tp_hash = PyObject_HashNotImplementedthere; it is unhashable now.hex(sep)never calledsep.__len__The separator's length came from its bytes rather than from the object, and the check came after the
bytes_per_sep == 0and empty-data shortcuts. Four ways forbytes,bytearrayandmemoryviewalike:b'abcd'.hex(S(b'::')),__len__→ 161:62:63:64ValueErrorb'abcd'.hex(S(b':')),__len__→ 2ValueError61:62:63:64b'abcd'.hex(S(b':')),__len__raisesb'abcd'.hex('::', 0)ValueError'61626364'Measuring runs Python, and the bytes to be written out must not be borrowed while it does —
bytearray.hexwould have called__len__holding its own lock. The separator is resolved from the arguments before the buffer is reached, which for a memoryview also means the view counts as exported for the duration. That istest_hex_use_after_free(gh-143195).A tuple key was only taken apart when every item was already an
intWhat kind of key it is now follows from the types in it, the way
is_multiindexdecides, and each item is converted where it sits. An item that answers__index__but raises on the way reports that rather than making the whole key invalid. The conversion runs Python and can release the view, so the multi-dimensional read goes throughunpack_singleand its re-check. That istest_use_released_memory(gh-92888).IndexErroralso named the index that was out of bounds wherelookup_dimensionnames the dimension, counted from one:and the one-dimensional path had a message of its own (
index out of range).cast()accepted any number of dimensionsTests
test_hash_use_after_free,test_hex_use_after_freeandtest_use_released_memorypass, and theirexpectedFailuredecorators are removed. Six regression tests were added toextra_tests/snippets/builtin_memoryview.py, all of which pass under CPython 3.14.6 as well.20 suites run clean locally — memoryview, buffer, bytes, struct, array, io, binascii, mmap, hashlib, codecs, zlib, bz2, marshal, socket, ssl, pickle, hmac, collections, set, dict.
🤖 Generated with Claude Code
https://claude.ai/code/session_01P9HewXGX8qcGSccUxGdSPV
Summary by CodeRabbit
Bug Fixes
memoryviewhandling for exported buffers, hashing, indexing, casting, and context-manager release behavior.bytes.hex()andbytearray.hex().array.arrayobjects as unhashable.Tests