Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Commit 221294f

Browse files
author
Patrick Häcker
committed
fix: memory-leak due to unbound type-parameter recursion
FakeTypeName expanded a type's parameters recursively with no size bound. A few types explode into enormous trees: type-domain packages whose types recurse into themselves (e.g. TypeDomainNaturalNumbers — naturals as nested types, rationals as continued fractions), and even some Base/LinearAlgebra signatures whose `where`-bounded `Union`s reach tens of thousands of nodes. This produced multi-gigabyte cache files (2.2 GB for TypeDomainNaturalNumbers) that exhausted memory — OOMing the machine — when read back into the store. Cap expansion with a per-type budget of expanded DataTypes (MAX_EXPANDED_TYPES = 128): once it is spent, a type's remaining parameters are dropped and only its name is kept. The budget bounds any shape of explosion (depth, width, or indirect recursion through variable bounds), while ordinary types stay far below it and are unaffected.
1 parent 9678f3d commit 221294f

2 files changed

Lines changed: 54 additions & 14 deletions

File tree

src/faketypes.jl

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,44 @@ struct FakeTypeName
1414
parameters::Vector{Any}
1515
end
1616

17-
function FakeTypeName(@nospecialize(x))
17+
# Type parameters are expanded recursively so cached signatures keep their
18+
# structure, but a few types expand into huge trees — type-domain packages
19+
# whose types recurse into themselves (naturals as nested `NonnegativeInteger`,
20+
# rationals as continued fractions), and even some Base/LinearAlgebra
21+
# signatures whose `where`-bounded `Union`s reach tens of thousands of nodes —
22+
# which unbounded would produce multi-gigabyte caches that exhaust memory on
23+
# read. To prevent that, `budget` caps how many `DataType`s are expanded while
24+
# building one type: once it is spent the remaining parameters are dropped,
25+
# keeping only their names. The limit sits far above any ordinary type, so only
26+
# these outliers are truncated; the serializer's own MAX_DEPTH guard is a
27+
# separate cycle check, not a size bound.
28+
const MAX_EXPANDED_TYPES = 128
29+
30+
mutable struct ExpandBudget
31+
remaining::Int
32+
end
33+
ExpandBudget() = ExpandBudget(MAX_EXPANDED_TYPES)
34+
35+
function FakeTypeName(@nospecialize(x), budget::ExpandBudget=ExpandBudget())
1836
@static if !(Vararg isa Type)
1937
x isa typeof(Vararg) && return FakeTypeofVararg(x)
2038
end
2139
if x isa DataType
2240
xname = x.name
23-
xnamename = xname.name
24-
ft = FakeTypeName(VarRef(VarRef(x.name.module), xnamename), [])
25-
for p in x.parameters
26-
push!(ft.parameters, _parameter(p))
41+
ft = FakeTypeName(VarRef(VarRef(xname.module), xname.name), [])
42+
if budget.remaining > 0
43+
budget.remaining -= 1
44+
for p in x.parameters
45+
push!(ft.parameters, _parameter(p, budget))
46+
end
2747
end
2848
ft
2949
elseif x isa Union
30-
FakeUnion(x)
50+
FakeUnion(x, budget)
3151
elseif x isa UnionAll
32-
FakeUnionAll(x)
52+
FakeUnionAll(x, budget)
3353
elseif x isa TypeVar
34-
FakeTypeVar(x)
54+
FakeTypeVar(x, budget)
3555
elseif x isa Core.TypeofBottom
3656
FakeTypeofBottom()
3757
elseif x isa Module
@@ -46,28 +66,28 @@ struct FakeUnion
4666
a
4767
b
4868
end
49-
FakeUnion(u::Union) = FakeUnion(FakeTypeName(u.a), FakeTypeName(u.b))
69+
FakeUnion(u::Union, budget::ExpandBudget=ExpandBudget()) = FakeUnion(FakeTypeName(u.a, budget), FakeTypeName(u.b, budget))
5070
struct FakeTypeVar
5171
name::Symbol
5272
lb
5373
ub
5474
end
55-
FakeTypeVar(tv::TypeVar) = FakeTypeVar(tv.name, FakeTypeName(tv.lb), FakeTypeName(tv.ub))
75+
FakeTypeVar(tv::TypeVar, budget::ExpandBudget=ExpandBudget()) = FakeTypeVar(tv.name, FakeTypeName(tv.lb, budget), FakeTypeName(tv.ub, budget))
5676
struct FakeUnionAll
5777
var::FakeTypeVar
5878
body::Any
5979
end
60-
FakeUnionAll(ua::UnionAll) = FakeUnionAll(FakeTypeVar(ua.var), FakeTypeName(ua.body))
80+
FakeUnionAll(ua::UnionAll, budget::ExpandBudget=ExpandBudget()) = FakeUnionAll(FakeTypeVar(ua.var, budget), FakeTypeName(ua.body, budget))
6181

62-
function _parameter(@nospecialize(p))
82+
function _parameter(@nospecialize(p), budget::ExpandBudget=ExpandBudget())
6383
if p isa Union{Int,Symbol,Bool,Char}
6484
p
6585
elseif !(p isa Type) && isbitstype(typeof(p))
6686
0
6787
elseif p isa Tuple
68-
_parameter.(p)
88+
_parameter.(p, Ref(budget))
6989
else
70-
FakeTypeName(p)
90+
FakeTypeName(p, budget)
7191
end
7292
end
7393

test/runtests.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,26 @@ end
280280
end
281281
end
282282

283+
@testitem "recursive type parameters are bounded" begin
284+
using SymbolServer: FakeTypeName, ExpandBudget
285+
286+
# Number of DataTypes that were expanded (i.e. kept their parameters).
287+
expanded(x) = x isa FakeTypeName && !isempty(x.parameters) ? 1 + sum(expanded, x.parameters) : 0
288+
289+
limit = 8
290+
deep = foldl((T, _) -> Tuple{T}, 1:100; init = Int) # Tuple{Tuple{…{Int}}}
291+
wide = Tuple{ntuple(i -> Val{i}, 100)...} # Tuple{Val{1},…,Val{100}}
292+
293+
# However a type explodes — by depth or by width — expansion stops at the budget.
294+
for T in (deep, wide)
295+
@test expanded(FakeTypeName(T, ExpandBudget(limit))) <= limit
296+
end
297+
298+
# An ordinary type has far fewer nodes than the budget, so nothing is dropped.
299+
ordinary = Dict{String,Vector{Tuple{Int,Float64}}}
300+
@test FakeTypeName(ordinary, ExpandBudget(limit)) == FakeTypeName(ordinary)
301+
end
302+
283303
@testitem "Pipe names" begin
284304
import UUIDs
285305

0 commit comments

Comments
 (0)