Skip to content

Commit 533efc2

Browse files
committed
memory_maps: make impl lock-free using atomics
This reformulates how threading works for memory maps. This gets rid of all of the locks and just uses an atomic counter in exchange for guaranteeing that each thread registers only once at the start (with `ia2_thread_metadata_new_for_current_thread`). `ia2_thread_metadata_new_for_current_thread` is called for the main thread in `ia2_start` and in new threads in `ia2_thread_begin`. Making this lock-free avoids any potential deadlocks and reentrancy issues, meaning the memory map functions can be called in any context, including in a debugger stopped at any point.
1 parent 3011d41 commit 533efc2

File tree

7 files changed

+100
-68
lines changed

7 files changed

+100
-68
lines changed

runtime/libia2/ia2.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ int protect_tls_pages(struct dl_phdr_info *info, size_t size, void *data) {
334334
exit(-1);
335335
}
336336
#if IA2_DEBUG_MEMORY
337+
// Atomic write.
337338
thread_metadata->tls_addr_compartment1_first = (uintptr_t)start_round_down;
338339
#endif
339340
}
@@ -348,6 +349,7 @@ int protect_tls_pages(struct dl_phdr_info *info, size_t size, void *data) {
348349
exit(-1);
349350
}
350351
#if IA2_DEBUG_MEMORY
352+
// Atomic write.
351353
thread_metadata->tls_addr_compartment1_second = (uintptr_t)after_untrusted_region_start;
352354
#endif
353355
}
@@ -360,6 +362,7 @@ int protect_tls_pages(struct dl_phdr_info *info, size_t size, void *data) {
360362
exit(-1);
361363
}
362364
#if IA2_DEBUG_MEMORY
365+
// Atomic write.
363366
thread_metadata->tls_addrs[pkey] = (uintptr_t)start_round_down;
364367
#endif
365368
}

runtime/libia2/include/ia2.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@
199199

200200
/// The data here is shared, so it should not be trusted for use as a pointer,
201201
/// but it can be used best effort for non-trusted purposes.
202+
///
203+
/// All fields should be used atomically.
202204
struct ia2_thread_metadata {
203205
pid_t tid;
204206
pthread_t thread;
@@ -231,9 +233,16 @@ struct ia2_thread_metadata {
231233
#define IA2_MAX_THREADS 512
232234

233235
struct ia2_all_threads_metadata {
234-
pthread_mutex_t lock;
235-
size_t num_threads;
236+
/// This is the number of threads registered,
237+
/// and it is monotonically increasing by 1.
238+
///
239+
/// It may be transiently higher than `IA2_MAX_THREADS`,
240+
/// but will `abort` if that happens (other threads may observe a higher value).
241+
_Atomic size_t num_threads;
242+
236243
pid_t tids[IA2_MAX_THREADS];
244+
245+
/// Should be initialized to 0.
237246
struct ia2_thread_metadata thread_metadata[IA2_MAX_THREADS];
238247
};
239248

runtime/libia2/include/ia2_internal.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,6 @@ __attribute__((__noreturn__)) void ia2_reinit_stack_err(int i);
448448
/* All zeroed, so this should go in `.bss` */ \
449449
/* and only have pages lazily allocated. */ \
450450
struct ia2_all_threads_metadata ia2_threads_metadata IA2_SHARED_DATA = { \
451-
.lock = PTHREAD_MUTEX_INITIALIZER, \
452451
.num_threads = 0, \
453452
.thread_metadata = {0}, \
454453
};

runtime/libia2/init.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ char *allocate_stack(int i) {
7979
ia2_log("allocating stack for compartment %d on thread %ld: %p..%p\n", i, (long)gettid(), stack, stack + STACK_SIZE);
8080
#if IA2_DEBUG_MEMORY
8181
struct ia2_thread_metadata *const thread_metadata = ia2_thread_metadata_get_for_current_thread();
82+
// Atomic write.
8283
thread_metadata->stack_addrs[i] = (uintptr_t)stack;
8384
#endif
8485
assert(stacks[i] == NULL); // We should only be setting this once per thread compartment right after thread creation.
@@ -286,6 +287,9 @@ void ia2_start(void) {
286287
/* Set up global resources. */
287288
ia2_set_up_tags();
288289
create_thread_keys();
290+
#if IA2_DEBUG_MEMORY
291+
ia2_thread_metadata_new_for_current_thread();
292+
#endif
289293
verify_tls_padding();
290294
/* allocate an unprotected stack for the untrusted compartment */
291295
allocate_stack_0();

runtime/libia2/memory_maps.c

Lines changed: 58 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#include "ia2.h"
33
#include "thread_name.h"
44

5+
#include <stdatomic.h>
6+
57
// Only enable this code that stores these addresses when debug logging is enabled.
68
// This reduces the trusted codebase and avoids runtime overhead.
79
#if IA2_DEBUG_MEMORY
@@ -10,97 +12,97 @@
1012
// so that it can be used in `ia2_internal.h` within `_IA2_INIT_RUNTIME`
1113
// to only initialize the `ia2_threads_metadata` global once.
1214

13-
#define array_len(a) (sizeof(a) / sizeof(*(a)))
15+
#define min(a, b) ((a) < (b) ? (a) : (b))
1416

15-
struct ia2_thread_metadata *ia2_all_threads_metadata_get_for_current_thread(struct ia2_all_threads_metadata *const this) {
16-
const pid_t tid = gettid();
17-
18-
if (pthread_mutex_lock(&this->lock) != 0) {
19-
perror("pthread_mutex_lock in ia2_all_threads_data_lookup failed");
17+
struct ia2_thread_metadata *ia2_all_threads_metadata_new_for_current_thread(struct ia2_all_threads_metadata *const this) {
18+
const size_t thread = atomic_fetch_add(&this->num_threads, 1);
19+
if (thread >= IA2_MAX_THREADS) {
20+
fprintf(stderr, "created %zu threads, but can't store them all (max is IA2_MAX_THREADS: %zu)\n",
21+
thread + 1, (size_t)IA2_MAX_THREADS);
2022
abort();
2123
}
2224

23-
if (this->num_threads >= array_len(this->thread_metadata)) {
24-
fprintf(stderr, "created %zu threads, but can't store them all (max is IA2_MAX_THREADS)\n", this->num_threads);
25-
abort();
26-
}
27-
28-
struct ia2_thread_metadata *metadata = NULL;
29-
for (size_t i = 0; i < this->num_threads; i++) {
30-
if (this->tids[i] == tid) {
31-
metadata = &this->thread_metadata[i];
32-
goto unlock;
33-
}
34-
}
35-
36-
metadata = &this->thread_metadata[this->num_threads];
37-
this->tids[this->num_threads] = tid;
38-
this->num_threads++;
25+
const pid_t tid = gettid();
26+
this->tids[thread] = tid;
3927

28+
struct ia2_thread_metadata *metadata = &this->thread_metadata[thread];
4029
metadata->tid = tid;
4130
metadata->thread = pthread_self();
31+
return metadata;
32+
}
4233

43-
unlock:
44-
if (pthread_mutex_unlock(&this->lock) != 0) {
45-
perror("pthread_mutex_unlock in ia2_all_threads_data_lookup failed");
46-
abort();
34+
struct ia2_thread_metadata *ia2_all_threads_metadata_get_for_current_thread(struct ia2_all_threads_metadata *const this) {
35+
const pid_t tid = gettid();
36+
37+
// We won't see threads created/registered after this,
38+
// but `ia2_all_threads_metadata_new_for_current_thread`
39+
// was supposed to be called first for this function to find it.
40+
const size_t num_threads = min(IA2_MAX_THREADS, atomic_load(&this->num_threads));
41+
42+
for (size_t thread = 0; thread < num_threads; thread++) {
43+
if (this->tids[thread] == tid) {
44+
return &this->thread_metadata[thread];
45+
}
4746
}
4847

49-
return metadata;
48+
fprintf(stderr,
49+
"ia2_thread_metadata not found for thread %ld\n"
50+
"ia2_thread_metadata_new_for_current_thread must not have been previously called on this thread\n",
51+
(long)tid);
52+
abort();
5053
}
5154

5255
struct ia2_addr_location ia2_all_threads_metadata_find_addr(struct ia2_all_threads_metadata *const this, const uintptr_t addr) {
53-
struct ia2_addr_location location = {
54-
.name = NULL,
55-
.thread_metadata = NULL,
56-
.compartment = -1,
57-
};
58-
if (pthread_mutex_lock(&this->lock) != 0) {
59-
perror("pthread_mutex_lock in ia2_all_threads_data_find_addr failed");
60-
goto ret;
61-
}
56+
// We won't see threads created/registered after this,
57+
// but this is supposed to be best effort, so that's okay.
58+
const size_t num_threads = min(IA2_MAX_THREADS, atomic_load(&this->num_threads));
59+
6260
for (size_t thread = 0; thread < this->num_threads; thread++) {
6361
const pid_t tid = this->tids[thread];
6462
const struct ia2_thread_metadata *const thread_metadata = &this->thread_metadata[thread];
6563

6664
if (addr == thread_metadata->tls_addr_compartment1_first || addr == thread_metadata->tls_addr_compartment1_second) {
67-
location.name = "tls";
68-
location.thread_metadata = thread_metadata;
69-
location.compartment = 1;
70-
goto unlock;
65+
return (struct ia2_addr_location){
66+
.name = "tls",
67+
.thread_metadata = thread_metadata,
68+
.compartment = 1,
69+
};
7170
}
7271

7372
for (int compartment = 0; compartment < IA2_MAX_COMPARTMENTS; compartment++) {
7473
if (addr == thread_metadata->stack_addrs[compartment]) {
75-
location.name = "stack";
76-
location.thread_metadata = thread_metadata;
77-
location.compartment = compartment;
78-
goto unlock;
74+
return (struct ia2_addr_location){
75+
.name = "stack",
76+
.thread_metadata = thread_metadata,
77+
.compartment = compartment,
78+
};
7979
}
8080
if (addr == thread_metadata->tls_addrs[compartment]) {
81-
location.name = "tls";
82-
location.thread_metadata = thread_metadata;
83-
location.compartment = compartment;
84-
goto unlock;
81+
return (struct ia2_addr_location){
82+
.name = "tls",
83+
.thread_metadata = thread_metadata,
84+
.compartment = compartment,
85+
};
8586
}
8687
}
8788
}
8889

89-
goto unlock;
90-
91-
unlock:
92-
if (pthread_mutex_unlock(&this->lock) != 0) {
93-
perror("pthread_mutex_unlock in ia2_all_threads_data_find_addr failed");
94-
}
95-
ret:
96-
return location;
90+
return (struct ia2_addr_location){
91+
.name = NULL,
92+
.thread_metadata = NULL,
93+
.compartment = -1,
94+
};
9795
}
9896

9997
// Moved `ia2_threads_metadata` from here to `ia2_internal.h`
10098
// so that it can be used in `_IA2_INIT_RUNTIME`
10199
// to only initialize the `ia2_threads_metadata` global once.
102100
extern struct ia2_all_threads_metadata ia2_threads_metadata;
103101

102+
struct ia2_thread_metadata *ia2_thread_metadata_new_for_current_thread(void) {
103+
return ia2_all_threads_metadata_new_for_current_thread(&ia2_threads_metadata);
104+
}
105+
104106
struct ia2_thread_metadata *ia2_thread_metadata_get_for_current_thread(void) {
105107
return ia2_all_threads_metadata_get_for_current_thread(&ia2_threads_metadata);
106108
}

runtime/libia2/memory_maps.h

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,26 @@
1313
// so that it can be used in `ia2_internal.h` within `_IA2_INIT_RUNTIME`
1414
// to only initialize the `ia2_threads_metadata` global once.
1515

16-
/// Find the `struct ia2_thread_metadata*` for the current thread,
17-
/// adding (but not allocating) one if there isn't one yet.
18-
/// If there is no memory for more or some unexpected error,
19-
/// it `abort`s. `NULL` is never returned.
20-
/// This is a purely lookup and/or additive operation,
21-
/// so the lifetime of the returned `struct ia2_thread_metadata*` is infinite,
22-
/// and since it's thread-specific,
23-
/// it is thread-safe to read and write.
16+
/// Allocate and initialize a new `ia2_thread_metadata` for the current thread.
17+
/// Importantly, this may only be called once per thread.
18+
///
19+
/// The returned pointer is stable, non-`NULL`,
20+
/// and will not be moved or deallocated/uninitialized.
21+
/// Operations on the `ia2_thread_metadata` must be atomic.
22+
///
23+
/// If too many threads are created, this will `abort`
24+
/// and `IA2_MAX_THREADS` can be increased.
25+
struct ia2_thread_metadata *ia2_thread_metadata_new_for_current_thread(void);
26+
27+
/// Find the `ia2_thread_metadata` for the current thread.
28+
///
29+
/// `ia2_thread_metadata_new_for_current_thread`
30+
/// must have been previously called for this thread,
31+
/// or else the `ia2_thread_metadata` will not be found and this will `abort`.
32+
///
33+
/// The returned pointer is stable, non-`NULL`,
34+
/// and will not be moved or deallocated/uninitialized.
35+
/// Operations on the `ia2_thread_metadata` must be atomic.
2436
struct ia2_thread_metadata *ia2_thread_metadata_get_for_current_thread(void);
2537

2638
struct ia2_addr_location {
@@ -31,6 +43,8 @@ struct ia2_addr_location {
3143
const char *name;
3244

3345
/// The metadata of the thread this address belongs to.
46+
///
47+
/// Fields must be read atomically.
3448
const struct ia2_thread_metadata *thread_metadata;
3549

3650
/// The compartment this address is in.

runtime/libia2/threads.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ void *ia2_thread_begin(void *arg) {
3131
munmap(arg, sizeof(struct ia2_thread_thunk));
3232

3333
#if IA2_DEBUG_MEMORY
34-
struct ia2_thread_metadata *const thread_metadata = ia2_thread_metadata_get_for_current_thread();
34+
struct ia2_thread_metadata *const thread_metadata = ia2_thread_metadata_new_for_current_thread();
35+
// Atomic write.
3536
thread_metadata->start_fn = fn;
3637
#endif
3738

0 commit comments

Comments
 (0)