Commit Graph

18889 Commits

Author SHA1 Message Date
HASUMI Hitoshi a502be4f9f Fix fallback when abnormal error happens
It is not likely happens but if happened, in pattern 1, the whole
process abort when mrb->jmp is NULL.
Instead, make the status MRB_TASK_STOPPED and delegate following
logic of "Handle task termination"
2026-05-29 01:30:24 +09:00
HASUMI Hitoshi f1232334c0 Fix execute_task() so unhandled task exceptions become task results
This patch fixes a bug that c09196c introduced.

## Background

`mrb_task_run()` has two usage patterns:

1. Called directly from `main()` as the top-level scheduler (PicoRuby and R2P2). There is no surrounding C exception handler, so mrb->jmp is NULL on entry
2. Called from Ruby code via Task.run, bootstrapped on top of mruby's regular call chain. mrb->jmp is non-NULL

Historically, an unhandled exception raised inside a task body was turned into the task's result value by `mrb_vm_exec()`: the L_RAISE path walked callinfo down to cibase, ran `fiber_terminate()`, and - because c->vmexec was TRUE and prev_jmp was NULL in pattern 1 - took `return mrb_obj_value(mrb->exc)`.
That value landed in t->result and could be read back through `mrb_task_value()` / `join()`.

## What c09196c broke

It consider only pattern 2 and wrapped `mrb_task_run()` in a protect frame (MRB_TRY / mrb_protect_error) to guarantee that loop_running is cleared on exception.
As a side effect, mrb->jmp is now always non-NULL while a task body is executing, so the L_RAISE path takes `MRB_THROW(prev_jmp)` instead of returning the exception value.

In pattern 2 this merely changed the semantics (exceptions started propagating out of `Task.run` instead of being stored as task results).
In pattern 1 it was FATAL: the throw unwound to mrb_task_run's catch handler, which called `mrb_exc_raise()` to re-propagate, and with no outer jmpbuf this aborted the process.
PicoRuby/R2P2 could no longer retrieve task exceptions via `mrb_task_value()`.

## Fix

Restore the "task exception becomes task result" contract uniformly for both patterns, independent of mrb->jmp:

* Add `mrb_task_state.exception_as_result`. When set, `mrb_vm_exec()`'s non-root_c L_RAISE branch returns the exception as a value even if prev_jmp is non-NULL, instead of throwing

* `execute_task_vm()` raises the flag around `mrb_vm_exec()`, captures the exception into `t->result`, and clears `mrb->exc`

* Wrap `execute_task_vm()` in `mrb_protect_error()` as a safety net for rare paths that still unwind via MRB_THROW (e.g. CINFO_SKIP frames). exception_as_result is reset both at the end of the body and immediately after `mrb_protect_error()` returns, so a caught throw does not leave the llag set

* Expose `Task#value` to retrieve t->result from Ruby, since Task#join cannot deliver the value through its return path under cooperative scheduling

* Add a test asserting that `Task#join` on a task that raised returns the exception object, matching the pre-c09196c observable behavior

## Notes

The "task exception becomes task result" semantics match the mruby/c's rrt0.c and the spirit of CRuby's Thread (an unhandled exception in a thread does not kill the scheduler / process; it surfaces when the thread is joined).
The visible API shape still differs from CRuby - `Task#join` here returns the exception object rather than re-raising it - but the scheduler is no longer destabilized by task errors in either invocation pattern.
2026-05-29 01:25:47 +09:00
Yukihiro "Matz" Matsumoto dad7c0fa36 Merge pull request #6869 from hasumikin/fix/task
Separate the union of timeslice and result in struct mrb_task
2026-05-28 20:20:43 +09:00
HASUMI Hitoshi e0d6d39ce9 Write validate flag (status) after setting timeslice
`mrb_tick()` observes `status == RUNNING` before touching `timeslice`,
so initializing `timeslice` first avoids exposing a partially initialized running state
2026-05-28 19:18:17 +09:00
HASUMI Hitoshi 564726c44f t->result now can be marked unconditionally 2026-05-28 19:16:04 +09:00
HASUMI Hitoshi 5e669560eb Amend field position to pack effectively 2026-05-28 19:07:20 +09:00
HASUMI Hitoshi 030a08092a Separate the union of timeslice and result in struct mrb_task
Bug scenario:
* VM returns an Exception `t->state.result = mrb_vm_exec(...);`
* Despite task is still MRB_TASK_STATUS_RUNNING, IRQ triggered by chance and `mrb_tick()` executes `t->state.timeslice--;`
* But the same memory area already holds the `result`, `timeslice--` reduces `result.value.p`'s top byte

The fix is to separate `timeslice` and `result` into different fields.
I have considered improving critical sections, but I ended up with this patch because I believe it is widely effective and less error-prone.
2026-05-28 18:48:13 +09:00
Yukihiro "Matz" Matsumoto d2a1c43a5f vm.c: defer task switch across C call boundary
A task switch (early return from mrb_vm_exec) is unsafe when execution
has re-entered the VM from C (mrb_funcall, mrb_yield, mrb_vm_run). The
C stack frame between the scheduler's mrb_vm_exec and the current frame
cannot be suspended, and returning early from the inner mrb_vm_exec
leaves the call-info stack drifted, so the enclosing mrb_vm_run trips
its `c->ci == c->cibase || ...` assertion (or corrupts state in a
non-debug build).

This happens when a block yielded from a C function wakes a task, for
example Task::Queue#push from inside a block passed to a C method via
mrb_yield_argv.

Defer the switch via task_across_c_boundary, which walks the call-info
stack for a C frame (cci > 0), mirroring the cooperative guard in
Task.pass. The switch resumes once execution unwinds back to a frame
with no C boundary. The gc.iterating short-circuit from #6862 is kept
as a cheap pre-check.

Fixes #6868.
Refs #6864.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 15:36:21 +09:00
Yukihiro "Matz" Matsumoto dc671f007a vm.c: restore mrb->jmp on early return for task switch
mrb_vm_exec sets mrb->jmp to its own stack-local c_jmp on entry and
restores the caller's prev_jmp on every normal return path. The early
return added for task switching (RETURN_IF_TASK_STOPPED) skipped that
restore, so after a task was preempted via Task.pass the dangling
mrb->jmp pointed into mrb_vm_exec's freed stack frame. A subsequent
raise then longjmp'd into that freed frame and crashed.

Restore prev_jmp in the early-return path, matching the normal returns.

  Task.new { Task.pass }
  Task.pass
  raise "boom"          # SIGSEGV before this change

Fixes #6863.
Refs #6864.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 13:19:10 +09:00
Yukihiro "Matz" Matsumoto c09196ca36 mruby-task: switch mrb_task_run to mrb_protect_error
mruby/throw.h is documented as a core-internal header that should not
be included from mrbgems or user code, and under MRB_USE_CXX_EXCEPTION
or MRB_USE_CXX_ABI the MRB_TRY/MRB_CATCH macros expand to C++
exception syntax that does not compile in a C source file. The
wrapping added in #6866 (commit ee82a7fcc6) accidentally tripped that
constraint.

Drop the throw.h include and use mrb_protect_error() from
mruby/error.h instead. The helper takes a body function plus
userdata, runs it under its own jmpbuf, and reports whether an
exception was caught. We re-raise via mrb_exc_raise so the visible
behavior matches the previous code: loop_running is cleared on both
success and exception, and an exception propagates back out.

Refs #6866.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 12:10:08 +09:00
Yukihiro "Matz" Matsumoto dfc542dbc5 mruby-task: terminate suspended task in suspend test
The "Task#suspend doesn't raise" test left its task parked in
q_suspended_. A later test ("Task.run inside Task.run is a noop")
calls Task.run, and the scheduler will not exit its loop while any
task sits in the suspended (or waiting) queue, so the whole run
hung.

Terminate the task at the end of the suspend test so it does not
leak into the shared scheduler state the next test depends on. This
fixes the hang at its source rather than scrubbing leaked tasks from
the consumer side.

Refs #6866, #6867.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 12:02:07 +09:00
Yukihiro "Matz" Matsumoto 20d8e202a2 Merge pull request #6866 from 0x1eef/task_oneloop
task: return `nil` when given a nested call to `Task.run`
2026-05-28 11:28:07 +09:00
0x1eef 4bdde3a0a8 task: add test 2026-05-27 23:27:51 -03:00
0x1eef ee82a7fcc6 fix: wrap mrb_task_run in MRB_TRY/MRB_CATCH 2026-05-27 23:19:43 -03:00
0x1eef 819156e678 task: return nil when given a nested call to Task.run
When you try to start an event loop inside an event loop,
the mruby process will SIGSEGV:

```ruby
Task.new { Task.run }
Task.run
```

This change turns the second call to `Task.run` into a noop
that returns nil instead.

Fix #6865
2026-05-27 23:10:09 -03:00
Yukihiro "Matz" Matsumoto d1289ac8f9 vm.c: defer task switches during gc.iterating
RETURN_IF_TASK_STOPPED used to bail out of mrb_vm_exec as soon as
task.switching was set, even when the exec was called re-entrantly
from inside a heap-walk callback (e.g. ObjectSpace.each_object via
mrb_yield). Bailing in that situation only unwinds the inner exec,
leaving the outer C iteration to keep calling the callback. The
call-info stack drifts on each subsequent mrb_yield, and the program
either trips the cibase assertion in mrb_vm_run or crashes in
__longjmp.

Hold off the switch while mrb->gc.iterating is set so the heap walk
finishes intact; the switch then fires at the next OP boundary once
the walk releases the flag. MRB_TASK_STOPPED is intentionally not
deferred, since exiting promptly is still correct when the task
itself is going away.

Fixes #6862.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 07:27:37 +09:00
Yukihiro "Matz" Matsumoto f5a5bfcbf6 limitations.md: replace binding note with general mrbgem pattern
The Kernel#binding entry was treating "feature provided by mrbgem"
as a per-method limitation, which does not scale. mruby implements
much of Ruby's standard surface area through mrbgems, so listing
each one would grow without bound.

Replace it with a single top-level note explaining the general
pattern: which features are available depends on the linked gems,
and a NoMethodError on a familiar Ruby method usually points to a
missing gem rather than a true mruby gap.

Refs #6861.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-27 08:49:07 +09:00
Yukihiro "Matz" Matsumoto 012691279d mruby-string-ext: skip scrub UTF-8 assertions on non-UTF-8 builds
When MRB_UTF8_STRING is undefined, String#scrub is a no-op that
returns the receiver as-is (the #else branch added in ccb62ceb57).
The unit tests assumed UTF-8 semantics unconditionally, so a build
of just mruby-string-ext without UTF-8 strings failed all five scrub
tests.

Guard the UTF-8-dependent assertions with `skip unless "あ".length
== 1`, and add a paired test that asserts the no-op behaviour on the
non-UTF-8 build (skipped on UTF-8 builds).

Verified against the build config from the report:
  - UTF-8 (host-debug): 5 tests pass, 1 skip
  - non-UTF-8 (noutf8):  5 skip, 1 test pass

Closes #6860.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-26 23:10:45 +09:00
Yukihiro "Matz" Matsumoto ccb62ceb57 mruby-string-ext: add String#scrub
Replaces each maximal run of invalid UTF-8 bytes with a replacement
string (U+FFFD by default), returning a valid UTF-8 copy. Mirrors
CRuby's String#scrub (Feature #6752) -- the recovery counterpart to
the existing String#valid_encoding? detection API.

Validation matches utf8code() in src/string.c after the RFC 3629 /
Unicode D93b conformance fixup (#2708): overlong encodings, UTF-16
surrogates, and codepoints above U+10FFFF are all treated as invalid.
This is stricter than the existing mrb_utf8len()-based check used by
valid_encoding?, so a string can report valid_encoding? = true and
still get scrubbed; aligning valid_encoding? is a follow-up.

The block form lives in mrblib on top of two C primitives -- __scrub
and __scrub_chunks -- to avoid VM re-entry from C per CLAUDE.md.
Non-String block return values are coerced via to_s (CRuby raises
TypeError instead; the choice is locked in by test).

Closes #6859.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-25 23:36:42 +09:00
Yukihiro "Matz" Matsumoto 4f398f6126 mruby-task: prefix queue helpers with mrb_task_
`q_insert_task` and `q_delete_task` were exporting bare `q_*` names
from libmruby.a -- single-letter prefixes don't belong to the gem's
namespace and risk colliding with anything else linked in.

Rename to `mrb_task_q_insert` / `mrb_task_q_delete`, matching the
`mrb_task_*` convention already used for the rest of the gem's
externally visible symbols. Callers in task.c and task_queue.c are
updated to the new names.

Closes #6858.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-25 06:48:13 +09:00
Yukihiro "Matz" Matsumoto 19c857a773 mruby-regexp: prefix exposed engine entry points with mrb_re_
The Pike VM and pattern compiler were exporting bare names like
`re_compile`, `re_exec`, `re_free`, `re_is_word_char`, `re_utf8_charlen`,
`re_utf8_decode`. `re_exec` in particular collides with the obsolete
BSD libc function of the same name (still present on FreeBSD/NetBSD
base), so embedding mruby alongside platform regex could surface a
link-time symbol clash.

Rename all six entry points to `mrb_re_*` to keep the gem's external
symbols inside mruby's namespace. Source file names and the public
header path are unchanged.

Refs #6858.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-25 06:09:11 +09:00
Yukihiro "Matz" Matsumoto f6564c7db1 class.c: make define_method_m static
The helper is used only inside class.c (mrb_mod_define_method_m,
mod_define_method, define_singleton_method) -- no need to expose it
through libmruby.a. Drops one of the non-`mrb_*` linkage symbols
flagged in #6858.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-25 06:06:05 +09:00
Yukihiro "Matz" Matsumoto 36dd9eab88 vformat.rb: fix format/arg type mismatch in %!d test case
The test pushed mrb_int via vf.i but the format read int via %!d. On
x86_64/aarch64 the va_arg slots overlap so reading the low 32 bits of
the pushed int64 returned the right value, but on strict-alignment
ABIs (MIPS o32) va_arg(ap, int) reads the alignment padding and the
format prints 0 instead of the value.

Make the format match the helper: vf.i pairs with %!i (reads mrb_int),
matching the surrounding lines 44-50 convention and the "inspect
mrb_int" label.

Reported by vobloeb in #6857.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-24 10:31:14 +09:00
Yukihiro "Matz" Matsumoto b1adab4e87 Merge pull request #6856 from vobloeb/patch-1
test/bintest.rb: tokenize ENV['EMULATOR'] via Shellwords.split
2026-05-23 12:16:39 +09:00
Yukihiro "Matz" Matsumoto d21eceb286 mruby-regexp: disable first-byte skip when pattern can match empty
first_set_walk returned TRUE when it reached RE_MATCH via epsilon
transitions, but that's exactly the case where the optimization is
wrong: an empty-matchable pattern can start matching at any position,
including bytes that aren't in the computed first-byte set. The
skip-ahead loop in pike_vm then advanced past valid empty-match
positions, producing a match at the wrong offset (e.g. /a?/.match("b")
reported the empty match at index 1 instead of 0).

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-23 09:58:31 +09:00
Yukihiro "Matz" Matsumoto b9b8186f00 mruby-regexp: don't bump jump offsets that already point at insertion site
insert_inst was incrementing every offset >= pos, but an offset equal to
pos already points to the new instruction's slot -- bumping it shifts
the target onto whatever code got displaced (typically the body of the
quantified atom). For patterns like /a?b?/ the SPLIT for `a?` then
landed on `CHAR 'b'` instead of the new SPLIT for `b?`, so the "skip a"
thread tried to consume 'b' and died, and both atoms failed to match
zero characters at once.

Fixes #6853.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-23 09:58:30 +09:00
Yukihiro "Matz" Matsumoto cddaec7264 mruby-compiler: bail out of array-literal pattern match opt on splat
The optimization that skips `deconstruct` and the size check when the
case/in value is an array literal trusted node count, ignoring splat.
An element like `*a` expands at runtime, so [*a] was treated as length
1 and matched only patterns of that length.

Fixes #6854.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-23 09:58:30 +09:00
Yukihiro "Matz" Matsumoto 3aea2f7e67 Merge pull request #6855 from mruby/dependabot/github_actions/github-actions-dependencies-1f5b201d0b
build(deps): bump the github-actions-dependencies group with 2 updates
2026-05-23 07:58:14 +09:00
vobloeb c0d8abd0a3 test/bintest.rb: tokenize ENV['EMULATOR'] via Shellwords.split
`Command::CrossTestRunner#emulator` returns a shell-quoted string, which `Build#run_test` un-quotes via `sh`, but `CrossBuild#run_bintest` propagates verbatim through `ENV['EMULATOR']` to `test/bintest.rb`. The latter splices it into an Open3 exec-mode argv, where the literal `"` survives into `execve(2)` and the kernel returns `ENOENT`.

Switching to `Shellwords.split(ENV['EMULATOR'])` round-trips the quoted string correctly and also fixes multi-token emulator commands (e.g. `qemu-aarch64 -L /sysroot`), which currently end up concatenated into `argv[0]`.

Verified against mruby `3.3.0`, `3.4.0`, `4.0.0`, and `master`. Cross-build of mruby for `aarch64-unknown-linux-musl` (qemu-user 8.2.10): bintests went from 17/75 crashing → 100/100 passing.
2026-05-22 17:01:38 +00:00
dependabot[bot] 309b99efa5 build(deps): bump the github-actions-dependencies group with 2 updates
Bumps the github-actions-dependencies group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [j178/prek-action](https://github.com/j178/prek-action).


Updates `github/codeql-action` from 4.35.4 to 4.35.5
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/68bde559dea0fdcac2102bfdf6230c5f70eb485e...9e0d7b8d25671d64c341c19c0152d693099fb5ba)

Updates `j178/prek-action` from 2.0.3 to 2.0.4
- [Release notes](https://github.com/j178/prek-action/releases)
- [Commits](https://github.com/j178/prek-action/compare/6ad80277337ad479fe43bd70701c3f7f8aa74db3...bdca6f102f98e2b4c7029491a53dfd366469e33d)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-dependencies
- dependency-name: j178/prek-action
  dependency-version: 2.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 14:54:27 +00:00
Yukihiro "Matz" Matsumoto 66f438d8fe mruby-bin-debugger: return on OOM in mrb_debug_set_break_method
mrb_debug_set_break_method() freed set_class after mrdb_strdup() of
method_name failed but did not return. Execution continued into
alloc_breakpoint(), which on failure double-freed set_class, or on
success stored the dangling pointer in the breakpoint table for later
use-after-free. Return MRB_DEBUG_NOBUF immediately after the free.

mrdb_strdup uses mrb_malloc_simple which returns NULL on OOM (it does
not raise), so the NULL check is reachable in practice.

close #6851

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-22 07:08:26 +09:00
Yukihiro "Matz" Matsumoto 4507b4a633 mruby-bigint: gate mpz_mod's Barrett path on the algorithm's precondition
Barrett reduction requires x < 2^(2*bits(m)); the gate condition only
required x->sz >= y->sz + 2, which let x.sz reach 25 limbs against a
4-limb modulus. When the precondition is violated, mpz_barrett_reduce
silently truncates high limbs and returns garbage.

Integer#remainder, which routes through mpz_mod, was affected:
(3**500).remainder((2**100)+3) returned the wrong value. Integer#%
took the udiv path via mpz_mmod and was unaffected.

Add x->sz <= 2 * y->sz to the gate so out-of-range inputs fall
through to the general udiv path.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-22 07:08:26 +09:00
Yukihiro "Matz" Matsumoto d30ddd4296 Merge pull request #6852 from mruby/dependabot/pre_commit/pre-commit-hooks-613cf0bdf4
build(deps): bump https://github.com/rubocop/rubocop from v1.86.1 to 1.86.2 in the pre-commit-hooks group
2026-05-22 07:03:27 +09:00
dependabot[bot] a4e12dfc42 build(deps): bump https://github.com/rubocop/rubocop
Bumps the pre-commit-hooks group with 1 update: [https://github.com/rubocop/rubocop](https://github.com/rubocop/rubocop).


Updates `https://github.com/rubocop/rubocop` from v1.86.1 to 1.86.2
- [Release notes](https://github.com/rubocop/rubocop/releases)
- [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop/compare/v1.86.1...v1.86.2)

---
updated-dependencies:
- dependency-name: https://github.com/rubocop/rubocop
  dependency-version: 1.86.2
  dependency-type: direct:production
  dependency-group: pre-commit-hooks
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 16:28:35 +00:00
Yukihiro "Matz" Matsumoto 206b9f7477 mruby-bigint: pre-reduce base in mpz_powm_montgomery to satisfy REDC
REDC's input T must satisfy T < R*N; mpz_powm_montgomery() computed
T = base * R^2 without first reducing base modulo n. When base >= n,
T exceeds R*N and REDC silently truncates upper limbs, producing a
wrong base_mont and ultimately a wrong result.

Use mpz_mmod (general division path) to pre-reduce base mod n before
the multiplication by R^2.

Visible effect: (2**160).pow(2, (2**40)+1) returned 0 instead of 1.
A separate pre-existing issue in mpz_mod's Barrett path (broken
precondition check) means that path could return a wrong reduction
for large operands; mmod sidesteps that path entirely.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-21 17:07:51 +09:00
Yukihiro "Matz" Matsumoto 6ac2c3dc56 mruby-bigint: maintain canonical sn=0 when trim reduces sz to 0
mpz_t treats sn (sign) as the canonical "is zero" flag (zero_p(x) :=
(x)->sn == 0). When an arithmetic operation produces a value whose
limbs trim to zero size, sn must be reset to 0 to preserve the
invariant. Several call sites already enforced this locally (e.g.
mpz_sub line 616); make trim() responsible so every caller benefits.

Without this, an inconsistent zero bignum (sn!=0, sz=0) can flow into
mpz_sqr, miss the zero_p guard, and reach mpz_init_heap with hint=0
where mpn_zero(NULL, 0) invokes UB (memset() declares its first
argument nonnull). The trip survives at runtime on glibc but is
formally undefined behavior, flagged by UBSan via Integer#pow(b,e,m)
with specific operands.

close #6849

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-21 17:04:08 +09:00
Yukihiro "Matz" Matsumoto 72c117cb9e variable.c: guard assign_class_name against unresolvable symbol
mrb_sym_name_len() returns NULL when sym is 0, out of range, or
references a freed symbol slot. assign_class_name() indexed [0]
without checking, so malformed bytecode whose OP_CLASS operand
indexed past irep->slen could feed a bogus sym here and crash on
NULL[0]. Skip the class-naming side effect when the sym does not
resolve to a name.

close #6842

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-21 16:11:48 +09:00
Yukihiro "Matz" Matsumoto 465e634d48 mruby-regexp: fix SEGV on uninitialized Regexp's hash and ==
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>
2026-05-21 12:32:40 +09:00
Yukihiro "Matz" Matsumoto 9a1c1eabf9 Merge pull request #6841 from dearblue/gc
Small refactor for `incremental_sweep_phase()`
2026-05-21 10:09:55 +09:00
Yukihiro "Matz" Matsumoto 54c8427df2 mruby-regexp: handle \b inside character class as backspace
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>
2026-05-21 08:23:15 +09:00
dearblue df38160d82 Omit the check whether the typetag is MRB_TT_FREE after calling obj_free()
With commit cbc3dbedb4, freed objects will always be `MRB_TT_FREE`.
2026-05-20 21:15:54 +09:00
dearblue 2c0bccab66 Use else to skip the while loop in incremental_sweep_phase()
As a result, the variables can also be localized.
2026-05-20 21:15:54 +09:00
Yukihiro "Matz" Matsumoto 17d124b00d array.c (mrb_ary_splice): re-modify a after self-aset ary_dup
`a[range] = a` on a long-enough array tripped a heap-buffer-overflow
in value_move(). mrb_ary_splice's self-aset branch calls ary_dup(a)
to get an independent copy of the source elements, but ary_dup ->
ary_replace converts the source to shared as a copy-on-write
optimization when the length exceeds ARY_REPLACE_SHARED_MIN. After
that, a->as.heap.aux is reinterpreted as `shared` (the union member)
and ARY_CAPA(a) reads from the shared pointer's bits rather than
the real capacity. The expand-capa check below then silently mis-
sizes and value_move walks past the buffer.

Re-modify `a` immediately after ary_dup to un-share before the in-
place mutation. The buffer reads through `argv` (which now points
into the dup's storage) stay valid because ary_modify on a multi-
reference shared array allocates a fresh buffer for `a` and leaves
the original buffer owned by the dup.

Found via clusterfuzz mruby_fuzzer testcase 6525563811725312;
regression test covers a[3, 2] = a on a 31-element array (above
the ARY_REPLACE_SHARED_MIN=20 threshold).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 14:27:22 +09:00
Yukihiro "Matz" Matsumoto 3e2ce88eab hw-spi: wrap splat-bearing method signatures as code in README
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>
2026-05-20 08:35:22 +09:00
Yukihiro "Matz" Matsumoto 7378be58cb Merge pull request #6838 from dearblue/gc_stat
Use `MRB_SYM()` in `gc_stat()`
2026-05-19 22:44:41 +09:00
Yukihiro "Matz" Matsumoto ebe4a7aed0 Merge pull request #6837 from dearblue/mrb_const_cache_clear
Avoid surrounding `#if` when using `mrb_const_cache_clear()`
2026-05-19 22:44:37 +09:00
dearblue 6d12f1f331 Use MRB_SYM() in gc_stat() 2026-05-19 22:28:15 +09:00
dearblue 6daa33c9a8 Avoid surrounding #if when using mrb_const_cache_clear()
`mrb_const_cache_clear()` is always available.
2026-05-19 21:22:27 +09:00
Yukihiro "Matz" Matsumoto ae29ab7db9 numeric.c: bounds-check float in Float#div before mrb_int cast
`Float#div(Integer)` cast its receiver to mrb_int unconditionally,
which is undefined behavior when the float is outside the
representable mrb_int range.  ASan reports the UB on inputs like
`5e+56.div(1)`.

Reported by ClusterFuzz testcase
clusterfuzz-testcase-minimized-mruby_fuzzer-5137605569347584.

Guard the cast with FIXABLE_FLOAT and route over-range receivers
to mrb_bint_div when MRB_USE_BIGINT is defined (matching
flo_rounding_int's pattern), or raise via mrb_int_overflow when
not.  Existing `(float in range).div(int)` semantics are
unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 12:15:38 +09:00
Yukihiro "Matz" Matsumoto 73255d3b70 mruby-numeric-ext: avoid signed overflow on Integer#gcd/lcm with MRB_INT_MIN
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>
2026-05-19 12:05:33 +09:00