Skip to content

Commit bd4d85b

Browse files
committed
fix(nif): refuse buffer grow while a memoryview pins it
py_buffer_write relocated and freed buf->data on growth even when a Python memoryview (PyBuffer_getbuffer) held a raw pointer into it, dangling the view (use-after-free, whole-node crash). Refuse to grow while view_count > 0; the write returns an error instead. Adds py_buffer_SUITE:buffer_grow_pinned_test (pinned grow -> error, release -> write succeeds).
1 parent 9e2de4b commit bd4d85b

3 files changed

Lines changed: 40 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
### Security
66

7+
- **Zero-copy buffer pinning** - `py_buffer` no longer relocates (and frees) its
8+
storage while a Python `memoryview` points into it. A write that would grow the
9+
buffer while a view is held now returns an error instead of dangling the view into
10+
freed memory (a use-after-free that crashed the whole node).
711
- **Bounded recursion in type conversion** - The Erlang<->Python converters now cap
812
nesting depth, so a deeply nested term (or Python structure) returns a clean error
913
instead of overflowing the C stack and crashing the whole node.

c_src/py_buffer.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ int py_buffer_write(py_buffer_resource_t *buf, const unsigned char *data, size_t
152152
/* Check if we need to grow the buffer */
153153
size_t required = buf->write_pos + size;
154154
if (required > buf->capacity) {
155+
/* A live Python memoryview (from PyBuffer_getbuffer) holds a raw pointer
156+
* into buf->data. Relocating/freeing the buffer now would leave that
157+
* pointer dangling -> use-after-free that crashes the whole node. Refuse
158+
* to grow while any view is pinned; the caller gets a write error. */
159+
if (buf->view_count > 0) {
160+
pthread_mutex_unlock(&buf->mutex);
161+
return -1;
162+
}
155163
/* Calculate new capacity */
156164
size_t new_capacity = buf->capacity;
157165
while (new_capacity < required) {

test/py_buffer_SUITE.erl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
seek_tell_test/1,
2323
find_test/1,
2424
memoryview_test/1,
25+
buffer_grow_pinned_test/1,
2526
iterator_test/1,
2627
closed_buffer_test/1,
2728
empty_buffer_test/1,
@@ -40,6 +41,7 @@ all() -> [
4041
seek_tell_test,
4142
find_test,
4243
memoryview_test,
44+
buffer_grow_pinned_test,
4345
iterator_test,
4446
closed_buffer_test,
4547
empty_buffer_test,
@@ -306,6 +308,32 @@ def read_buffer(buf):
306308

307309
ok.
308310

311+
%% @doc A buffer write that must grow the storage is refused while a Python
312+
%% memoryview pins it (relocating the storage would dangle the view -> UAF).
313+
%% Regression for the zero-copy view_count guard in py_buffer_write.
314+
buffer_grow_pinned_test(_Config) ->
315+
{ok, Buf} = py_buffer:new(8), %% capacity 8 bytes
316+
ok = py_buffer:write(Buf, <<"ab">>), %% write_pos 2, within capacity
317+
Ctx = py:context(1),
318+
319+
%% Python holds a memoryview pinning the buffer (kept in a persistent global).
320+
{ok, <<"pinned">>} = py:eval(Ctx,
321+
<<"globals().setdefault('_keep', []).append(memoryview(buf)) or 'pinned'">>,
322+
#{<<"buf">> => Buf}),
323+
324+
%% Growing the buffer now would relocate buf->data and dangle the memoryview,
325+
%% so the write is refused (no crash) while the view is pinned.
326+
{error, _} = py_buffer:write(Buf, binary:copy(<<"x">>, 1000)),
327+
328+
%% Release the view; the same write now succeeds.
329+
{ok, <<"released">>} = py:eval(Ctx, <<"_keep[0].release() or 'released'">>),
330+
ok = py_buffer:write(Buf, binary:copy(<<"x">>, 1000)),
331+
332+
%% Context still alive and usable.
333+
{ok, 2} = py:eval(Ctx, <<"1+1">>),
334+
ok = py_buffer:close(Buf),
335+
ok.
336+
309337
%% @doc Test that buffer resources are properly garbage collected
310338
%% Verifies reference counting between Erlang and Python
311339
gc_refcount_test(_Config) ->

0 commit comments

Comments
 (0)