Skip to content

Commit ccc03ce

Browse files
authored
Protection against fork (#735)
If a thread forks, while another thread is holding an snmalloc lock, then the allocator could stop working. This patch attempts to protect against the cases of this. There is one case that is not covered. If a fork occurs during the very first allocation. This can result in the installation of the fork handler racing with the fork, and all bets are off.
1 parent c2e22cc commit ccc03ce

File tree

9 files changed

+312
-16
lines changed

9 files changed

+312
-16
lines changed

CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,19 @@ int main()
120120
SNMALLOC_LINKER_SUPPORT_NOSTDLIBXX)
121121
set(CMAKE_REQUIRED_LINK_OPTIONS "")
122122

123+
# Detect if pthread_atfork works
124+
CHECK_CXX_SOURCE_COMPILES("
125+
#include <pthread.h>
126+
void prepare() {}
127+
void parent() {}
128+
void child() {}
129+
int main() {
130+
pthread_atfork(prepare, parent, child);
131+
return 0;
132+
}
133+
" SNMALLOC_PTHREAD_ATFORK_WORKS)
134+
135+
123136
if (NOT MSVC AND NOT (SNMALLOC_CLEANUP STREQUAL CXX11_DESTRUCTORS))
124137
# If the target compiler doesn't support -nostdlib++ then we must enable C at
125138
# the global scope for the fallbacks to work.
@@ -320,6 +333,7 @@ add_as_define(SNMALLOC_QEMU_WORKAROUND)
320333
add_as_define(SNMALLOC_TRACING)
321334
add_as_define(SNMALLOC_CI_BUILD)
322335
add_as_define(SNMALLOC_PLATFORM_HAS_GETENTROPY)
336+
add_as_define(SNMALLOC_PTHREAD_ATFORK_WORKS)
323337
add_as_define(SNMALLOC_HAS_LINUX_RANDOM_H)
324338
add_as_define(SNMALLOC_HAS_LINUX_FUTEX_H)
325339
if (SNMALLOC_NO_REALLOCARRAY)

src/snmalloc/ds/combininglock.h

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -274,26 +274,32 @@ namespace snmalloc
274274
template<typename F>
275275
inline void with(CombiningLock& lock, F&& f)
276276
{
277-
// Test if no one is waiting
278-
if (SNMALLOC_LIKELY(lock.last.load(stl::memory_order_relaxed) == nullptr))
277+
// A unix fork while holding a lock can lead to deadlock. Protect against
278+
// this by not allowing a fork while holding a lock.
279+
PreventFork pf;
280+
snmalloc::UNUSED(pf);
279281
{
280-
// No one was waiting so low contention. Attempt to acquire the flag
281-
// lock.
282-
if (SNMALLOC_LIKELY(
283-
lock.flag.exchange(true, stl::memory_order_acquire) == false))
282+
// Test if no one is waiting
283+
if (SNMALLOC_LIKELY(lock.last.load(stl::memory_order_relaxed) == nullptr))
284284
{
285-
// We grabbed the lock.
286-
// Execute the thunk.
287-
f();
285+
// No one was waiting so low contention. Attempt to acquire the flag
286+
// lock.
287+
if (SNMALLOC_LIKELY(
288+
lock.flag.exchange(true, stl::memory_order_acquire) == false))
289+
{
290+
// We grabbed the lock.
291+
// Execute the thunk.
292+
f();
288293

289-
// Release the lock
290-
lock.release();
291-
return;
294+
// Release the lock
295+
lock.release();
296+
return;
297+
}
292298
}
293-
}
294299

295-
// There is contention for the lock, we need to take the slow path
296-
// with the queue.
297-
CombiningLockNodeTempl<F> node(lock, stl::forward<F>(f));
300+
// There is contention for the lock, we need to take the slow path
301+
// with the queue.
302+
CombiningLockNodeTempl<F> node(lock, stl::forward<F>(f));
303+
}
298304
}
299305
} // namespace snmalloc

src/snmalloc/ds_aal/ds_aal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
#pragma once
77
#include "../aal/aal.h"
88
#include "flaglock.h"
9+
#include "prevent_fork.h"
910
#include "singleton.h"

src/snmalloc/ds_aal/flaglock.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include "../aal/aal.h"
4+
#include "prevent_fork.h"
45
#include "snmalloc/ds_core/ds_core.h"
56
#include "snmalloc/stl/atomic.h"
67

@@ -108,6 +109,10 @@ namespace snmalloc
108109
private:
109110
FlagWord& lock;
110111

112+
// A unix fork while holding a lock can lead to deadlock. Protect against
113+
// this by not allowing a fork while holding a lock.
114+
PreventFork pf{};
115+
111116
public:
112117
FlagLock(FlagWord& lock) : lock(lock)
113118
{

src/snmalloc/ds_aal/prevent_fork.h

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#pragma once
2+
3+
#include <snmalloc/aal/aal.h>
4+
#include <snmalloc/stl/atomic.h>
5+
#include <stddef.h>
6+
7+
#ifdef SNMALLOC_PTHREAD_ATFORK_WORKS
8+
# include <pthread.h>
9+
#endif
10+
11+
namespace snmalloc
12+
{
13+
// This is a simple implementation of a class that can be
14+
// used to prevent a process from forking. Holding a lock
15+
// in the allocator while forking can lead to deadlocks.
16+
// This causes the fork to wait out any other threads inside
17+
// the allocators locks.
18+
//
19+
// The use is
20+
// ```
21+
// {
22+
// PreventFork pf;
23+
// // Code that should not be running during a fork.
24+
// }
25+
// ```
26+
class PreventFork
27+
{
28+
// Global atomic counter of the number of threads currently preventing the
29+
// system from forking. The bottom bit is used to signal that a thread is
30+
// wanting to fork.
31+
static inline stl::Atomic<size_t> threads_preventing_fork{0};
32+
33+
// The depth of the current thread's prevention of forking.
34+
// This is used to enable reentrant prevention of forking.
35+
static inline thread_local size_t depth_of_prevention{0};
36+
37+
// There could be multiple copies of the atfork handler installed.
38+
// Only perform work for the first prefork and final postfork.
39+
static inline thread_local size_t depth_of_handlers{0};
40+
41+
// This function ensures that the fork handler has been installed at least
42+
// once. It might be installed more than once, this is safe. As subsequent
43+
// calls would be ignored.
44+
static void ensure_init()
45+
{
46+
#ifdef SNMALLOC_PTHREAD_ATFORK_WORKS
47+
static stl::Atomic<bool> initialised{false};
48+
49+
if (initialised.load(stl::memory_order_acquire))
50+
return;
51+
52+
pthread_atfork(prefork, postfork_parent, postfork_child);
53+
initialised.store(true, stl::memory_order_release);
54+
#endif
55+
};
56+
57+
public:
58+
PreventFork()
59+
{
60+
if (depth_of_prevention++ == 0)
61+
{
62+
// Ensure that the system is initialised before we start.
63+
// Don't do this on nested Prevent calls.
64+
ensure_init();
65+
while (true)
66+
{
67+
auto prev = threads_preventing_fork.fetch_add(2);
68+
if (prev % 2 == 0)
69+
break;
70+
71+
threads_preventing_fork.fetch_sub(2);
72+
73+
while ((threads_preventing_fork.load() % 2) == 1)
74+
{
75+
Aal::pause();
76+
}
77+
};
78+
}
79+
}
80+
81+
~PreventFork()
82+
{
83+
if (--depth_of_prevention == 0)
84+
{
85+
threads_preventing_fork -= 2;
86+
}
87+
}
88+
89+
// The function that notifies new threads not to enter PreventFork regions
90+
// It waits until all threads are no longer in a PreventFork region before
91+
// returning.
92+
static void prefork()
93+
{
94+
if (depth_of_handlers++ != 0)
95+
return;
96+
97+
if (depth_of_prevention != 0)
98+
error("Fork attempted while in PreventFork region.");
99+
100+
while (true)
101+
{
102+
auto current = threads_preventing_fork.load();
103+
if (
104+
(current % 2 == 0) &&
105+
(threads_preventing_fork.compare_exchange_weak(current, current + 1)))
106+
{
107+
break;
108+
}
109+
Aal::pause();
110+
};
111+
112+
while (threads_preventing_fork.load() != 1)
113+
{
114+
Aal::pause();
115+
}
116+
117+
// Finally set the flag that allows this thread to enter PreventFork
118+
// regions This is safe as the only other calls here are to other prefork
119+
// handlers.
120+
depth_of_prevention++;
121+
}
122+
123+
// Unsets the flag that allows threads to enter PreventFork regions
124+
// and for another thread to request a fork.
125+
static void postfork_child()
126+
{
127+
// Count out the number of handlers that have been called, and
128+
// only perform on the last.
129+
if (--depth_of_handlers != 0)
130+
return;
131+
132+
// This thread is no longer preventing a fork, so decrement the counter.
133+
depth_of_prevention--;
134+
135+
// Allow other threads to allocate
136+
// There could have been threads spinning in the prefork handler having
137+
// optimistically increasing thread_preventing_fork by 2, but now the
138+
// threads do not exist due to the fork. So restart the counter in the
139+
// child.
140+
threads_preventing_fork = 0;
141+
}
142+
143+
// Unsets the flag that allows threads to enter PreventFork regions
144+
// and for another thread to request a fork.
145+
static void postfork_parent()
146+
{
147+
// Count out the number of handlers that have been called, and
148+
// only perform on the last.
149+
if (--depth_of_handlers != 0)
150+
return;
151+
152+
// This thread is no longer preventing a fork, so decrement the counter.
153+
depth_of_prevention--;
154+
155+
// Allow other threads to allocate
156+
// Just remove the bit, and let the potential other threads in prefork
157+
// remove their counts.
158+
threads_preventing_fork--;
159+
}
160+
};
161+
} // namespace snmalloc

src/snmalloc/ds_aal/singleton.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include "prevent_fork.h"
34
#include "snmalloc/stl/atomic.h"
45

56
namespace snmalloc
@@ -36,6 +37,11 @@ namespace snmalloc
3637
auto state = initialised.load(stl::memory_order_acquire);
3738
if (SNMALLOC_UNLIKELY(state == State::Uninitialised))
3839
{
40+
// A unix fork while initialising a singleton can lead to deadlock.
41+
// Protect against this by not allowing a fork while attempting
42+
// initialisation.
43+
PreventFork pf;
44+
snmalloc::UNUSED(pf);
3945
if (initialised.compare_exchange_strong(
4046
state, State::Initialising, stl::memory_order_relaxed))
4147
{

src/snmalloc/mem/freelist_queue.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ namespace snmalloc
110110
invariant();
111111
freelist::Object::atomic_store_null(last, Key, Key_tweak);
112112

113+
// The following non-linearisable effect is normally benign,
114+
// but could lead to a remote list become completely detached
115+
// during a fork in a multi-threaded process. This would lead
116+
// to a memory leak, which is probably the least of your problems
117+
// if you forked in during a deallocation.
118+
PreventFork pf;
119+
snmalloc::UNUSED(pf);
120+
113121
// Exchange needs to be acq_rel.
114122
// * It needs to be a release, so nullptr in next is visible.
115123
// * Needs to be acquire, so linking into the list does not race with

src/snmalloc/stl/gnu/atomic.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,27 @@ namespace snmalloc
214214
addressof(val), 1, order(MemoryOrder::SEQ_CST));
215215
}
216216

217+
SNMALLOC_FAST_PATH T operator--()
218+
{
219+
static_assert(stl::is_integral_v<T>, "T must be an integral type.");
220+
return __atomic_sub_fetch(
221+
addressof(val), 1, order(MemoryOrder::SEQ_CST));
222+
}
223+
217224
SNMALLOC_FAST_PATH const T operator++(int)
218225
{
219226
static_assert(stl::is_integral_v<T>, "T must be an integral type.");
220227
return __atomic_fetch_add(
221228
addressof(val), 1, order(MemoryOrder::SEQ_CST));
222229
}
223230

231+
SNMALLOC_FAST_PATH const T operator--(int)
232+
{
233+
static_assert(stl::is_integral_v<T>, "T must be an integral type.");
234+
return __atomic_fetch_sub(
235+
addressof(val), 1, order(MemoryOrder::SEQ_CST));
236+
}
237+
224238
SNMALLOC_FAST_PATH T operator-=(T decrement)
225239
{
226240
static_assert(stl::is_integral_v<T>, "T must be an integral type.");

0 commit comments

Comments
 (0)