regexp_init() called re_compile() before setting @source / @flags
IVs, so a Regexp that survived a compile-time exception (e.g. picked
up via ObjectSpace.each_object after `Regexp.new("(")` raised) was
left with no @source. obj.hash then dereferenced nil through
mrb_str_hash() and crashed.
Set the IVs before re_compile(), and make regexp_hash / regexp_eql
defensive against a non-String @source so Regexp.allocate.hash also
behaves.
Co-authored-by: Claude <noreply@anthropic.com>
Inside `[...]`, `\b` denotes U+0008 (backspace) -- the same as
MRI/Onigmo and PCRE. parse_escape() was missing the case, so
the backslash was dropped and the bare letter `b` was inserted
into the class. `[\b]` therefore matched every `b` instead of
backspace.
Add `case 'b': return '\b';` to parse_escape(). The function
is only reached from the character-class body and range
endpoints; the top-level dispatcher emits RE_WBOUND for `\b`
before falling through, so the word-boundary semantics outside
`[...]` are unchanged.
Reported by Sam Ruby in matz/spinel#632; same engine bug
affects both spinel and mruby.
Co-authored-by: Claude <noreply@anthropic.com>
After f91936b06e kept the splat notation unescaped, prettier
kept flagging the standalone `*` in the SPI#write / SPI#transfer
headings every CI run. Wrap the two signatures in backticks so
prettier treats them as inline code (which they are), and the
`*data` form stays unescaped without further conflict. Same
convention is already used in mruby-kernel-ext/README.md
(e.g. `### \`fail(*args)\``).
Co-authored-by: Claude <noreply@anthropic.com>
Negating MRB_INT_MIN (-2^63) is signed overflow (UB) because
2^63 does not fit in mrb_int. Both `mrb_int_gcd` and `int_lcm`
took the absolute value via `if (x < 0) x = -x`, which trips on
MRB_INT_MIN.
Reported by ClusterFuzz testcase
clusterfuzz-testcase-minimized-mruby_fuzzer-5137605569347584.
* mrb_int_gcd: cast each input to mrb_uint before negating; the
Euclidean reduction runs in unsigned. The cast back at the
end yields MRB_INT_MIN only when the mathematical gcd is 2^63
(i.e., gcd(MIN, 0) or gcd(MIN, MIN)).
* int_gcd: detect the negative return value from mrb_int_gcd
and raise via mrb_int_overflow, since the true result does
not fit.
* int_lcm: short-circuit raise when either operand is
MRB_INT_MIN (after the existing zero check), since the abs
would overflow and the lcm with any non-zero operand could
not fit anyway.
Co-authored-by: Claude <noreply@anthropic.com>
The RE_BACKREF execution path read `captures[group * 2]` and
`captures[group * 2 + 1]` without verifying that the group
index fit in the allocated captures array. A pattern like
`/\1/` (no capture group, but a backreference to group 1) is
accepted by the compiler and lands in execution with `ncap = 2`
(only group 0 slots) and an instruction asking for group 1 --
a 4-byte read past the end of the allocation.
Reported by ClusterFuzz testcase
clusterfuzz-testcase-minimized-mruby_fuzzer-5474946829844480.
Add `if (group * 2 + 1 >= ncap) return FALSE;` ahead of the
captures access, mirroring the bounds guard already present in
RE_SAVE. The compiler's permissive `\<digit>` handling stays
unchanged; the runtime now treats a reference to a non-existent
group as a non-match rather than UB.
Co-authored-by: Claude <noreply@anthropic.com>
The backtracking engine recurses via C function calls at RE_SPLIT,
RE_SPLITNG, RE_SAVE, RE_LOOKAHEAD, RE_NEG_LOOKAHEAD, RE_LOOKBEHIND,
and RE_NEG_LOOKBEHIND. Patterns like `(?=)+` make the engine
recurse without consuming input, exhausting the C stack and
triggering SIGSEGV long before MRB_REGEXP_STEP_LIMIT is reached
(each recursion charges only ~1 step, but each frame costs ~150
bytes of stack).
Reported by ClusterFuzz testcase
clusterfuzz-testcase-minimized-mruby_fuzzer-4653331195953152.
Add an integer recursion-depth counter passed alongside the step
counter, and abort the current branch with FALSE when it exceeds
MRB_REGEXP_RECURSION_LIMIT (default 1000, configurable like
STEP_LIMIT). Legitimate patterns nest only a few levels;
pathological inputs bail without crashing the VM.
Co-authored-by: Claude <noreply@anthropic.com>
- This patch implements `Task::Queue` mirroring `Thread::Queue` in CRuby.
- Producer/Consumer pattern is now possible with no polling
## No Top-level pollution
- No top-level `Queue` defined
- No `TaskError`. Instead, `Task::Error` happens when `Task::Queue#pop(true)` when empty
- No `ClosedQueueError`. `Task::Error` also happens when pushing to closed queue
## Future Work
- `Task::SizedQueue`
Lets mruby-task embed cleanly in any GLib-based event loop -- GTK,
libsoup, GStreamer, or anything else built on GMainContext. Tasks
become regular GSources, so the scheduler runs alongside whatever
else is on the loop without polling or busy-waiting.
Sleeping tasks cost zero CPU: the HAL parks until the next wakeup
deadline rather than ticking on a fixed cadence. Multiple mrb_states
on the same thread share one dispatcher and one ticker. Preemption,
Task.run, sleeper wakes, and foreign-loop integration all use the
same primitives, so embedders can mix Task.run with g_main_loop_run
freely.
ref mruby#6825
A gem can declare
spec.hal_pattern = /\Ahal-.*-task\z/
to indicate that another gem whose name matches the pattern (and
which depends on this gem for headers) replaces the built-in
ports/<conf.ports>/ HAL implementation. After all gems are set
up, List#resolve_external_hal! drops the target's ports/* objs
from its object list so the matching gem supplies the HAL
symbols. Two or more matches is reported as a build error.
This restores the pre-be6413f0d8 ability to maintain an
out-of-tree HAL via add_dependency + naming convention, without
reintroducing the "HAL information scattered across gems"
problem: the parent gem still owns the scheduler, headers, and
bundled posix/win ports; external HAL gems are an explicit,
opt-in override.
Declare /\Ahal-.*-task\z/ for mruby-task -- the same naming
pattern used before be6413f0d8.
ref #6825
Co-authored-by: Claude <noreply@anthropic.com>
Enhance wakeup logic for sleeping tasks to handle UINT32_MAX sentinel case and prevent race conditions with mrb_tick.
This enables tickless Task hals to be build.
We got a off by one error here, checking against UINT32_MAX doesn't silently set a wrong value.
When running the task mgem with a busy loop this doesn't cause an issue, but with a tickless timer only one timer ever gets fired and then the task mgem stops working.
The p_value rule only accepted bare tSTRING tokens, which the lexer
emits for single-quoted strings. Double-quoted strings emit
tSTRING_BEG ... tSTRING (or with interpolation, tSTRING_BEG
string_rep tSTRING), so
case "hello"
in "hello"
:match
end
raised "syntax error, unexpected string literal" at the `"` after
`in`. Use the existing `string` non-terminal instead of bare
tSTRING, which also enables alternation (`"a" | "b"`), interpolation
(`"hel#{x}"`), and concatenation by juxtaposition in patterns.
close#6830
Co-authored-by: Claude <noreply@anthropic.com>
The bintest added in 0d87198c92 runs the compiled output via the
mruby binary and uses Kernel#puts in the script body. In builds
that omit mruby-bin-mruby, the test failed with "sh: bin/mruby:
not found" before reaching the actual assertion.
Skip the assert block when bin/mruby isn't built, and switch the
script's puts to print since Kernel#puts only exists when mruby-io
is loaded (same pattern as #6814).
close#6831
Co-authored-by: Claude <noreply@anthropic.com>
The bintest added in 9d8d41006b uses `"hello world".split(/\s+/)`,
which raises `uninitialized constant Regexp` in builds that omit
mruby-regexp. Probe whether the mruby binary defines Regexp and
skip the whole assert block when it doesn't; the test is regression
coverage for mruby-regexp's String#split override, so when regexp
isn't loaded there is nothing to verify.
close#6832
Co-authored-by: Claude <noreply@anthropic.com>
The HAL was integrated into mruby-task/ports/{posix,win}/ in
be6413f0d8 and the function names were updated in 610ff67906, but
the docs and one header still described the old separate-gem layout:
- mrbgems/mruby-task/README.md described hal-posix-task and
hal-win-task as separate gems, used the pre-rename function
names (mrb_task_hal_*), and omitted mrb_hal_task_sleep_us.
Rewrote the HAL section to match the current ports/ model.
- mrbgems/mruby-task/include/task.h had three orphan declarations
(mrb_task_hal_init / _final / _idle_cpu) from before the
rename. Removed; the real declarations are in task_hal.h.
- mrbgems/mruby-task/include/task_hal.h had a comment referring
to the removed hal-* gems.
- doc/guides/amalgamation.md listed hal-posix-io and hal-posix-task
as platform-specific gems alongside mruby-io and mruby-task; both
are now ports under the parent gem.
Reported by Asmod4n in #6825.
Co-authored-by: Claude <noreply@anthropic.com>
re_compile stored raw pointers into the pattern source in
pat->named_captures[i].name. Two ways this could dangle:
- With /x, the source was c.stripped, freed at end of compile.
Later reads (regexp construction, MatchData[:name] lookup) hit
freed memory.
- Without /x, the pointer aliased the input string's RSTRING_PTR.
Mutating that string after Regexp.new could re-buffer it, leaving
name dangling.
Allocate one arena buffer per regexp (only when num_named > 0) and
copy all names in. Common-case regexps without named captures pay
zero bytes.
Reported by OSS-Fuzz (testcase 5695283416858624).
Co-authored-by: Claude <noreply@anthropic.com>
When sprintf is called with a precision larger than the double's
significand width (e.g. "%.51g"), fixed_width() indexed pow10 tables
out of bounds and produced a negative shift exponent. Cap the
internal digit count to 18 in the %g branch, matching the existing
%e and %f branches; downstream loops already zero-pad to the
caller's precision so visible output is unchanged.
Co-authored-by: Claude <noreply@anthropic.com>
`lambda {|a, b=nil|}.curry(3)` used to silently return a curried Proc;
CRuby raises `ArgumentError` because the lambda accepts at most 2 args.
Use `self.parameters` to compute the upper bound (count `:req`/`:opt`
entries, unbounded if `:rest`/`:keyrest` is present) and add the
corresponding range check alongside the existing minimum check.
close#2855
Co-authored-by: Claude <noreply@anthropic.com>
`class String; def split` in mruby-regexp/mrblib/string_regexp.rb
replaced the C-defined String#split rather than overriding it, so
the in-Ruby `return super if pattern.nil?` paths raised
NoMethodError for any `"x".split(...)` call once mruby-regexp was
loaded (the default full-core production binary).
The regression wasn't caught by the test suite because per-gem
tests run under mrb_open_core() with only the gem's dep_list, so
the broken override is never visible from test/t/string.rb
(mruby-test, not mruby-regexp).
Add `alias __split split` at the top of the override class body,
which captures the C-defined method, and change the Ruby override
to delegate via `__split(pattern, limit)` for non-regexp fallback
paths. Add a bintest under mruby-bin-mruby that runs through
bin/mruby (full gem load) to catch this regression class.
Co-authored-by: Claude <noreply@anthropic.com>
The previous commit's prettier pass escaped `*data` to `\*data` in
the SPI#write / SPI#transfer method signatures. The escape is
defensive (prettier avoids any standalone `*` to dodge italic
misparses) but unnecessary in these positions: GFM only treats `*`
as italic when paired with a closing `*`, which never happens in a
method signature. The unescaped form reads more naturally to Ruby
users.
Co-authored-by: Claude <noreply@anthropic.com>
When mrbc compiles multiple input files (e.g. `mrbc -g -o out.mrb
a.rb b.rb`), the bison parser's one-token lookahead can buffer the
final token of one file before partial_hook switches to the next.
By the time bison reduces that token into an AST node,
`mrb_parser_set_filename` has already reset `p->lineno` to 0, so
init_var_header recorded lineno=0 for the previous file's last
statement and codegen propagated the previous instruction's line.
Save the lineno into `prev_file_lineno` immediately before the
reset so init_var_header can restore the correct value when it
detects the lookahead edge case (lineno==0 && filename_index>0).
close#1316
Co-authored-by: Claude <noreply@anthropic.com>
Make `Integer#chr("UTF-8")` reject UTF-16 surrogate code points
(U+D800..U+DFFF), and make `String#ord` reject ill-formed UTF-8 byte
sequences (overlong encodings, surrogates encoded as UTF-8, and code
points above U+10FFFF), matching CRuby and RFC 3629.
The `utf8code()` helper now decodes the code point first and then
validates the range per byte length:
len=2: cp >= 0x80 (rejects overlong)
len=3: cp >= 0x800 and not D800..DFFF
len=4: 0x10000 <= cp <= 0x10FFFF
close#2708
Co-authored-by: Claude <noreply@anthropic.com>
Replace the 12 in-tree mrb_funcall_id call sites (all argc=1 or
argc=2) with the typed inline helpers added in the previous commit.
After inlining each call site allocates exactly the slots it needs
on its own frame instead of going through mrb_funcall_id's fixed
16-element argv buffer.
ref #5804
Co-authored-by: Claude <noreply@anthropic.com>
The peephole in gen_addsub() rewrote `q + -n` into OP_SUBI n (and
`q - -n` into OP_ADDI n) by negating n. For numeric receivers this is
equivalent, but for receivers overriding + or - the runtime fallback
dispatches the flipped method, losing the original operator. Restrict
the fold to non-negative immediates; negative falls through to the
normal OP_ADD/OP_SUB path with LOADI of the literal value.
close#2557
ref #2579
Co-authored-by: Claude <noreply@anthropic.com>
Returns an Array of Addrinfo objects for all local IP addresses
(IPv4 and IPv6) on every network interface, matching CRuby's API.
Backed by getifaddrs(3) on POSIX and GetAdaptersAddresses on
Windows (requires iphlpapi.lib, added to the Windows linker libs).
The HAL returns binary sockaddr strings; src/socket.c wraps each
into Addrinfo so the wrapping code stays platform-agnostic.
Resolves the second error reported in #5659 (after the IO.select
fix from the HAL split): "undefined method 'ip_address_list' for
Class". Issue #5659 itself is closed; this lands the missing API.
Ref #5659.
Co-authored-by: Claude <noreply@anthropic.com>
Their bodies were nearly identical: same argument parsing,
same proc creation, same target-class plumbing. The only
differences are which method to delegate to in the block-given
case (mrb_obj_instance_eval vs. mrb_mod_module_eval) and
which class to use as the target (singleton vs. self-as-class).
Extract the shared logic into object_eval(self, class_eval).
The two top-level dispatchers become one-line wrappers.
Closes#6579, picked from PR by dearblue.
Co-authored-by: Claude <noreply@anthropic.com>
Two related bugs uncovered by OSS-Fuzz testcase 6692915710853120:
1. add_class allowed unbounded growth of c->classes. Class IDs are
stored in re_inst.a (uint8_t), so any ID >= 256 silently aliases
another class via the cast at emit sites. Worse, c->class_capa
(uint16_t) overflows on doubling past 32768 -> 0, then
mrb_realloc(..., 0) returns NULL, and the next memset(&c->classes[id])
segfaults at NULL+offset. Cap with RE_MAX_CLASSES = 256 (the encoding
limit) and raise via compile_error past that.
2. Once the crash is fixed, the testcase exposes a leak of
c->named_captures: compile_error frees c->code, c->classes, and
c->stripped (commit 3f321f09bc) but missed named_captures. Add it
to the same cleanup block.
Reported by OSS-Fuzz (clusterfuzz testcase 6692915710853120).
Co-authored-by: Claude <noreply@anthropic.com>
parse_quantifier read digits via min = min * 10 + d with no upper
bound, allowing patterns like /a{1111558833}/ to overflow int and
trigger signed-integer-overflow UB. Even without UB, the value
flows into compile_quantified's emit loop where it would attempt
to emit a billion copies of the atom.
Add RE_MAX_REPEAT = 32768 (the largest value that still fits in
re_inst.offset, the uint16_t jump field) and reject quantifiers
beyond that during parsing via compile_error. Apply the same cap
to the max field.
Reported by OSS-Fuzz (clusterfuzz testcase 6152367367323648).
Co-authored-by: Claude <noreply@anthropic.com>
Winsock APIs (socket, bind, connect, accept, recv, send, ...) report
errors via WSAGetLastError() and do not set errno, so mrb_sys_fail
on Windows was reading a stale or zero errno. Result: every socket
failure raised SystemCallError with errno 0 ("Success") instead of
the appropriate Errno::* class.
Add mrb_hal_socket_set_errno_from_last_error() to the HAL:
- POSIX: no-op (failed calls already set errno)
- Windows: maps WSAGetLastError() to a POSIX errno via wsa_to_errno()
with 32 cases covering the common Winsock error codes; unmapped
codes fall back to EIO. Each case is #ifdef-guarded against older
MSVC CRTs that lack a particular Exxx.
In src/socket.c, route the 22 socket-API failure sites through a new
sock_sys_fail() helper that calls the HAL translator before
mrb_sys_fail. Also fix mrb_hal_socket_set_nonblock() on Windows,
which was returning -1 without setting errno after ioctlsocket
failure.
POSIX behavior unchanged (verified: TCPSocket connect refused ->
Errno::ECONNREFUSED, bind to privileged port -> Errno::EACCES, bad
sockopt -> Errno::EOPNOTSUPP).
Closes#6819, reported by Asmod4n.
Co-authored-by: Claude <noreply@anthropic.com>
Compared to a pure Ruby implementation, this results in faster performance, eliminates recursive calls, and removes the creation of intermediate objects.
The "permutation" implementation in `ary_combination_next()` is slow for C.
However, it does not require a heap other than the index array.
- Modify the `mrb_combination_state` structure to accommodate feature extensions
- Rename `Array#__repeated_combination` to `__combination`
- Consolidate integer checks for arguments into `__combination`
- Since checking for integer types using both `__to_int` and `0 <=>` is redundant, use only `__to_int`
- Since `__combination` now accepts symbols instead of booleans, the call to `to_enum` has also been consolidated
Since these are expressed as "nPk" or "nCk" in mathematics, rename `n` to `k` and `array_size` to `n`.
Additionally, rename the parameters `#__repeated_combination` and `#__combination_init` from `n` to `k`.
However, the parameter `n` in `#repeated_permutation` and `#repeated_combination` remains unchanged to align with CRuby.
compile_error is the chokepoint for all regex-compile errors;
mrb_raisef longjmps out of re_compile, abandoning the stack-local
re_compiler struct. Three connected bugs:
1. Memory leak: c->code and c->classes (grown by emit/add_class
via mrb_realloc) were never freed before raising, leaking on
any compile error like /[/. c->stripped was already cleaned up
here for the same reason; the other two buffers were missed.
2. Use-after-free: c->src aliases c->stripped when RE_FLAG_EXTENDED
is set, but the original code freed c->stripped before passing
c->src to mrb_raisef's "%s" formatter. Format the message into
an mrb_value first (mruby's GC-managed string survives the
longjmp), then free, then raise.
3. Heap-buffer-overflow: strip_extended returns a non-NUL-terminated
buffer of size len. Even with format-before-free, "%s" called
strlen and read past the buffer end. Use mruby's %l directive
which takes an explicit (char*, size_t) and avoids strlen.
Reported by OSS-Fuzz (clusterfuzz testcase 5394267353972736).
Co-authored-by: Claude <noreply@anthropic.com>
The first-byte bitmap (bm[16]) is intentionally ASCII-only
(include/re_internal.h:75 documents it as 128 bits / ASCII), and
the matcher at re_exec.c:39 short-circuits for bytes >= 128. But
first_set_walk's RE_CHAR case wrote bm[a >> 3] without checking
a, overflowing the 16-byte stack buffer for any pattern
containing a byte >= 128.
When a >= 128, return FALSE so compute_first_set marks the filter
unusable, matching the bail-out pattern already used for RE_NCLASS
and RE_ANY. The pattern still compiles and matches; only the
first-byte optimization is skipped.
Reported by OSS-Fuzz (clusterfuzz testcase 4909069193510912).
Co-authored-by: Claude <noreply@anthropic.com>
Since `Kernel#puts` is undefined in unit tests, using `Kernel#print` is required.
Previously, for example, running `rake test` with the following build configuration caused the tests to fail.
```ruby
MRuby::Build.new do
toolchain
enable_debug
enable_test
enable_bintest
gem core: "mruby-bin-mrb"
end
```