Python wrapper holds dangling pointers when key buffers go out of scope #42

Closed
opened 2026-06-19 00:09:51 +00:00 by weaselbot · 2 comments
Member

conflict_set.py constructs _Key objects from ephemeral bytearray/ctypes arrays without keeping a reference to the underlying buffer. Once the local variables in write()/read() go out of scope, the _Key.p pointer may point to freed memory by the time ConflictSet_check/ConflictSet_addWrites is called.

Location:

  • conflict_set.py:31 and conflict_set.py:41: b = (ctypes.c_ubyte * len(begin)).from_buffer(bytearray(begin))
  • conflict_set.py:36 and conflict_set.py:45: e = (ctypes.c_ubyte * len(end)).from_buffer(bytearray(end))
  • conflict_set.py:37 and conflict_set.py:46: return ... _Key(b, len(b)), _Key(e, len(e)) ...

Expected behavior: A WriteRange or ReadRange returned by write()/read() keeps its key bytes alive for the lifetime of the object, so the C library always reads valid memory.
Actual behavior: The WriteRange/ReadRange only stores a raw pointer and length; the ctypes arrays and backing bytearray objects are discarded when the helper returns. This is a classic ctypes use-after-free pattern.

Impact: Any caller that passes the returned range object to addWrites()/check() after the original bytes/buffer is no longer directly referenced can read freed memory, leading to incorrect conflict-set results or a crash. For example:

from conflict_set import *
cs = ConflictSet(0)
# The temporary bytearray is gone after this expression
cs.addWrites(1, write(b"key"))

Even when the bug does not crash immediately, it is a latent memory-safety issue in the public Python API.

Suggested fix: Store the backing ctypes arrays (or the original bytes/bytearray) inside the WriteRange/ReadRange objects, e.g. by adding private _begin_buf / _end_buf attributes, so Python keeps them alive as long as the range object exists.

`conflict_set.py` constructs `_Key` objects from ephemeral `bytearray`/`ctypes` arrays without keeping a reference to the underlying buffer. Once the local variables in `write()`/`read()` go out of scope, the `_Key.p` pointer may point to freed memory by the time `ConflictSet_check`/`ConflictSet_addWrites` is called. **Location:** - `conflict_set.py:31` and `conflict_set.py:41`: `b = (ctypes.c_ubyte * len(begin)).from_buffer(bytearray(begin))` - `conflict_set.py:36` and `conflict_set.py:45`: `e = (ctypes.c_ubyte * len(end)).from_buffer(bytearray(end))` - `conflict_set.py:37` and `conflict_set.py:46`: `return ... _Key(b, len(b)), _Key(e, len(e)) ...` **Expected behavior:** A `WriteRange` or `ReadRange` returned by `write()`/`read()` keeps its key bytes alive for the lifetime of the object, so the C library always reads valid memory. **Actual behavior:** The `WriteRange`/`ReadRange` only stores a raw pointer and length; the `ctypes` arrays and backing `bytearray` objects are discarded when the helper returns. This is a classic ctypes use-after-free pattern. **Impact:** Any caller that passes the returned range object to `addWrites()`/`check()` after the original `bytes`/buffer is no longer directly referenced can read freed memory, leading to incorrect conflict-set results or a crash. For example: ```python from conflict_set import * cs = ConflictSet(0) # The temporary bytearray is gone after this expression cs.addWrites(1, write(b"key")) ``` Even when the bug does not crash immediately, it is a latent memory-safety issue in the public Python API. **Suggested fix:** Store the backing `ctypes` arrays (or the original `bytes`/`bytearray`) inside the `WriteRange`/`ReadRange` objects, e.g. by adding private `_begin_buf` / `_end_buf` attributes, so Python keeps them alive as long as the range object exists.
weaselbot was assigned by andrew 2026-06-19 01:15:23 +00:00
weaselbot was assigned by andrew 2026-06-19 01:19:33 +00:00
Author
Member

I have implemented the fix for this issue on branch weaselbot/issue-42 and pushed it to my fork, but I cannot open a pull request because the upstream repository weaselab/conflict-set has pull requests disabled (has_pull_requests: false in the repo API response, and POST /api/v1/repos/weaselab/conflict-set/pulls returns not found).

Could a maintainer either enable pull requests on the upstream repo or let me know the preferred way to submit this change? The code change and tests are ready for review.

I have implemented the fix for this issue on branch `weaselbot/issue-42` and pushed it to my fork, but I cannot open a pull request because the upstream repository `weaselab/conflict-set` has pull requests disabled (`has_pull_requests: false` in the repo API response, and `POST /api/v1/repos/weaselab/conflict-set/pulls` returns `not found`). Could a maintainer either enable pull requests on the upstream repo or let me know the preferred way to submit this change? The code change and tests are ready for review.
Owner

Pull requests should be enabled now

Pull requests should be enabled now
weaselbot was unassigned by andrew 2026-06-19 17:10:24 +00:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: weaselab/conflict-set#42