Skip to content

Fix match registers when position is larger than INT_MAX - #220

Merged
kou merged 1 commit into
ruby:masterfrom
Gkasgd:fix-fixed-anchor-pos-over-int-max
Oct 6, 2026
Merged

kou merged 1 commit into
ruby:masterfrom
Gkasgd:fix-fixed-anchor-pos-over-int-max

Conversation

@Gkasgd

@Gkasgd Gkasgd commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What

With StringScanner.new(str, fixed_anchor: true), a scan position beyond INT_MAX (a string longer than 2 GiB) makes StringScanner#getch / #get_byte read out of bounds: a deterministic SIGSEGV (and a bogus string returned when the address happens to be mapped) instead of returning the byte.

Reproducer

require "strscan"

len = 2**31 + 64     # > INT_MAX
pos = 2**31 + 10

s  = "a" * len
sc = StringScanner.new(s, fixed_anchor: true)
sc.pos = pos
p sc.getch           # expected "a"; actual: SIGSEGV (exit 139)

On current master (STRSCAN_VERSION 3.1.9, built against Ruby 3.3.8):

control: fixed_anchor=false getch OK size=1 matched=1
trigger: calling getch with fixed_anchor=true ...
[BUG] Segmentation fault
strscan.so(extract_range)  strscan.c:170
strscan.so(strscan_getch)  strscan.c:1204
ruby exit code: 139

With the fix the same script completes normally and returns "a".

Root cause

adjust_registers_to_matched() stored the (64-bit) scan positions through
onig_region_set(region, 0, (int)p->prev, (int)p->curr) — the int parameters silently narrow the
positions. For pos > INT_MAX the stored beg[0] becomes negative; extract_range() /
extract_beg_len() only checked the upper bound, so S_PBEG(p) + beg_i is computed before the
string and rb_str_new() reads out of bounds.

Changes

  • store the match registers directly (OnigPosition values, no narrowing).

Tests

  • Reproducer: crashes before the change / returns "a" after.
  • test/strscan/test_stringscanner.rb: no new failures vs master (only the pre-existing
    standalone-harness issues).

Notes

This was also reported through HackerOne (#4061020), where Hiroshi SHIBATA closed it as
Informative (the effect is a crash that requires an application-controlled buffer larger than
2 GiB, rather than a security vulnerability) and asked to send it here as a pull request.

Comment thread ext/strscan/strscan.c Outdated
extract_range(struct strscanner *p, long beg_i, long end_i)
{
if (beg_i > S_LEN(p)) return Qnil;
if (beg_i < 0 || beg_i > S_LEN(p)) return Qnil;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we don't need < 0 check with the adjust_registers_to_matched() change. Can we revert this change?

Comment thread ext/strscan/strscan.c Outdated
extract_beg_len(struct strscanner *p, long beg_i, long len)
{
if (beg_i > S_LEN(p)) return Qnil;
if (beg_i < 0 || beg_i > S_LEN(p)) return Qnil;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that beg_i < 0 is never true with/without the adjust_registers_to_matched() . Can we revert this change?

Comment thread test/strscan/test_stringscanner.rb Outdated
Comment on lines +1267 to +1286
def test_pos_over_int_max
# Regression: scan positions beyond INT_MAX were truncated to a negative
# int when storing match registers in fixed_anchor mode, which made
# getch/get_byte read out of bounds instead of returning the byte.
# Needs a string longer than INT_MAX: skip when memory is tight.
begin
string = "a" * (2**31 + 64)
rescue NoMemoryError
skip "requires more than 2GB of memory"
end
pos = 2**31 + 10

scanner = create_string_scanner(string)
scanner.pos = pos
assert_equal("a", scanner.getch)

scanner = create_string_scanner(string)
scanner.pos = pos
assert_equal("a", scanner.get_byte)
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to allocate 2GB memory in test. Could you remove this? We have a reproduce code in the description. It's enough.

adjust_registers_to_matched() narrowed 64-bit scan positions to int via
onig_region_set(), so pos > INT_MAX produced negative match registers and
getch/get_byte read out of bounds in fixed_anchor mode. Store the
positions directly in the region instead.
@Gkasgd
Gkasgd force-pushed the fix-fixed-anchor-pos-over-int-max branch from 4b3292c to 851c879 Compare October 6, 2026 02:57
@Gkasgd

Gkasgd commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Both done:

  • reverted the < 0 lower-bound checks in extract_range() / extract_beg_len() — the diff is now only the registers fix in adjust_registers_to_matched();
  • removed the 2 GiB regression test (test_pos_over_int_max); the reproducer stays in the description.

Re-pushed as 851c879; diff is ext/strscan/strscan.c only.

@kou kou changed the title Fix scan position truncation with fixed_anchor on 64-bit Fix match registers when position is larger than INT_MAX Oct 6, 2026
@kou
kou merged commit d976e3c into ruby:master Oct 6, 2026
43 checks passed
@kou

kou commented Oct 6, 2026

Copy link
Copy Markdown
Member

Thanks.

matzbot pushed a commit to ruby/ruby that referenced this pull request Oct 6, 2026
INT_MAX
(ruby/strscan#220)

## What

With `StringScanner.new(str, fixed_anchor: true)`, a scan position
beyond `INT_MAX` (a string longer than 2 GiB) makes
`StringScanner#getch` / `#get_byte` read out of bounds: a deterministic
`SIGSEGV` (and a bogus string returned when the address happens to be
mapped) instead of returning the byte.

## Reproducer

```ruby
require "strscan"

len = 2**31 + 64     # > INT_MAX
pos = 2**31 + 10

s  = "a" * len
sc = StringScanner.new(s, fixed_anchor: true)
sc.pos = pos
p sc.getch           # expected "a"; actual: SIGSEGV (exit 139)
```

On current master (`STRSCAN_VERSION 3.1.9`, built against Ruby 3.3.8):

```
control: fixed_anchor=false getch OK size=1 matched=1
trigger: calling getch with fixed_anchor=true ...
[BUG] Segmentation fault
strscan.so(extract_range)  strscan.c:170
strscan.so(strscan_getch)  strscan.c:1204
ruby exit code: 139
```

With the fix the same script completes normally and returns `"a"`.

## Root cause

`adjust_registers_to_matched()` stored the (64-bit) scan positions
through
`onig_region_set(region, 0, (int)p->prev, (int)p->curr)` — the `int`
parameters silently narrow the
positions. For `pos > INT_MAX` the stored `beg[0]` becomes negative;
`extract_range()` /
`extract_beg_len()` only checked the upper bound, so `S_PBEG(p) + beg_i`
is computed before the
string and `rb_str_new()` reads out of bounds.

## Changes

- store the match registers directly (`OnigPosition` values, no
narrowing).

## Tests

- Reproducer: crashes before the change / returns `"a"` after.
- `test/strscan/test_stringscanner.rb`: no new failures vs master (only
the pre-existing
  standalone-harness issues).

## Notes

This was also reported through HackerOne (`#4061020`), where Hiroshi
SHIBATA closed it as
Informative (the effect is a crash that requires an
application-controlled buffer larger than
2 GiB, rather than a security vulnerability) and asked to send it here
as a pull request.

ruby/strscan@d976e3cbfc

Co-authored-by: Gkasgd <7713860+Gkasgd@users.noreply.github.com>
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.

2 participants