Skip to content

mruby-pack: check the pack("U") range before the cast to uint32_t - #7114

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:pack-u-range-check
Aug 12, 2026
Merged

mruby-pack: check the pack("U") range before the cast to uint32_t#7114
matz merged 1 commit into
mruby:masterfrom
takumin:pack-u-range-check

Conversation

@takumin

@takumin takumin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

[(1 << 32) + 0x41].pack("U") packs "A" on a build with a 64 bit
mrb_int.

pack_utf8() hands its argument to mrb_utf8_to_buf() as a uint32_t
and treats a return of 0 as out of range:

uint32_t c = (uint32_t)mrb_integer(o);

len = (int)mrb_utf8_to_buf(utf8, c);
if (len == 0) {
  mrb_raise(mrb, E_RANGE_ERROR, "pack(U): value out of range");
}

The cast wraps, so the encoder never sees the value that was passed in and
answers with a perfectly good byte count:

# mruby
[(1 << 32) + 0x41].pack("U").bytes      #=> [65]
[(1 << 32) + 0x110000].pack("U")        # RangeError, but only because it wraps to 0x110000

# CRuby
[(1 << 32) + 0x41].pack("U")            # RangeError: pack(U): value out of range

Fix

Check c < 0 || 0x10FFFF < c on the mrb_int before the cast and raise
the same RangeError with the same message. int_chr_utf8() in
mruby-string-ext guards the shared encoder the same way. With the range
checked, mrb_utf8_to_buf() cannot answer 0, so the test on its return
value is dropped.

A negative argument raised before this too, but by wrapping to 0xFFFFFFFF
and landing above U+10FFFF, not because its sign was ever examined. The
result is unchanged and now follows from the check.

Scope

The accepted range stays at U+10FFFF. CRuby reaches 0x7FFFFFFF using the 5
and 6 byte sequences of the pre RFC 3629 encoding:

# CRuby
[0x200000].pack("U").bytes    #=> [248, 136, 128, 128, 128]
[0x7FFFFFFF].pack("U").bytes  #=> [253, 191, 191, 191, 191, 191]

mruby's unpack("U") rejects those sequences as malformed, so widening
pack("U") alone would leave the pair inconsistent, and widening both
would put the core UTF-8 helpers back outside RFC 3629. That is a separate
decision from this bug, so the range difference is left as it is.

Related: #7113 fixes the same wrapping cast in sprintf("%c"), where the
symptom was sharper because the encoder writes nothing to its buffer for a
value it rejects.

Tests

A new case in mrbgems/mruby-pack/test/pack.rb covers U+10FFFF, the first
value above it, and the wrapping argument. The shift is computed at run time
rather than written as a literal, because on a build with a 32 bit mrb_int
and no bigint the constant folder rejects 1 << 32 while the file is being
compiled and the whole 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:

  • full-core with MRB_UTF8_STRING: 2250 tests, 0 failures
  • default build: 2059 tests plus 105 bintests, 0 failures
  • MRB_INT32 without bigint: 1504 tests, 0 failures. With the literal form
    it is 1456, the difference being every mruby-pack test.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed Unicode packing to reject negative values and code points above the Unicode maximum.
    • Prevented invalid values from being converted into seemingly valid Unicode characters.
    • Added clear range errors for out-of-range input.
  • Tests

    • Added regression coverage for the maximum valid code point and invalid boundary and overflow values.
    • Confirmed valid Unicode boundaries continue to pack correctly.

@takumin
takumin requested a review from matz as a code owner August 12, 2026 11:27
@coderabbitai

coderabbitai Bot commented Aug 12, 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: 9dd1b37e-47d4-4295-b7a7-e156fdb70c6c

📥 Commits

Reviewing files that changed from the base of the PR and between 1e5b809 and c1b6b30.

📒 Files selected for processing (1)
  • mrbgems/mruby-pack/test/pack.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • mrbgems/mruby-pack/test/pack.rb

📝 Walkthrough

Walkthrough

pack("U") now validates integer code points before UTF-8 encoding. Tests cover valid and invalid Unicode boundaries, including 32-bit wrapping values.

Changes

Unicode pack validation

Layer / File(s) Summary
Validate and test Unicode code points
mrbgems/mruby-pack/src/pack.c, mrbgems/mruby-pack/test/pack.rb
pack_utf8 raises RangeError for values below zero or above 0x10FFFF. Tests cover the maximum valid code point, the first invalid code point, and 32-bit wrapping values.

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

Possibly related PRs

  • mruby/mruby#7068: Both changes enforce Unicode code-point validity in different components.
  • mruby/mruby#7113: Both changes validate Unicode ranges before encoding in different code paths.

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: validating the pack("U") range before casting to uint32_t.
✨ 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.

@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 `@mrbgems/mruby-pack/test/pack.rb`:
- Around line 219-220: Guard the wrapping test setup before evaluating the shift
expression: only construct wrapping and run the pack assertion when the build
supports the required integer width, such as under the existing
MRB_USE_BIGINT/64-bit condition. Keep the RangeError assertion unchanged for
supported builds and avoid evaluating 1 << 32 on MRB_INT32 configurations.
🪄 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: 8eb22d89-8f9f-403e-8cf5-ed44eba5cc56

📥 Commits

Reviewing files that changed from the base of the PR and between 19de6ff and 1e5b809.

📒 Files selected for processing (2)
  • mrbgems/mruby-pack/src/pack.c
  • mrbgems/mruby-pack/test/pack.rb

Comment thread mrbgems/mruby-pack/test/pack.rb Outdated
`pack_utf8()` cast its `mrb_int` argument to `uint32_t` and let
`mrb_utf8_to_buf()` report an out of range value by answering 0. The cast
wraps, so a value above 0xFFFFFFFF came back inside the Unicode range and
packed as the character it wrapped to:

```ruby
[(1 << 32) + 0x41].pack("U").bytes  #=> [65]
```

CRuby raises there:

```ruby
[(1 << 32) + 0x41].pack("U")  # RangeError: pack(U): value out of range
```

Check the range on the `mrb_int` before the cast, the way `int_chr_utf8()`
in mruby-string-ext already guards the shared encoder. Once the range is
checked `mrb_utf8_to_buf()` can no longer answer 0, so the test on the way
back is gone.

A negative argument raised before this too, but by wrapping to 0xFFFFFFFF
and landing above U+10FFFF, not because its sign was ever examined. The
result is the same, the reason for it is now written down.

`pack("U")` keeps rejecting everything above U+10FFFF. CRuby accepts up to
0x7FFFFFFF through the 5 and 6 byte sequences of the pre RFC 3629
encoding, which mruby's own `unpack("U")` does not read back, so that
range difference is left alone.
@takumin
takumin force-pushed the pack-u-range-check branch from 1e5b809 to c1b6b30 Compare August 12, 2026 11:42
@matz
matz merged commit ef97e38 into mruby:master Aug 12, 2026
21 checks passed
@takumin
takumin deleted the pack-u-range-check branch August 12, 2026 12:38
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