Skip to content

Commit b3993f4

Browse files
authored
flambda-backend: Fix segfault when signal handlers invoked in non-OCaml threads on runtime5 (#2154)
* Cherry-pick 59029b9 (part of ocaml/ocaml PR11307) * Add a test, which would segfault before this PR
1 parent 0b9333c commit b3993f4

File tree

6 files changed

+107
-21
lines changed

6 files changed

+107
-21
lines changed

runtime/caml/domain.h

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ void caml_handle_gc_interrupt(void);
6666
void caml_handle_incoming_interrupts(void);
6767

6868
CAMLextern void caml_interrupt_self(void);
69+
void caml_interrupt_all_for_signal(void);
6970
void caml_reset_young_limit(caml_domain_state *);
7071

7172
CAMLextern void caml_reset_domain_lock(void);

runtime/domain.c

+34-6
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,10 @@ typedef cpuset_t cpu_set_t;
141141

142142
/* control of STW interrupts */
143143
struct interruptor {
144-
atomic_uintnat* interrupt_word;
144+
/* The outermost atomic is for synchronization with
145+
caml_interrupt_all_for_signal. The innermost atomic is also for
146+
cross-domain communication.*/
147+
_Atomic(atomic_uintnat *) interrupt_word;
145148
caml_plat_mutex lock;
146149
caml_plat_cond cond;
147150

@@ -205,7 +208,7 @@ static caml_plat_mutex all_domains_lock = CAML_PLAT_MUTEX_INITIALIZER;
205208
static caml_plat_cond all_domains_cond =
206209
CAML_PLAT_COND_INITIALIZER(&all_domains_lock);
207210
static atomic_uintnat /* dom_internal* */ stw_leader = 0;
208-
static struct dom_internal all_domains[Max_domains];
211+
static dom_internal all_domains[Max_domains];
209212

210213
CAMLexport atomic_uintnat caml_num_domains_running;
211214

@@ -294,7 +297,8 @@ CAMLexport caml_domain_state* caml_get_domain_state(void)
294297

295298
Caml_inline void interrupt_domain(struct interruptor* s)
296299
{
297-
atomic_store_release(s->interrupt_word, (uintnat)(-1));
300+
atomic_uintnat * interrupt_word = atomic_load_relaxed(&s->interrupt_word);
301+
atomic_store_release(interrupt_word, (uintnat)(-1));
298302
}
299303

300304
int caml_incoming_interrupts_queued(void)
@@ -585,8 +589,14 @@ static void domain_create(uintnat initial_minor_heap_wsize) {
585589

586590
caml_state = domain_state;
587591

592+
domain_state->young_limit = 0;
593+
/* Synchronized with [caml_interrupt_all_for_signal], so that the
594+
initializing write of young_limit happens before any
595+
interrupt. */
596+
atomic_store_explicit(&s->interrupt_word, &domain_state->young_limit,
597+
memory_order_release);
598+
588599
s->unique_id = fresh_domain_unique_id();
589-
s->interrupt_word = &domain_state->young_limit;
590600
s->running = 1;
591601
atomic_fetch_add(&caml_num_domains_running, 1);
592602

@@ -873,7 +883,7 @@ void caml_init_domains(uintnat minor_heap_wsz) {
873883

874884
dom->id = i;
875885

876-
dom->interruptor.interrupt_word = 0;
886+
dom->interruptor.interrupt_word = NULL;
877887
caml_plat_mutex_init(&dom->interruptor.lock);
878888
caml_plat_cond_init(&dom->interruptor.cond,
879889
&dom->interruptor.lock);
@@ -1529,10 +1539,28 @@ int caml_try_run_on_all_domains_async(
15291539
leader_setup, 0, 0);
15301540
}
15311541

1532-
void caml_interrupt_self(void) {
1542+
void caml_interrupt_self(void)
1543+
{
15331544
interrupt_domain(&domain_self->interruptor);
15341545
}
15351546

1547+
/* async-signal-safe */
1548+
void caml_interrupt_all_for_signal(void)
1549+
{
1550+
for (dom_internal *d = all_domains; d < &all_domains[Max_domains]; d++) {
1551+
/* [all_domains] is an array of values. So we can access
1552+
[interrupt_word] directly without synchronisation other than
1553+
with other people who access the same [interrupt_word].*/
1554+
atomic_uintnat * interrupt_word =
1555+
atomic_load_explicit(&d->interruptor.interrupt_word,
1556+
memory_order_acquire);
1557+
/* Early exit: if the current domain was never initialized, then
1558+
neither have been any of the remaining ones. */
1559+
if (interrupt_word == NULL) return;
1560+
interrupt_domain(&d->interruptor);
1561+
}
1562+
}
1563+
15361564
void caml_reset_young_limit(caml_domain_state * dom_st)
15371565
{
15381566
CAMLassert ((uintnat)dom_st->young_ptr > (uintnat)dom_st->young_trigger);

runtime/minor_gc.c

+6-3
Original file line numberDiff line numberDiff line change
@@ -844,9 +844,12 @@ void caml_alloc_small_dispatch (caml_domain_state * dom_st,
844844
"minor GC");
845845
else {
846846
caml_handle_gc_interrupt();
847-
/* In the case of long-running C code that regularly polls with
848-
[caml_process_pending_actions], still force a query of all
849-
callbacks at every minor collection or major slice. */
847+
/* We might be here due to a recently-recorded signal, so we
848+
need to remember that we must run signal handlers. In
849+
addition, in the case of long-running C code that regularly
850+
polls with caml_process_pending_actions, we want to force a
851+
query of all callbacks at every minor collection or major
852+
slice (similarly to OCaml behaviour). */
850853
dom_st->action_pending = 1;
851854
}
852855

runtime/signals.c

+26-12
Original file line numberDiff line numberDiff line change
@@ -107,21 +107,37 @@ CAMLexport value caml_process_pending_signals_exn(void)
107107
}
108108

109109
/* Record the delivery of a signal, and arrange for it to be processed
110-
as soon as possible:
111-
- via Caml_state->action_pending, processed in
112-
caml_process_pending_actions.
113-
- by playing with the allocation limit, processed in
114-
caml_alloc_small_dispatch.
115-
*/
110+
as soon as possible, by playing with the allocation limit,
111+
processed in caml_alloc_small_dispatch. */
116112
CAMLexport void caml_record_signal(int signal_number)
117113
{
118114
unsigned int i;
119115
if (signal_number <= 0 || signal_number >= NSIG) return;
120116
i = signal_number - 1;
121117
atomic_fetch_or(&caml_pending_signals[i / BITS_PER_WORD],
122118
(uintnat)1 << (i % BITS_PER_WORD));
123-
// FIXME: the TLS variable is not thread-safe
124-
caml_interrupt_self();
119+
/* We interrupt all domains when a signal arrives. Signals (SIGINT,
120+
SIGALRM...) arrive infrequently-enough that this is affordable.
121+
This is a strategy that makes as little assumptions as possible
122+
about signal-safety, threads, and domains.
123+
124+
* In mixed C/OCaml applications there is no guarantee that the
125+
POSIX signal handler runs in an OCaml thread, so Caml_state might
126+
be unavailable.
127+
128+
* While C11 mandates that atomic thread-local variables are
129+
async-signal-safe for reading, gcc does not conform and can
130+
allocate in corner cases involving dynamic linking. It is also
131+
unclear whether the OSX implementation conforms, but this might
132+
be a theoretical concern only.
133+
134+
* The thread executing a POSIX signal handler is not necessarily
135+
the most ready to execute the corresponding OCaml signal handler.
136+
Examples:
137+
- Ctrl-C in the toplevel when domain 0 is stuck inside [Domain.join].
138+
- a thread that has just spawned, before the appropriate mask is set.
139+
*/
140+
caml_interrupt_all_for_signal();
125141
}
126142

127143
/* Management of blocking sections. */
@@ -303,7 +319,7 @@ void caml_request_minor_gc (void)
303319
[Caml_state->action_pending] is 1, or there is a function currently
304320
running which is executing all actions.
305321
306-
This is used to ensure [Caml_state->young_limit] is always set
322+
This is used to ensure that [Caml_state->young_limit] is always set
307323
appropriately.
308324
309325
In case there are two different callbacks (say, a signal and a
@@ -316,9 +332,7 @@ void caml_request_minor_gc (void)
316332
calling them first.
317333
*/
318334

319-
CAMLno_tsan /* When called from [caml_record_signal], these memory
320-
accesses may not be synchronized. Otherwise we assume
321-
that we have unique access to dom_st. */
335+
/* We assume that we have unique access to dom_st. */
322336
void caml_set_action_pending(caml_domain_state * dom_st)
323337
{
324338
dom_st->action_pending = 1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
(* TEST
2+
modules = "signal_handler_run_in_c_thread_stubs.c"
3+
* native
4+
** hassysthreads
5+
*)
6+
7+
(* This doesn't actually need systhreads, but the requirement should
8+
ensure the C pthreads-using code will build. *)
9+
10+
external c_stub : unit -> unit = "test_signal_handler_run_in_c_thread"
11+
12+
let () =
13+
Sys.set_signal Sys.sigusr1 (Signal_handle (fun _ -> exit 0));
14+
c_stub ();
15+
while true do
16+
(* Ensure pending actions are run, by forcing allocation *)
17+
ignore (Sys.opaque_identity (Random.int 42, Random.int 42))
18+
done
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#include <pthread.h>
2+
#include <signal.h>
3+
#include <caml/mlvalues.h>
4+
5+
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
6+
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
7+
8+
static void* in_thread(void* unused)
9+
{
10+
(void) pthread_cond_signal(&cond);
11+
/* Signal to be received in this thread by the OCaml signal handler */
12+
while (1);
13+
}
14+
15+
value test_signal_handler_run_in_c_thread(value unit)
16+
{
17+
pthread_t thread;
18+
pthread_create(&thread, NULL, &in_thread, NULL);
19+
pthread_cond_wait(&cond, &mutex);
20+
pthread_kill(thread, SIGUSR1);
21+
return Val_unit;
22+
}

0 commit comments

Comments
 (0)