diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e82b5eb81..1901f512b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -22,12 +22,12 @@ jobs: with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: category: "Security" diff --git a/.github/workflows/pre-commit-manual.yml b/.github/workflows/pre-commit-manual.yml index 0c176e5d4..bd7ac01c6 100644 --- a/.github/workflows/pre-commit-manual.yml +++ b/.github/workflows/pre-commit-manual.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3 + - uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4 with: install-only: true - name: Run manual pre-commit hooks diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index c0e1d3f38..cec73055d 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -15,6 +15,6 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3 + - uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4 with: extra-args: --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0128bdd6a..928f2df46 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -118,7 +118,7 @@ repos: types: [markdown] files: \.md$ - repo: https://github.com/rubocop/rubocop - rev: v1.86.1 + rev: v1.86.2 hooks: - id: rubocop name: run rubocop diff --git a/AUTHORS b/AUTHORS index 19e7d0a12..25b3359af 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,25 +1,25 @@ # Authors of mruby (mruby developers) -## The List of Contributors sorted by number of commits (as of 2026-03-02 02877f0) +## The List of Contributors sorted by number of commits (as of 2026-05-31 9d084b0) - 7532 Yukihiro "Matz" Matsumoto (@matz)* - 712 dearblue (@dearblue)* + 7747 Yukihiro "Matz" Matsumoto (@matz)* + 749 dearblue (@dearblue)* 587 KOBAYASHI Shuji (@shuujii) 353 Daniel Bovensiepen (@bovi)* 345 Takeshi Watanabe (@take-cheeze)* 333 Masaki Muranaka (@monaka) - 255 John Bampton (@jbampton) + 266 John Bampton (@jbampton) 234 Jun Hiroe (@suzukaze) 228 Tomoyuki Sahara (@tsahara)* 220 Cremno (@cremno)* 209 Yuki Kurihara (@ksss)+ - 144 Yasuhiro Matsumoto (@mattn)* + 146 Yasuhiro Matsumoto (@mattn)* 113 Carson McDonald (@carsonmcdonald) 104 Tomasz Pędraszewski (@dabroz)* 83 Akira Yumiyama (@akiray03)* 83 skandhas (@skandhas) 80 Masamitsu MURASE (@masamitsu-murase) - 73 Hiroshi Mimaki (@mimaki)* + 79 Hiroshi Mimaki (@mimaki)* 71 Tatsuhiko Kubo (@cubicdaiya)* 71 Yuichiro MASUI (@masuidrive) 62 Yuichiro Kaneko (@yui-knk)+ @@ -36,8 +36,10 @@ 32 Masayoshi Takahashi (@takahashim)+ 31 MATSUMOTO Ryosuke (@matsumotory)* 30 Nobuyoshi Nakada (@nobu) + 29 HASUMI Hitoshi (@hasumikin) 26 Hoshiumi Arata (@hoshiumiarata)* 25 Julian Aron Prenner (@furunkel)* + 23 leviongit (@leviongit) 22 Clayton Smith (@clayton-shopify) 22 Uchio Kondo (@udzura)* 22 Zachary Scott (@zzak)* @@ -50,10 +52,9 @@ 18 Corey Powell (@IceDragon200) 18 Hidetaka Takano (@TJ-Hidetaka-Takano) 18 Jon Maken (@jonforums)+ - 18 leviongit (@leviongit) 18 mirichi (@mirichi) 17 Mitchell Blank Jr (@mitchblank)* - 16 HASUMI Hitoshi (@hasumikin) + 16 Hendrik (@Asmod4n) 16 bggd (@bggd) 16 kano4 (@kano4) 15 Felix Jones (@felixjones)* @@ -76,7 +77,7 @@ 11 RIZAL Reckordp (@Reckordp)+ 11 Seeker (@SeekingMeaning) 11 takkaw (@takkaw) - 10 Hendrik (@Asmod4n) + 10 Chris Hasiński (@khasinski) 10 Miura Hideki (@miura1729) 10 Narihiro Nakamura (@authorNari) 10 YAMAMOTO Masaya (pandax381) @@ -88,6 +89,7 @@ 8 Wataru Ashihara (@wataash)* 7 Bhargava Shastry (@bshastry)* 7 Kouichi Nakanishi (@keizo042) + 7 Paweł Świątkowski (@katafrakt) 7 Rubyist (@expeditiousRubyist) 7 Simon Génier (@simon-shopify) 7 Terence Lee (@hone) @@ -101,7 +103,6 @@ 6 INOUE Yasuyuki (@yasuyuki) 6 Junji Sawada (@junjis0203) 6 Kenji Okimoto (@okkez)+ - 6 Paweł Świątkowski (@katafrakt) 6 Selman ULUG (@selman) 6 Yusuke Endoh (@mame)* 6 buty4649 (@buty4649) @@ -120,7 +121,6 @@ 5 dreamedge (@dreamedge) 5 nkshigeru (@nkshigeru) 5 xuejianqing (@joans321) - 4 Chris Hasiński (@khasinski) 4 Dante Catalfamo (@dantecatalfamo) 4 Goro Kikuchi (@gorogit) 4 Herwin Weststrate (@herwinw) @@ -140,6 +140,7 @@ 4 Yuji Yamano (@yyamano) 4 kurodash (@kurodash)* 4 wanabe (@wanabe)* + 2 0x1eef (@0x1eef) 3 Anton Davydov (@davydovanton) 3 Aurora Nockert (@auroranockert) 3 Carlo Prelz (@asfluido)* @@ -192,6 +193,7 @@ 2 Masahiro Wakame (@vvkame)+ 2 Minao Yamamoto (@tarosay)+ 2 Nihad Abbasov (@NARKOZ) + 2 Pete Kinnecom (@petekinnecom) 2 Robert Mosolgo (@rmosolgo) 2 Russel Hunter Yukawa (@rhykw)+ 2 Ryunosuke SATO (@tricknotes) @@ -220,6 +222,7 @@ 1 Colin MacKenzie IV (@sinisterchipmunk) 1 Daehyub Kim (@lateau) 1 Daniel Varga (@vargad) + 1 David Korczynski (@DavidKorczynski) 1 Diamond Rivero (@diamant3) 1 Edgar Boda-Majer (@eboda) 1 Fangrui Song (@MaskRay) @@ -274,7 +277,6 @@ 1 Patrick Ellis (@pje) 1 Patrick Pokatilo (@SHyx0rmZ) 1 Pavel Evstigneev (@Paxa)+ - 1 Pete Kinnecom (@petekinnecom) 1 Piotr Usewicz (@pusewicz) 1 Prayag Verma (@pra85) 1 Ranmocy (@ranmocy) @@ -282,6 +284,7 @@ 1 Ryan Scott Lewis (@RyanScottLewis) 1 Ryo Okubo (@syucream) 1 SAkira a.k.a. Akira Suzuki (@sakisakira) + 1 SaekiMototsune (@saeki-mototsune) 1 Santiago Rodriguez (@sanrodari) 1 Satoh, Hiroh (@cho45)+ 1 Satoru Naba (@snaba)+ @@ -325,6 +328,7 @@ 1 sbsoftware (@sbsoftware) 1 ssmallkirby (@smallkirby) 1 taku toyama (@tsuichu) + 1 vobloeb (@vobloeb) `*` - Entries unified according to names and addresses `+` - Entries with names different from commits diff --git a/Gemfile.lock b/Gemfile.lock index a3b8f6ef6..694383326 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,7 @@ GEM specs: coderay (1.1.3) rake (13.4.2) - yard (0.9.43) + yard (0.9.44) yard-coderay (0.1.0) coderay yard diff --git a/doc/limitations.md b/doc/limitations.md index 4d8fc4708..b0908583a 100644 --- a/doc/limitations.md +++ b/doc/limitations.md @@ -15,6 +15,23 @@ This document is collecting these limitations. This document does not contain a complete list of limitations. Please help to improve it by submitting your findings. +## Features provided by mrbgems + +Many Ruby features that CRuby builds into its core are provided by +mrbgems in mruby. Which features are actually available depends on +which mrbgems are linked into the build. The `default.gembox` and +`stdlib.gembox` cover the common cases, but a minimal build can omit +familiar features such as `Kernel#binding` (provided by +`mruby-binding`), `Kernel#catch`/`throw` (by `mruby-catch`), +`Enumerable` extensions, `Comparable`, IO, regular expressions, and +many more. + +This is by design rather than a limitation per se. When porting Ruby +code to mruby, a `NoMethodError` or `NameError` often means "the gem +providing this feature is not linked in" rather than "mruby does not +support it." Adding the relevant gem to the build configuration is +usually enough. + ## `Kernel.raise` in rescue clause `Kernel.raise` without arguments does not raise the current exception within @@ -133,12 +150,6 @@ The re-defined `+` operator does not accept any arguments. `'ab'` Behavior of the operator wasn't changed. -## `Kernel#binding` is not supported without mruby-binding gem - -`Kernel#binding` method requires the `mruby-binding` gem (included -in the `metaprog` gembox). Without this gem, `binding` is not -available. - ## `nil?` redefinition in conditional expressions Redefinition of `nil?` is ignored in conditional expressions. diff --git a/include/mruby.h b/include/mruby.h index db89b6236..4a51b0973 100644 --- a/include/mruby.h +++ b/include/mruby.h @@ -274,6 +274,8 @@ typedef struct mrb_task_state { volatile mrb_bool switching; /* Context switch pending flag */ struct mrb_task *main_task; /* Main task wrapper for root context */ uint8_t scheduler_lock; /* Lock counter for synchronous execution */ + mrb_bool loop_running; /* Active mrb_task_run loop flag */ + mrb_bool exception_as_result; /* Return unhandled task exceptions as values */ } mrb_task_state; #endif diff --git a/mrbgems/hw-spi/README.md b/mrbgems/hw-spi/README.md index 50d5e78af..a29403bc5 100644 --- a/mrbgems/hw-spi/README.md +++ b/mrbgems/hw-spi/README.md @@ -50,7 +50,7 @@ spi = SPI.new( \*SPI3 availability depends on ESP32 variant. -### SPI#write(*data) +### `SPI#write(*data)` Write data to the SPI bus. Data can be Integer, Array, or String. @@ -68,7 +68,7 @@ data = spi.read(4) # transmits 0x00 while reading data = spi.read(4, 0xFF) # transmits 0xFF while reading ``` -### SPI#transfer(*data, additional_read_bytes: 0) +### `SPI#transfer(*data, additional_read_bytes: 0)` Full-duplex transfer. Sends data and returns received bytes. Use `additional_read_bytes:` to append zero-filled read bytes. diff --git a/mrbgems/mruby-array-ext/src/array.c b/mrbgems/mruby-array-ext/src/array.c index 285ac908a..62a731c36 100644 --- a/mrbgems/mruby-array-ext/src/array.c +++ b/mrbgems/mruby-array-ext/src/array.c @@ -1636,6 +1636,8 @@ ary_combination_next(mrb_state *mrb, mrb_value self) for (mrb_int i = 0; i < state->k; i++) { if (state->indices[i] >= state->n) { state->mode = comb_finished; + mrb_free(mrb, state->indices); + state->indices = NULL; return mrb_nil_value(); } } @@ -1698,6 +1700,8 @@ ary_combination_next(mrb_state *mrb, mrb_value self) } state->mode = comb_finished; + mrb_free(mrb, state->indices); + state->indices = NULL; return result; } diff --git a/mrbgems/mruby-bigint/core/bigint.c b/mrbgems/mruby-bigint/core/bigint.c index 549b9df19..839798b2e 100644 --- a/mrbgems/mruby-bigint/core/bigint.c +++ b/mrbgems/mruby-bigint/core/bigint.c @@ -373,6 +373,8 @@ trim(mpz_t *x) while (x->sz && x->p[x->sz-1] == 0) { x->sz--; } + /* Maintain invariant: sz == 0 implies sn == 0 (zero is canonical). */ + if (x->sz == 0) x->sn = 0; } /* z = x + y, without regard for sign */ @@ -3147,8 +3149,11 @@ mpz_mod(mpz_ctx_t *ctx, mpz_t *r, mpz_t *x, mpz_t *y) return; } - /* Barrett reduction for moderate-sized moduli (>= 4 limbs where setup is worthwhile) */ - if (y->sz >= 4 && y->sz <= 16 && x->sz >= y->sz + 2) { + /* Barrett reduction for moderate-sized moduli (>= 4 limbs where setup is worthwhile). + * Barrett's precondition is x < 2^(2*bits(m)); inputs beyond ~2*m.sz limbs + * violate it and the algorithm silently truncates high limbs. Fall through + * to general division for those. */ + if (y->sz >= 4 && y->sz <= 16 && x->sz >= y->sz + 2 && x->sz <= 2 * y->sz) { mpz_t mu; mpz_init_temp(ctx, &mu, y->sz + 1); mpz_barrett_mu(ctx, &mu, y); @@ -5248,12 +5253,19 @@ mpz_powm_montgomery(mpz_ctx_t *ctx, mpz_t *result, mpz_init(ctx, &one_mont); mpz_montgomery_reduce(ctx, &one_mont, &R2, n, rho); - /* Convert base to Montgomery form: base_mont = base * R mod n = REDC(base * R^2) */ - mpz_t base_mont, temp; + /* Convert base to Montgomery form: base_mont = base * R mod n = REDC(base * R^2). + * REDC requires its input T to satisfy T < R*N. If `base` is not already + * reduced (e.g. base >= n), `base * R^2` can exceed R*N and REDC produces + * a wrong result. Pre-reduce base modulo n via mpz_mmod (the general + * division path) -- both operands are non-negative here so this is + * semantically equivalent to mpz_mod. */ + mpz_t base_mont, base_reduced, temp; mpz_init(ctx, &base_mont); + mpz_init(ctx, &base_reduced); mpz_init_temp(ctx, &temp, n->sz * 4); - mpz_mul(ctx, &temp, (mpz_t*)base, &R2); + mpz_mmod(ctx, &base_reduced, (mpz_t*)base, (mpz_t*)n); + mpz_mul(ctx, &temp, &base_reduced, &R2); mpz_montgomery_reduce(ctx, &base_mont, &temp, n, rho); /* Initialize accumulator to 1 in Montgomery form */ @@ -5285,6 +5297,7 @@ mpz_powm_montgomery(mpz_ctx_t *ctx, mpz_t *result, mpz_clear(ctx, &R2); mpz_clear(ctx, &one_mont); mpz_clear(ctx, &base_mont); + mpz_clear(ctx, &base_reduced); mpz_clear(ctx, &temp); mpz_clear(ctx, &acc); pool_restore(ctx, pool_state); diff --git a/mrbgems/mruby-bigint/test/bigint.rb b/mrbgems/mruby-bigint/test/bigint.rb index 289e93da8..4f8827edd 100644 --- a/mrbgems/mruby-bigint/test/bigint.rb +++ b/mrbgems/mruby-bigint/test/bigint.rb @@ -150,6 +150,32 @@ assert 'Bigint pow' do # assert_equal(-1041439304, n.pow(n, -1234567890)) end +assert 'Bigint Integer#pow(e, m) - Montgomery path' do + # Regression: mpz_powm_montgomery() failed to pre-reduce base mod n, + # producing wrong results when base >= n. Also trim() must restore + # the canonical sn=0 when sz becomes 0, otherwise an inconsistent + # zero bignum (sn!=0, sz=0) propagates through the squaring loop. + m = (2**40) + 1 + assert_equal 1, (2**160).pow(2, m) + assert_equal 1, (2**320).pow(2, m) + assert_equal 8, ((2**160) + 1).pow(3, m) + m2 = (2**100) + 3 + assert_equal (3**500) % m2, (3**500).pow(1, m2) + assert_equal ((5**300) ** 7) % m2, (5**300).pow(7, m2) +end + +assert 'Bigint Integer#remainder large operand' do + # Regression: mpz_mod's Barrett path didn't enforce its precondition + # x < 2^(2*bits(m)), so it silently truncated high limbs when x was + # much larger than m^2, producing the wrong remainder. Integer#% + # took the udiv path and worked, but Integer#remainder went through + # mpz_mod and was broken. + m = (2**100) + 3 + assert_equal (3**500) % m, (3**500).remainder(m) + assert_equal (5**500) % ((2**150) + 1), (5**500).remainder((2**150) + 1) + assert_equal (2**400) % ((2**130) + 1), (2**400).remainder((2**130) + 1) +end + assert 'Bigint abs' do n = 1<<65 assert_equal 36893488147419103232, n.abs diff --git a/mrbgems/mruby-bin-debugger/tools/mrdb/apibreak.c b/mrbgems/mruby-bin-debugger/tools/mrdb/apibreak.c index 942b9c4cd..6eb4973f1 100644 --- a/mrbgems/mruby-bin-debugger/tools/mrdb/apibreak.c +++ b/mrbgems/mruby-bin-debugger/tools/mrdb/apibreak.c @@ -239,6 +239,7 @@ mrb_debug_set_break_method(mrb_state *mrb, mrb_debug_context *dbg, const char *c set_method = mrdb_strdup(mrb, method_name); if (set_method == NULL) { mrb_free(mrb, set_class); + return MRB_DEBUG_NOBUF; } index = alloc_breakpoint(dbg, MRB_DEBUG_BPTYPE_METHOD); diff --git a/mrbgems/mruby-compiler/core/codegen.c b/mrbgems/mruby-compiler/core/codegen.c index 7a695cdc5..f041a2c99 100644 --- a/mrbgems/mruby-compiler/core/codegen.c +++ b/mrbgems/mruby-compiler/core/codegen.c @@ -3974,7 +3974,13 @@ codegen_if(codegen_scope *s, node *varnode, int val) mrb_sym sym_nil_p = MRB_SYM_Q(nil); if (call_n->method_name == sym_nil_p && callargs_empty(call_n->args)) { nil_p = TRUE; - codegen(s, call_n->receiver, VAL); + if (call_n->receiver) { + codegen(s, call_n->receiver, VAL); + } + else { + /* implicit receiver: bare `nil?` means `self.nil?` */ + gen_load_op1(s, OP_LOADSELF, VAL); + } } } @@ -4285,6 +4291,22 @@ codegen_case(codegen_scope *s, node *varnode, int val) */ static void codegen_pattern(codegen_scope *s, node *pattern, int target, uint32_t *fail_pos, int known_array_len); +/* Return the static element count of an array literal AST node, or -1 if any + * element is a splat (whose runtime length is unknown). + */ +static int +array_literal_known_len(node *value) +{ + if (node_type(value) != NODE_ARRAY) return -1; + struct mrb_ast_array_node *arr = array_node(value); + int len = 0; + for (node *elem = arr->elements; elem; elem = elem->cdr) { + if (is_splat_node(elem->car)) return -1; + len++; + } + return len; +} + /* Pattern matching case/in expression */ static void codegen_case_match(codegen_scope *s, node *varnode, int val) @@ -4298,13 +4320,7 @@ codegen_case_match(codegen_scope *s, node *varnode, int val) uint32_t tmp; /* Check if value is an array literal - allows optimizations in pattern matching */ - int known_array_len = -1; - if (node_type(value) == NODE_ARRAY) { - struct mrb_ast_array_node *arr = array_node(value); - node *elem; - known_array_len = 0; - for (elem = arr->elements; elem; elem = elem->cdr) known_array_len++; - } + int known_array_len = array_literal_known_len(value); /* Generate code for the case value */ codegen(s, value, VAL); @@ -6552,16 +6568,15 @@ codegen(codegen_scope *s, node *tree, int val) /* Optimize: array literal => array pattern with matching sizes */ if (node_type(mp->value) == NODE_ARRAY && node_type(mp->pattern) == NODE_PAT_ARRAY) { - struct mrb_ast_array_node *arr = array_node(mp->value); struct mrb_ast_pat_array_node *pat = pat_array_node(mp->pattern); /* Only optimize for exact match (no rest, no post) */ if (pat->rest == 0 && pat->post == NULL) { - /* Count array elements and pattern pre elements */ - int arr_len = 0, pat_len = 0; + /* Count array elements (bail if splat present) and pattern pre elements */ + int arr_len = array_literal_known_len(mp->value); + int pat_len = 0; node *e; - for (e = arr->elements; e; e = e->cdr) arr_len++; for (e = pat->pre; e; e = e->cdr) pat_len++; - if (arr_len == pat_len) { + if (arr_len >= 0 && arr_len == pat_len) { /* Sizes match - skip deconstruct and size check */ int arr_reg = cursp(); int i = 0; @@ -6600,12 +6615,7 @@ codegen(codegen_scope *s, node *tree, int val) head = cursp(); /* Check if value is array literal for optimization */ - if (node_type(mp->value) == NODE_ARRAY) { - struct mrb_ast_array_node *arr = array_node(mp->value); - node *elem; - known_array_len = 0; - for (elem = arr->elements; elem; elem = elem->cdr) known_array_len++; - } + known_array_len = array_literal_known_len(mp->value); /* Evaluate the value */ codegen(s, mp->value, VAL); diff --git a/mrbgems/mruby-io/src/io.c b/mrbgems/mruby-io/src/io.c index e38545c2c..87d6f9a46 100644 --- a/mrbgems/mruby-io/src/io.c +++ b/mrbgems/mruby-io/src/io.c @@ -911,18 +911,12 @@ io_syswrite(mrb_state *mrb, mrb_value io) /* end */ static mrb_int -fd_write(mrb_state *mrb, int fd, mrb_value str) +fd_write_buf(mrb_state *mrb, int fd, const char *ptr, mrb_int len) { - fssize_t n; - - str = mrb_obj_as_string(mrb, str); - fssize_t len = (fssize_t)RSTRING_LEN(str); if (len == 0) return 0; - - const char *ptr = RSTRING_PTR(str); fssize_t sum = 0; - while (sum < len) { - n = write(fd, ptr + sum, len - sum); + while (sum < (fssize_t)len) { + fssize_t n = write(fd, ptr + sum, (size_t)(len - sum)); if (n == -1) { if (errno == EINTR) continue; mrb_sys_fail(mrb, "syswrite"); @@ -932,6 +926,15 @@ fd_write(mrb_state *mrb, int fd, mrb_value str) return len; } +static mrb_int +fd_write(mrb_state *mrb, int fd, mrb_value str) +{ + str = mrb_obj_as_string(mrb, str); + return fd_write_buf(mrb, fd, RSTRING_PTR(str), RSTRING_LEN(str)); +} + +#define FD_WRITE_LIT(mrb, fd, s) fd_write_buf(mrb, fd, "" s "", sizeof(s) - 1) + /* Helper function to prepare IO object for writing by adjusting buffer state */ static void io_prepare_write(mrb_state *mrb, struct mrb_io *fptr) @@ -987,8 +990,7 @@ io_puts_str(mrb_state *mrb, int fd, mrb_value str) /* Add newline if string doesn't end with one */ if (len == 0 || ptr[len-1] != '\n') { - mrb_value newline = mrb_str_new_lit(mrb, "\n"); - fd_write(mrb, fd, newline); + FD_WRITE_LIT(mrb, fd, "\n"); } } @@ -1001,8 +1003,7 @@ static void io_puts_ary(mrb_state *mrb, int fd, mrb_value ary, int depth) { if (depth >= IO_PUTS_MAX_DEPTH) { - mrb_value mark = mrb_str_new_lit(mrb, "[...]\n"); - fd_write(mrb, fd, mark); + FD_WRITE_LIT(mrb, fd, "[...]\n"); return; } @@ -1010,8 +1011,7 @@ io_puts_ary(mrb_state *mrb, int fd, mrb_value ary, int depth) if (len == 0) { /* Empty array - write a single newline */ - mrb_value newline = mrb_str_new_lit(mrb, "\n"); - fd_write(mrb, fd, newline); + FD_WRITE_LIT(mrb, fd, "\n"); return; } @@ -1041,8 +1041,7 @@ io_puts(mrb_state *mrb, mrb_value io) if (argc == 0) { /* No arguments - just write a newline */ - mrb_value newline = mrb_str_new_lit(mrb, "\n"); - fd_write(mrb, fd, newline); + FD_WRITE_LIT(mrb, fd, "\n"); return mrb_nil_value(); } diff --git a/mrbgems/mruby-numeric-ext/src/numeric_ext.c b/mrbgems/mruby-numeric-ext/src/numeric_ext.c index 1c80a5055..13159b75d 100644 --- a/mrbgems/mruby-numeric-ext/src/numeric_ext.c +++ b/mrbgems/mruby-numeric-ext/src/numeric_ext.c @@ -84,16 +84,20 @@ mrb_value mrb_int_pow(mrb_state *mrb, mrb_value x, mrb_value y); static mrb_int mrb_int_gcd(mrb_int x, mrb_int y) { - if (x < 0) x = -x; - if (y < 0) y = -y; + /* Negate via unsigned so MRB_INT_MIN doesn't overflow. + The cast back at the end produces MRB_INT_MIN only when the + true result is 2^63 (gcd of MRB_INT_MIN with itself or 0); + callers detect that case from the negative return value. */ + mrb_uint ux = (x < 0) ? -(mrb_uint)x : (mrb_uint)x; + mrb_uint uy = (y < 0) ? -(mrb_uint)y : (mrb_uint)y; - while (y != 0) { - mrb_int temp = y; - y = x % y; - x = temp; + while (uy != 0) { + mrb_uint temp = uy; + uy = ux % uy; + ux = temp; } - return x; + return (mrb_int)ux; } /* @@ -122,7 +126,11 @@ int_gcd(mrb_state *mrb, mrb_value x) if (!mrb_integer_p(y)) { mrb_raisef(mrb, E_TYPE_ERROR, "can't convert %Y into Integer", y); } - return mrb_int_value(mrb, mrb_int_gcd(mrb_integer(x), mrb_integer(y))); + mrb_int g = mrb_int_gcd(mrb_integer(x), mrb_integer(y)); + /* g < 0 only when the mathematical result is 2^63 (= |MRB_INT_MIN|), + which does not fit in mrb_int. */ + if (g < 0) mrb_int_overflow(mrb, "gcd"); + return mrb_int_value(mrb, g); } /* @@ -158,6 +166,10 @@ int_lcm(mrb_state *mrb, mrb_value x) if (a == 0 || b == 0) return mrb_int_value(mrb, 0); + /* Negation of MRB_INT_MIN is UB and the lcm with any non-zero + operand would not fit in mrb_int anyway. */ + if (a == MRB_INT_MIN || b == MRB_INT_MIN) mrb_int_overflow(mrb, "lcm"); + gcd_val = mrb_int_gcd(a, b); if (a < 0) a = -a; if (b < 0) b = -b; diff --git a/mrbgems/mruby-regexp/include/re_internal.h b/mrbgems/mruby-regexp/include/re_internal.h index 07e022677..5a60696af 100644 --- a/mrbgems/mruby-regexp/include/re_internal.h +++ b/mrbgems/mruby-regexp/include/re_internal.h @@ -76,7 +76,7 @@ typedef struct mrb_regexp_pattern { uint8_t first_bytes[16]; /* bitmap of possible first bytes (128-bit, ASCII) */ mrb_bool has_first_bytes; /* true if first_bytes is usable for skipping */ mrb_bool is_literal; /* true if pattern is pure literal (no metacharacters) */ - /* Cached VM state for pike_vm (avoids malloc per re_exec call) */ + /* Cached VM state for pike_vm (avoids malloc per mrb_re_exec call) */ uint32_t *cached_visited; /* generation-based visited array */ void *cached_threads[2]; /* curr/next thread lists */ int cached_list_capa; /* capacity of cached thread lists */ @@ -97,6 +97,12 @@ typedef struct mrb_regexp_pattern { #define MRB_REGEXP_STEP_LIMIT 1000000 #endif +/* Recursion-depth limit for bt_match: bounds C stack growth on + patterns like `(?=)+` that recurse without consuming input. */ +#ifndef MRB_REGEXP_RECURSION_LIMIT +#define MRB_REGEXP_RECURSION_LIMIT 1000 +#endif + /* Maximum captures */ #define RE_MAX_CAPTURES 32 @@ -107,21 +113,21 @@ typedef struct { } re_thread_cache; /* Compile a pattern string into bytecode */ -mrb_regexp_pattern* re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags); +mrb_regexp_pattern* mrb_re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags); /* Free a compiled pattern */ -void re_free(mrb_state *mrb, mrb_regexp_pattern *pat); +void mrb_re_free(mrb_state *mrb, mrb_regexp_pattern *pat); /* Execute a match. Returns number of captures filled (0 = no match). captures[2*n] = start, captures[2*n+1] = end for group n. */ -int re_exec(mrb_state *mrb, const mrb_regexp_pattern *pat, +int mrb_re_exec(mrb_state *mrb, const mrb_regexp_pattern *pat, const char *str, mrb_int len, mrb_int start, int *captures, int captures_size); /* UTF-8 helpers */ -int re_utf8_charlen(const char *s, const char *end); -uint32_t re_utf8_decode(const char *s, int *len); -mrb_bool re_is_word_char(uint32_t c); +int mrb_re_utf8_charlen(const char *s, const char *end); +uint32_t mrb_re_utf8_decode(const char *s, int *len); +mrb_bool mrb_re_is_word_char(uint32_t c); #endif /* MRB_RE_INTERNAL_H */ diff --git a/mrbgems/mruby-regexp/src/re_compile.c b/mrbgems/mruby-regexp/src/re_compile.c index 9ffcab203..edbb96890 100644 --- a/mrbgems/mruby-regexp/src/re_compile.c +++ b/mrbgems/mruby-regexp/src/re_compile.c @@ -91,12 +91,16 @@ insert_inst(re_compiler *c, uint32_t pos, uint8_t op, uint8_t a, uint16_t offset c->code[pos].a = a; c->code[pos].offset = offset; - /* fix all jump targets that point at or past the insertion point */ + /* Fix jump targets that point past the insertion point. An offset equal + to `pos` already points to the inserted instruction's new location and + must not be bumped -- bumping it would shift the target to whatever + code got displaced by the insertion (e.g. the body of the quantified + atom), corrupting "skip past this atom" jumps emitted earlier. */ for (uint32_t i = 0; i < c->code_len; i++) { if (i == pos) continue; switch (c->code[i].op) { case RE_JMP: case RE_SPLIT: case RE_SPLITNG: - if (c->code[i].offset >= pos && c->code[i].offset < 0xffff) { + if (c->code[i].offset > pos && c->code[i].offset < 0xffff) { c->code[i].offset++; } break; @@ -179,7 +183,7 @@ class_add_shorthand(re_charclass *cc, int ch) break; case 'W': for (int i = 0; i < 128; i++) { - if (!re_is_word_char(i)) class_set_bit(cc, (uint8_t)i); + if (!mrb_re_is_word_char(i)) class_set_bit(cc, (uint8_t)i); } cc->utf8_any = TRUE; break; @@ -214,6 +218,8 @@ parse_escape(re_compiler *c) case 'v': return '\v'; case 'a': return '\a'; case 'e': return 0x1b; + case 'b': return '\b'; /* backspace; only reachable inside [...] since the + top-level dispatcher emits RE_WBOUND for `\b` */ default: return ch; /* literal: \., \\, \/, \(, etc. */ } } @@ -817,12 +823,18 @@ first_set_walk(const re_inst *code, uint32_t code_len, case RE_ANY: case RE_ANY_NL: return FALSE; /* any byte possible */ case RE_MATCH: - return TRUE; /* empty match; first_bytes still valid for other branches */ + /* Reaching MATCH via epsilon transitions means the regex can match + zero characters at any position. Skipping bytes that aren't in the + first-byte set would skip past valid empty-match positions, so the + optimization isn't safe -- bail out and accept any starting byte. */ + return FALSE; default: return FALSE; } } - return TRUE; + /* Walked off the end without hitting MATCH or a consuming op. Treat as + empty-matchable, same as RE_MATCH. */ + return FALSE; } static mrb_bool @@ -845,7 +857,7 @@ compute_first_set(const re_inst *code, uint32_t code_len, } mrb_regexp_pattern* -re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags) +mrb_re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags) { re_compiler c; memset(&c, 0, sizeof(c)); @@ -972,7 +984,7 @@ re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags) } void -re_free(mrb_state *mrb, mrb_regexp_pattern *pat) +mrb_re_free(mrb_state *mrb, mrb_regexp_pattern *pat) { if (pat) { mrb_free(mrb, pat->code); diff --git a/mrbgems/mruby-regexp/src/re_exec.c b/mrbgems/mruby-regexp/src/re_exec.c index 948f6d080..8f6e04731 100644 --- a/mrbgems/mruby-regexp/src/re_exec.c +++ b/mrbgems/mruby-regexp/src/re_exec.c @@ -170,16 +170,16 @@ add_thread(pike_state *s, re_threadlist *list, case RE_WBOUND: { - mrb_bool before = (sp > s->str) && re_is_word_char((uint8_t)sp[-1]); - mrb_bool after = (sp < s->str_end) && re_is_word_char((uint8_t)*sp); + mrb_bool before = (sp > s->str) && mrb_re_is_word_char((uint8_t)sp[-1]); + mrb_bool after = (sp < s->str_end) && mrb_re_is_word_char((uint8_t)*sp); if (before != after) { pc++; continue; } } return; case RE_NWBOUND: { - mrb_bool before = (sp > s->str) && re_is_word_char((uint8_t)sp[-1]); - mrb_bool after = (sp < s->str_end) && re_is_word_char((uint8_t)*sp); + mrb_bool before = (sp > s->str) && mrb_re_is_word_char((uint8_t)sp[-1]); + mrb_bool after = (sp < s->str_end) && mrb_re_is_word_char((uint8_t)*sp); if (before == after) { pc++; continue; } } return; @@ -301,7 +301,7 @@ pike_vm(mrb_state *mrb, const mrb_regexp_pattern *pat, next.count = 0; int ch = (uint8_t)*sp; - int advance = re_utf8_charlen(sp, str_end); + int advance = mrb_re_utf8_charlen(sp, str_end); for (int i = 0; i < curr.count; i++) { re_thread *th = &curr.threads[i]; @@ -388,8 +388,10 @@ pike_vm(mrb_state *mrb, const mrb_regexp_pattern *pat, */ static mrb_bool bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end, - const char *sp, uint32_t pc, int *captures, int ncap, int *steps) + const char *sp, uint32_t pc, int *captures, int ncap, int *steps, + int depth) { + if (depth > MRB_REGEXP_RECURSION_LIMIT) return FALSE; while (pc < pat->code_len) { if (++(*steps) > MRB_REGEXP_STEP_LIMIT) return FALSE; @@ -402,22 +404,22 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end, case RE_ANY: if (sp >= str_end || *sp == '\n') return FALSE; - sp += re_utf8_charlen(sp, str_end); pc++; + sp += mrb_re_utf8_charlen(sp, str_end); pc++; break; case RE_ANY_NL: if (sp >= str_end) return FALSE; - sp += re_utf8_charlen(sp, str_end); pc++; + sp += mrb_re_utf8_charlen(sp, str_end); pc++; break; case RE_CLASS: if (sp >= str_end || !class_match(&pat->classes[inst.a], (uint8_t)*sp)) return FALSE; - sp += re_utf8_charlen(sp, str_end); pc++; + sp += mrb_re_utf8_charlen(sp, str_end); pc++; break; case RE_NCLASS: if (sp >= str_end || class_match(&pat->classes[inst.a], (uint8_t)*sp)) return FALSE; - sp += re_utf8_charlen(sp, str_end); pc++; + sp += mrb_re_utf8_charlen(sp, str_end); pc++; break; case RE_MATCH: @@ -428,12 +430,12 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end, break; case RE_SPLIT: - if (bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps)) return TRUE; + if (bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps, depth + 1)) return TRUE; pc = inst.offset; break; case RE_SPLITNG: - if (bt_match(pat, str, str_end, sp, inst.offset, captures, ncap, steps)) return TRUE; + if (bt_match(pat, str, str_end, sp, inst.offset, captures, ncap, steps, depth + 1)) return TRUE; pc++; break; @@ -443,7 +445,7 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end, if (slot < ncap) { int old = captures[slot]; captures[slot] = (int)(sp - str); - if (bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps)) return TRUE; + if (bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps, depth + 1)) return TRUE; captures[slot] = old; } return FALSE; @@ -471,8 +473,8 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end, case RE_WBOUND: { - mrb_bool before = (sp > str) && re_is_word_char((uint8_t)sp[-1]); - mrb_bool after = (sp < str_end) && re_is_word_char((uint8_t)*sp); + mrb_bool before = (sp > str) && mrb_re_is_word_char((uint8_t)sp[-1]); + mrb_bool after = (sp < str_end) && mrb_re_is_word_char((uint8_t)*sp); if (before == after) return FALSE; } pc++; @@ -480,8 +482,8 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end, case RE_NWBOUND: { - mrb_bool before = (sp > str) && re_is_word_char((uint8_t)sp[-1]); - mrb_bool after = (sp < str_end) && re_is_word_char((uint8_t)*sp); + mrb_bool before = (sp > str) && mrb_re_is_word_char((uint8_t)sp[-1]); + mrb_bool after = (sp < str_end) && mrb_re_is_word_char((uint8_t)*sp); if (before != after) return FALSE; } pc++; @@ -490,6 +492,7 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end, case RE_BACKREF: { int group = inst.a; + if (group * 2 + 1 >= ncap) return FALSE; int gs = captures[group * 2]; int ge = captures[group * 2 + 1]; if (gs < 0 || ge < 0) return FALSE; @@ -502,13 +505,13 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end, break; case RE_LOOKAHEAD: - if (!bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps)) + if (!bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps, depth + 1)) return FALSE; pc = inst.offset; break; case RE_NEG_LOOKAHEAD: - if (bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps)) + if (bt_match(pat, str, str_end, sp, pc + 1, captures, ncap, steps, depth + 1)) return FALSE; pc = inst.offset; break; @@ -517,7 +520,7 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end, { int lb_len = inst.a; if (sp - str < lb_len) return FALSE; /* not enough text before */ - if (!bt_match(pat, str, str_end, sp - lb_len, pc + 1, captures, ncap, steps)) + if (!bt_match(pat, str, str_end, sp - lb_len, pc + 1, captures, ncap, steps, depth + 1)) return FALSE; pc = inst.offset; } @@ -527,7 +530,7 @@ bt_match(const mrb_regexp_pattern *pat, const char *str, const char *str_end, { int lb_len = inst.a; if (sp - str >= lb_len) { - if (bt_match(pat, str, str_end, sp - lb_len, pc + 1, captures, ncap, steps)) + if (bt_match(pat, str, str_end, sp - lb_len, pc + 1, captures, ncap, steps, depth + 1)) return FALSE; } /* if not enough text before, negative lookbehind succeeds */ @@ -567,7 +570,7 @@ backtrack_exec(mrb_state *mrb, const mrb_regexp_pattern *pat, memset(caps, -1, sizeof(int) * ncap); int steps = 0; - if (bt_match(pat, str, str_end, sp, 0, caps, ncap, &steps)) { + if (bt_match(pat, str, str_end, sp, 0, caps, ncap, &steps, 0)) { if (captures) { int copy = ncap < captures_size ? ncap : captures_size; memcpy(captures, caps, sizeof(int) * copy); @@ -608,7 +611,7 @@ literal_exec(const mrb_regexp_pattern *pat, /* Public entry point */ int -re_exec(mrb_state *mrb, const mrb_regexp_pattern *pat, +mrb_re_exec(mrb_state *mrb, const mrb_regexp_pattern *pat, const char *str, mrb_int len, mrb_int start, int *captures, int captures_size) { diff --git a/mrbgems/mruby-regexp/src/re_utf8.c b/mrbgems/mruby-regexp/src/re_utf8.c index 82f22b1bb..148d59858 100644 --- a/mrbgems/mruby-regexp/src/re_utf8.c +++ b/mrbgems/mruby-regexp/src/re_utf8.c @@ -9,7 +9,7 @@ /* Return byte length of UTF-8 character at s. Returns 1 for invalid sequences (treat as single byte). */ int -re_utf8_charlen(const char *s, const char *end) +mrb_re_utf8_charlen(const char *s, const char *end) { uint8_t c = (uint8_t)*s; int len; @@ -28,7 +28,7 @@ re_utf8_charlen(const char *s, const char *end) /* Decode a UTF-8 character and return its codepoint. *len is set to the byte length consumed. */ uint32_t -re_utf8_decode(const char *s, int *len) +mrb_re_utf8_decode(const char *s, int *len) { uint8_t c = (uint8_t)s[0]; uint32_t cp; @@ -66,7 +66,7 @@ re_utf8_decode(const char *s, int *len) /* Check if character is a "word" character (\w): [a-zA-Z0-9_] */ mrb_bool -re_is_word_char(uint32_t c) +mrb_re_is_word_char(uint32_t c) { if (c >= 'a' && c <= 'z') return TRUE; if (c >= 'A' && c <= 'Z') return TRUE; diff --git a/mrbgems/mruby-regexp/src/regexp.c b/mrbgems/mruby-regexp/src/regexp.c index 4ecbbf0d3..fa06557f5 100644 --- a/mrbgems/mruby-regexp/src/regexp.c +++ b/mrbgems/mruby-regexp/src/regexp.c @@ -19,7 +19,7 @@ /* Regexp data type */ static void regexp_free(mrb_state *mrb, void *ptr) { - re_free(mrb, (mrb_regexp_pattern*)ptr); + mrb_re_free(mrb, (mrb_regexp_pattern*)ptr); } static const struct mrb_data_type regexp_type = { "Regexp", regexp_free }; @@ -107,15 +107,17 @@ regexp_init(mrb_state *mrb, mrb_value self) flags = parse_flags(mrb, flags_val); } - pat = re_compile(mrb, RSTRING_PTR(pattern), RSTRING_LEN(pattern), flags); + /* Set @source and @flags before mrb_re_compile() so a Regexp that survives + a compile-time exception (e.g. picked up by ObjectSpace.each_object) + still has usable IVs for hash/eql?/inspect. */ + mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@source"), pattern); + mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@flags"), mrb_int_value(mrb, (mrb_int)flags)); + + pat = mrb_re_compile(mrb, RSTRING_PTR(pattern), RSTRING_LEN(pattern), flags); DATA_TYPE(self) = ®exp_type; DATA_PTR(self) = pat; - /* store source for #source and #inspect */ - mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@source"), pattern); - mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@flags"), mrb_int_value(mrb, (mrb_int)flags)); - /* store named captures as hash */ if (pat->num_named > 0) { mrb_value nc = mrb_hash_new_capa(mrb, pat->num_named); @@ -204,7 +206,7 @@ exec_match(mrb_state *mrb, mrb_value self, mrb_value str, mrb_int pos) int cap_size = pat->num_captures * 2; int *captures = (int*)mrb_malloc(mrb, sizeof(int) * cap_size); memset(captures, -1, sizeof(int) * cap_size); - int ncap = re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), pos, + int ncap = mrb_re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), pos, captures, cap_size); if (ncap == 0) { @@ -242,7 +244,7 @@ regexp_match_p(mrb_state *mrb, mrb_value self) mrb_regexp_pattern *pat = DATA_GET_PTR(mrb, self, ®exp_type, mrb_regexp_pattern); if (!pat) mrb_raise(mrb, E_ARGUMENT_ERROR, "uninitialized Regexp"); - int ncap = re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), pos, NULL, 0); + int ncap = mrb_re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), pos, NULL, 0); return mrb_bool_value(ncap > 0); } @@ -279,7 +281,7 @@ regexp_case_match(mrb_state *mrb, mrb_value self) pat = DATA_GET_PTR(mrb, self, ®exp_type, mrb_regexp_pattern); if (!pat) return mrb_false_value(); - int ncap = re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), 0, NULL, 0); + int ncap = mrb_re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), 0, NULL, 0); return mrb_bool_value(ncap > 0); } @@ -364,6 +366,9 @@ regexp_eql(mrb_state *mrb, mrb_value self) } mrb_value src1 = mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@source")); mrb_value src2 = mrb_iv_get(mrb, other, mrb_intern_lit(mrb, "@source")); + if (!mrb_string_p(src1) || !mrb_string_p(src2)) { + return mrb_bool_value(mrb_obj_eq(mrb, self, other)); + } if (!mrb_str_equal(mrb, src1, src2)) return mrb_false_value(); return mrb_bool_value(get_iflags(mrb, self) == get_iflags(mrb, other)); } @@ -375,7 +380,7 @@ static mrb_value regexp_hash(mrb_state *mrb, mrb_value self) { mrb_value src = mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@source")); - uint32_t h = mrb_str_hash(mrb, src); + uint32_t h = mrb_string_p(src) ? mrb_str_hash(mrb, src) : 0; h ^= get_iflags(mrb, self) * 0x9e3779b9; /* mix flags into hash */ return mrb_int_value(mrb, (mrb_int)h); } @@ -721,7 +726,7 @@ regexp_gsub_str(mrb_state *mrb, mrb_value self) while (pos <= slen) { memset(captures, -1, sizeof(int) * cap_size); - int n = re_exec(mrb, pat, s, slen, pos, captures, cap_size); + int n = mrb_re_exec(mrb, pat, s, slen, pos, captures, cap_size); if (n == 0) break; /* save last match for $~ */ @@ -795,7 +800,7 @@ regexp_sub_str(mrb_state *mrb, mrb_value self) int *captures = (int*)mrb_malloc(mrb, sizeof(int) * cap_size); memset(captures, -1, sizeof(int) * cap_size); - int n = re_exec(mrb, pat, s, slen, 0, captures, cap_size); + int n = mrb_re_exec(mrb, pat, s, slen, 0, captures, cap_size); if (n == 0) { mrb_free(mrb, captures); clear_match_globals(mrb); @@ -853,7 +858,7 @@ regexp_scan(mrb_state *mrb, mrb_value self) while (pos <= slen) { memset(captures, -1, sizeof(int) * cap_size); - int n = re_exec(mrb, pat, s, slen, pos, captures, cap_size); + int n = mrb_re_exec(mrb, pat, s, slen, pos, captures, cap_size); if (n == 0) break; last_ncap = cap_size; diff --git a/mrbgems/mruby-regexp/test/regexp.rb b/mrbgems/mruby-regexp/test/regexp.rb index 8f1499c27..787c6898d 100644 --- a/mrbgems/mruby-regexp/test/regexp.rb +++ b/mrbgems/mruby-regexp/test/regexp.rb @@ -47,6 +47,14 @@ assert("Regexp - character class") do assert_equal "abc", md[0] end +assert("Regexp - \\b inside character class is backspace") do + # Outside [...], \b is the word boundary assertion; inside [...] + # it must mean U+0008 (backspace), matching MRI/Onigmo. + assert_equal "Ruby", "Ruby".gsub(/[\b]/, "X") + assert_equal "aXc", "a\bc".gsub(/[\b]/, "X") + assert_equal ["\b", "\t", "\n"], "ABC\b\t\n".scan(/[\b-\n]/) +end + assert("Regexp - dot") do re = Regexp.new("a.c") assert_true re.match?("abc") @@ -173,6 +181,17 @@ assert("Regexp#hash") do assert_not_equal r1.hash, r3.hash end +assert("Regexp#hash/== on uninitialized regexp") do + # Regexp.allocate yields an object with no @source IV; hash/== must + # not crash (regression: ObjectSpace.each_object could expose a + # half-initialized Regexp after Regexp.new raised a compile error). + r = Regexp.allocate + assert_kind_of Integer, r.hash + assert_true r == r + assert_false r == Regexp.allocate + assert_false r == Regexp.new("abc") +end + assert("Regexp#options") do assert_equal 0, Regexp.new("abc").options assert_equal Regexp::IGNORECASE, Regexp.new("abc", Regexp::IGNORECASE).options @@ -362,7 +381,7 @@ assert("MatchData#named_captures") do end assert("Regexp - named captures survive /x preprocessing") do - # Regression: with /x, re_compile freed the stripped buffer that + # Regression: with /x, mrb_re_compile freed the stripped buffer that # named_captures[i].name pointed into. re = /(?\d+) # comment \s* (?\w+) /x @@ -444,3 +463,32 @@ assert("$1-$9 cleared on no match") do /xyz/ =~ "abc" assert_nil $1 end + +assert("Regexp - consecutive optional quantifiers (#6853)") do + # insert_inst was over-incrementing jump offsets that pointed *at* the + # insertion site, sending earlier "skip this atom" SPLITs into the next + # atom's body. Two adjacent zero-matchable atoms then both failed even + # when both should match zero characters. + assert_equal ["a", nil], /\Aa(b)?c?\z/.match("a").to_a + assert_equal ["ab", "b"], /\Aa(b)?c?\z/.match("ab").to_a + assert_equal ["ac", nil], /\Aa(b)?c?\z/.match("ac").to_a + assert_equal ["abc", "b"], /\Aa(b)?c?\z/.match("abc").to_a + + assert_equal [""], /a?b?/.match("").to_a + assert_equal [""], /a*b*/.match("").to_a + assert_equal [""], /a?b?c?d?/.match("").to_a +end + +assert("Regexp - empty-matchable patterns find earliest match position") do + # When a regex can match zero characters via epsilon transitions, the + # first-byte skip-ahead optimization is unsafe: skipping past bytes + # that aren't in the first-byte set would also skip past valid + # empty-match positions. + md = /a?/.match("b") + assert_equal "", md[0] + assert_equal 0, md.begin(0) + + md = /a?b?/.match("c") + assert_equal "", md[0] + assert_equal 0, md.begin(0) +end diff --git a/mrbgems/mruby-string-ext/mrblib/string.rb b/mrbgems/mruby-string-ext/mrblib/string.rb index 6c3c791f4..15d930a93 100644 --- a/mrbgems/mruby-string-ext/mrblib/string.rb +++ b/mrbgems/mruby-string-ext/mrblib/string.rb @@ -163,4 +163,34 @@ class String end self end + + ## + # call-seq: + # str.scrub -> new_str + # str.scrub(repl) -> new_str + # str.scrub {|bytes| block } -> new_str + # + # Returns a copy of +self+ with each maximal run of invalid UTF-8 bytes + # replaced by +repl+ (U+FFFD if +repl+ is omitted), or by the value + # returned from the block when one is given. The block receives the + # invalid bytes as a String. + # + # "abc\x80def".scrub #=> "abc\u{FFFD}def" + # "abc\x80def".scrub("?") #=> "abc?def" + # "\xE3\x81".scrub #=> "\u{FFFD}" + # "\x80\x81".scrub { |b| b.bytes.map { |c| "<%02X>" % c }.join } + # #=> "<80><81>" + def scrub(repl = nil, &block) + return __scrub(repl) unless block + chunks = __scrub_chunks + return chunks[0] if chunks.length == 1 + result = chunks[0].dup + i = 1 + while i < chunks.length + result << yield(chunks[i]).to_s + result << chunks[i + 1] if i + 1 < chunks.length + i += 2 + end + result + end end diff --git a/mrbgems/mruby-string-ext/src/string.c b/mrbgems/mruby-string-ext/src/string.c index d54edd8d7..4878db56e 100644 --- a/mrbgems/mruby-string-ext/src/string.c +++ b/mrbgems/mruby-string-ext/src/string.c @@ -1020,6 +1020,143 @@ str_ord(mrb_state* mrb, mrb_value str) return mrb_fixnum_value(c); } +/* Returns the byte length of a valid UTF-8 char starting at p, or -1 for + any invalid sequence (illegal lead byte, truncated tail, invalid + continuation byte, overlong encoding, UTF-16 surrogate, or codepoint + above U+10FFFF). Like utf8code() but reports rather than raises. */ +static mrb_int +str_scrub_char_len(const unsigned char *p, const unsigned char *e) +{ + if (p[0] < 0x80) return 1; + mrb_int len = mrb_utf8len_table[p[0]>>3]; + if (len < 2 || len > e - p) return -1; + for (mrb_int i = 1; i < len; i++) { + if ((p[i] & 0xc0) != 0x80) return -1; + } + mrb_int cp; + if (len == 2) { + cp = ((p[0] & 0x1f) << 6) | (p[1] & 0x3f); + if (cp < 0x80) return -1; + } + else if (len == 3) { + cp = ((p[0] & 0x0f) << 12) | ((p[1] & 0x3f) << 6) | (p[2] & 0x3f); + if (cp < 0x800) return -1; + if (cp >= 0xD800 && cp <= 0xDFFF) return -1; + } + else { /* len == 4 */ + cp = ((p[0] & 0x07) << 18) | ((p[1] & 0x3f) << 12) + | ((p[2] & 0x3f) << 6) | (p[3] & 0x3f); + if (cp < 0x10000 || cp > 0x10FFFF) return -1; + } + return len; +} + +static void +str_scrub_validate_replacement(mrb_state *mrb, mrb_value repl) +{ + const unsigned char *p = (const unsigned char*)RSTRING_PTR(repl); + const unsigned char *e = p + RSTRING_LEN(repl); + while (p < e) { + mrb_int len = str_scrub_char_len(p, e); + if (len < 0) { + mrb_raise(mrb, E_ARGUMENT_ERROR, "replacement must be valid UTF-8"); + } + p += len; + } +} + +/* Core of String#scrub for the no-block case. Returns a new string with + each maximal run of invalid UTF-8 bytes replaced by `repl` (or U+FFFD + if `repl` is nil). Already-valid strings are returned via mrb_str_dup. */ +static mrb_value +str_scrub_core(mrb_state *mrb, mrb_value self) +{ + mrb_value repl = mrb_nil_value(); + mrb_get_args(mrb, "|S!", &repl); + + const char *replace; + mrb_int replace_len; + if (mrb_nil_p(repl)) { + replace = "\xEF\xBF\xBD"; /* U+FFFD REPLACEMENT CHARACTER */ + replace_len = 3; + } + else { + str_scrub_validate_replacement(mrb, repl); + replace = RSTRING_PTR(repl); + replace_len = RSTRING_LEN(repl); + } + + struct RString *s = mrb_str_ptr(self); + if (RSTR_SINGLE_BYTE_P(s) || RSTR_BINARY_P(s)) { + return mrb_str_dup(mrb, self); + } + + const unsigned char *p = (const unsigned char*)RSTR_PTR(s); + const unsigned char *e = p + RSTR_LEN(s); + const unsigned char *valid_start = p; + const unsigned char *q = p; + mrb_value result = mrb_nil_value(); /* lazily allocated on first invalid byte */ + + while (q < e) { + mrb_int len = str_scrub_char_len(q, e); + if (len < 0) { + if (mrb_nil_p(result)) { + result = mrb_str_new(mrb, NULL, 0); + } + mrb_str_cat(mrb, result, (const char*)valid_start, q - valid_start); + mrb_str_cat(mrb, result, replace, replace_len); + q++; + while (q < e && str_scrub_char_len(q, e) < 0) q++; + valid_start = q; + } + else { + q += len; + } + } + + if (mrb_nil_p(result)) { + return mrb_str_dup(mrb, self); /* already valid */ + } + mrb_str_cat(mrb, result, (const char*)valid_start, q - valid_start); + return result; +} + +/* Splits self into alternating valid/invalid byte runs and returns them + as an Array of strings ([valid, invalid, valid, ...], odd length). + Used by the block form of String#scrub in mrblib; the block can then + map each invalid run to a replacement of its choosing without the C + side having to call back into the VM. */ +static mrb_value +str_scrub_chunks(mrb_state *mrb, mrb_value self) +{ + mrb_value ary = mrb_ary_new(mrb); + struct RString *s = mrb_str_ptr(self); + if (RSTR_SINGLE_BYTE_P(s) || RSTR_BINARY_P(s)) { + mrb_ary_push(mrb, ary, mrb_str_dup(mrb, self)); + return ary; + } + const unsigned char *p = (const unsigned char*)RSTR_PTR(s); + const unsigned char *e = p + RSTR_LEN(s); + const unsigned char *valid_start = p; + const unsigned char *q = p; + while (q < e) { + mrb_int len = str_scrub_char_len(q, e); + if (len < 0) { + mrb_ary_push(mrb, ary, mrb_str_new(mrb, (const char*)valid_start, q - valid_start)); + const unsigned char *invalid_start = q; + q++; + while (q < e && str_scrub_char_len(q, e) < 0) q++; + mrb_ary_push(mrb, ary, mrb_str_new(mrb, (const char*)invalid_start, q - invalid_start)); + valid_start = q; + } + else { + q += len; + } + } + mrb_ary_push(mrb, ary, mrb_str_new(mrb, (const char*)valid_start, q - valid_start)); + return ary; +} + /* Internal helper for String#codepoints - returns array of character codepoints */ static mrb_value str_codepoints(mrb_state *mrb, mrb_value str) @@ -1068,6 +1205,24 @@ str_codepoints(mrb_state *mrb, mrb_value self) } return result; } + +/* Non-UTF-8 builds: scrub is a no-op. The replacement arg is accepted + (and validated only as a String) for API parity with the UTF-8 build. */ +static mrb_value +str_scrub_core(mrb_state *mrb, mrb_value self) +{ + mrb_value repl = mrb_nil_value(); + mrb_get_args(mrb, "|S!", &repl); + return mrb_str_dup(mrb, self); +} + +static mrb_value +str_scrub_chunks(mrb_state *mrb, mrb_value self) +{ + mrb_value ary = mrb_ary_new(mrb); + mrb_ary_push(mrb, ary, mrb_str_dup(mrb, self)); + return ary; +} #endif static mrb_bool @@ -2282,6 +2437,8 @@ static const mrb_mt_entry string_ext_rom_entries[] = { MRB_MT_ENTRY(str_b, MRB_SYM(b), MRB_ARGS_NONE()), MRB_MT_ENTRY(str_lines, MRB_SYM(__lines), MRB_ARGS_NONE()), MRB_MT_ENTRY(str_codepoints, MRB_SYM(__codepoints), MRB_ARGS_NONE()), + MRB_MT_ENTRY(str_scrub_core, MRB_SYM(__scrub), MRB_ARGS_OPT(1)), + MRB_MT_ENTRY(str_scrub_chunks, MRB_SYM(__scrub_chunks), MRB_ARGS_NONE()), MRB_MT_ENTRY(str_lstrip, MRB_SYM(lstrip), MRB_ARGS_NONE()), MRB_MT_ENTRY(str_rstrip, MRB_SYM(rstrip), MRB_ARGS_NONE()), MRB_MT_ENTRY(str_strip, MRB_SYM(strip), MRB_ARGS_NONE()), diff --git a/mrbgems/mruby-string-ext/test/string.rb b/mrbgems/mruby-string-ext/test/string.rb index 335c4f81b..c0fd381db 100644 --- a/mrbgems/mruby-string-ext/test/string.rb +++ b/mrbgems/mruby-string-ext/test/string.rb @@ -791,3 +791,60 @@ assert('String#-@') do a = -(a.freeze) assert_true(a.frozen?) end + +assert('String#scrub default replacement (U+FFFD)') do + # scrub has UTF-8 semantics; on builds without MRB_UTF8_STRING it + # degrades to a no-op (verified separately below). + skip unless "あ".length == 1 + assert_equal "\u{FFFD}", "\xE3\x81".scrub + assert_equal "abc\u{FFFD}def", "abc\x80def".scrub + assert_equal "\u{FFFD}", "\x80\x81\x82".scrub # run collapsed + assert_equal "", "".scrub + assert_equal "hello", "hello".scrub # already valid + assert_equal "あい", "あい".scrub # already valid multibyte +end + +assert('String#scrub rejects malformed sequences') do + skip unless "あ".length == 1 + # overlong, UTF-16 surrogate, codepoint above U+10FFFF + assert_equal "\u{FFFD}", "\xC0\xAF".scrub # overlong "/" + assert_equal "\u{FFFD}", "\xED\xA0\x80".scrub # surrogate U+D800 + assert_equal "\u{FFFD}", "\xF4\x90\x80\x80".scrub # > U+10FFFF +end + +assert('String#scrub with replacement string') do + skip unless "あ".length == 1 + assert_equal "abc?def", "abc\x80def".scrub("?") + assert_equal "abcdef", "abc\x80def".scrub("") + assert_equal "abcdef", "abc\x80def".scrub("") +end + +assert('String#scrub raises on invalid replacement') do + skip unless "あ".length == 1 + assert_raise(ArgumentError) { "abc\x80".scrub("\xFF") } +end + +assert('String#scrub with block') do + skip unless "あ".length == 1 + assert_equal "abc<80>def", + "abc\x80def".scrub { |b| "<" + b.bytes.first.to_s(16) + ">" } + # Block not called when string is already valid + called = false + "hello".scrub { |_| called = true; "X" } + assert_false called + # Multiple invalid runs each get their own block invocation + result = "a\x80b\x81c".scrub { |b| "[#{b.bytes.first}]" } + assert_equal "a[128]b[129]c", result + # Non-String block return values are coerced via to_s (mruby leniency; + # CRuby raises TypeError instead). Locking this in so the choice is + # explicit and doesn't drift accidentally. + assert_equal "abc42def", "abc\x80def".scrub { 42 } +end + +assert('String#scrub is a no-op without MRB_UTF8_STRING') do + skip if "あ".length == 1 + # Method is still defined and returns a (string-equal) copy. + assert_equal "abc\x80def", "abc\x80def".scrub + assert_equal "abc\x80def", "abc\x80def".scrub("?") + assert_equal "abc\x80def", "abc\x80def".scrub { |_| "?" } +end diff --git a/mrbgems/mruby-task/include/task.h b/mrbgems/mruby-task/include/task.h index db5e8b7c5..6a464eb9c 100644 --- a/mrbgems/mruby-task/include/task.h +++ b/mrbgems/mruby-task/include/task.h @@ -41,7 +41,6 @@ struct mrb_task_queue; * - Removed started flag (inferred from c.status): 1 byte * - Unified wakeup_tick/join/mutex into single union: 4 bytes * - Removed redundant proc field (stored in c.ci->proc): 8 bytes - * - Unified timeslice/result into state union: ~4 bytes * Total savings: ~18 bytes per task (14% reduction) */ typedef struct mrb_task { @@ -49,6 +48,7 @@ typedef struct mrb_task { uint8_t priority; /* Priority (0-255, 0=highest) */ uint8_t status; /* Current status (TASKSTATUS enum) */ uint8_t reason; /* Wait reason (TASKREASON enum) */ + volatile uint8_t timeslice; /* Remaining ticks while RUNNING */ mrb_value name; /* Optional task name */ /* Wait-specific data - mutually exclusive based on reason field */ @@ -61,11 +61,7 @@ typedef struct mrb_task { mrb_value self; /* Ruby Task object reference */ - /* State-specific data - mutually exclusive based on status */ - union { - volatile uint8_t timeslice; /* Remaining ticks (RUNNING only) */ - mrb_value result; /* Task return value (DORMANT only) */ - } state; + mrb_value result; /* Task return value */ struct mrb_context c; /* Execution context (stack, callinfo, etc) */ } mrb_task; @@ -166,8 +162,8 @@ task_check_scheduler_lock(mrb_state *mrb) } /* Priority-queue insert/delete - defined in task.c */ -void q_insert_task(mrb_state *mrb, mrb_task *t); -void q_delete_task(mrb_state *mrb, mrb_task *t); +void mrb_task_q_insert(mrb_state *mrb, mrb_task *t); +void mrb_task_q_delete(mrb_state *mrb, mrb_task *t); /* Task::Queue class registration - defined in task_queue.c */ void mrb_init_task_queue(mrb_state *mrb, struct RClass *task_class); diff --git a/mrbgems/mruby-task/src/task.c b/mrbgems/mruby-task/src/task.c index e6fdfc4fb..4e66a4053 100644 --- a/mrbgems/mruby-task/src/task.c +++ b/mrbgems/mruby-task/src/task.c @@ -94,6 +94,16 @@ mrb_task_mark_all(mrb_state *mrb) for (i = 0; i < e; i++) { mrb_gc_mark_value(mrb, c->stbase[i]); } + /* Clear the dead slots above the live range, matching + mark_context_stack() in gc.c. A preempted task whose live range + later shrinks (a frame returned) would otherwise leave stale + object pointers in those slots; the objects get swept while the + pointers survive, and a subsequent mark of the resumed task trips + the MRB_TT_FREE assertion in mrb_gc_mark (issue #6870). */ + size_t stend = c->stend - c->stbase; + for (; i < stend; i++) { + SET_NIL_VALUE(c->stbase[i]); + } } /* Mark call stack */ @@ -113,9 +123,7 @@ mrb_task_mark_all(mrb_state *mrb) /* Mark task-specific values */ mrb_gc_mark_value(mrb, t->self); - if (t->status == MRB_TASK_STATUS_DORMANT) { - mrb_gc_mark_value(mrb, t->state.result); - } + mrb_gc_mark_value(mrb, t->result); mrb_gc_mark_value(mrb, t->name); t = t->next; @@ -148,7 +156,7 @@ q_get_queue(mrb_state *mrb, mrb_task *t) /* Insert task into queue based on priority (higher priority = lower number = earlier in queue) */ void -q_insert_task(mrb_state *mrb, mrb_task *t) +mrb_task_q_insert(mrb_state *mrb, mrb_task *t) { mrb_task **q = q_get_queue(mrb, t); mrb_task *curr = *q; @@ -172,7 +180,7 @@ q_insert_task(mrb_state *mrb, mrb_task *t) /* Delete task from its current queue */ void -q_delete_task(mrb_state *mrb, mrb_task *t) +mrb_task_q_delete(mrb_state *mrb, mrb_task *t) { mrb_task **q = q_get_queue(mrb, t); mrb_task *curr = *q; @@ -202,10 +210,10 @@ task_cleanup_if_stopped(mrb_state *mrb, mrb_task *t) if (t->status == MRB_TASK_STATUS_DORMANT || t->c.status == MRB_TASK_STOPPED) { /* Task is terminated but still in queue - remove it */ mrb_task_disable_irq(); - q_delete_task(mrb, t); + mrb_task_q_delete(mrb, t); if (t->status != MRB_TASK_STATUS_DORMANT) { t->status = MRB_TASK_STATUS_DORMANT; - q_insert_task(mrb, t); + mrb_task_q_insert(mrb, t); } mrb_task_enable_irq(); return TRUE; @@ -286,11 +294,11 @@ wake_up_join_waiters(mrb_state *mrb, mrb_task *completed_task) while (curr != NULL) { mrb_task *next = curr->next; if (curr->reason == MRB_TASK_REASON_JOIN && curr->wait.join == completed_task) { - q_delete_task(mrb, curr); + mrb_task_q_delete(mrb, curr); curr->status = MRB_TASK_STATUS_READY; curr->reason = MRB_TASK_REASON_NONE; curr->wait.join = NULL; - q_insert_task(mrb, curr); + mrb_task_q_insert(mrb, curr); /* If a higher-priority waiter is resumed from task context, * request a context switch after leaving the critical section. */ if (mrb->c != mrb->root_c && !switching_) { @@ -310,12 +318,33 @@ static void task_change_state(mrb_state *mrb, mrb_task *t, uint8_t new_status) { mrb_task_disable_irq(); - q_delete_task(mrb, t); + mrb_task_q_delete(mrb, t); t->status = new_status; - q_insert_task(mrb, t); + mrb_task_q_insert(mrb, t); mrb_task_enable_irq(); } +typedef struct execute_task_vm_args { + mrb_task *t; + const struct RProc *proc; + const mrb_code *pc; +} execute_task_vm_args; + +static mrb_value +execute_task_vm(mrb_state *mrb, void *ud) +{ + execute_task_vm_args *args = (execute_task_vm_args*)ud; + + mrb->task.exception_as_result = TRUE; + args->t->result = mrb_vm_exec(mrb, args->proc, args->pc); + if (mrb->exc) { + args->t->result = mrb_obj_value(mrb->exc); + mrb->exc = NULL; + } + mrb->task.exception_as_result = FALSE; + return args->t->result; +} + /* Execute a single task - core task execution logic */ static void execute_task(mrb_state *mrb, mrb_task *t) @@ -325,8 +354,8 @@ execute_task(mrb_state *mrb, mrb_task *t) uint8_t prev_cci; /* Set task as running */ + t->timeslice = MRB_TIMESLICE_TICK_COUNT; t->status = MRB_TASK_STATUS_RUNNING; - t->state.timeslice = MRB_TIMESLICE_TICK_COUNT; /* Switch to task context */ prev_c = mrb->c; @@ -350,8 +379,13 @@ execute_task(mrb_state *mrb, mrb_task *t) /* Set vmexec flag to prevent fiber_terminate from being called */ t->c.vmexec = TRUE; - /* Execute task - PC is saved in ci->pc from previous run */ - t->state.result = mrb_vm_exec(mrb, proc, pc); + /* Execute task - PC is saved in ci->pc from previous run. + Unhandled task exceptions are converted to the task result by + mrb_vm_exec() in task mode, so the scheduler protect frame stays intact. */ + execute_task_vm_args args = { t, proc, pc }; + mrb_bool error = FALSE; + t->result = mrb_protect_error(mrb, execute_task_vm, &args, &error); + mrb->task.exception_as_result = FALSE; /* Clear vmexec flag */ t->c.vmexec = FALSE; @@ -365,13 +399,25 @@ execute_task(mrb_state *mrb, mrb_task *t) prev_c->ci = prev_ci; prev_ci->cci = prev_cci; + /* If an abnormal path inside mrb_vm_exec() bypassed + exception_as_result and unwound via MRB_THROW (e.g. a + CINFO_SKIP frame), mrb_protect_error caught it and stored the + exception object in t->result. Force the task to terminate + cleanly so the scheduler keeps running instead of aborting - + re-raising into the scheduler would abort in pattern 1, where + no outer jmpbuf exists. The exception remains observable via + mrb_task_value() / Task#value. */ + if (error) { + t->c.status = MRB_TASK_STOPPED; + } + /* Handle task termination */ if (t->c.status == MRB_TASK_STOPPED) { switching_ = FALSE; mrb_task_disable_irq(); - q_delete_task(mrb, t); + mrb_task_q_delete(mrb, t); t->status = MRB_TASK_STATUS_DORMANT; - q_insert_task(mrb, t); + mrb_task_q_insert(mrb, t); mrb_task_enable_irq(); /* Wake up tasks waiting on join */ @@ -394,9 +440,9 @@ mrb_tick(mrb_state *mrb) /* Decrease timeslice for running task */ t = q_ready_; - if (t && t->status == MRB_TASK_STATUS_RUNNING && t->state.timeslice > 0) { - t->state.timeslice--; - if (t->state.timeslice == 0) { + if (t && t->status == MRB_TASK_STATUS_RUNNING && t->timeslice > 0) { + t->timeslice--; + if (t->timeslice == 0) { switching_ = TRUE; /* Trigger context switch */ } } @@ -421,10 +467,10 @@ mrb_tick(mrb_state *mrb) if (curr->reason == MRB_TASK_REASON_SLEEP) { if ((int32_t)(curr->wait.wakeup_tick - tick_) <= 0) { /* Time to wake up */ - q_delete_task(mrb, curr); + mrb_task_q_delete(mrb, curr); curr->status = MRB_TASK_STATUS_READY; curr->reason = MRB_TASK_REASON_NONE; - q_insert_task(mrb, curr); + mrb_task_q_insert(mrb, curr); switching_ = TRUE; } else if (curr->wait.wakeup_tick < next_wakeup) { @@ -439,11 +485,14 @@ mrb_tick(mrb_state *mrb) } } -/* Main scheduler loop */ -MRB_API mrb_value -mrb_task_run(mrb_state *mrb) +/* Body of the main scheduler loop. Wrapped by mrb_task_run() under + mrb_protect_error so an exception raised from a task body unwinds + cleanly without leaving `loop_running` set. */ +static mrb_value +task_run_body(mrb_state *mrb, void *ud) { mrb_task *t; + (void)ud; while (1) { t = q_ready_; @@ -480,10 +529,27 @@ mrb_task_run(mrb_state *mrb) mrb_incremental_gc(mrb); } } - return mrb_nil_value(); } +/* Main scheduler loop */ +MRB_API mrb_value +mrb_task_run(mrb_state *mrb) +{ + if (mrb->task.loop_running) { + return mrb_nil_value(); + } + mrb->task.loop_running = TRUE; + + mrb_bool error = FALSE; + mrb_value result = mrb_protect_error(mrb, task_run_body, NULL, &error); + mrb->task.loop_running = FALSE; + if (error) { + mrb_exc_raise(mrb, result); + } + return result; +} + /* Single-step task execution for WASM event loop integration */ MRB_API mrb_value mrb_task_run_once(mrb_state *mrb) @@ -551,7 +617,7 @@ sleep_us_impl(mrb_state *mrb, uint32_t usec) mrb_task_disable_irq(); /* Remove from ready queue */ - q_delete_task(mrb, t); + mrb_task_q_delete(mrb, t); /* Move to waiting queue */ t->status = MRB_TASK_STATUS_WAITING; @@ -572,7 +638,7 @@ sleep_us_impl(mrb_state *mrb, uint32_t usec) (int32_t)(t->wait.wakeup_tick - wakeup_tick_) < 0) { wakeup_tick_ = t->wait.wakeup_tick; } - q_insert_task(mrb, t); + mrb_task_q_insert(mrb, t); mrb_task_enable_irq(); @@ -597,9 +663,9 @@ mrb_f_sleep(mrb_state *mrb, mrb_value self) mrb_task *t = q_ready_; if (t) { mrb_task_disable_irq(); - q_delete_task(mrb, t); + mrb_task_q_delete(mrb, t); t->status = MRB_TASK_STATUS_SUSPENDED; - q_insert_task(mrb, t); + mrb_task_q_insert(mrb, t); mrb_task_enable_irq(); switching_ = TRUE; } @@ -666,7 +732,7 @@ task_create_common(mrb_state *mrb, const struct RProc *proc, task_init_context(mrb, t, proc); mrb_task_disable_irq(); - q_insert_task(mrb, t); + mrb_task_q_insert(mrb, t); mrb_task_enable_irq(); if (q_ready_ && q_ready_->status == MRB_TASK_STATUS_RUNNING) { @@ -1024,8 +1090,8 @@ mrb_task_set_priority(mrb_state *mrb, mrb_value self) /* Re-sort in queue if task is ready */ if (t->status == MRB_TASK_STATUS_READY || t->status == MRB_TASK_STATUS_RUNNING) { - q_delete_task(mrb, t); - q_insert_task(mrb, t); + mrb_task_q_delete(mrb, t); + mrb_task_q_insert(mrb, t); } mrb_task_enable_irq(); @@ -1095,22 +1161,22 @@ mrb_task_join(mrb_state *mrb, mrb_value self) /* If task is already dormant, return immediately */ if (t->status == MRB_TASK_STATUS_DORMANT) { - return t->state.result; + return t->result; } /* Wait for task to complete */ mrb_task_disable_irq(); - q_delete_task(mrb, current); + mrb_task_q_delete(mrb, current); current->status = MRB_TASK_STATUS_WAITING; current->reason = MRB_TASK_REASON_JOIN; current->wait.join = t; - q_insert_task(mrb, current); + mrb_task_q_insert(mrb, current); mrb_task_enable_irq(); /* Trigger context switch */ switching_ = TRUE; - return t->state.result; + return t->result; } /* @@ -1162,7 +1228,7 @@ mrb_execute_proc_synchronously(mrb_state *mrb, mrb_value proc_val, mrb_int argc, /* 3. Move task from DORMANT to READY */ mrb_task_disable_irq(); t->status = MRB_TASK_STATUS_READY; - q_insert_task(mrb, t); + mrb_task_q_insert(mrb, t); mrb_task_enable_irq(); /* 4. Execute the task in a dedicated loop (no context switching) */ @@ -1170,23 +1236,23 @@ mrb_execute_proc_synchronously(mrb_state *mrb, mrb_value proc_val, mrb_int argc, mrb->c = &t->c; while (t->c.status != MRB_TASK_STOPPED) { - t->state.result = mrb_vm_exec(mrb, mrb->c->ci->proc, mrb->c->ci->pc); + t->result = mrb_vm_exec(mrb, mrb->c->ci->proc, mrb->c->ci->pc); } /* If there's an unhandled exception after VM stops, save it as result */ if (mrb->exc) { - t->state.result = mrb_obj_value(mrb->exc); + t->result = mrb_obj_value(mrb->exc); } /* 5. Get result and clean up */ - mrb_value result = t->state.result; + mrb_value result = t->result; if (mrb_obj_ptr(result) == mrb->exc) { mrb->exc = NULL; /* Clear exception */ } /* 6. Free the temporary task's resources */ mrb_task_disable_irq(); - q_delete_task(mrb, t); + mrb_task_q_delete(mrb, t); mrb_task_enable_irq(); /* Prevent double-free: clear Data object's type before freeing task */ @@ -1362,10 +1428,10 @@ terminate_task_internal(mrb_state *mrb, mrb_task *t) if (t->status == MRB_TASK_STATUS_DORMANT) return; mrb_task_disable_irq(); - q_delete_task(mrb, t); + mrb_task_q_delete(mrb, t); t->status = MRB_TASK_STATUS_DORMANT; t->c.status = MRB_TASK_STOPPED; - q_insert_task(mrb, t); + mrb_task_q_insert(mrb, t); mrb_task_enable_irq(); wake_up_join_waiters(mrb, t); @@ -1417,7 +1483,7 @@ mrb_task_value(mrb_state *mrb, mrb_value task) mrb_task *t = (mrb_task*)mrb_data_check_get_ptr(mrb, task, &mrb_task_type); if (!t) return mrb_nil_value(); - return t->state.result; + return t->result; } /* @@ -1505,6 +1571,8 @@ mrb_mruby_task_gem_init(mrb_state *mrb) /* Initialize main task to NULL and scheduler_lock to 0 */ mrb->task.main_task = NULL; mrb->task.scheduler_lock = 0; + mrb->task.loop_running = FALSE; + mrb->task.exception_as_result = FALSE; task_class = mrb_define_class_id(mrb, MRB_SYM(Task), mrb->object_class); MRB_SET_INSTANCE_TT(task_class, MRB_TT_DATA); @@ -1536,6 +1604,7 @@ mrb_mruby_task_gem_init(mrb_state *mrb) mrb_define_method_id(mrb, task_class, MRB_SYM(resume), mrb_task_resume, MRB_ARGS_NONE()); mrb_define_method_id(mrb, task_class, MRB_SYM(terminate), mrb_task_terminate, MRB_ARGS_NONE()); mrb_define_method_id(mrb, task_class, MRB_SYM(join), mrb_task_join, MRB_ARGS_NONE()); + mrb_define_method_id(mrb, task_class, MRB_SYM(value), mrb_task_value, MRB_ARGS_NONE()); /* Kernel methods (module functions like CRuby) * Note: sleep and usleep override mruby-sleep's implementation to be task-aware diff --git a/mrbgems/mruby-task/src/task_queue.c b/mrbgems/mruby-task/src/task_queue.c index bf1884c38..11175ff6f 100644 --- a/mrbgems/mruby-task/src/task_queue.c +++ b/mrbgems/mruby-task/src/task_queue.c @@ -36,11 +36,11 @@ queue_wake_one_waiter(mrb_state *mrb, mrb_task_queue *q) while (curr) { mrb_task *next = curr->next; if (curr->reason == MRB_TASK_REASON_QUEUE && curr->wait.queue == q) { - q_delete_task(mrb, curr); + mrb_task_q_delete(mrb, curr); curr->status = MRB_TASK_STATUS_READY; curr->reason = MRB_TASK_REASON_NONE; curr->wait.queue = NULL; - q_insert_task(mrb, curr); + mrb_task_q_insert(mrb, curr); switching_ = TRUE; break; } @@ -59,11 +59,11 @@ queue_wake_all_waiters(mrb_state *mrb, mrb_task_queue *q) while (curr) { mrb_task *next = curr->next; if (curr->reason == MRB_TASK_REASON_QUEUE && curr->wait.queue == q) { - q_delete_task(mrb, curr); + mrb_task_q_delete(mrb, curr); curr->status = MRB_TASK_STATUS_READY; curr->reason = MRB_TASK_REASON_NONE; curr->wait.queue = NULL; - q_insert_task(mrb, curr); + mrb_task_q_insert(mrb, curr); woke_any = TRUE; } curr = next; @@ -154,11 +154,11 @@ queue_pop_try(mrb_state *mrb, mrb_value self) /* Move current task to WAITING */ mrb_task *current = MRB2TASK(mrb); mrb_task_disable_irq(); - q_delete_task(mrb, current); + mrb_task_q_delete(mrb, current); current->status = MRB_TASK_STATUS_WAITING; current->reason = MRB_TASK_REASON_QUEUE; current->wait.queue = q; - q_insert_task(mrb, current); + mrb_task_q_insert(mrb, current); mrb_task_enable_irq(); switching_ = TRUE; diff --git a/mrbgems/mruby-task/test/task.rb b/mrbgems/mruby-task/test/task.rb index e93c525af..5bbe844e3 100644 --- a/mrbgems/mruby-task/test/task.rb +++ b/mrbgems/mruby-task/test/task.rb @@ -81,6 +81,10 @@ end assert("Task#suspend doesn't raise") do task = Task.new { } assert_nothing_raised { task.suspend } + # Clean up: a suspended task left in q_suspended_ keeps a later + # Task.run from terminating (the scheduler idles waiting on it + # instead of exiting). + task.terminate end assert("Task#resume doesn't raise") do @@ -183,3 +187,24 @@ assert("Task.new with block doesn't execute immediately") do # Block should not execute until scheduler runs assert_false executed end + +assert("Task.run inside Task.run is a noop") do + assert_nothing_raised do + Task.new { Task.run } + Task.run + end +end + +assert("Task#value returns exception object for unhandled task errors") do + child = nil + + Task.new do + child = Task.new { raise "boom" } + end + + Task.run + + result = child.value + assert_kind_of RuntimeError, result + assert_equal "boom", result.message +end diff --git a/src/array.c b/src/array.c index 8396e9a15..166960f2f 100644 --- a/src/array.c +++ b/src/array.c @@ -1197,6 +1197,14 @@ mrb_ary_splice(mrb_state *mrb, mrb_value ary, mrb_int head, mrb_int len, mrb_val } r = ary_dup(mrb, a); argv = ARY_PTR(r); + /* ary_dup -> ary_replace converts `a` to shared as a + copy-on-write optimization when len > ARY_REPLACE_SHARED_MIN. + Subsequent ARY_CAPA(a) reads would land on aux.shared's + pointer bits instead of the actual capacity, so the + expand-capa check below silently mis-sizes and value_move + walks past the buffer. Re-modify here to unshare before + mutating `a` in place. */ + ary_modify(mrb, a); } } else if (mrb_undef_p(rpl)) { diff --git a/src/class.c b/src/class.c index 7b216a516..74d021ce7 100644 --- a/src/class.c +++ b/src/class.c @@ -3966,7 +3966,7 @@ mrb_method_added(mrb_state *mrb, struct RClass *c, mrb_sym mid) } } -mrb_value +static mrb_value define_method_m(mrb_state *mrb, struct RClass *c, int vis) { mrb_sym mid; diff --git a/src/gc.c b/src/gc.c index ed5f099fb..f8021c6b1 100644 --- a/src/gc.c +++ b/src/gc.c @@ -1216,36 +1216,33 @@ incremental_sweep_phase(mrb_state *mrb, mrb_gc *gc, size_t limit) size_t tried_sweep = 0; while (page && (tried_sweep < limit)) { - RVALUE *p = page->objects; - RVALUE *e = p + MRB_HEAP_PAGE_SIZE; size_t freed = 0; mrb_bool dead_slot = TRUE; if (is_minor_gc(gc) && page->old) { /* skip a slot which doesn't contain any young object */ - p = e; dead_slot = FALSE; } - while (pas.basic)) { - if (p->as.basic.tt != MRB_TT_FREE) { - obj_free(mrb, &p->as.basic, FALSE); - if (p->as.basic.tt == MRB_TT_FREE) { + else { + RVALUE *p = page->objects; + RVALUE *e = p + MRB_HEAP_PAGE_SIZE; + while (pas.basic)) { + if (p->as.basic.tt != MRB_TT_FREE) { + obj_free(mrb, &p->as.basic, FALSE); + mrb_assert(p->as.basic.tt == MRB_TT_FREE); p->as.free.next = page->freelist; page->freelist = p; freed++; } - else { - dead_slot = FALSE; - } } + else { + if (!is_generational(gc)) + paint_partial_white(gc, &p->as.basic); /* next gc target */ + dead_slot = FALSE; + } + p++; } - else { - if (!is_generational(gc)) - paint_partial_white(gc, &p->as.basic); /* next gc target */ - dead_slot = FALSE; - } - p++; } /* free dead slot */ @@ -1802,21 +1799,21 @@ gc_stat(mrb_state *mrb, mrb_value self) mrb_gc *gc = &mrb->gc; mrb_value hash = mrb_hash_new_capa(mrb, 8); - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "live")), mrb_int_value(mrb, (mrb_int)gc->live)); - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "debt")), mrb_int_value(mrb, gc->gc_debt)); - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "state")), mrb_int_value(mrb, (mrb_int)gc->state)); - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "generational")), mrb_bool_value(gc->generational)); - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "full")), mrb_bool_value(gc->full)); - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "step_limit")), mrb_int_value(mrb, (mrb_int)gc->step_limit)); - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "malloc_increase")), mrb_int_value(mrb, (mrb_int)gc->malloc_increase)); - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "malloc_threshold")), mrb_int_value(mrb, (mrb_int)gc->malloc_threshold)); - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "symbol_count")), mrb_int_value(mrb, (mrb_int)(MRB_PRESYM_MAX + mrb->symidx))); - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "dynamic_symbol_count")), mrb_int_value(mrb, (mrb_int)mrb->dynamic_sym_count)); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(live)), mrb_int_value(mrb, (mrb_int)gc->live)); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(debt)), mrb_int_value(mrb, gc->gc_debt)); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(state)), mrb_int_value(mrb, (mrb_int)gc->state)); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(generational)), mrb_bool_value(gc->generational)); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(full)), mrb_bool_value(gc->full)); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(step_limit)), mrb_int_value(mrb, (mrb_int)gc->step_limit)); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(malloc_increase)), mrb_int_value(mrb, (mrb_int)gc->malloc_increase)); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(malloc_threshold)), mrb_int_value(mrb, (mrb_int)gc->malloc_threshold)); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(symbol_count)), mrb_int_value(mrb, (mrb_int)(MRB_PRESYM_MAX + mrb->symidx))); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(dynamic_symbol_count)), mrb_int_value(mrb, (mrb_int)mrb->dynamic_sym_count)); #ifdef MRB_GC_STATS - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "total")), mrb_int_value(mrb, (mrb_int)gc->gc_total_count)); - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "minor")), mrb_int_value(mrb, (mrb_int)gc->minor_gc_count)); - mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "major")), mrb_int_value(mrb, (mrb_int)gc->major_gc_count)); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(total)), mrb_int_value(mrb, (mrb_int)gc->gc_total_count)); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(minor)), mrb_int_value(mrb, (mrb_int)gc->minor_gc_count)); + mrb_hash_set(mrb, hash, mrb_symbol_value(MRB_SYM(major)), mrb_int_value(mrb, (mrb_int)gc->major_gc_count)); #endif return hash; diff --git a/src/numeric.c b/src/numeric.c index d3f370d7f..5d695c77e 100644 --- a/src/numeric.c +++ b/src/numeric.c @@ -351,6 +351,14 @@ flo_idiv(mrb_state *mrb, mrb_value xv) mrb_float x = mrb_float(xv); mrb_check_num_exact(mrb, x); mrb_int y = mrb_as_int(mrb, mrb_get_arg1(mrb)); + /* (mrb_int)x is UB when x is outside mrb_int range. */ + if (!FIXABLE_FLOAT(x)) { +#ifdef MRB_USE_BIGINT + return mrb_bint_div(mrb, mrb_bint_new_float(mrb, x), mrb_int_value(mrb, y)); +#else + mrb_int_overflow(mrb, "div"); +#endif + } return mrb_div_int_value(mrb, (mrb_int)x, y); } diff --git a/src/variable.c b/src/variable.c index 92c56f76b..401692b19 100644 --- a/src/variable.c +++ b/src/variable.c @@ -665,7 +665,8 @@ assign_class_name(mrb_state *mrb, struct RObject *obj, mrb_sym sym, mrb_value v) { if (namespace_p(mrb_type(v))) { struct RObject *c = mrb_obj_ptr(v); - if (obj != c && ISUPPER(mrb_sym_name_len(mrb, sym, NULL)[0])) { + const char *name = mrb_sym_name_len(mrb, sym, NULL); + if (obj != c && name && ISUPPER(name[0])) { mrb_sym id_classname = MRB_SYM(__classname__); mrb_value o = mrb_obj_iv_get(mrb, c, id_classname); @@ -1443,9 +1444,7 @@ mrb_const_set(mrb_state *mrb, mrb_value mod, mrb_sym sym, mrb_value v) mrb_class_name_class(mrb, mrb_class_ptr(mod), mrb_class_ptr(v), sym); } mrb_obj_iv_set(mrb, mrb_obj_ptr(mod), sym, v); -#ifndef MRB_NO_CONST_CACHE mrb_const_cache_clear(mrb); -#endif if (!mrb->bootstrapping) { mrb_value name = mrb_symbol_value(sym); @@ -1472,9 +1471,7 @@ mrb_const_remove(mrb_state *mrb, mrb_value mod, mrb_sym sym) { mod_const_check(mrb, mod); mrb_iv_remove(mrb, mod, sym); -#ifndef MRB_NO_CONST_CACHE mrb_const_cache_clear(mrb); -#endif } /* @@ -1492,9 +1489,7 @@ MRB_API void mrb_define_const_id(mrb_state *mrb, struct RClass *mod, mrb_sym name, mrb_value v) { mrb_obj_iv_set(mrb, (struct RObject*)mod, name, v); -#ifndef MRB_NO_CONST_CACHE mrb_const_cache_clear(mrb); -#endif } /* diff --git a/src/vm.c b/src/vm.c index b1de67637..0a84798b0 100644 --- a/src/vm.c +++ b/src/vm.c @@ -1588,17 +1588,58 @@ prepare_tagged_break(mrb_state *mrb, uint32_t tag, const mrb_callinfo *return_ci #define CALL_CODE_HOOKS() do { insn = BYTECODE_DECODER(*ci->pc); CODE_FETCH_HOOK(mrb, irep, ci->pc, regs); } while (0) #ifdef MRB_USE_TASK_SCHEDULER +/* TRUE when the current context is executing across a C call boundary, i.e. + a C function on the stack re-entered the VM (mrb_funcall / mrb_yield / + mrb_vm_run). A task cannot be suspended at such a point: the C stack + frame between the scheduler's mrb_vm_exec and the current frame cannot + be saved or restored, and returning early from the inner mrb_vm_exec + would leave the call-info stack drifted, tripping the assertion in + mrb_vm_run (issues #6864, #6868). The scheduler defers the switch until + execution unwinds back to a frame with no C boundary. This mirrors the + cooperative guard in Task.pass, which raises rather than defers. + cibase is excluded: it is the entry frame of this mrb_vm_exec. */ +static mrb_bool +task_across_c_boundary(mrb_state *mrb) +{ + for (mrb_callinfo *ci = mrb->c->ci; ci > mrb->c->cibase; ci--) { + if (ci->cci > 0) return TRUE; + } + return FALSE; +} + +/* Defer task switches while a C-level ObjectSpace walk holds gc.iterating + true. The walk runs callbacks (which may call back into mrb_vm_exec via + mrb_yield); returning early from an inner exec while the outer C + iteration is still active drifts the call-info stack and eventually + crashes (issue #6862). Switches resume at the next OP boundary after + the walk releases gc.iterating. A pending switch is also deferred while + executing across a C call boundary (see task_across_c_boundary). A + pending MRB_TASK_STOPPED is not deferred, since the task is going away. + + mrb->jmp is restored to prev_jmp before returning, exactly as the + normal return paths below do. mrb_vm_exec set mrb->jmp to its own + stack-local c_jmp on entry; leaving it dangling after this early return + means a later raise longjmps into a freed frame (issue #6863). + + This macro must only be expanded where prev_jmp is in scope, i.e. + inside mrb_vm_exec (via NEXT / END_DISPATCH). */ #define RETURN_IF_TASK_STOPPED(mrb) do { \ - if ((mrb)->task.switching || (mrb)->c->status == MRB_TASK_STOPPED) \ + if (((mrb)->task.switching && !(mrb)->gc.iterating && \ + !task_across_c_boundary(mrb)) || \ + (mrb)->c->status == MRB_TASK_STOPPED) { \ + (mrb)->jmp = prev_jmp; \ return mrb_nil_value(); \ + } \ } while (0) #define TASK_STOP(mrb) do { \ if (mrb->c->status != MRB_TASK_STOPPED) \ mrb->c->status = MRB_TASK_STOPPED; \ } while (0) +#define TASK_RETURN_EXCEPTION_AS_VALUE(mrb) ((mrb)->task.exception_as_result) #else #define RETURN_IF_TASK_STOPPED(mrb) #define TASK_STOP(mrb) +#define TASK_RETURN_EXCEPTION_AS_VALUE(mrb) FALSE #endif /** @@ -2652,6 +2693,7 @@ RETRY_TRY_BLOCK: fiber_terminate(mrb, c, ci); if (mrb_unlikely(!c->vmexec)) goto L_RAISE; mrb->jmp = prev_jmp; + if (TASK_RETURN_EXCEPTION_AS_VALUE(mrb)) return mrb_obj_value(mrb->exc); if (!prev_jmp) return mrb_obj_value(mrb->exc); MRB_THROW(prev_jmp); } diff --git a/test/bintest.rb b/test/bintest.rb index 94bdf10ca..e5ec309ff 100644 --- a/test/bintest.rb +++ b/test/bintest.rb @@ -1,4 +1,5 @@ $:.unshift File.dirname(File.dirname(File.expand_path(__FILE__))) +require 'shellwords' require 'test/assert.rb' GEMNAME = "" @@ -16,7 +17,7 @@ def cmd_list(s) path_list = [cmd_bin(s)] emu = ENV['EMULATOR'] - path_list.unshift emu if emu && !emu.empty? + path_list.unshift(*Shellwords.split(emu)) if emu && !emu.empty? path_list end diff --git a/test/t/array.rb b/test/t/array.rb index ab34264f0..bccdbb815 100644 --- a/test/t/array.rb +++ b/test/t/array.rb @@ -107,6 +107,17 @@ assert('Array#[]=', '15.2.12.5.5') do a = [1,2,3] a[-1,0] = a assert_equal([1,2,1,2,3,3], a) + + # passing self with length above ARY_REPLACE_SHARED_MIN (=20). + # ary_dup -> ary_replace converts the source to shared as a + # copy-on-write optimization; without re-modifying `a` afterwards, + # ARY_CAPA(a) reads from aux.shared's pointer bits and the + # expand-capa check silently mis-sizes -> heap-buffer-overflow in + # value_move. Reported via clusterfuzz mruby_fuzzer. + a = (0..30).to_a + a[3, 2] = a + assert_equal(60, a.length) + assert_equal([0, 1, 2] + (0..30).to_a + (5..30).to_a, a) end assert('Array#clear', '15.2.12.5.6') do diff --git a/test/t/codegen.rb b/test/t/codegen.rb index c4e031bd3..1e4d37559 100644 --- a/test/t/codegen.rb +++ b/test/t/codegen.rb @@ -194,3 +194,29 @@ assert('register window of calls (#3783)') do end end end + +assert('bare `nil?` in if/unless uses self as receiver (#6874)') do + klass = Class.new do + def unless_form + reached = false + unless nil? + reached = true + end + reached + end + + def if_form + if nil? + :yes + else + :no + end + end + end + + assert_true klass.new.unless_form + assert_equal :no, klass.new.if_form + # Sanity: explicit literal nil receiver still optimized correctly. + result = if nil.nil? then :yes else :no end + assert_equal :yes, result +end diff --git a/test/t/syntax.rb b/test/t/syntax.rb index 90f2bd6ac..5f43a1bfa 100644 --- a/test/t/syntax.rb +++ b/test/t/syntax.rb @@ -1043,6 +1043,21 @@ assert('pattern matching - array patterns') do x end assert_equal 3, result + + # array literal with splat as case value (#6854): + # the array-literal-length optimization must bail out for splat, + # since the runtime length is unknown statically. + a = [1, 2] + result = case [*a] + in [1, 2] then :match + else :nomatch + end + assert_equal :match, result + + # same bug in one-line `in` pattern + assert_true ([*a] in [1, 2]) + assert_false ([*a] in [1, 2, 3]) + assert_true ([1, *a, 4] in [1, 1, 2, 4]) end assert('pattern matching - find patterns') do diff --git a/test/t/vformat.rb b/test/t/vformat.rb index 956870e02..06a30a969 100644 --- a/test/t/vformat.rb +++ b/test/t/vformat.rb @@ -42,7 +42,7 @@ assert('mrb_vformat') do assert_equal '`S`: {a: 1, "b" => "c"}', vf.v('`S`: %S', {a: 1, "b" => ?c}) assert_equal 'percent: %', vf.z('percent: %%') assert_equal '"I": inspect char', vf.c('%!c: inspect char', ?I) - assert_equal '709: inspect mrb_int', vf.i('%!d: inspect mrb_int', 709) + assert_equal '709: inspect mrb_int', vf.i('%!i: inspect mrb_int', 709) assert_equal '"a\x00b\xff"', vf.l('%!l', "a\000b\xFFc\000d", 4) assert_equal ':"&.": inspect symbol', vf.n('%!n: inspect symbol', :'&.') assert_equal 'inspect "String"', vf.v('inspect %!v', 'String')