Skip to content

string.c: record the source overlap before str_modify_cat() runs - #7043

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:string-cat-overlap-offset
Aug 9, 2026
Merged

string.c: record the source overlap before str_modify_cat() runs#7043
matz merged 1 commit into
mruby:masterfrom
takumin:string-cat-overlap-offset

Conversation

@takumin

@takumin takumin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

mrb_str_cat() has to know whether ptr points into the string it is appending to, so
that the source can be followed if the destination moves. It asks that question after
str_modify_cat() has run:

  mrb_int capa = str_modify_cat(mrb, s, (mrb_int)len);
  if (ptr >= RSTR_PTR(s) && ptr <= RSTR_PTR(s) + (size_t)RSTR_LEN(s)) {
      off = ptr - RSTR_PTR(s);
  }

By then s may hold a different buffer. str_modify_cat() takes one of two paths. It
appends inside the shared allocation, which leaves the buffer where it is and ptr valid;
or it detaches s through mrb_str_modify(), which copies the string into a fresh
allocation and frees the old one when the copy takes the last reference to it. On the
second path ptr is outside the new buffer, the comparison records no overlap, off
stays -1, and the memcpy() at the end of the function reads through ptr regardless.

Reachability

Not from inside mruby. It needs a ptr into a shared buffer whose only remaining
reference is s itself, and the paths that pass an interior pointer do not produce that:
mrb_str_cat_str() on two strings over one buffer means two references, so the buffer
outlives the copy, and on one string it modifies up front. An extension that kept a
RSTRING_PTR() across an operation that dropped the other sharer can produce it, which is
the same class of caller the length of mrb_str_cat() already comes from.

That makes this a fix for the public API contract rather than a live bug.

The change

Compare before the call rather than after it. The offset is what the rest of the function
wants on either path, and it stays valid across the copy, since a copy preserves the
contents and the offsets into them alike, whether the string ends up in a fresh allocation
or embedded.

Where the buffer stays put, the offset is the same answer reached another way, and nothing
changes. Where it moves, off is now recorded instead of lost, so ptr is re-derived
from the new buffer at the memcpy() rather than left pointing at the old one. Recording
ahead of the call covers both without having to know which path ran.

Rebase

Rebased onto 9360b3fd0. #7038 and #7039 have both landed and both touch the lines above
the overlap check, so two things move. The size check from #7038 stays ahead of
everything, since it is the one that leaves the string untouched when it raises. And what
the overlap check has to sit in front of is now str_modify_cat() rather than
mrb_str_modify(), which is also why the reason for recording first is broader than it
was: the in-place path leaves ptr valid, so there the offset is merely correct rather
than necessary.

Testing

rake test with build_config/default.rb, MRUBY_CONFIG=asan rake test
(address,undefined), and MRUBY_CONFIG=ci/gcc-clang rake test, which covers
MRB_GC_STRESS with MRB_USE_DEBUG_HOOK, MRB_GC_FIXED_ARENA, and the C++ ABI build.
KO: 0 and Crash: 0 on all of them.

No test comes with this. Reproducing it takes an extension holding a stale RSTRING_PTR(),
which is not something the Ruby level suite can express.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed string concatenation when the source and destination overlap.
    • Self-appending and overlapping appends now remain correct even when the string buffer is resized.

@takumin
takumin requested a review from matz as a code owner August 9, 2026 13:21
@github-actions github-actions Bot added the core label Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

mrb_str_cat records overlapping source offsets before destination-buffer modification and restores the source pointer afterward. This preserves self-appending and overlapping append operations when the destination buffer is reallocated or released.

Changes

String append safety

Layer / File(s) Summary
Track overlapping append sources
src/string.c
mrb_str_cat records the source offset before str_modify_cat() and restores the source pointer after modification.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • mruby/mruby#7039: Both changes modify mrb_str_cat() and str_modify_cat() to handle overlapping append behavior.

Suggested reviewers: matz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: recording source overlap before str_modify_cat() runs.
✨ 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.

@takumin

takumin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/string.c`:
- Around line 3144-3146: Update the pointer containment and offset calculation
in mrb_str_cat to convert ptr and RSTR_PTR(s) to uintptr_t before comparing or
subtracting, avoiding unrelated-pointer operations while preserving the existing
length bounds and computed off value before mrb_str_modify().
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2682a036-b29d-481f-84de-061470a92076

📥 Commits

Reviewing files that changed from the base of the PR and between 9a3d566 and 3c3f171.

📒 Files selected for processing (1)
  • src/string.c

Comment thread src/string.c
Comment on lines 3144 to 3146
if (ptr >= RSTR_PTR(s) && ptr <= RSTR_PTR(s) + (size_t)RSTR_LEN(s)) {
off = ptr - RSTR_PTR(s);
}

@coderabbitai coderabbitai Bot Aug 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
set -eu

rg -n -C 4 'uintptr_t|RSTR_CAPA|RSTR_PTR\(s\)|ptrdiff_t off' \
  src/string.c include/mruby

Repository: mruby/mruby

Length of output: 20879


🏁 Script executed:

#!/bin/bash
set -eu

printf '--- files ---\n'
git ls-files | rg '(^|/)(README|.*\.md|.*\.txt|CMakeLists\.txt|Makefile|\.clang.*|.*\.inc|.*\.h)$' | head -100 || true

printf '\n--- mrb_str_cat and call sites ---\n'
rg -n -C 3 '\bmrb_str_cat\(|mrb_str_cat_' -- .

printf '\n--- pointer relational/subtraction patterns around RSTR_PTR ---\n'
rg -n -C 2 '\b(ptr|p|end|boundary|mrb_str_ptr).*[><]=|[><=].*(ptr|p|end|boundary|mrb_str_ptr)|ptr - RSTR_PTR\(|RSTR_PTR\(s\) -|RSTRING_PTR\(str0\)' src/include include src string.c -g '*.[ch]' | head -250

Repository: mruby/mruby

Length of output: 240


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- files ---'
git ls-files | rg '(^|/)(README|.*\.md|.*\.txt|CMakeLists\.txt|Makefile|\.clang.*|.*\.inc|.*\.h)$' | head -100 || true

printf '%s\n'
printf '%s\n' '--- mrb_str_cat and call sites ---'
rg -n -C 3 '\bmrb_str_cat\(|mrb_str_cat_' -- .

printf '%s\n'
printf '%s\n' '--- pointer relational/subtraction patterns around RSTR_PTR ---'
rg -n -C 2 '\b(ptr|p|end|boundary|mrb_str_ptr).*[><]=|[><=].*(ptr|p|end|boundary|mrb_str_ptr)|ptr - RSTR_PTR\(|RSTR_PTR\(s\) -|RSTRING_PTR\(str0\)' src include -g '*.[ch]' | head -250 || true

Repository: mruby/mruby

Length of output: 50367


Use integer address math before modifying str.

mrb_str_cat() accepts pointers from any object. In src/string.c:3144-3145, comparing/subtracting ptr against RSTR_PTR(s) before mrb_str_modify() can compare unrelated pointers. Use uintptr_t arithmetic for the containment check before mrb_str_modify() moves the string buffer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/string.c` around lines 3144 - 3146, Update the pointer containment and
offset calculation in mrb_str_cat to convert ptr and RSTR_PTR(s) to uintptr_t
before comparing or subtracting, avoiding unrelated-pointer operations while
preserving the existing length bounds and computed off value before
mrb_str_modify().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparison this points at is not introduced here. Master carries the same expression, and this PR moves it above str_modify_cat() without changing its form, so the two pointers are as related or unrelated as they were before. The position is what this PR is about.

The formal point stands on its own merits: relational comparison and subtraction between pointers into different objects is undefined by C11 6.5.8p5 and 6.5.6p9, and a containment check has no portable spelling other than uintptr_t. mruby already spells one that way in is_pool_memory() in mrbgems/mruby-bigint/core/bigint.c. If that is worth applying to mrb_str_cat(), it should come as its own change against master rather than ride along with an ordering fix.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@takumin, agreed. This finding is outside the scope of #7043.

The PR preserves the existing pointer expression and changes only when mrb_str_cat() records overlap. I will not treat the uintptr_t portability concern as a required change for this PR.

If you want, I can open a separate issue for a master-targeted update to the containment check in mrb_str_cat().

(ᵔᴥᵔ)

You are interacting with an AI system.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issue needed. That change is already up as #7051 against master, which converts both operands of the containment check to uintptr_t. It also records that whichever of the two lands first, the other gets rebased onto it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@takumin, understood. #7051 already contains the uintptr_t containment-check fix and the rebasing behavior for either allocation order.

No follow-up issue is needed. #7043 remains scoped to recording the overlap offset before str_modify_cat().

You are interacting with an AI system.

@matz

matz commented Aug 9, 2026

Copy link
Copy Markdown
Member

This needs a rebase. #7038 landed as 28e42c8 and #7039 as 9360b3f, and both touch the lines above the overlap check.

The fix is still wanted and is still the same move. What it has to sit in front of is now str_modify_cat() rather than mrb_str_modify(), and the size check from #7038 has to stay ahead of both, since it is the one that leaves the string untouched when it raises:

  if (len == 0) return str;
  /* size check from #7038 */
  mrb_int total;
  if (len > (size_t)MRB_INT_MAX ||
      mrb_int_add_overflow(RSTR_LEN(s), (mrb_int)len, &total)) {
  size_error:
    mrb_raise(mrb, E_ARGUMENT_ERROR, "string size too big");
  }
  /* this change */
  if (ptr >= RSTR_PTR(s) && ptr <= RSTR_PTR(s) + (size_t)RSTR_LEN(s)) {
      off = ptr - RSTR_PTR(s);
  }
  mrb_int capa = str_modify_cat(mrb, s, (mrb_int)len);

I built that ordering while checking the three against each other and the suite passed, so I do not expect a surprise, but the shape is yours to write.

Worth stating for the record, since #7039 changed what the function does between the two points: the reason the overlap has to be recorded first is now broader than the one in your description. str_modify_cat() still detaches and frees the old buffer when the string grows past the shared allocation, which is the case you described. It also has an in-place path that leaves ptr valid, so the recorded offset is simply correct there rather than necessary. Recording ahead of the call covers both without the caller having to know which path ran.

On reachability I agree with you: an interior ptr whose buffer holds one reference does not arise from mruby's own callers, and the ones that pass an interior pointer either hold two references or modify up front. That makes this a fix for the public API contract rather than a live bug, which is why I merged the other two first rather than this one.

`mrb_str_cat()` decides whether `ptr` points into the string it is
appending to by comparing it against `RSTR_PTR(s)` after
`str_modify_cat()` has run. By then `s` may hold a different buffer.
`str_modify_cat()` takes one of two paths: it appends inside the shared
allocation, leaving the buffer where it is, or it detaches `s` through
`mrb_str_modify()`, which copies the string into a fresh allocation and
frees the old one when the last reference to it goes away with the copy.
On the second path `ptr` is outside the new buffer, so the comparison
records no overlap, and the `memcpy()` at the end reads through it
anyway.

Reaching that needs a `ptr` into a shared buffer whose only remaining
reference is `s` itself, which no path inside mruby produces: two mruby
strings over one buffer hold two references, so the buffer outlives the
copy. An extension that kept a `RSTRING_PTR()` from earlier can.

Compare before the call instead. The offset is what the rest of the
function wants on either path, and it stays valid across the copy, since
a copy preserves the contents and the offsets into them alike. Where the
buffer stays put the offset is the same answer reached another way;
where it moves the offset is the only way to reach it. Recording ahead
of the call covers both without having to know which path ran.
@takumin
takumin force-pushed the string-cat-overlap-offset branch from 3c3f171 to 7e2776c Compare August 9, 2026 14:13
@takumin takumin changed the title string.c: record the source overlap before mrb_str_cat() modifies string.c: record the source overlap before str_modify_cat() runs Aug 9, 2026
@takumin

takumin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto 9360b3fd0 and pushed. I wrote the ordering you gave: the size check from
#7038 stays ahead of everything, the overlap is recorded next, and str_modify_cat()
follows.

The description now carries the broader reason rather than the one it had, and the comment
at the call site draws the same distinction. On the in-place path ptr stays valid, so the
recorded offset is the same answer reached another way; on the detaching path it is the
only way to reach it. Recording ahead of the call covers both without the function having
to know which one ran. The commit message says it the same way.

Retested on the rebased tree: rake test with build_config/default.rb,
MRUBY_CONFIG=asan rake test with address,undefined, and
MRUBY_CONFIG=ci/gcc-clang rake test. KO: 0 and Crash: 0 on all of them.

@matz
matz merged commit 0aeae4b into mruby:master Aug 9, 2026
21 checks passed
@takumin
takumin deleted the string-cat-overlap-offset branch August 9, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants