Skip to content

Commit a153923

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 33f5cc0 commit a153923

File tree

6 files changed

+100
-68
lines changed

6 files changed

+100
-68
lines changed

runtime/libia2/ia2.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ int protect_tls_pages(struct dl_phdr_info *info, size_t size, void *data) {
381381
exit(-1);
382382
}
383383
#if IA2_DEBUG_MEMORY
384+
// Atomic write.
384385
thread_metadata->tls_addr_compartment1_first = (uintptr_t)start_round_down;
385386
#endif
386387
}
@@ -396,6 +397,7 @@ int protect_tls_pages(struct dl_phdr_info *info, size_t size, void *data) {
396397
exit(-1);
397398
}
398399
#if IA2_DEBUG_MEMORY
400+
// Atomic write.
399401
thread_metadata->tls_addr_compartment1_second = (uintptr_t)after_untrusted_region_start;
400402
#endif
401403
}
@@ -409,6 +411,7 @@ int protect_tls_pages(struct dl_phdr_info *info, size_t size, void *data) {
409411
exit(-1);
410412
}
411413
#if IA2_DEBUG_MEMORY
414+
// Atomic write.
412415
thread_metadata->tls_addrs[pkey] = (uintptr_t)start_round_down;
413416
#endif
414417
}

runtime/libia2/include/ia2_internal.h

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ struct dl_phdr_info;
4949

5050
/// The data here is shared, so it should not be trusted for use as a pointer,
5151
/// but it can be used best effort for non-trusted purposes.
52+
///
53+
/// All fields should be used atomically.
5254
struct ia2_thread_metadata {
5355
pid_t tid;
5456
pthread_t thread;
@@ -81,9 +83,16 @@ struct ia2_thread_metadata {
8183
#define IA2_MAX_THREADS 512
8284

8385
struct ia2_all_threads_metadata {
84-
pthread_mutex_t lock;
85-
size_t num_threads;
86+
/// This is the number of threads registered,
87+
/// and it is monotonically increasing by 1.
88+
///
89+
/// It may be transiently higher than `IA2_MAX_THREADS`,
90+
/// but will `abort` if that happens (other threads may observe a higher value).
91+
_Atomic size_t num_threads;
92+
8693
pid_t tids[IA2_MAX_THREADS];
94+
95+
/// Should be initialized to 0.
8796
struct ia2_thread_metadata thread_metadata[IA2_MAX_THREADS];
8897
};
8998

@@ -493,7 +502,6 @@ __attribute__((__noreturn__)) void ia2_reinit_stack_err(int i);
493502
/* All zeroed, so this should go in `.bss` */ \
494503
/* and only have pages lazily allocated. */ \
495504
struct ia2_all_threads_metadata ia2_threads_metadata IA2_SHARED_DATA = { \
496-
.lock = PTHREAD_MUTEX_INITIALIZER, \
497505
.num_threads = 0, \
498506
.thread_metadata = {0}, \
499507
};

runtime/libia2/init.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ char *allocate_stack(int i) {
8484
ia2_log("allocating stack for compartment %d on thread %ld: %p..%p\n", i, (long)gettid(), stack, stack + STACK_SIZE);
8585
#if IA2_DEBUG_MEMORY
8686
struct ia2_thread_metadata *const thread_metadata = ia2_thread_metadata_get_for_current_thread();
87+
// Atomic write.
8788
thread_metadata->stack_addrs[i] = (uintptr_t)stack;
8889
#endif
8990
assert(stacks[i] == NULL); // We should only be setting this once per thread compartment right after thread creation.
@@ -291,6 +292,9 @@ void ia2_start(void) {
291292
/* Set up global resources. */
292293
ia2_set_up_tags();
293294
create_thread_keys();
295+
#if IA2_DEBUG_MEMORY
296+
ia2_thread_metadata_new_for_current_thread();
297+
#endif
294298
verify_tls_padding();
295299
/* allocate an unprotected stack for the untrusted compartment */
296300
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
@@ -11,14 +11,26 @@
1111
// so that it can be used in `ia2_internal.h` within `_IA2_INIT_RUNTIME`
1212
// to only initialize the `ia2_threads_metadata` global once.
1313

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

2436
struct ia2_addr_location {
@@ -29,6 +41,8 @@ struct ia2_addr_location {
2941
const char *name;
3042

3143
/// The metadata of the thread this address belongs to.
44+
///
45+
/// Fields must be read atomically.
3246
const struct ia2_thread_metadata *thread_metadata;
3347

3448
/// 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)