Skip to content

test: growing a shared string detaches its buffer - #7167

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:string-shared-buffer-growth-tests
Aug 14, 2026
Merged

test: growing a shared string detaches its buffer#7167
matz merged 1 commit into
mruby:masterfrom
takumin:string-shared-buffer-growth-tests

Conversation

@takumin

@takumin takumin commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

mrb_str_cat() appends into a buffer it shares with other strings, because an append only writes [len, len + addlen), a range no other sharer can see. Growth through mrb_str_resize() looks like the same opportunity, and it is not. Nothing in the suite said so; these tests do.

mrb_str_resize() is a public API that hands the buffer back to its caller and lets it write wherever it likes, and its callers write from the front: String#insert memmoves from the insertion index, String#prepend moves the whole content up from 0, String#succ! writes over the string from 0, String#bytesplice writes from idx1, and the callers in mruby-pack and mruby-io fill their buffers from offset 0. Keeping the buffer shared across the growth lets any of them overwrite bytes another string is still reading.

Most of those call mrb_str_modify() themselves before resizing, so they detach for their own reasons. IO#sysread and BasicSocket#recv do not: both resize a buffer supplied from Ruby and then read into it from offset 0, io.c calling mrb_str_modify() only on the branch where the length already matches and no resize happens. So the invariant is load-bearing today, and untested.

Two variants of the optimisation were built to check that:

  • A: str_modify_cat()'s logic in mrb_str_resize(), nothing else changed.
  • B: A, plus dropping the now-redundant mrb_str_modify() in String#insert, String#prepend and String#bytesplice.

Both pass the pre-existing suite with KO: 0. Variant A silently rewrites a string through IO#sysread:

a = "a" * 100 + "z" * 100
a.bytesplice(100, 100, "")   # shorten, leaving spare capacity
buf = a.dup                  # shares that buffer
io.sysread(150, buf)         # grows it, then reads in from offset 0
a                            # overwritten, and never touched

and variant B additionally does it through String#insert(0, ...) and String#prepend:

$ ./build/host/bin/mruby -e 'a = "a"*100; a << "z"*100; b = a[0,150]; a.insert(0,"XXXX"); p b[0,8]'
"XXXXaaaa"      # expected "aaaaaaaa"

With the tests in this PR, variant A fails on IO#sysread into a shared buffer (KO: 1) and variant B fails on that plus String growth on a shared buffer and String#bytesplice on a shared buffer (KO: 3).

The optimisation was measured before being rejected: on s = ""; N.times { s.insert(-1, "...20 bytes..."); s[0, 30] } it is indistinguishable from master over 9 repetitions per point, and both remain quadratic. mrb_str_resize() sizes the buffer to exactly the requested length, so there is none of the geometric growth that makes mrb_str_cat()'s in-place append amortize.

Some assertions here pin the invariant on shapes that cannot currently distinguish the two behaviours: insert at the end writes above the other sharer, and succ! writes a terminator in place before it grows. They are kept as contract and marked as such in the comments.

Tests only, no behaviour change. rake -m test: mrbtest Total: 2078, OK: 2049, KO: 0, Crash: 0, Warning: 0, Skip: 29, up from 2075 by the three new blocks; bintest Total: 105, OK: 105, KO: 0.

Summary by CodeRabbit

  • Tests
    • Added regression coverage for reading into resized, shared string buffers without altering the original string.
    • Added tests confirming string growth operations preserve independent contents when buffers are shared.
    • Added coverage for byte-level splicing with shared target and replacement strings.
    • Improved resource cleanup verification during I/O tests.

`mrb_str_cat()` appends into a buffer it shares, because an append only
writes `[len, len + addlen)`, which no other sharer can see. Growth through
`mrb_str_resize()` looks like the same opportunity and is not: that function
hands the buffer back to its caller, and the callers write from offset 0.

`String#insert` memmoves from the insertion index, `String#prepend` moves the
whole content up, `String#succ!` writes over the string from the front, and
`String#bytesplice` writes from `idx1`. Most of them call `mrb_str_modify()`
before `mrb_str_resize()`, so they detach for their own reasons. `IO#sysread`
and `BasicSocket#recv` do not: both resize a buffer handed in from Ruby and
then read into it from offset 0, `io.c` calling `mrb_str_modify()` only on the
branch where the length already matches and no resize happens.

Nothing in the suite covered any of this. Letting `mrb_str_resize()` claim
spare capacity in a shared buffer the way `mrb_str_cat()` does, changing
nothing else, left all 2049 tests passing while a `sysread` into a shared
buffer of a different length wrote through to a string that was never touched.
Dropping the redundant `mrb_str_modify()` calls on top of it does the same to

    a = "a" * 100
    a << "z" * 100
    b = a[0, 150]
    a.insert(0, "XXXX")
    b                     # "XXXXaaaa..." instead of "a" * 100 + "z" * 50

The `IO#sysread` test fails on the first of those, the `insert` at 0, the
`prepend` and the `bytesplice` cases on the second. The remaining cases assert
the same invariant on shapes that cannot currently distinguish the two
behaviours, and are marked as such.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8aa09cb1-31a5-4313-b3a8-74afa57013a3

📥 Commits

Reviewing files that changed from the base of the PR and between 783e3b2 and a15ead3.

📒 Files selected for processing (3)
  • mrbgems/mruby-io/test/io.rb
  • mrbgems/mruby-string-ext/test/string.rb
  • test/t/string.rb

📝 Walkthrough

Walkthrough

Adds regression tests for shared-buffer string growth, bytesplice, and IO#sysread. The tests verify that modifying or reading into one string does not change another string that shares its buffer.

Changes

Shared Buffer Regression Tests

Layer / File(s) Summary
String growth isolation
mrbgems/mruby-string-ext/test/string.rb, test/t/string.rb
Tests cover shared-buffer growth through insert, prepend, succ!, slice insertion, and bytesplice. They verify independent contents after modification.
IO read destination isolation
mrbgems/mruby-io/test/io.rb
Tests verify that IO#sysread into a resized duplicate preserves the original shared string and closes the IO with ensure.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to a15ea

This PR adds regression coverage without changing product behavior; after normal checks and review, no actionable merge-blocking risk remains.

Possibly related PRs

  • mruby/mruby#7039: Covers shared-buffer and copy-on-write string growth behavior.
  • mruby/mruby#7043: Addresses overlapping string concatenation paths covered by these growth tests.

Suggested reviewers: matz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the added tests for detaching buffers when shared strings grow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@matz
matz merged commit bcb1cff into mruby:master Aug 14, 2026
21 checks passed
@takumin
takumin deleted the string-shared-buffer-growth-tests branch August 14, 2026 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants