rand_range_int() computed `end - begin + 1` in mrb_int before checking
for a reversed or empty range. For extreme bounds the subtraction itself
overflowed mrb_int (UndefinedBehaviorSanitizer signed-integer-overflow),
and the wraparound could turn a reversed range into a positive span,
defeating the guard.
Reject reversed or empty ranges before subtracting, and compute the
candidate count in unsigned arithmetic, so no signed overflow is
possible. A new unsigned uniform sampler draws from the full mrb_int
domain, so valid ranges whose width exceeds MRB_INT_MAX now return a
uniform in-range value instead of nil, matching CRuby.
Co-authored-by: Claude <noreply@anthropic.com>
io_puts_str, io_puts_ary, and io_puts allocated a fresh mruby String
for every "\n", empty-array marker, "[...]" overflow marker, and
no-arg newline, only for fd_write to unpack it back to ptr/len.
The allocations also stayed on the GC arena across the recursive
walk, scaling pressure with array length.
Split fd_write into fd_write_buf (the EINTR-resilient write loop
over ptr/len) plus the existing mrb_value wrapper, and add a
FD_WRITE_LIT macro for compile-time-known literals. Replace the
four mrb_str_new_lit + fd_write pairs with FD_WRITE_LIT. The "" s
"" inside the macro enforces that the argument is a string literal
so sizeof(s) - 1 is the correct length.
Co-authored-by: Claude <noreply@anthropic.com>
The if/unless nil? optimization called codegen() on the call node's
receiver, but a bare `nil?` is parsed as an FCALL whose receiver is
NULL. codegen(NULL) emits OP_LOADNIL, so the JMPNIL was testing the
literal nil instead of self, making `if nil?` always behave as
`if nil.nil?` (always true) and `unless nil?` always skip its body.
Load self when the receiver is implicit. Fixes#6874.
Co-authored-by: Claude <noreply@anthropic.com>
mrb_task_mark_all marked a task's live registers but, unlike
mark_context_stack in gc.c, never cleared the slots above the live
range. When a preempted task's live range later shrank (a frame had
returned), the stale object pointers left in those slots were neither
marked nor cleared: the objects were swept while the pointers survived.
Re-entering the same frame reused those slots, and the next mark of the
resumed task hit a freed object, tripping the MRB_TT_FREE assertion in
mrb_gc_mark.
Clear the dead slots after marking, exactly as mark_context_stack does
for the running context.
Fixes#6870.
Co-authored-by: Claude <noreply@anthropic.com>
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"
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.
`mrb_tick()` observes `status == RUNNING` before touching `timeslice`,
so initializing `timeslice` first avoids exposing a partially initialized running state
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.
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>
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>
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
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>
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>
`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>
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>
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>
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>
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>
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>
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>
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>
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>
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>