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>
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>
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>
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>
`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.
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>
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>
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>
`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>
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>
`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>
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
Add a section on the `ports/<name>/` mechanism (`conf.ports`
selection, fallback chain) and on the external HAL provider
naming convention (`hal-<short>-<conf>` overrides the bundled
ports/* of the gem whose name ends in `-<short>`).
Also add `ports/<name>/` to the GEM structure tree.
ref #6825
Co-authored-by: Claude <noreply@anthropic.com>
67f137e201 introduced spec.hal_pattern on the target gem to
declare its expected HAL provider names. But the decision to
override is naturally an opt-in choice belonging to the external
HAL gem (or build_config), not the target. Drop hal_pattern and
hardcode the naming rule in resolve_external_hal!: a gem named
`hal-<short>-<conf>` overrides the gem whose last `-`-separated
segment is <short>. No spec attribute on the target gem; no flag
on add_dependency; the convention does all the work.
The pre-be6413f0d8 convention was `hal-<platform>-<gem>` (e.g.,
hal-posix-task) for platform sort across gems. Now that the
common platforms live under ports/, sorting by which target gem
a HAL is for is more useful, so the order is reversed:
`hal-<gem>-<conf>` (e.g., hal-task-glib).
ref #6825
Co-authored-by: Claude <noreply@anthropic.com>
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.
When `conf.ports` was given multiple names, every matching ports/
directory for each gem was compiled -- fine for single-name builds
and for gems that have at most one matching port, but a footgun
once HAL-using gems start providing multiple alternative ports
(e.g. mruby-task with both posix and an alternative runloop).
Iterate the chain and break on the first match. Existing builds
that pass at most one name per gem keep the same behavior;
`conf.ports :rp2040, :posix` now reads as "use rp2040 if a gem has
that port, otherwise posix" rather than "compile both".
ref #6825
Co-authored-by: Claude <noreply@anthropic.com>
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>