From 4963798a1e088df17ea4751544c6ff33f62da0db Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Mon, 31 Mar 2025 19:08:23 +0300 Subject: [PATCH] Fix rb_str_locktmp and rb_str_unlocktmp to raise FrozenError when string is frozen --- CHANGELOG.md | 1 + spec/ruby/optional/capi/string_spec.rb | 30 ++++++++++++++++--- .../java/org/truffleruby/cext/CExtNodes.java | 16 ++++++---- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cea97b78dc1..5a117407def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Compatibility: * Fix numeric coercing when `#coerce` method is not public (#3848, @andrykonchin). * Fix `Kernel#raise` and don't override `cause` at exception re-raising (#3831, @andrykonchin). * Return a pointer with `#type_size` of 1 for `Pointer#read_pointer` (@eregon). +* Fix `rb_str_locktmp()` and `rb_str_unlocktmp()` to raise `FrozenError` when string argument is frozen (#3752, @andrykonchin). Performance: diff --git a/spec/ruby/optional/capi/string_spec.rb b/spec/ruby/optional/capi/string_spec.rb index 715f76eaea2..d75aa2f61d4 100644 --- a/spec/ruby/optional/capi/string_spec.rb +++ b/spec/ruby/optional/capi/string_spec.rb @@ -1201,28 +1201,50 @@ def inspect describe "rb_str_locktmp" do it "raises an error when trying to lock an already locked string" do - str = "test" + str = +"test" @s.rb_str_locktmp(str).should == str -> { @s.rb_str_locktmp(str) }.should raise_error(RuntimeError, 'temporal locking already locked string') end it "locks a string so that modifications would raise an error" do - str = "test" + str = +"test" @s.rb_str_locktmp(str).should == str -> { str.upcase! }.should raise_error(RuntimeError, 'can\'t modify string; temporarily locked') end + + ruby_bug "#20998", ""..."3.5" do + it "raises FrozenError if string is frozen" do + str = -"rb_str_locktmp" + -> { @s.rb_str_locktmp(str) }.should raise_error(FrozenError) + + str = +"rb_str_locktmp" + str.freeze + -> { @s.rb_str_locktmp(str) }.should raise_error(FrozenError) + end + end end describe "rb_str_unlocktmp" do it "unlocks a locked string" do - str = "test" + str = +"test" @s.rb_str_locktmp(str) @s.rb_str_unlocktmp(str).should == str str.upcase!.should == "TEST" end it "raises an error when trying to unlock an already unlocked string" do - -> { @s.rb_str_unlocktmp("test") }.should raise_error(RuntimeError, 'temporal unlocking already unlocked string') + -> { @s.rb_str_unlocktmp(+"test") }.should raise_error(RuntimeError, 'temporal unlocking already unlocked string') + end + + ruby_bug "#20998", ""..."3.5" do + it "raises FrozenError if string is frozen" do + str = -"rb_str_locktmp" + -> { @s.rb_str_unlocktmp(str) }.should raise_error(FrozenError) + + str = +"rb_str_locktmp" + str.freeze + -> { @s.rb_str_unlocktmp(str) }.should raise_error(FrozenError) + end end end diff --git a/src/main/java/org/truffleruby/cext/CExtNodes.java b/src/main/java/org/truffleruby/cext/CExtNodes.java index 7b0b503a64d..db5b3eba1c6 100644 --- a/src/main/java/org/truffleruby/cext/CExtNodes.java +++ b/src/main/java/org/truffleruby/cext/CExtNodes.java @@ -964,7 +964,10 @@ public abstract static class RbStrLockTmpNode extends CoreMethodArrayArgumentsNo @Specialization RubyString rbStrLockTmp(RubyString string, - @Cached InlinedBranchProfile errorProfile) { + @Cached InlinedBranchProfile errorProfile, + @Cached TypeNodes.CheckFrozenNode raiseIfFrozenNode) { + raiseIfFrozenNode.execute(this, string); + if (string.locked) { errorProfile.enter(this); throw new RaiseException(getContext(), @@ -976,8 +979,7 @@ RubyString rbStrLockTmp(RubyString string, @Specialization RubyString rbStrLockTmpImmutable(ImmutableRubyString string) { - throw new RaiseException(getContext(), - coreExceptions().runtimeError("temporal locking immutable string", this)); + throw new RaiseException(getContext(this), coreExceptions(this).frozenError(string, this)); } } @@ -987,7 +989,10 @@ public abstract static class RbStrUnlockTmpNode extends CoreMethodArrayArguments @Specialization RubyString rbStrUnlockTmp(RubyString string, - @Cached InlinedBranchProfile errorProfile) { + @Cached InlinedBranchProfile errorProfile, + @Cached TypeNodes.CheckFrozenNode raiseIfFrozenNode) { + raiseIfFrozenNode.execute(this, string); + if (!string.locked) { errorProfile.enter(this); throw new RaiseException(getContext(), @@ -999,8 +1004,7 @@ RubyString rbStrUnlockTmp(RubyString string, @Specialization ImmutableRubyString rbStrUnlockTmpImmutable(ImmutableRubyString string) { - throw new RaiseException(getContext(), - coreExceptions().runtimeError("temporal unlocking immutable string", this)); + throw new RaiseException(getContext(this), coreExceptions(this).frozenError(string, this)); } }