Skip to content

Commit 3469ae1

Browse files
committed
Fix exit handler segfault with union PKRU for destructor wrappers
Library destructors need to call libc functions (e.g., __cxa_finalize) which access libc's .bss section protected by pkey 1. Previously, destructor wrappers switched exclusively to the target compartment (e.g., pkey 2), causing SEGV_PKUERR when the destructor called back into libc. Solution: Use union PKRU value (0xffffffc0 for pkeys 1+2) that allows simultaneous access to both the exit compartment (libc, pkey 1) and target compartment (pkey 2) during destructor execution. This prevents protection key violations when destructors call libc functions via PLT without requiring wrappers for every libc call. Changes: - tools/rewriter/GenCallAsm.cpp: Add optional union_pkey parameter to emit_set_pkru() - tools/rewriter/GenCallAsm.h: Add use_union_pkru parameter to emit_asm_wrapper() - tools/rewriter/SourceRewriter.cpp: Pass use_union_pkru=true for destructor wrappers - runtime/libia2/init.c: Re-enable ia2_setup_destructors() call - runtime/libia2/CMakeLists.txt: Add trace_exit.c to build - runtime/libia2/include/ia2_internal.h: Add ia2_trace_exit_record() declaration
1 parent 8bbf186 commit 3469ae1

File tree

13 files changed

+421
-419
lines changed

13 files changed

+421
-419
lines changed

runtime/libia2/CMakeLists.txt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
cmake_minimum_required(VERSION 4.0)
22
project(libia2)
33

4-
add_library(libia2 ia2.c init.c threads.c main.c exit.c memory_maps.c thread_name.c)
4+
add_library(libia2
5+
ia2.c
6+
init.c
7+
destructor_runtime.c
8+
threads.c
9+
main.c
10+
exit.c
11+
memory_maps.c
12+
thread_name.c
13+
trace_exit.c)
514
target_compile_options(libia2 PRIVATE "-fPIC")
615

716
if (LIBIA2_AARCH64)
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#ifndef _GNU_SOURCE
2+
#define _GNU_SOURCE
3+
#endif
4+
5+
#include "ia2_destructor_runtime.h"
6+
7+
#include "ia2.h"
8+
#include "ia2_internal.h"
9+
10+
#include <dlfcn.h>
11+
#include <stdbool.h>
12+
#include <stddef.h>
13+
#include <stdint.h>
14+
#include <stdio.h>
15+
#include <stdlib.h>
16+
17+
struct ia2_destructor_runtime_entry {
18+
void (*wrapper)(void);
19+
const struct ia2_destructor_metadata_record *record;
20+
uint32_t pkru_value;
21+
};
22+
23+
static struct ia2_destructor_runtime_entry *destructor_entries;
24+
static size_t destructor_entry_count;
25+
static bool initialized;
26+
27+
static uint32_t compute_union_pkru(int exit_pkey, int target_pkey) {
28+
uint32_t mask = ~0u;
29+
mask &= ~0b11u; // Always allow shared key 0.
30+
mask &= ~(0b11u << (2 * exit_pkey));
31+
mask &= ~(0b11u << (2 * target_pkey));
32+
return mask;
33+
}
34+
35+
static void ensure_initialized(void) {
36+
if (initialized) {
37+
return;
38+
}
39+
initialized = true;
40+
41+
if (ia2_destructor_metadata_count == 0) {
42+
return;
43+
}
44+
45+
destructor_entries =
46+
calloc(ia2_destructor_metadata_count, sizeof(struct ia2_destructor_runtime_entry));
47+
if (!destructor_entries) {
48+
fprintf(stderr, "ia2: failed to allocate destructor metadata cache\n");
49+
return;
50+
}
51+
52+
for (unsigned int i = 0; i < ia2_destructor_metadata_count; ++i) {
53+
const struct ia2_destructor_metadata_record *record = &ia2_destructor_metadata[i];
54+
55+
dlerror();
56+
void *symbol = dlsym(RTLD_DEFAULT, record->wrapper);
57+
const char *dlerr = dlerror();
58+
if (dlerr != NULL || symbol == NULL) {
59+
fprintf(stderr, "ia2: failed to resolve destructor wrapper '%s': %s\n",
60+
record->wrapper, dlerr ? dlerr : "symbol not found");
61+
continue;
62+
}
63+
64+
struct ia2_destructor_runtime_entry *entry = &destructor_entries[destructor_entry_count++];
65+
entry->wrapper = (void (*)(void))symbol;
66+
entry->record = record;
67+
if (record->uses_union_pkru) {
68+
entry->pkru_value = compute_union_pkru(ia2_destructor_exit_pkey, record->compartment_pkey);
69+
} else {
70+
entry->pkru_value = PKRU(record->compartment_pkey);
71+
}
72+
}
73+
}
74+
75+
void ia2_destructor_runtime_init(void) {
76+
ensure_initialized();
77+
}
78+
79+
bool ia2_destructor_metadata_lookup(void (*wrapper)(void),
80+
const struct ia2_destructor_metadata_record **out_record,
81+
uint32_t *out_pkru_value) {
82+
ensure_initialized();
83+
84+
if (!wrapper || destructor_entry_count == 0) {
85+
return false;
86+
}
87+
88+
for (size_t i = 0; i < destructor_entry_count; ++i) {
89+
if (destructor_entries[i].wrapper == wrapper) {
90+
if (out_record) {
91+
*out_record = destructor_entries[i].record;
92+
}
93+
if (out_pkru_value) {
94+
*out_pkru_value = destructor_entries[i].pkru_value;
95+
}
96+
return true;
97+
}
98+
}
99+
return false;
100+
}
101+
102+
uint32_t ia2_destructor_pkru_for(void (*wrapper)(void), int target_compartment_pkey) {
103+
uint32_t pkru_value;
104+
if (ia2_destructor_metadata_lookup(wrapper, NULL, &pkru_value)) {
105+
return pkru_value;
106+
}
107+
return PKRU(target_compartment_pkey);
108+
}
109+
110+
// Helper to read/write PKRU, guarded for non-x86 architectures.
111+
#if defined(__x86_64__)
112+
static inline uint32_t read_pkru(void) {
113+
uint32_t pkru;
114+
__asm__ volatile("xor %%ecx, %%ecx\n"
115+
"rdpkru\n"
116+
: "=a"(pkru)
117+
:
118+
: "ecx", "edx");
119+
return pkru;
120+
}
121+
122+
static inline void write_pkru(uint32_t pkru) {
123+
__asm__ volatile("xor %%ecx, %%ecx\n"
124+
"xor %%edx, %%edx\n"
125+
"wrpkru\n"
126+
:
127+
: "a"(pkru)
128+
: "ecx", "edx");
129+
}
130+
#endif
131+
132+
uint32_t ia2_destructor_enter(void *wrapper_addr, int target_pkey) {
133+
#if defined(__x86_64__)
134+
uint32_t original_pkru = read_pkru();
135+
uint32_t desired_pkru = ia2_destructor_pkru_for((void (*)(void))wrapper_addr, target_pkey);
136+
if (desired_pkru != original_pkru) {
137+
write_pkru(desired_pkru);
138+
#ifdef IA2_TRACE_EXIT
139+
ia2_trace_exit_record((int)ia2_get_compartment(), target_pkey, desired_pkru);
140+
#endif
141+
}
142+
return original_pkru;
143+
#else
144+
(void)wrapper_addr;
145+
(void)target_pkey;
146+
return 0;
147+
#endif
148+
}
149+
150+
void ia2_destructor_leave(uint32_t original_pkru) {
151+
#if defined(__x86_64__)
152+
write_pkru(original_pkru);
153+
#else
154+
(void)original_pkru;
155+
#endif
156+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#pragma once
2+
3+
#include <stdbool.h>
4+
#include <stdint.h>
5+
6+
struct ia2_destructor_metadata_record {
7+
const char *wrapper;
8+
const char *target;
9+
int compartment_pkey;
10+
int uses_union_pkru;
11+
};
12+
13+
extern const struct ia2_destructor_metadata_record ia2_destructor_metadata[];
14+
extern const unsigned int ia2_destructor_metadata_count;
15+
extern const int ia2_destructor_exit_pkey;
16+
17+
/// Initialize runtime bookkeeping for destructor metadata.
18+
///
19+
/// Must be called after all compartments have been set up so the rewriter
20+
/// generated symbols are visible via dlsym(). Safe to call multiple times; the
21+
/// function short-circuits if re-invoked.
22+
void ia2_destructor_runtime_init(void);
23+
24+
/// Compute the PKRU mask that should be applied while running a destructor
25+
/// wrapper.
26+
///
27+
/// `wrapper` must be the exact function pointer registered with `__cxa_atexit`.
28+
/// When metadata is present the returned value reflects whether the
29+
/// destructor requires the union (exit compartment + target compartment)
30+
/// permissions; otherwise the caller gets the default single-compartment mask
31+
/// derived from `target_compartment`.
32+
uint32_t ia2_destructor_pkru_for(void (*wrapper)(void), int target_compartment_pkey);
33+
34+
/// Lookup helper primarily intended for diagnostics and tracing.
35+
///
36+
/// Returns true when metadata was found for `wrapper` and fills the out
37+
/// parameters. Callers may pass NULL for any output they do not care about.
38+
bool ia2_destructor_metadata_lookup(void (*wrapper)(void),
39+
const struct ia2_destructor_metadata_record **out_record,
40+
uint32_t *out_pkru_value);
41+
42+
/// Destructor wrapper enter hook - called before switching compartments.
43+
///
44+
/// Reads the current PKRU, computes the desired value for the wrapper based on
45+
/// metadata, applies it, and returns the original PKRU so the caller can
46+
/// restore it afterwards. `wrapper_addr` must point at the wrapper function
47+
/// itself; `target_pkey` is used as a fallback if metadata is unavailable.
48+
uint32_t ia2_destructor_enter(void *wrapper_addr, int target_pkey);
49+
50+
/// Destructor wrapper leave hook - called before returning from wrapper.
51+
///
52+
/// Restores the PKRU value captured by ia2_destructor_enter.
53+
void ia2_destructor_leave(uint32_t original_pkru);

runtime/libia2/include/ia2_internal.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ struct dl_phdr_info;
1818
#include <link.h>
1919
#include <locale.h>
2020
#include <stdbool.h>
21+
#include <stdint.h>
2122
#include <stdio.h>
2223
#include <stdlib.h>
2324
#include <string.h>
@@ -448,3 +449,4 @@ __attribute__((__noreturn__)) void ia2_reinit_stack_err(int i);
448449
void **ia2_stackptr_for_tag(size_t tag);
449450
void **ia2_stackptr_for_compartment(int compartment);
450451
void ia2_setup_destructors(void);
452+
void ia2_trace_exit_record(int caller_pkey, int target_pkey, uint32_t pkru_value);

runtime/libia2/init.c

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#endif
44
#include "ia2.h"
55
#include "ia2_internal.h"
6+
#include "ia2_destructor_runtime.h"
67
#include "memory_maps.h"
78
#include "thread_name.h"
89
#include <dlfcn.h>
@@ -287,10 +288,7 @@ void ia2_start(void) {
287288
ia2_log("initializing ia2 runtime\n");
288289
/* Get the user config before doing anything else */
289290
ia2_main();
290-
// DISABLED FOR TESTING: Compartment destructors cause exit handler violations
291-
// when libc is protected in compartment 1. The destructor wrappers switch
292-
// to compartment 0, causing SEGV_PKUERR when accessing libc's .bss
293-
// ia2_setup_destructors();
291+
ia2_setup_destructors();
294292
/* Set up global resources. */
295293
ia2_set_up_tags();
296294
create_thread_keys();
@@ -311,5 +309,6 @@ void ia2_start(void) {
311309
exit(rc);
312310
}
313311
}
312+
ia2_destructor_runtime_init();
314313
mark_init_finished();
315314
}

runtime/libia2/main.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ __asm__(
4848
// NOTE: Removed switch to compartment 0 to allow exit handlers to run
4949
// in compartment 1 (where libc lives). This prevents SEGV_PKUERR when
5050
// exit() tries to acquire __exit_funcs_lock in libc's .bss section.
51-
// See: tests/dl_debug_test/*_ANALYSIS.md for details
5251
// "xor %ecx,%ecx\n"
5352
// "xor %edx,%edx\n"
5453
// "mov_pkru_eax 0\n"

runtime/libia2/trace_exit.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#include "ia2.h"
2+
3+
#include <stdint.h>
4+
#include <stdio.h>
5+
6+
void ia2_trace_exit_record(int caller_pkey, int target_pkey, uint32_t pkru_value) {
7+
#if IA2_TRACE_EXIT
8+
fprintf(stderr,
9+
"[TRACE_EXIT] caller_pkru=0x%08x caller_pkey=%d target_pkey=%d\n",
10+
pkru_value, caller_pkey, target_pkey);
11+
#else
12+
(void)caller_pkey;
13+
(void)target_pkey;
14+
(void)pkru_value;
15+
#endif
16+
}

0 commit comments

Comments
 (0)