1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! Release FLINT's per-thread caches when a thread that used FLINT exits.
//!
//! # The leak
//!
//! FLINT keeps several caches in thread-local storage and frees them only in
//! `flint_cleanup`, which nothing in this crate called. The large one is the
//! `fmpz` cache (`fmpz_single.c`): the first time a thread promotes an `fmpz`
//! to a GMP integer, FLINT allocates a 16-page block of `mpz_t` structs,
//! initialises every one of them (a GMP allocation each) and threads them onto
//! a thread-local free list. A cleared big integer goes back on that list
//! rather than to the allocator. When the thread exits the list is simply
//! forgotten, and with it the block and every limb allocation behind it —
//! about 228 kB per thread. 5000 short-lived threads that each made one
//! `3^200` grew the process by 1.14 GB. Thread-per-request servers and any
//! pool that retires its workers pay that on every thread. `mpfr_free_cache`
//! (MPFR's constant caches) and Arb's per-thread constant caches are released
//! by the same call.
//!
//! # The fix
//!
//! [`note_thread_uses_flint`] touches a thread-local guard whose `Drop` calls
//! `flint_cleanup`. The FLINT wrappers call it from their constructors, so the
//! guard is registered on the first FLINT use of each thread and runs when the
//! thread exits. A thread that never touches FLINT registers nothing, and a
//! long-lived thread (Rayon's pool workers, the Python main thread) pays one
//! thread-local access per constructor and the cleanup once, at exit.
//!
//! # Why it is sound
//!
//! `flint_cleanup` (FLINT 3.3–3.5; a no-op in a reentrant build without TLS)
//! touches only the *calling* thread's caches:
//!
//! * The `mpz_t` structs on this thread's free list are cleared and counted
//! against their block; a block is freed only once *every* struct in it has
//! been counted. An integer that is still alive — held by another thread,
//! or by a value this thread has not dropped yet — keeps its block alive.
//! When it is eventually cleared, FLINT sees the block's non-zero count (or
//! a foreign owning thread) and takes its cross-thread path: `mpz_clear`
//! plus an atomic increment, freeing the block on the last one. That path
//! is FLINT's own, used today for any integer dropped on a thread other
//! than the one that made it.
//! * The thread-local pointers are reset to empty afterwards, so FLINT use
//! *after* the cleanup (from a later thread-local destructor) starts a
//! fresh cache instead of touching freed memory. That cache is then not
//! released, which is the old behaviour, not a new hazard.
//! * GMP memory is returned through GMP's free function, which is the
//! accounting wrapper of [`crate::budget::install_memory_accounting`] when
//! installed. The wrapper only updates a process-wide atomic, so it is safe
//! to run from a thread-local destructor.
//!
//! The guard is reached through `try_with`, so FLINT use from another
//! thread-local destructor that runs after this one is not a panic.
use ffi;
thread_local!
/// Tags of the threads whose guard ran `flint_cleanup`.
static CLEANED_TAGS: Mutex = new;
/// Register the calling thread for `flint_cleanup` when it exits.
///
/// Cheap and idempotent; call it before a thread's first FLINT allocation.
pub