Skip to content

string.c: reject UTF-8 sequences forbidden by RFC 3629 - #7093

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:string-utf8len-rfc3629
Aug 12, 2026
Merged

string.c: reject UTF-8 sequences forbidden by RFC 3629#7093
matz merged 1 commit into
mruby:masterfrom
takumin:string-utf8len-rfc3629

Conversation

@takumin

@takumin takumin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Problem

mrb_utf8len() checks only the length implied by the lead byte and that
the following bytes are continuation bytes (80-BF). That accepts three
classes of sequences the RFC 3629 grammar excludes from UTF-8:

  • overlong encodings: C0/C1 leads, E0 80-9F, F0 80-8F
  • UTF-16 surrogates U+D800 to U+DFFF: ED A0-BF
  • code points above U+10FFFF: F4 90-BF, F5 to F7 leads

Since mrb_utf8len() backs String#size, character indexing, and
String#valid_encoding?, these sequences were treated as valid
characters:

s = "\xED\xA0\x80"     # encodes the surrogate U+D800
s.valid_encoding?      # CRuby: false, mruby before: true, after: false
s.size                 # CRuby: 3, mruby before: 1, after: 3

"\xC0\x80".size        # CRuby: 2, mruby before: 1, after: 2 (overlong NUL)

Change

Add the lead-specific second byte range checks after the existing
continuation byte checks. A rejected sequence takes the same path as
every other invalid sequence: it counts one byte and moves on.

mruby-string-ext already enforces these rules in utf8code() behind
String#ord and in str_scrub_char_len() behind String#scrub; this
brings mrb_utf8len() in line with them. Boundary code points (U+D7FF,
U+E000, U+10FFFF) remain valid.

Validation

  • Exhaustive comparison against a direct transcription of the RFC 3629
    ABNF: all 1 to 3 byte windows and all 4 byte windows with F0-FF
    leads (285,278,464 windows in total, the remaining leads never read a
    fourth byte) return the character length for valid sequences and 1
    for everything else, with zero mismatches. The throwaway harness is
    not part of this PR.
  • New tests: String#size counts each byte of a rejected sequence
    (test/t/string.rb) and String#valid_encoding? covers all three
    classes plus the valid boundary code points
    (mrbgems/mruby-encoding/test/string.rb).
  • rake test passes with the default gembox and with full-core
    (MRB_UTF8_STRING enabled).

Summary by CodeRabbit

  • Bug Fixes

    • Improved UTF-8 validation for string operations.
    • Invalid sequences, including overlong encodings, surrogate code points, and values above U+10FFFF, are now rejected correctly.
    • Valid boundary code points continue to be recognized correctly.
  • Tests

    • Added coverage for valid and invalid UTF-8 byte sequences and string size handling.

`mrb_utf8len` checked only the length implied by the lead byte and the
continuation bytes, so it accepted three classes of sequences that the
RFC 3629 grammar excludes from UTF-8:

- overlong encodings: `C0`/`C1` leads, `E0 80-9F`, `F0 80-8F`
- UTF-16 surrogates U+D800 to U+DFFF: `ED A0-BF`
- code points above U+10FFFF: `F4 90-BF`, `F5` to `F7` leads

Add the lead-specific second byte range checks after the existing
continuation byte checks. A rejected sequence takes the same path as
every other invalid sequence: it counts one byte and moves on.

`mrb_utf8len` backs `String#size`, character indexing, and
`String#valid_encoding?`, which now agree with CRuby on such input:

```ruby
s = "\xED\xA0\x80"     # encodes the surrogate U+D800
s.valid_encoding?      # CRuby: false, mruby before: true, after: false
s.size                 # CRuby: 3, mruby before: 1, after: 3
```

mruby-string-ext already enforces these rules in `utf8code()` behind
`String#ord` and in `str_scrub_char_len()` behind `String#scrub`; this
brings `mrb_utf8len` in line with them.
@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: b782be7d-977d-4943-8c31-f81b9b1ef61f

📥 Commits

Reviewing files that changed from the base of the PR and between 77eca77 and 377279d.

📒 Files selected for processing (3)
  • mrbgems/mruby-encoding/test/string.rb
  • src/string.c
  • test/t/string.rb

📝 Walkthrough

Walkthrough

The UTF-8 decoder now enforces RFC 3629 limits. Tests cover overlong encodings, surrogate code points, out-of-range code points, invalid byte counting, and valid boundary code points.

Changes

UTF-8 validation

Layer / File(s) Summary
RFC 3629 decoder validation
src/string.c
mrb_utf8len rejects overlong sequences, UTF-16 surrogates, and code points above U+10FFFF.
Invalid and boundary sequence tests
mrbgems/mruby-encoding/test/string.rb, test/t/string.rb
Tests verify invalid UTF-8 handling and valid surrogate-boundary and maximum-Unicode code points.

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

Possibly related PRs

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 and concisely describes the main change: rejecting UTF-8 sequences forbidden by RFC 3629.
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 260b172 into mruby:master Aug 12, 2026
21 checks passed
@takumin
takumin deleted the string-utf8len-rfc3629 branch August 12, 2026 03:53
matz pushed a commit that referenced this pull request Aug 12, 2026
Core stopped accepting the UTF-8 sequences RFC 3629 forbids in #7093,
and `utf8code()` in this gem was taught the same rules by hand. The
tests for those rules go through `String#scrub`, which reads its lengths
from a different helper, so nothing holds `ord` and `codepoints` to
them.

Pin them: a stray continuation byte, a truncated sequence, an overlong
encoding, a surrogate, a code point above U+10FFFF and a five-byte lead
all raise. `String#codepoints` also has to keep its place across a
4-byte character, so a string that continues past one is checked too.
matz pushed a commit that referenced this pull request Aug 12, 2026
`str_scrub_char_len()` asks whether the bytes at p start a valid UTF-8
character and, if they do, how many bytes it takes. `mrb_utf8len()`
answers that question in core, and answers it by the same rules: both
were brought to RFC 3629 in #7093, one after the other.

What has to be bridged is how a refusal comes back. `mrb_utf8len()`
answers 1 for every sequence it turns down, and a real one-byte
character is a lead byte below 0x80, so a length of 1 on a byte at or
above 0x80 is the refusal.

The two were checked against each other over both leading bytes, nine
representative tail bytes and every available length from 1 to 4:
21,233,664 cases, no difference.
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