From bcdad7f2867c5955ce06cd6fabd3b3c06361eb47 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 30 Nov 2025 20:25:55 -0500 Subject: [PATCH 1/4] [ruby/prism] Properly remove references https://github.com/ruby/prism/commit/17b246fd6a --- prism/prism.c | 183 +++++++++++++++++++++++++------------------------- 1 file changed, 90 insertions(+), 93 deletions(-) diff --git a/prism/prism.c b/prism/prism.c index 8c886bb050b06a..58a6a9987db237 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -13404,6 +13404,78 @@ parse_starred_expression(pm_parser_t *parser, pm_binding_power_t binding_power, return parse_value_expression(parser, binding_power, accepts_command_call, false, diag_id, depth); } +static bool +pm_node_unreference_each(const pm_node_t *node, void *data) { + switch (PM_NODE_TYPE(node)) { + /* When we are about to destroy a set of nodes that could potentially + * contain block exits for the current scope, we need to check if they + * are contained in the list of block exits and remove them if they are. + */ + case PM_BREAK_NODE: + case PM_NEXT_NODE: + case PM_REDO_NODE: { + pm_parser_t *parser = (pm_parser_t *) data; + size_t index = 0; + + while (index < parser->current_block_exits->size) { + pm_node_t *block_exit = parser->current_block_exits->nodes[index]; + + if (block_exit == node) { + if (index + 1 < parser->current_block_exits->size) { + memmove( + &parser->current_block_exits->nodes[index], + &parser->current_block_exits->nodes[index + 1], + (parser->current_block_exits->size - index - 1) * sizeof(pm_node_t *) + ); + } + parser->current_block_exits->size--; + return false; + } + + index++; + } + + return true; + } + /* When an implicit local variable is written to or targeted, it becomes + * a regular, named local variable. This branch removes it from the list + * of implicit parameters when that happens. */ + case PM_LOCAL_VARIABLE_READ_NODE: + case PM_IT_LOCAL_VARIABLE_READ_NODE: { + pm_parser_t *parser = (pm_parser_t *) data; + pm_node_list_t *implicit_parameters = &parser->current_scope->implicit_parameters; + + for (size_t index = 0; index < implicit_parameters->size; index++) { + if (implicit_parameters->nodes[index] == node) { + /* If the node is not the last one in the list, we need to + * shift the remaining nodes down to fill the gap. This is + * extremely unlikely to happen. */ + if (index != implicit_parameters->size - 1) { + memmove(&implicit_parameters->nodes[index], &implicit_parameters->nodes[index + 1], (implicit_parameters->size - index - 1) * sizeof(pm_node_t *)); + } + + implicit_parameters->size--; + break; + } + } + + return false; + } + default: + return true; + } +} + +/** + * When we are about to destroy a set of nodes that could potentially be + * referenced by one or more lists on the parser, then remove them from those + * lists so we don't get a use-after-free. + */ +static void +pm_node_unreference(pm_parser_t *parser, const pm_node_t *node) { + pm_visit_node(node, pm_node_unreference_each, parser); +} + /** * Convert the name of a method into the corresponding write method name. For * example, foo would be turned into foo=. @@ -13454,45 +13526,6 @@ parse_unwriteable_target(pm_parser_t *parser, pm_node_t *target) { return (pm_node_t *) result; } -static bool -parse_target_implicit_parameter_each(const pm_node_t *node, void *data) { - switch (PM_NODE_TYPE(node)) { - case PM_LOCAL_VARIABLE_READ_NODE: - case PM_IT_LOCAL_VARIABLE_READ_NODE: { - pm_parser_t *parser = (pm_parser_t *) data; - pm_node_list_t *implicit_parameters = &parser->current_scope->implicit_parameters; - - for (size_t index = 0; index < implicit_parameters->size; index++) { - if (implicit_parameters->nodes[index] == node) { - // If the node is not the last one in the list, we need to - // shift the remaining nodes down to fill the gap. This is - // extremely unlikely to happen. - if (index != implicit_parameters->size - 1) { - memmove(&implicit_parameters->nodes[index], &implicit_parameters->nodes[index + 1], (implicit_parameters->size - index - 1) * sizeof(pm_node_t *)); - } - - implicit_parameters->size--; - break; - } - } - - return false; - } - default: - return true; - } -} - -/** - * When an implicit local variable is written to or targeted, it becomes a - * regular, named local variable. This function removes it from the list of - * implicit parameters when that happens. - */ -static void -parse_target_implicit_parameter(pm_parser_t *parser, const pm_node_t *node) { - pm_visit_node(node, parse_target_implicit_parameter_each, parser); -} - /** * Convert the given node into a valid target node. * @@ -13550,7 +13583,7 @@ parse_target(pm_parser_t *parser, pm_node_t *target, bool multiple, bool splat_p case PM_LOCAL_VARIABLE_READ_NODE: { if (pm_token_is_numbered_parameter(target->location.start, target->location.end)) { PM_PARSER_ERR_FORMAT(parser, target->location.start, target->location.end, PM_ERR_PARAMETER_NUMBERED_RESERVED, target->location.start); - parse_target_implicit_parameter(parser, target); + pm_node_unreference(parser, target); } const pm_local_variable_read_node_t *cast = (const pm_local_variable_read_node_t *) target; @@ -13567,7 +13600,7 @@ parse_target(pm_parser_t *parser, pm_node_t *target, bool multiple, bool splat_p pm_constant_id_t name = pm_parser_local_add_constant(parser, "it", 2); pm_node_t *node = (pm_node_t *) pm_local_variable_target_node_create(parser, &target->location, name, 0); - parse_target_implicit_parameter(parser, target); + pm_node_unreference(parser, target); pm_node_destroy(parser, target); return node; @@ -13742,7 +13775,7 @@ parse_write(pm_parser_t *parser, pm_node_t *target, pm_token_t *operator, pm_nod if (pm_token_is_numbered_parameter(target->location.start, target->location.end)) { pm_diagnostic_id_t diag_id = (scope->parameters & PM_SCOPE_PARAMETERS_NUMBERED_FOUND) ? PM_ERR_EXPRESSION_NOT_WRITABLE_NUMBERED : PM_ERR_PARAMETER_NUMBERED_RESERVED; PM_PARSER_ERR_FORMAT(parser, target->location.start, target->location.end, diag_id, target->location.start); - parse_target_implicit_parameter(parser, target); + pm_node_unreference(parser, target); } pm_locals_unread(&scope->locals, name); @@ -13754,7 +13787,7 @@ parse_write(pm_parser_t *parser, pm_node_t *target, pm_token_t *operator, pm_nod pm_constant_id_t name = pm_parser_local_add_constant(parser, "it", 2); pm_node_t *node = (pm_node_t *) pm_local_variable_write_node_create(parser, name, 0, value, &target->location, operator); - parse_target_implicit_parameter(parser, target); + pm_node_unreference(parser, target); pm_node_destroy(parser, target); return node; @@ -13861,9 +13894,9 @@ parse_write(pm_parser_t *parser, pm_node_t *target, pm_token_t *operator, pm_nod return target; } - // If there are arguments on the call node, then it can't be a method - // call ending with = or a local variable write, so it must be a - // syntax error. In this case we'll fall through to our default + // If there are arguments on the call node, then it can't be a + // method call ending with = or a local variable write, so it must + // be a syntax error. In this case we'll fall through to our default // handling. We need to free the value that we parsed because there // is no way for us to attach it to the tree at this point. // @@ -13871,7 +13904,7 @@ parse_write(pm_parser_t *parser, pm_node_t *target, pm_token_t *operator, pm_nod // parameter somewhere in its subtree, we need to walk it and remove // any implicit parameters from the list of implicit parameters for // the current scope. - parse_target_implicit_parameter(parser, value); + pm_node_unreference(parser, value); pm_node_destroy(parser, value); } PRISM_FALLTHROUGH @@ -18772,7 +18805,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b // If we're about to convert an 'it' implicit local // variable read into a method call, we need to remove // it from the list of implicit local variables. - parse_target_implicit_parameter(parser, node); + pm_node_unreference(parser, node); } else { // Otherwise, we're about to convert a regular local // variable read into a method call, in which case we @@ -18781,7 +18814,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b assert(PM_NODE_TYPE_P(node, PM_LOCAL_VARIABLE_READ_NODE)); if (pm_token_is_numbered_parameter(identifier.start, identifier.end)) { - parse_target_implicit_parameter(parser, node); + pm_node_unreference(parser, node); } else { pm_local_variable_read_node_t *cast = (pm_local_variable_read_node_t *) node; pm_locals_unread(&pm_parser_scope_find(parser, cast->depth)->locals, cast->name); @@ -21071,42 +21104,6 @@ parse_assignment_values(pm_parser_t *parser, pm_binding_power_t previous_binding return value; } -static bool -parse_call_operator_write_block_exits_each(const pm_node_t *node, void *data) { - pm_parser_t *parser = (pm_parser_t *) data; - size_t index = 0; - - while (index < parser->current_block_exits->size) { - pm_node_t *block_exit = parser->current_block_exits->nodes[index]; - - if (block_exit == node) { - if (index + 1 < parser->current_block_exits->size) { - memmove( - &parser->current_block_exits->nodes[index], - &parser->current_block_exits->nodes[index + 1], - (parser->current_block_exits->size - index - 1) * sizeof(pm_node_t *) - ); - } - parser->current_block_exits->size--; - return false; - } - - index++; - } - - return true; -} - -/** - * When we are about to destroy a set of nodes that could potentially contain - * block exits for the current scope, we need to check if they are contained in - * the list of block exits and remove them if they are. - */ -static void -parse_call_operator_write_block_exits(pm_parser_t *parser, const pm_node_t *node) { - pm_visit_node(node, parse_call_operator_write_block_exits_each, parser); -} - /** * Ensure a call node that is about to become a call operator node does not * have arguments or a block attached. If it does, then we'll need to add an @@ -21118,14 +21115,14 @@ static void parse_call_operator_write(pm_parser_t *parser, pm_call_node_t *call_node, const pm_token_t *operator) { if (call_node->arguments != NULL) { pm_parser_err_token(parser, operator, PM_ERR_OPERATOR_WRITE_ARGUMENTS); - parse_call_operator_write_block_exits(parser, (pm_node_t *) call_node->arguments); + pm_node_unreference(parser, (pm_node_t *) call_node->arguments); pm_node_destroy(parser, (pm_node_t *) call_node->arguments); call_node->arguments = NULL; } if (call_node->block != NULL) { pm_parser_err_token(parser, operator, PM_ERR_OPERATOR_WRITE_BLOCK); - parse_call_operator_write_block_exits(parser, (pm_node_t *) call_node->block); + pm_node_unreference(parser, (pm_node_t *) call_node->block); pm_node_destroy(parser, (pm_node_t *) call_node->block); call_node->block = NULL; } @@ -21517,14 +21514,14 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t pm_node_t *value = parse_assignment_value(parser, previous_binding_power, binding_power, accepts_command_call, PM_ERR_EXPECT_EXPRESSION_AFTER_AMPAMPEQ, (uint16_t) (depth + 1)); pm_node_t *result = (pm_node_t *) pm_local_variable_and_write_node_create(parser, node, &token, value, name, 0); - parse_target_implicit_parameter(parser, node); + pm_node_unreference(parser, node); pm_node_destroy(parser, node); return result; } case PM_LOCAL_VARIABLE_READ_NODE: { if (pm_token_is_numbered_parameter(node->location.start, node->location.end)) { PM_PARSER_ERR_FORMAT(parser, node->location.start, node->location.end, PM_ERR_PARAMETER_NUMBERED_RESERVED, node->location.start); - parse_target_implicit_parameter(parser, node); + pm_node_unreference(parser, node); } pm_local_variable_read_node_t *cast = (pm_local_variable_read_node_t *) node; @@ -21651,14 +21648,14 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t pm_node_t *value = parse_assignment_value(parser, previous_binding_power, binding_power, accepts_command_call, PM_ERR_EXPECT_EXPRESSION_AFTER_PIPEPIPEEQ, (uint16_t) (depth + 1)); pm_node_t *result = (pm_node_t *) pm_local_variable_or_write_node_create(parser, node, &token, value, name, 0); - parse_target_implicit_parameter(parser, node); + pm_node_unreference(parser, node); pm_node_destroy(parser, node); return result; } case PM_LOCAL_VARIABLE_READ_NODE: { if (pm_token_is_numbered_parameter(node->location.start, node->location.end)) { PM_PARSER_ERR_FORMAT(parser, node->location.start, node->location.end, PM_ERR_PARAMETER_NUMBERED_RESERVED, node->location.start); - parse_target_implicit_parameter(parser, node); + pm_node_unreference(parser, node); } pm_local_variable_read_node_t *cast = (pm_local_variable_read_node_t *) node; @@ -21795,14 +21792,14 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t pm_node_t *value = parse_assignment_value(parser, previous_binding_power, binding_power, accepts_command_call, PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR, (uint16_t) (depth + 1)); pm_node_t *result = (pm_node_t *) pm_local_variable_operator_write_node_create(parser, node, &token, value, name, 0); - parse_target_implicit_parameter(parser, node); + pm_node_unreference(parser, node); pm_node_destroy(parser, node); return result; } case PM_LOCAL_VARIABLE_READ_NODE: { if (pm_token_is_numbered_parameter(node->location.start, node->location.end)) { PM_PARSER_ERR_FORMAT(parser, node->location.start, node->location.end, PM_ERR_PARAMETER_NUMBERED_RESERVED, node->location.start); - parse_target_implicit_parameter(parser, node); + pm_node_unreference(parser, node); } pm_local_variable_read_node_t *cast = (pm_local_variable_read_node_t *) node; From 4c56001d27c4b2431fee65e6e9426319c475c45d Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 30 Nov 2025 20:45:44 -0500 Subject: [PATCH 2/4] [ruby/prism] Fix up newlines in newline-delimited-literals When you have a %-literal that is delimited by newlines, and you are also interpolating a heredoc into that literal, then both concepts will attempt to add the same newline to the newline list. https://github.com/ruby/prism/commit/c831abb888 --- prism/prism.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/prism/prism.c b/prism/prism.c index 58a6a9987db237..793346f855ea4d 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -12249,7 +12249,13 @@ parser_lex(pm_parser_t *parser) { size_t eol_length = match_eol_at(parser, breakpoint); if (eol_length) { parser->current.end = breakpoint + eol_length; - pm_newline_list_append(&parser->newline_list, parser->current.end - 1); + + // Track the newline if we're not in a heredoc that + // would have already have added the newline to the + // list. + if (parser->heredoc_end == NULL) { + pm_newline_list_append(&parser->newline_list, parser->current.end - 1); + } } else { parser->current.end = breakpoint + 1; } @@ -12503,7 +12509,13 @@ parser_lex(pm_parser_t *parser) { size_t eol_length = match_eol_at(parser, breakpoint); if (eol_length) { parser->current.end = breakpoint + eol_length; - pm_newline_list_append(&parser->newline_list, parser->current.end - 1); + + // Track the newline if we're not in a heredoc that + // would have already have added the newline to the + // list. + if (parser->heredoc_end == NULL) { + pm_newline_list_append(&parser->newline_list, parser->current.end - 1); + } } else { parser->current.end = breakpoint + 1; } From 8eea9a502031e866f210accc7d02347fc55f65c9 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 1 Dec 2025 17:19:42 +1300 Subject: [PATCH 3/4] Nullify scheduler during `terminate_atfork_i`. (#15354) --- thread.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/thread.c b/thread.c index f545a5377d8ed6..6e5c8e89ff075a 100644 --- a/thread.c +++ b/thread.c @@ -4979,6 +4979,9 @@ static void terminate_atfork_i(rb_thread_t *th, const rb_thread_t *current_th) { if (th != current_th) { + // Clear the scheduler as it is no longer operational: + th->scheduler = Qnil; + rb_native_mutex_initialize(&th->interrupt_lock); rb_mutex_abandon_keeping_mutexes(th); rb_mutex_abandon_locking_mutex(th); From bc9ea585bee075480b4f12f84c8ab99315766595 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 1 Dec 2025 17:49:31 +1300 Subject: [PATCH 4/4] Add `rb_ec_close` function to manage execution context cleanup. (#15253) --- cont.c | 1 + vm.c | 7 +++++++ vm_core.h | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/cont.c b/cont.c index c49256c977a5b3..50e8ffb3498ec4 100644 --- a/cont.c +++ b/cont.c @@ -2907,6 +2907,7 @@ void rb_fiber_close(rb_fiber_t *fiber) { fiber_status_set(fiber, FIBER_TERMINATED); + rb_ec_close(&fiber->cont.saved_ec); } static void diff --git a/vm.c b/vm.c index f9c615c3e23c30..1a8a6d059b569b 100644 --- a/vm.c +++ b/vm.c @@ -3890,6 +3890,13 @@ rb_ec_clear_vm_stack(rb_execution_context_t *ec) rb_ec_set_vm_stack(ec, NULL, 0); } +void +rb_ec_close(rb_execution_context_t *ec) +{ + // Fiber storage is not accessible from outside the running fiber, so it is safe to clear it here. + ec->storage = Qnil; +} + static void th_init(rb_thread_t *th, VALUE self, rb_vm_t *vm) { diff --git a/vm_core.h b/vm_core.h index 28d585deb2ab01..9d11457966ed71 100644 --- a/vm_core.h +++ b/vm_core.h @@ -1104,6 +1104,10 @@ void rb_ec_initialize_vm_stack(rb_execution_context_t *ec, VALUE *stack, size_t // @param ec the execution context to update. void rb_ec_clear_vm_stack(rb_execution_context_t *ec); +// Close an execution context and free related resources that are no longer needed. +// @param ec the execution context to close. +void rb_ec_close(rb_execution_context_t *ec); + struct rb_ext_config { bool ractor_safe; };