Skip to content

Fix 32-bit byte accounting in Buffer - #105

Merged
Watson1978 merged 1 commit into
socketry:mainfrom
Watson1978:fix-buffer-size-accounting
Jul 31, 2026
Merged

Fix 32-bit byte accounting in Buffer#105
Watson1978 merged 1 commit into
socketry:mainfrom
Watson1978:fix-buffer-size-accounting

Conversation

@Watson1978

Copy link
Copy Markdown
Collaborator

Problem

The byte counts in buffer.c are 32-bit, and the two I/O helpers accumulate into int. None of this corrupts the heap — the memcpy bounds are computed per node and stay inside it — but each case makes the buffer misreport or mishandle its own contents.

  • struct buffer's size / node_size and buffer_node's start / end are unsigned. A buffer holding more than 4 GiB wraps its accounting, after which Buffer#size and the clamp inside Buffer#read no longer describe what the buffer actually holds.
  • buffer_read_from() and buffer_write_to() accumulate into int. A single call transferring more than 2 GiB overflows a signed integer — undefined behaviour — and returns a meaningless count.
  • Buffer#read with no argument assigns buf->size to an int length. Past INT_MAX that goes negative, and rb_str_new(0, length) is then handed a negative length.

Fix

Carry the counts in size_t and the transfer totals in ssize_t throughout. Buffer#size and Buffer.default_node_size convert with SIZET2NUM, which yields the same Integer for every value they could return before.

Buffer#read reads its argument with NUM2LONG rather than NUM2INT, so the existing length < 1 check still raises ArgumentError — not RangeError out of the conversion — for zero and negative lengths.

Behaviour change worth naming

A length that does not fit in a C int (e.g. buffer.read(2**31)) used to raise RangeError from NUM2INT. It is now clamped to the buffer size, like any other oversized length. That is the point of the change, but it is a visible difference for a caller who relied on the exception.

Everything else is unchanged: MAX_SIZE still reports the 1 GiB node-size cap, Buffer#read_from still returns nil at EOF, and no method changes its return type.

Not addressed here

The buffer still has no total-size limit — it grows to whatever the application feeds it, and an application that never drains a slow peer can be pushed into OOM. A cap belongs behind an opt-in setting rather than a silent default, so it is left for a separate change.

Tests

spec/iobuffer_spec.rb gains three examples: ArgumentError for a length below one, the clamp for a length that does not fit in an int, and the resulting buffer state. The clamp example fails on the parent commit (RangeError) and passes with this change.

  • rspec — 67 examples, 0 failures.
  • Compiler warnings for buffer.c go from 6 to 3; the three that disappear are -Wsign-compare. The remaining three (unsigned char * signedness in buffer_read_frame, unused tmp) are pre-existing and untouched here.

🤖 Generated with Claude Code

The byte counts in buffer.c were 32-bit, and the two I/O helpers accumulated
into int. Three consequences, none of which corrupt the heap -- the memcpy
bounds are computed per node and stay within it -- but all of which make the
buffer misreport or mishandle its own contents:

  * struct buffer's size and node_size, and buffer_node's start and end, were
    unsigned. A buffer holding more than 4 GiB wrapped its accounting, so
    Buffer#size and the clamp in Buffer#read no longer described what the
    buffer actually held.

  * buffer_read_from() and buffer_write_to() accumulated into int. A single
    call transferring more than 2 GiB overflowed a signed integer, which is
    undefined behaviour, and returned a meaningless count.

  * Buffer#read with no argument assigned buf->size to an int length. Past
    INT_MAX that goes negative, and rb_str_new(0, length) is then handed a
    negative length.

Carry the counts in size_t and the transfer totals in ssize_t throughout.
Buffer#size and Buffer.default_node_size now convert with SIZET2NUM, which is
the same Integer for every value they could return before.

Buffer#read reads its argument as long rather than int, so a length that does
not fit in an int is clamped to the buffer size like any other oversized
length instead of raising RangeError out of the conversion. A negative or zero
length still raises ArgumentError from the same check as before.

Not addressed here: the buffer still has no total-size limit, so it grows to
whatever the application feeds it. A cap belongs behind an opt-in setting
rather than a silent default, which is a separate change.

Removes three -Wsign-compare warnings in buffer.c.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Watson1978
Watson1978 merged commit 557934f into socketry:main Jul 31, 2026
14 checks passed
@Watson1978
Watson1978 deleted the fix-buffer-size-accounting branch July 31, 2026 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant