[ruby-core:126326] [Ruby Bug#22234] IO::Buffer#resize(0) copies uninitialized memory into the buffer
Issue #22234 has been reported by himura467 (Akito Shitara). ---------------------------------------- Bug #22234: IO::Buffer#resize(0) copies uninitialized memory into the buffer https://bugs.ruby-lang.org/issues/22234 * Author: himura467 (Akito Shitara) * Status: Open * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- ## Reproduction ```ruby buffer = IO::Buffer.new(64) slice = buffer.slice(0, 8) slice.resize(0) slice.size # => an arbitrary size or a SEGV ``` The outcome depends on what the stack happens to hold. Reading `slice.inspect` instead of `slice.size`, which dereferences `source`, gives either a crash or an unrelated `TypeError` such as `wrong argument type VM/cc_table (expected IO::Buffer)`. A mapped buffer is affected too, on platforms where `mremap` is unavailable (macOS, Windows), and `IO::Buffer.new.resize(0)` is enough to hit it because the 65536 byte default is larger than `PAGE_SIZE`. On Linux that case raises `Errno::EINVAL: Invalid argument - rb_io_buffer_resize:mremap` instead, as `mremap` rejects a zero new length. ## What happens `io_buffer_resize_copy()` fills a stack `struct rb_io_buffer` and then copies it over the live one: ```c struct rb_io_buffer resized; io_buffer_initialize(self, &resized, NULL, size, io_flags_for_size(size), Qnil); ... io_buffer_free(buffer); *buffer = resized; ``` `io_buffer_initialize()` returns without assigning any field when the size is zero, because there is nothing to allocate. `resized` is therefore never initialized, and reading it is already undefined behaviour; all four of `base`, `size`, `flags` and `source` hold indeterminate values once it has been copied over the live buffer. `source` is the dangerous one: it is marked by the GC on every collection, and `io_buffer_validate()` dereferences it as a slice source whenever it is not `Qnil`. `rb_io_buffer_resize()` reaches that path because a slice carries no flag other than `READONLY`, so the `EXTERNAL`, `MAPPED` and `INTERNAL` branches are all skipped. The example above uses an internal source buffer, so no mapping is involved and the platform does not matter. ## Background The `INTERNAL` branch is the only one with a `size == 0` shortcut. It was added by GH-7569 for #19543, where `resize(0)` reached `realloc(base, 0)` and aborted with `[BUG] rb_sys_fail(rb_io_buffer_resize:realloc) - errno == 0`, the null return being taken for an allocation failure. As GH-7569 describes it, glibc frees the object and returns a null pointer for a zero size, while BSDs return an inaccessible object; passing zero to `realloc` is implementation defined in C17 and undefined in C23, so the shortcut keeps it from reaching `realloc` at all. Zero is not a size that any of the allocation APIs behind a buffer can express, and the mapped and slice paths still pass it down. -- https://bugs.ruby-lang.org/
participants (1)
-
himura467 (Akito Shitara)