Freeze $/ and make it ractor safe

[Feature #21109]

By always freezing when setting the global rb_rs variable, we can ensure
it is not modified and can be accessed from a ractor.

We're also making sure it's an instance of String and does not have any
instance variables.

Of course, if $/ is changed at runtime, it may cause surprising behavior
but doing so is deprecated already anyway.

Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
This commit is contained in:
Étienne Barrié 2025-03-24 12:17:58 +01:00 committed by Jean Boussier
parent 49d49d5985
commit 6ecfe643b5
Notes: git 2025-03-27 16:55:12 +00:00
6 changed files with 110 additions and 12 deletions

View File

@ -61,6 +61,7 @@ size_t rb_str_size_as_embedded(VALUE);
bool rb_str_reembeddable_p(VALUE);
VALUE rb_str_upto_endless_each(VALUE, int (*each)(VALUE, VALUE), VALUE);
VALUE rb_str_with_debug_created_info(VALUE, VALUE, int);
VALUE rb_str_frozen_bare_string(VALUE);
/* error.c */
void rb_warn_unchilled_literal(VALUE str);

25
io.c
View File

@ -8673,6 +8673,25 @@ deprecated_str_setter(VALUE val, ID id, VALUE *var)
*var = val;
}
static void
deprecated_rs_setter(VALUE val, ID id, VALUE *var)
{
if (!NIL_P(val)) {
if (!RB_TYPE_P(val, T_STRING)) {
rb_raise(rb_eTypeError, "value of %"PRIsVALUE" must be String", rb_id2str(id));
}
if (rb_str_equal(val, rb_default_rs)) {
val = rb_default_rs;
}
else {
val = rb_str_frozen_bare_string(val);
}
rb_enc_str_coderange(val);
rb_warn_deprecated("'%s'", NULL, rb_id2name(id));
}
*var = val;
}
/*
* call-seq:
* print(*objects) -> nil
@ -15713,8 +15732,10 @@ Init_IO(void)
rb_vm_register_global_object(rb_default_rs);
rb_rs = rb_default_rs;
rb_output_rs = Qnil;
rb_define_hooked_variable("$/", &rb_rs, 0, deprecated_str_setter);
rb_define_hooked_variable("$-0", &rb_rs, 0, deprecated_str_setter);
rb_define_hooked_variable("$/", &rb_rs, 0, deprecated_rs_setter);
rb_gvar_ractor_local("$/"); // not local but ractor safe
rb_define_hooked_variable("$-0", &rb_rs, 0, deprecated_rs_setter);
rb_gvar_ractor_local("$-0"); // not local but ractor safe
rb_define_hooked_variable("$\\", &rb_output_rs, 0, deprecated_str_setter);
rb_define_virtual_variable("$_", get_LAST_READ_LINE, set_LAST_READ_LINE);

4
ruby.c
View File

@ -1325,11 +1325,11 @@ proc_0_option(ruby_cmdline_options_t *opt, const char *s)
if (v > 0377)
rb_rs = Qnil;
else if (v == 0 && numlen >= 2) {
rb_rs = rb_str_new2("");
rb_rs = rb_fstring_lit("");
}
else {
c = v & 0xff;
rb_rs = rb_str_new(&c, 1);
rb_rs = rb_str_freeze(rb_str_new(&c, 1));
}
return s;
}

View File

@ -0,0 +1,13 @@
require_relative '../spec_helper'
describe "The -0 command line option" do
it "sets $/ and $-0" do
ruby_exe("puts $/, $-0", options: "-072").should == ":\n:\n"
end
ruby_version_is "3.5" do
it "sets $/ and $-0 as a frozen string" do
ruby_exe("puts $/.frozen?, $-0.frozen?", options: "-072").should == "true\ntrue\n"
end
end
end

View File

@ -559,12 +559,39 @@ describe "Predefined global $/" do
$VERBOSE = @verbose
end
it "can be assigned a String" do
str = "abc"
$/ = str
$/.should equal(str)
ruby_version_is ""..."3.5" do
it "can be assigned a String" do
str = +"abc"
$/ = str
$/.should equal(str)
end
end
ruby_version_is "3.5" do
it "makes a new frozen String from the assigned String" do
string_subclass = Class.new(String)
str = string_subclass.new("abc")
str.instance_variable_set(:@ivar, 1)
$/ = str
$/.should.frozen?
$/.should be_an_instance_of(String)
$/.should_not.instance_variable_defined?(:@ivar)
$/.should == str
end
it "makes a new frozen String if it's not frozen" do
str = +"abc"
$/ = str
$/.should.frozen?
$/.should == str
end
it "assigns the given String if it's frozen and has no instance variables" do
str = "abc".freeze
$/ = str
$/.should equal(str)
end
end
it "can be assigned nil" do
$/ = nil
$/.should be_nil
@ -608,10 +635,38 @@ describe "Predefined global $-0" do
$VERBOSE = @verbose
end
it "can be assigned a String" do
str = "abc"
$-0 = str
$-0.should equal(str)
ruby_version_is ""..."3.5" do
it "can be assigned a String" do
str = +"abc"
$-0 = str
$-0.should equal(str)
end
end
ruby_version_is "3.5" do
it "makes a new frozen String from the assigned String" do
string_subclass = Class.new(String)
str = string_subclass.new("abc")
str.instance_variable_set(:@ivar, 1)
$-0 = str
$-0.should.frozen?
$-0.should be_an_instance_of(String)
$-0.should_not.instance_variable_defined?(:@ivar)
$-0.should == str
end
it "makes a new frozen String if it's not frozen" do
str = +"abc"
$-0 = str
$-0.should.frozen?
$-0.should == str
end
it "assigns the given String if it's frozen and has no instance variables" do
str = "abc".freeze
$-0 = str
$-0.should equal(str)
end
end
it "can be assigned nil" do

View File

@ -1480,6 +1480,14 @@ rb_str_new_frozen_String(VALUE orig)
return str_new_frozen(rb_cString, orig);
}
VALUE
rb_str_frozen_bare_string(VALUE orig)
{
if (RB_LIKELY(BARE_STRING_P(orig) && OBJ_FROZEN_RAW(orig))) return orig;
return str_new_frozen(rb_cString, orig);
}
VALUE
rb_str_tmp_frozen_acquire(VALUE orig)
{