Skip to content

Backport rbs 4.1 fixes for the supported range - #230

Merged
zonuexe merged 3 commits into
masterfrom
backport-rbs41-fixes
Jul 29, 2026
Merged

zonuexe merged 3 commits into
masterfrom
backport-rbs41-fixes

Conversation

@zonuexe

@zonuexe zonuexe commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #225, from the "what can be backported for rbs 3.x/4.0 users" review. The gemspec supports rbs >= 3.0, < 5.0; these two changes bring 4.1's fixes to the rest of that range.

1. Resolv#initialize core overlay

rbs before 4.1 types Resolv#initialize as taking a single resolver, but the runtime contract is an array — so the idiomatic Resolv.new([Resolv::Hosts.new, dns]) false-fired call.argument-type-mismatch. Upstream fixed the signature in 4.1 (ruby/rbs#2960); the overlay backports the corrected overload, following the StringScanner#[] precedent (appended via | ..., duplicate-at-no-cost on 4.1, resolv already in DEFAULT_LIBRARIES).

Proof on rbs 4.0.3 against Mastodon app/lib/request.rb: without the overlay 88 diagnostics including the Resolv FP at :296; with it 87, the FP alone gone.

2. Invalid-UTF-8 quarantine before the parser

Writing the spec for this exposed that the hazard is live on the pinned rbs too, in a different shape:

  • rbs 4.1: RBS::Parser.magic_comment's regex raises a bare ArgumentError on invalid bytes — not a ParsingError, so it escapes every existing rescue and aborts rigor check with a stack trace. (Observed directly: the new virtual-RBS spec crashed through parseable_rbs? before the guard existed.)
  • rbs < 4.1: the C lexer could infinite-loop or abort the process (fixed upstream in Fix lexer infinite loop / abort on invalid UTF-8 byte ruby/rbs#2973/#2983) — a hang no rescue can catch.

The guard is one valid_encoding? predicate run before every parse of externally-controlled content (project sig files, the quarantine detector, virtual RBS). A bad project file lands in the existing loud quarantine warning; a bad virtual contribution is skipped like a parse failure. Rigor-synthesized strings stay unguarded.

The same pattern applies to sig-gen's re-parse of existing project .rbs (layout_index.rb / writer.rb) — deliberately out of scope here (different command path, different UX call on how to refuse); flagged as a follow-up task.

Verification

  • make verify green on pinned rbs 4.1.0 (8,289 examples), git diff --check clean.
  • spec/rigor/environment (the rbs-compat CI job's exact scope) run locally on rbs 4.0.3 and rbs 3.10.4: 195 examples, 0 failures each — both ends of the range reproduced before push, per the 0.3.1 lesson.
  • Mastodon A/B above for the overlay's causality.

zonuexe added 3 commits July 29, 2026 17:56
rbs before 4.1 declares `Resolv#initialize` as taking a single
`Resolv::Hosts | Resolv::DNS`, but the method's runtime contract is an
*array* of resolvers (the default is `[Hosts.new, DNS.new]`, and
`each_address` iterates the argument). Against that signature the
idiomatic `Resolv.new([Resolv::Hosts.new, dns])` has no accepting
overload and `call.argument-type-mismatch` false-fires — measured on
Mastodon's `app/lib/request.rb:296`, the one diagnostic that the rbs
4.1.0 bump (#225) removed from the corpus A/B.

Upstream fixed the signature in 4.1 (ruby/rbs#2960). The gemspec still
supports `>= 3.0, < 5.0`, so this overlay backports the corrected
overload for the older releases, following the StringScanner#[]
precedent: appended via `| ...`, so on 4.1 it duplicates the upstream
overload at no cost, and `resolv` is already in DEFAULT_LIBRARIES so the
reopen never introduces the class on its own.

Verified on rbs 4.0.3 against Mastodon: without the overlay the FP fires
(88 diagnostics on request.rb), with it the FP alone disappears (87).
Drop the file when the rbs floor reaches 4.1.
Rigor hands three kinds of externally-controlled content to
`RBS::Parser.parse_signature`: project `signature_paths:` files, the
quarantine detector's re-parse of the same files, and plugin-synthesized
virtual RBS (which can echo project bytes). None of them checked the
encoding first, and the parser's behaviour on invalid UTF-8 is bad on
every release in the supported range, in two different ways:

- On rbs 4.1 (the pinned version), `RBS::Parser.magic_comment`'s regex
  raises a bare `ArgumentError` before the lexer runs. That is not a
  `ParsingError`, so it sails through every existing rescue and aborts
  `rigor check` with a stack trace. Found live: the new virtual-RBS spec
  crashed through `parseable_rbs?` exactly this way.
- On the older releases the gemspec supports (`>= 3.0, < 5.0`), the C
  lexer could infinite-loop or abort the process on an invalid byte in a
  comment (fixed upstream in ruby/rbs#2973 / #2983) — a hang no rescue
  can catch, which is precisely the failure mode the quarantine's
  fail-soft rescues cannot absorb.

The guard is one predicate (`invalid_encoding?`) run before every parse
of external content, on every rbs version: a project file is quarantined
with the existing loud warning (the note carries the path itself, since
a ParsingError message embeds its own `path:line:` prefix and the warn
composer prints only that element), and a virtual contribution is
skipped like a parse failure. Rigor-synthesized strings (namespace
stubs, sig-gen validity checks of its own output) stay unguarded — their
bytes come from already-parsed names.

Verified across the range: full suite on rbs 4.1.0, and the
`spec/rigor/environment` compat set on 4.0.3 and 3.10.4 — the CI job's
exact scope, run locally per the 0.3.1 lesson.
@zonuexe
zonuexe merged commit 0295795 into master Jul 29, 2026
9 checks passed
@zonuexe
zonuexe deleted the backport-rbs41-fixes branch July 29, 2026 10:51
zonuexe added a commit that referenced this pull request Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant