mruby-sprintf: reject a %c argument with no UTF-8 encoding - #7113
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe ChangesUTF-8 character formatting
Estimated code review effort: 1 (Trivial) | ~5 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 |
`mrb_utf8_to_buf()` leaves its buffer untouched and returns 0 for a value
it cannot encode. `%c` turned that 0 into a length of 1 and pushed the
uninitialized first byte of its stack buffer, so what came out was
whatever the stack happened to hold at that call site:
```ruby
sprintf("%c", 0x110000).bytes #=> [64]
sprintf("hello world %c", 0x110000).bytes #=> [..., 200]
sprintf("%c", -1).bytes #=> [200]
```
The exact bytes vary with the build and the surrounding format, so the
result is not reproducible even between two call sites in one program.
CRuby raises for both arguments:
```ruby
sprintf("%c", 0x110000) # ArgumentError: invalid character
sprintf("%c", -1) # ArgumentError: invalid character
```
The range has to be checked before the encoder call, not after it, because
`mrb_utf8_to_buf()` takes a `uint32_t`: `(1 << 32) + 0x41` wraps to U+0041
and would print as `A`.
Reject the value up front, the way `int_chr_utf8()` in mruby-string-ext
already does, and raise the `ArgumentError` CRuby raises here. Once the
range is checked, `mrb_utf8_to_buf()` can no longer answer 0, so nothing
is left to test on the way back.
A surrogate still encodes, since CRuby encodes it too, and builds without
`MRB_UTF8_STRING` keep taking the low byte as before.
c9a0ae0 to
0543f3a
Compare
|
Pushed a test fix (the C change is unchanged). The test built its wrapping argument from the literal Measured on
The shift now comes from a variable, so the value is built at run time and is The same review point applies to #7114, which is fixed there too. |
sprintf("%c", cp)wrote an uninitialized stack byte for any integermrb_utf8_to_buf()cannot encode.The encoder leaves its buffer untouched and returns 0 for a codepoint above
U+10FFFF.
%cread that 0 as "invalid codepoint: write single byte" andpushed
cbuf[0], which nothing had written:On a
MRB_UTF8_STRINGbuild the emitted byte tracks whatever was on thestack, so it changes with the surrounding format string:
A negative argument reaches the same path, since
mrb_intis cast touint32_tand-1becomes 0xFFFFFFFF.CRuby raises for both:
Fix
Check the range before calling the encoder and raise the same
ArgumentErrorCRuby raises. The check has to come first rather thantesting the return value, because the cast to
uint32_twraps anout-of-range value into a valid codepoint:
(1 << 32) + 0x41wouldotherwise print as
A. With the range checked,mrb_utf8_to_buf()can nolonger return 0.
This matches
int_chr_utf8()in mruby-string-ext, which likewise rejectsthe value before calling the shared encoder.
Scope
Surrogates are left alone.
mrb_utf8_to_buf()encodes U+D800 to U+DFFFwhile
mrb_utf8len()rejects the resulting bytes, but CRuby has the sameasymmetry and produces the same output, so
%ckeeps matching it:The other callers of
mrb_utf8_to_buf()are already safe:pack_utf8()raises
RangeErrorwhen the encoder answers 0, andint_chr_utf8()andcheck_unicode_cp()reject the value beforehand.Builds without
MRB_UTF8_STRINGare untouched:%cstill takes the lowbyte of the argument.
Tests
A new case in
mrbgems/mruby-sprintf/test/sprintf.rbcovers the U+10FFFF,negative, and wrapping arguments, skipped on builds that are not UTF-8. The
shift for the wrapping argument is computed at run time rather than written as
a literal, because on a build with a 32 bit
mrb_intand no bigint theconstant folder rejects
1 << 32while the file is being compiled and thewhole test file is dropped without a word. On such a build the value cannot be
constructed at all, so the assertion is skipped.
Suites pass:
MRB_UTF8_STRING: 2250 tests, 0 failuresMRB_INT32without bigint: 1504 tests, 0 failures. With the literal form itis 1493, the difference being every mruby-sprintf test.
Summary by CodeRabbit
Bug Fixes
ArgumentErrorinstead of producing unintended characters.Tests
sprintf.