From 2a23415062802ed4f130b61bb7d00e9f176b72b2 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 23 Jun 2026 16:33:45 -0400 Subject: [PATCH 1/2] [ruby/prism] Pop block stack _before_ ending in parenthesized method call in command call https://github.com/ruby/prism/commit/c5da02a516 --- prism/prism.c | 9 ++++++++- test/prism/errors/do_block_after_command_block.txt | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 test/prism/errors/do_block_after_command_block.txt diff --git a/prism/prism.c b/prism/prism.c index e3137d3167e7db..d997c63d166e06 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -15290,6 +15290,12 @@ parse_block(pm_parser_t *parser, uint16_t depth) { statements = UP(parse_statements(parser, PM_CONTEXT_BLOCK_BRACES, (uint16_t) (depth + 1))); } + /* Pop before consuming the closing `}` so the following token (e.g. a + * `do`) is lexed in the enclosing context rather than as a block + * belonging to this block's interior. Otherwise a `do` block would + * wrongly bind to a command whose argument ends in a brace block, as in + * `foo(m a { } do end)`. */ + pm_accepts_block_stack_pop(parser); expect1_opening(parser, PM_TOKEN_BRACE_RIGHT, PM_ERR_BLOCK_TERM_BRACE, &opening); } else { if (!match1(parser, PM_TOKEN_KEYWORD_END)) { @@ -15305,6 +15311,8 @@ parse_block(pm_parser_t *parser, uint16_t depth) { } } + /* As with the brace case above, pop before consuming `end`. */ + pm_accepts_block_stack_pop(parser); expect1_opening(parser, PM_TOKEN_KEYWORD_END, PM_ERR_BLOCK_TERM_END, &opening); } @@ -15313,7 +15321,6 @@ parse_block(pm_parser_t *parser, uint16_t depth) { pm_node_t *parameters = parse_blocklike_parameters(parser, UP(block_parameters), &opening, &parser->previous); pm_parser_scope_pop(parser); - pm_accepts_block_stack_pop(parser); return pm_block_node_create(parser, &locals, &opening, parameters, statements, &parser->previous); } diff --git a/test/prism/errors/do_block_after_command_block.txt b/test/prism/errors/do_block_after_command_block.txt new file mode 100644 index 00000000000000..0989695af0c130 --- /dev/null +++ b/test/prism/errors/do_block_after_command_block.txt @@ -0,0 +1,14 @@ +foo(m a { } do end) + ^~ unexpected 'do'; expected a `)` to close the arguments + ^~ unexpected 'do', expecting end-of-input + ^~ unexpected 'do', ignoring it + ^~~ unexpected 'end', ignoring it + ^ unexpected ')', ignoring it + +foo(m a.b { } do end) + ^~ unexpected 'do'; expected a `)` to close the arguments + ^~ unexpected 'do', expecting end-of-input + ^~ unexpected 'do', ignoring it + ^~~ unexpected 'end', ignoring it + ^ unexpected ')', ignoring it + From 1ca2d2d471f68ebae14603e0a19f0cdfb22c949a Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Mon, 22 Jun 2026 20:27:04 -0400 Subject: [PATCH 2/2] Fix pm_compile_call for inline_new with keyword args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pm_setup_args mallocs one kw_arg buffer with references = 0. The inline_new branch then feeds that same pointer to three callinfo constructions: - 3789 — new_callinfo(... method_id="new" ..., kw_arg, 0) -> the opt_new ci - 3795 — PUSH_SEND_R(... "initialize", ..., flags | VM_CALL_FCALL, kw_arg) - 3800 — PUSH_SEND_R(... method_id="new", ..., flags, kw_arg) (fallback) Tracing the refcount through rb_vm_ci_lookup() 1. It increments kwarg->references and allocates a fresh new_ci before the dedup st_update 2. The dedup (vm_ci_hash_cmp) compares kwarg contents, not the pointer. So if an earlier line already interned a new ci with the identical keyword set, st_update returns that pre-existing ci (which holds a different buffer) and discards our new_ci. 3. Our kw_arg is now orphaned: references == 1, but the only holder is the discarded new_ci, which is a normal collectable imemo 4. An allocation like PUSH_INSN2 opt_new at or new_callinfo() can trigger a GC. References back to 0, kw_arg buffer freed. 5. new_callinfo() using the freed buffer, does argc += kw_arg->keyword_len (use-after-free) The fix: Keep the buffer alive across the allocations in inline_new. Fixes [Bug #22104] --- imemo.c | 6 +----- prism_compile.c | 4 ++++ vm_callinfo.h | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/imemo.c b/imemo.c index 9154f6bc6ac280..3814fbd4538d85 100644 --- a/imemo.c +++ b/imemo.c @@ -645,11 +645,7 @@ rb_imemo_free(VALUE obj) case imemo_callinfo:{ const struct rb_callinfo *ci = ((const struct rb_callinfo *)obj); - if (ci->kwarg) { - if (RUBY_ATOMIC_FETCH_SUB(((struct rb_callinfo_kwarg *)ci->kwarg)->references, 1) == 1) { - ruby_xfree_sized((void *)ci->kwarg, rb_callinfo_kwarg_bytes(ci->kwarg->keyword_len)); - } - } + rb_callinfo_kwarg_release((struct rb_callinfo_kwarg *)ci->kwarg); RB_DEBUG_COUNTER_INC(obj_imemo_callinfo); break; diff --git a/prism_compile.c b/prism_compile.c index 353e018d878493..e70477a51c786e 100644 --- a/prism_compile.c +++ b/prism_compile.c @@ -3887,6 +3887,8 @@ pm_compile_call(rb_iseq_t *iseq, const pm_call_node_t *call_node, LINK_ANCHOR *c ELEM_INSERT_NEXT(opt_new_prelude, &new_insn_body(iseq, location.line, location.node_id, BIN(putnil), 0)->link); } + rb_callinfo_kwarg_retain(kw_arg); + // Jump unless the receiver uses the "basic" implementation of "new" VALUE ci; if (flags & VM_CALL_FORWARDING) { @@ -3909,6 +3911,8 @@ pm_compile_call(rb_iseq_t *iseq, const pm_call_node_t *call_node, LINK_ANCHOR *c PUSH_LABEL(ret, not_basic_new_finish); PUSH_INSN(ret, location, pop); + + rb_callinfo_kwarg_release(kw_arg); } else { PUSH_SEND_R(ret, location, method_id, INT2FIX(orig_argc), block_iseq, INT2FIX(flags), kw_arg); diff --git a/vm_callinfo.h b/vm_callinfo.h index 9a6c69deaee35d..0ff0d89d1a5617 100644 --- a/vm_callinfo.h +++ b/vm_callinfo.h @@ -61,6 +61,20 @@ rb_callinfo_kwarg_bytes(int keyword_len) rb_eRuntimeError); } +static inline void +rb_callinfo_kwarg_retain(struct rb_callinfo_kwarg *kwarg) +{ + if (kwarg) RUBY_ATOMIC_INC(kwarg->references); +} + +static inline void +rb_callinfo_kwarg_release(struct rb_callinfo_kwarg *kwarg) +{ + if (kwarg && RUBY_ATOMIC_FETCH_SUB(kwarg->references, 1) == 1) { + ruby_xfree_sized(kwarg, rb_callinfo_kwarg_bytes(kwarg->keyword_len)); + } +} + // imemo_callinfo struct rb_callinfo { VALUE flags;