These still held String and Array lengths and indices in long, or cast them to long. rb_len_t is still long everywhere, so the generated code does not change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
```
../src/io_buffer.c(609): warning C5286: implicit conversion from enum type '<unnamed-enum-RB_IO_BUFFER_HEXDUMP_DEFAULT_WIDTH>' to enum type 'rb_io_buffer_flags'; use an explicit cast to silence this warning
```
Treat encoding coercion, `to_io`, and scheduler capability checks as
callouts that can change the buffer before byte access begins.
- Copy `get_string` bytes through the existing locked-for-reading helper
after encoding coercion, without retaining metadata pointers across it.
- Obtain the writable buffer after `to_io` conversion in `read` and `pread`.
- Reacquire and validate the buffer before native I/O fallback when the
scheduler does not handle `read`, `write`, `pread`, or `pwrite`.
Add regression coverage using the existing buffer and slice APIs. Scheduler
probes shrink a slice without freeing its underlying storage, making stale
length handling observable without accessing released memory. Also cover
encoding callbacks and frozen-state changes during I/O coercion.
This is an independent correctness extraction from #18911. It introduces
no new classes, slice representation, or public API.
For strings larger than or equal to IO_BUFFER_BLOCKING_SIZE (which is 1KB),
IO::Buffer#set_string will perform the memmove without GVL. In this time,
another thread can resize the IO::Buffer object. In that case, the buffer
may be freed and thus cause an use-after-free.
For example, the following script crashes:
buf = IO::Buffer.new(64 * 1024 * 1024)
src = "x" * (64 * 1024 * 1024)
stop = false
writer = Thread.new do
until stop
begin
buf.set_string(src)
rescue ArgumentError
end
end
end
resizer = Thread.new do
until stop
buf.resize(8)
buf.resize(64 * 1024 * 1024)
end
end
sleep 2
stop = true
writer.join
resizer.join
puts "done"
Using the `container->obj = rb_gc_location(container->obj)` pattern dirties pages unconditionally.
If we're in a forked process, this is bad for CoW. If the object hasn't moved, don't reassign.
With gcc 15.2.0 on aarch64 Ubuntu 26.4:
```console
io_buffer.c: In function ‘io_buffer_flags_for_new’:
io_buffer.c:978:20: warning: comparison between ‘enum rb_io_buffer_flags’ and ‘enum <anonymous>’ [-Wenum-compare]
978 | if (allocation == RB_IO_BUFFER_ALLOCATION_FLAGS) {
| ^~
```
`null?` buffer isn't MemoryView available.
Not `null?` buffer is MemoryView available. It can be exported as a
1-dimensional contiguous array of bytes.
Users can't change the underlying memory of the exporting IO::Buffer
while exported MemoryView is using.
Share each lock count across a buffer and its slices so independent native operations retain the backing allocation until all users release it. Always acquire a lock for blocking I/O and protect each/each_byte callbacks from invalidating their backing pointers.
IO::Buffer.new takes a size as its first argument, so passing a string
raises TypeError. Adjust the sample output to the flags the buffer
created by IO::Buffer.for actually shows.
The call-seq of IO::Buffer#<=> says it returns "true or false", but
rb_io_buffer_compare always returns an integer: -1 or 1 when the sizes
differ, and the result of memcmp as is when the sizes are equal. Since
the value comes straight from memcmp it is not normalized to -1, 0 or 1,
so only its sign is meaningful.
p(IO::Buffer.for("abc") <=> IO::Buffer.for("abc")) # => 0
p(IO::Buffer.for("abc") <=> IO::Buffer.for("ab")) # => 1
p(IO::Buffer.for("abc") <=> IO::Buffer.for("abz")) # => -23
IO::Buffer#hexdump returns nil when the buffer does not reference any
memory, that is, when #null? returns true, but this was not documented.
buffer = IO::Buffer.new(4)
buffer.free
p buffer.hexdump # => nil
The example shows the result as `+4`, but the operation returns a buffer of
the same size as the source, which is 10 bytes here:
$ ruby -e 'p((IO::Buffer.for("1234567890") & IO::Buffer.for("\xFF\x00\x00\xFF")).size)'
10
The rest of the example is already correct. The hex dump and the ASCII column
show all 10 bytes, and the description right above says "Generate a new buffer
the same size as the source", so the example contradicts both. The size of the
mask is 4, which is likely where the number came from.
The neighbouring examples for #|, #^ and #~ all show `+10` and need no change.
The documentation for `IO::Buffer#set_value` shows:
buffer.set_value(:U8, 1, 111)
# => 1
but it actually returns 2, the offset just after the written value:
$ ruby -e 'p IO::Buffer.new(8).set_value(:U8, 1, 111)'
2
`io_buffer_set_value` passes `&offset` to `rb_io_buffer_set_value`, which
advances it by the size of the written value, and then returns the advanced
offset:
static VALUE
io_buffer_set_value(VALUE self, VALUE type, VALUE _offset, VALUE value)
{
struct rb_io_buffer *buffer = get_io_buffer_for_writing(self);
size_t offset = io_buffer_extract_offset(_offset);
rb_io_buffer_set_value(buffer, type, &offset, value);
return SIZET2NUM(offset);
}
This changed in 025b8701c0 (#6434, released in
3.2.0); before that the method returned the given offset, so the example was
correct at the time it was written. Checked with all-ruby: 3.1 returns 1 and
3.2 through 4.0 return 2.
Also state the return value explicitly for both `set_value` and `set_values`.
The call-seq says `-> offset`, which reads as if the given offset were
returned. `set_values` returns the offset after the last written value in the
same way (3 for the example in its documentation).
The documentation says "Frozen strings and read-only files create
read-only buffers", but this omits the most common case: a buffer
created by IO::Buffer.for without a block is always read-only, even
when the source string is not frozen.
This is because the no-block form of IO::Buffer.for uses an internal
frozen copy of the string as the buffer source (see rb_io_buffer_type_for,
which passes RB_IO_BUFFER_READONLY unconditionally), and its call-seq is
already documented as `IO::Buffer.for(string) -> readonly io_buffer`.
p IO::Buffer.for("test").readonly? # => true (not frozen)
p IO::Buffer.for("test") {|b| b.readonly? } # => false (block form)
p IO::Buffer.new(4).readonly? # => false
Set RUBY_TYPED_THREAD_SAFE_FREE on TypedData types whose dfree function
is trivially safe, and only frees its own memory
Types not yet marked as THREAD_SAFE_FREE:
- id2ref_tbl_type: sets the process-global id2ref_tbl to NULL.
- mutex_data_type: unlinks itself from another thread's keeping_mutexes list.
- autoload_data_type: deletes nodes from a shared intrusive list.
- autoload_const_type: deletes itself from a shared list.
- rb_cont_data_type: mutates the shared fiber pool and the global first_jit_cont list.
- rb_fiber_data_type: delegates to cont_free.
- FiberPoolDataType: mutates shared fiber pool free-lists.
- ractor_data_type: reconfigures global VM event-hook flags and call caches.
- exported_object_registry: frees a global table under the VM lock.
- rb_box_data_type (Box::Entry): unlinks classext from other live class and module objects.
- box_ext_cleanup_type: dereferences another String object and calls unlink.
- monitor_data_type: uses the default free, so the flag has no effect.
Co-authored-by: Luke Gruber <luke.gruber@shopify.com>
`io_buffer_not` accessed `buffer->base` directly without validating that the buffer was still live. A slice whose parent had been freed retained its stale base pointer, so calling `~` on it caused a UAF.
Use `io_buffer_get_bytes_for_reading`, which raises `IO::Buffer::InvalidatedError` before any memory access if the buffer has been invalidated.