From ce96a4c4d0a209fb5297d017cbdeaab81e205766 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Thu, 29 Jan 2026 16:17:50 +0100 Subject: [PATCH] prevent stack overflows from user-defined constructor methods: strings Due to issue #42372, it can not be assumed that a constructor call will return a value of the requested type. Enforce that assumption in code where it matters. Example stack overflows that this change prevents: ```julia struct FaultyInt <: Integer end # new `Int`-like function Base.Int(x::FaultyInt) x end # invalid constructor iterate("", FaultyInt()) # overflows the stack isvalid("", FaultyInt()) # overflows the stack struct MyString <: AbstractString # valid new subtype of `AbstractString` str::String end isvalid(MyString(""), FaultyInt()) # overflows the stack codeunit(MyString(""), FaultyInt()) # overflows the stack ``` Follow up on PR #59506. --- base/strings/basic.jl | 6 ++-- ...method_should_not_cause_stack_overflows.jl | 33 ++++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/base/strings/basic.jl b/base/strings/basic.jl index 85a4dacbd323c..1d563890998fe 100644 --- a/base/strings/basic.jl +++ b/base/strings/basic.jl @@ -105,7 +105,7 @@ UInt8 See also [`ncodeunits`](@ref), [`checkbounds`](@ref). """ @propagate_inbounds codeunit(s::AbstractString, i::Integer) = i isa Int ? - throw(MethodError(codeunit, (s, i))) : codeunit(s, Int(i)) + throw(MethodError(codeunit, (s, i))) : codeunit(s, Int(i)::Int) """ isvalid(s::AbstractString, i::Integer)::Bool @@ -141,7 +141,7 @@ Stacktrace: ``` """ @propagate_inbounds isvalid(s::AbstractString, i::Integer) = i isa Int ? - throw(MethodError(isvalid, (s, i))) : isvalid(s, Int(i)) + throw(MethodError(isvalid, (s, i))) : isvalid(s, Int(i)::Int) """ iterate(s::AbstractString, i::Integer)::Union{Tuple{<:AbstractChar, Int}, Nothing} @@ -154,7 +154,7 @@ of the iteration protocol may assume that `i` is the start of a character in `s` See also [`getindex`](@ref), [`checkbounds`](@ref). """ @propagate_inbounds iterate(s::AbstractString, i::Integer) = i isa Int ? - throw(MethodError(iterate, (s, i))) : iterate(s, Int(i)) + throw(MethodError(iterate, (s, i))) : iterate(s, Int(i)::Int) ## basic generic definitions ## diff --git a/test/faulty_constructor_method_should_not_cause_stack_overflows.jl b/test/faulty_constructor_method_should_not_cause_stack_overflows.jl index 8dede73e2ae7a..08c9c1d1e3b09 100644 --- a/test/faulty_constructor_method_should_not_cause_stack_overflows.jl +++ b/test/faulty_constructor_method_should_not_cause_stack_overflows.jl @@ -1,3 +1,4 @@ +# new types with invalid constructors for (typ, sup) in ( (:Char, :AbstractChar), (:String, :AbstractString), @@ -9,6 +10,18 @@ for (typ, sup) in ( @eval function Base.$typ(x::$fau) x end end +# valid new subtype of `AbstractString` +struct MyString <: AbstractString + str::String +end +Base.lastindex(s::MyString) = lastindex(s.str) +Base.iterate(s::MyString) = iterate(s, 1) +Base.iterate(s::MyString, state) = iterate(s, Int(state)::Int) +Base.iterate(s::MyString, state::Integer) = iterate(s, Int(state)::Int) +Base.iterate(s::MyString, state::Int) = iterate(s.str, state) +Base.isequal(a::MyString, b::MyString) = isequal(a.str, b.str) +Base.:(==)(a::MyString, b::MyString) = (a.str == b.str) + using Test using Unicode: Unicode @@ -39,14 +52,18 @@ using Unicode: Unicode end @testset let x = FaultyInt() @test_throws exc readbytes!(IOBuffer(), Vector{UInt8}(undef, 0), x) - @test_throws exc length("", x, x) - @test_throws exc thisind("", x) - @test_throws exc prevind("", x) - @test_throws exc prevind("", x, x) - @test_throws exc nextind("", x) - @test_throws exc nextind("", x, x) - @test_throws exc codeunit("", x) - @test_throws exc SubString("", x, x) + for s in ("", MyString("")) + @test_throws exc iterate(s, x) + @test_throws exc isvalid(s, x) + @test_throws exc length(s, x, x) + @test_throws exc thisind(s, x) + @test_throws exc prevind(s, x) + @test_throws exc prevind(s, x, x) + @test_throws exc nextind(s, x) + @test_throws exc nextind(s, x, x) + @test_throws exc codeunit(s, x) + @test_throws exc SubString(s, x, x) + end end @testset let x = FaultyUInt32() @test_throws exc Char(x)