string.c: record the source overlap before str_modify_cat() runs - #7043
Conversation
📝 WalkthroughWalkthrough
ChangesString append safety
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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
| if (ptr >= RSTR_PTR(s) && ptr <= RSTR_PTR(s) + (size_t)RSTR_LEN(s)) { | ||
| off = ptr - RSTR_PTR(s); | ||
| } |
There was a problem hiding this comment.
🩺 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/mrubyRepository: 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 -250Repository: 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 || trueRepository: 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().
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
|
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 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. On reachability I agree with you: an interior |
`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.
3c3f171 to
7e2776c
Compare
|
Rebased onto The description now carries the broader reason rather than the one it had, and the comment Retested on the rebased tree: |
Summary
mrb_str_cat()has to know whetherptrpoints into the string it is appending to, sothat the source can be followed if the destination moves. It asks that question after
str_modify_cat()has run:By then
smay hold a different buffer.str_modify_cat()takes one of two paths. Itappends inside the shared allocation, which leaves the buffer where it is and
ptrvalid;or it detaches
sthroughmrb_str_modify(), which copies the string into a freshallocation and frees the old one when the copy takes the last reference to it. On the
second path
ptris outside the new buffer, the comparison records no overlap,offstays
-1, and thememcpy()at the end of the function reads throughptrregardless.Reachability
Not from inside mruby. It needs a
ptrinto a shared buffer whose only remainingreference is
sitself, 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 bufferoutlives 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 isthe 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,
offis now recorded instead of lost, soptris re-derivedfrom the new buffer at the
memcpy()rather than left pointing at the old one. Recordingahead 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 abovethe 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 thanmrb_str_modify(), which is also why the reason for recording first is broader than itwas: the in-place path leaves
ptrvalid, so there the offset is merely correct ratherthan necessary.
Testing
rake testwithbuild_config/default.rb,MRUBY_CONFIG=asan rake test(
address,undefined), andMRUBY_CONFIG=ci/gcc-clang rake test, which coversMRB_GC_STRESSwithMRB_USE_DEBUG_HOOK,MRB_GC_FIXED_ARENA, and the C++ ABI build.KO: 0andCrash: 0on 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