libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
coro_preempt.h
1/*-
2 * Copyright (c) 2026, The XTC Project
3 * Use of this source code is governed by the ISC License.
4 *
5 * src/inc/coro_preempt.h
6 * The involuntary-preemption pieces both mmap-stack coroutine
7 * substrates share (coro_uctx.c, coro_fctx.c): the per-thread
8 * in-switch guard, the x86-64 vector-state probe, and the x86-64 and
9 * AArch64 Linux signal-context PC redirects into the substrate-
10 * agnostic trampolines (src/os/asm/preempt_trampoline_*.S, which only
11 * preserve the interrupted state, call xtc_yield and resume it).
12 * Moved here verbatim from coro_uctx.c. Exactly one substrate is
13 * compiled per build, so the `static` definitions are instantiated
14 * once. Include after coro_int.h.
15 */
16
17#ifndef XTC_CORO_PREEMPT_H
18#define XTC_CORO_PREEMPT_H
19
20#include <stdatomic.h>
21#include <stdint.h>
22
23/*
24 * XTC_CORO_REDIRECT_X86_64: this build implements the x86-64 Linux
25 * signal-context redirect (Phase 2b-arch). ucontext: as before, every
26 * x86-64 Linux build outside the amalgamation. fcontext: glibc only --
27 * the code is libc-independent (the kernel's signal ucontext, the
28 * trampoline, xtc_yield), but it has been verified on glibc alone; musl
29 * keeps declining (cooperative-assisted preemption) until it is.
30 */
31#if defined(__x86_64__) && defined(__linux__) && !defined(__APPLE__) && \
32 !defined(XTC_AMALGAMATION) && \
33 (!defined(XTC_CORO_FCTX) || defined(__GLIBC__))
34# define XTC_CORO_REDIRECT_X86_64 1
35#endif
36/*
37 * XTC_CORO_REDIRECT_AARCH64: the AArch64 Linux redirect (rt_sigreturn
38 * on the kernel's in-place signal frame; preempt_trampoline_aarch64.S).
39 * Any libc: it reads only the kernel's frame (offsets asserted below
40 * against the libc's <ucontext.h>) and the trampoline makes raw
41 * syscalls. Tested on glibc (both substrates) and on musl (fcontext,
42 * its only substrate there): test/m14/test_preempt*.
43 */
44#if defined(__aarch64__) && defined(__linux__) && !defined(__APPLE__) && \
45 !defined(XTC_AMALGAMATION)
46# define XTC_CORO_REDIRECT_AARCH64 1
47#endif
48#if defined(XTC_CORO_REDIRECT_X86_64) || defined(XTC_CORO_REDIRECT_AARCH64)
49#include <signal.h>
50#include <ucontext.h> /* the signal-delivered ucontext_t (mcontext) */
51/* 1 if an involuntary yield may land at pc (preempt.c, SAFE POINTS). */
52extern int __xtc_preempt_pc_ok(uintptr_t pc);
53#endif
54
55/*
56 * Per-thread guard for the involuntary-preemption redirect (Phase 2b).
57 * It marks the brief windows during which the running context's
58 * machine state is being saved/restored by a ucontext swapcontext (or
59 * the trampoline is mid-save) -- exactly the windows in which an
60 * involuntary redirect would corrupt a half-written context (verified:
61 * a rare SIGSEGV in the scheduler's swapcontext resume under
62 * aggressive slicing).
63 *
64 * It is a FLAG set immediately before each swapcontext and cleared
65 * immediately after it returns -- on whichever side runs next, since a
66 * swapcontext hands control to a context that itself returns just
67 * after its own swapcontext. So the flag is 1 only while a
68 * swapcontext instruction is actually executing, NOT for the whole
69 * time a fiber is parked (that earlier, depth-held-across-park design
70 * wrongly blocked ALL preemption whenever any fiber sat parked).
71 * __xtc_coro_preempt declines while the flag is set, deferring to the
72 * cooperative pending flag. volatile: the timer handler reads it on
73 * the same thread that maintains it; a stale read only errs toward
74 * "decline", which is safe.
75 */
76static XTC_THREAD_LOCAL volatile int g_in_preempt_tls;
77/*
78 * Every access goes through a non-inlinable accessor so it addresses the
79 * CURRENT thread's slot. A migratable fiber can resume from swapcontext
80 * on another OS thread, and clang keeps a thread-local's address
81 * (%fs:0 + off or a __tls_get_addr result) live across the call: the
82 * post-swap `g_in_preempt = 0` then cleared the OLD thread's guard.
83 * Same hazard and fix as __current_proc in src/ptc/proc.c.
84 */
85__attribute__((noinline))
86static volatile int *
87__in_preempt_slot(void)
88{
89 volatile int *volatile slot = &g_in_preempt_tls;
90 return slot;
91}
92#define g_in_preempt (*__in_preempt_slot())
93
94#if defined(XTC_CORO_REDIRECT_X86_64)
95#include <cpuid.h>
96/*
97 * The vector state the x86-64 trampoline saves (preempt_trampoline_x86_64.S
98 * reads both words): an XSAVE feature mask and the XSAVE area size, or a
99 * zero mask for its FXSAVE path. FXSAVE alone covers only x87 and
100 * XMM0-15's low halves, so a fiber preempted inside AVX code (glibc's
101 * memcpy on an AVX-512 host) resumed with another fiber's YMM upper
102 * halves, ZMM16-31 and opmask registers (test/m14/test_preempt_p2
103 * /vector_state: 100 of 69k checks corrupted per 0.4 s run before).
104 *
105 * The mask is XCR0 limited to x87, SSE, AVX, opmask, the two ZMM
106 * components and PKRU: the state a fiber's own code can hold live across
107 * an arbitrary instruction. PKRU (the protection-key rights, xtc_pkey)
108 * is per thread, so a fiber that ran in the slice could leave the
109 * preempted code a different set of rights between its own wrpkru and
110 * the access it guarded (test_preempt_p2 /tick_pkru: 64 of 64 ticks
111 * resumed with the peer's PKRU before). AMX tile data (8 KiB) is left out on purpose
112 * -- it would put ~11 KiB on a 64 KiB fiber stack, and Linux gates AMX
113 * behind an explicit per-process permission request. Probed once (CPUID
114 * and XGETBV are plain instructions, safe in the signal handler that
115 * calls this); the size is published before the mask so a trampoline
116 * that sees the mask also sees the size (x86 store order).
117 */
118extern uint64_t __xtc_preempt_xsave_mask __attribute__((visibility("hidden")));
119extern uint64_t __xtc_preempt_xsave_size __attribute__((visibility("hidden")));
120static _Atomic int g_xsave_probed;
121
122/*
123 * The interrupted PC, handed from the signal handler to the trampoline
124 * on the same thread (sigreturn resumes the thread that took the
125 * signal, and the trampoline reads it, via __xtc_preempt_take_pc,
126 * before its first yield). It used to be stored on the fiber stack at
127 * orig_sp - 8 -- inside the interrupted code's red zone, which the ABI
128 * promises a signal handler never touches. A nested tick cannot
129 * overwrite it: g_in_preempt stays raised until the trampoline's
130 * xtc_yield, and the handler declines while it is. The slot and its
131 * reader live in preempt.c (see there for why).
132 */
133extern XTC_THREAD_LOCAL uintptr_t __xtc_preempt_pc
134 __attribute__((visibility("hidden")));
135
136#define XTC_XSAVE_WANT 0x2E7ull /* x87 SSE AVX opmask ZMM_Hi256 Hi16_ZMM PKRU */
137
138static void
139__preempt_xsave_probe(void)
140{
141 unsigned a, b, c, d, i, lo, hi;
142 uint64_t mask, size = 576; /* legacy area + XSAVE header */
143
144 if (atomic_load_explicit(&g_xsave_probed, memory_order_acquire))
145 return;
146 mask = 0;
147 if (__get_cpuid(1, &a, &b, &c, &d) && (c & bit_OSXSAVE) != 0 &&
148 __get_cpuid_max(0, NULL) >= 0xD) {
149 __asm__ volatile("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0));
150 mask = (((uint64_t)hi << 32) | lo) & XTC_XSAVE_WANT;
151 for (i = 2; i < 10; i++) {
152 if ((mask & (1ull << i)) == 0)
153 continue;
154 __cpuid_count(0xD, i, a, b, c, d); /* a size, b offset */
155 if ((uint64_t)a + b > size)
156 size = (uint64_t)a + b;
157 }
158 size = (size + 63) & ~63ull;
159 }
160 __xtc_preempt_xsave_size = size;
161 atomic_signal_fence(memory_order_release);
162 __xtc_preempt_xsave_mask = mask;
163 atomic_store_explicit(&g_xsave_probed, 1, memory_order_release);
164}
165#endif
166
167#if defined(XTC_CORO_REDIRECT_X86_64)
168/*
169 * The x86-64 System V redirect, called from __xtc_coro_preempt with the
170 * interrupted (signal-delivered) ucontext. Returns 1 if it armed the
171 * redirect, 0 if it declined.
172 */
173static int
174__coro_preempt_redirect_x86_64(void *uctx)
175{
176 /*
177 * Phase 2b-arch, x86-64 System V: Go's async-preemption PC
178 * redirect. We do NOT copy the (unsound-to-reuse) signal
179 * ucontext; we rewrite it so that on sigreturn the fiber resumes
180 * -- on its own stack, with its own real registers -- at
181 * __xtc_preempt_trampoline, which does a normal cooperative
182 * xtc_yield() and later returns to the interrupted PC.
183 *
184 * Only a real fiber can be preempted (the scheduler thread itself
185 * has no coro; nothing to yield). The handler already checked
186 * crit_depth == 0 and unsafe_depth == 0.
187 *
188 * RE-ENTRANCY GUARD: a second timer tick that fires after this
189 * arms the redirect but before the trampoline has finished saving
190 * the interrupted register file would rewrite the same fiber's
191 * RIP/RSP a second time and corrupt c->ctx (verified: a rare
192 * SIGSEGV in the scheduler's swapcontext resume under aggressive
193 * slicing). g_in_preempt closes that window: it is set here and
194 * cleared by the trampoline the instant its saves are complete
195 * (see __xtc_preempt_armed_clear), so a nested tick in the
196 * vulnerable window declines and falls back to the pending flag.
197 */
198 extern void __xtc_preempt_trampoline(void);
199 extern void __xtc_preempt_trampoline_end(void);
200 ucontext_t *uc = (ucontext_t *)uctx;
201 greg_t orig_pc, orig_sp, new_sp;
202
203 if (__xtc_current_coro == NULL)
204 return 0; /* not in a fiber; leave the tick pending */
205 if (g_in_preempt > 0)
206 return 0; /* inside a swapcontext or a redirect in flight */
207 __preempt_xsave_probe(); /* once: what the trampoline must save */
208
209 orig_pc = uc->uc_mcontext.gregs[REG_RIP];
210 orig_sp = uc->uc_mcontext.gregs[REG_RSP];
211
212 /*
213 * Decline if the fiber is executing inside the trampoline itself
214 * (prologue saving the register file, the xtc_yield call, or the
215 * epilogue restoring it): a nested redirect there would corrupt a
216 * half-saved/half-restored register file. The epilogue runs with
217 * g_in_preempt already back to 0, so the flag alone cannot cover
218 * it -- this instruction-pointer range check does.
219 */
220 if (orig_pc >= (greg_t)(uintptr_t)&__xtc_preempt_trampoline &&
221 orig_pc < (greg_t)(uintptr_t)&__xtc_preempt_trampoline_end)
222 return 0;
223 /* Not a safe point: libc, another library, libxtc (preempt.c). */
224 if (!__xtc_preempt_pc_ok((uintptr_t)orig_pc))
225 return 0;
226
227 /*
228 * Enter the trampoline BELOW the interrupted code's 128-byte red
229 * zone plus an 8-byte slot the trampoline fills with orig_pc, so its
230 * terminal `ret $128` resumes at orig_pc with RSP == orig_sp
231 * exactly. Nothing at or above orig_sp - 128 is written: a leaf
232 * function may keep live data there across any instruction,
233 * asynchronous events included (SysV x86-64 ABI 3.2.2).
234 */
235 new_sp = orig_sp - 136;
236 __xtc_preempt_pc = (uintptr_t)orig_pc;
237
238 g_in_preempt++;
239 uc->uc_mcontext.gregs[REG_RSP] = new_sp;
240 uc->uc_mcontext.gregs[REG_RIP] =
241 (greg_t)(uintptr_t)&__xtc_preempt_trampoline;
242 return 1; /* armed: sigreturn lands in the trampoline */
243}
244#endif
245
246#if defined(XTC_CORO_REDIRECT_AARCH64)
247#include <stddef.h> /* offsetof */
248/*
249 * The AArch64 Linux redirect. The kernel built the signal frame
250 * (struct rt_sigframe: siginfo_t, then the ucontext_t `uctx` whose
251 * uc_mcontext carries the FP/SIMD and SVE records) on the interrupted
252 * fiber's stack, just below its sp, holding the WHOLE interrupted
253 * state. Leave the frame there: the handler's sigreturn then enters
254 * __xtc_preempt_trampoline with sp = the frame; it calls xtc_yield and
255 * issues a second rt_sigreturn on the same frame, which restores
256 * everything exactly (preempt_trampoline_aarch64.S says why registers
257 * are not saved by hand, and what it refreshes in the frame first).
258 * The interrupted pc / sp / pstate wait in the frame's siginfo, which
259 * rt_sigreturn never reads (stash[0..2]). The trampoline is
260 * entered with PSTATE.BTYPE cleared (a tick between an indirect branch
261 * and its landing pad must not demand one at the trampoline); its
262 * second sigreturn puts the interrupted pstate back. It addresses the
263 * frame with the offsets asserted here.
264 */
265_Static_assert(sizeof(siginfo_t) == 128, "rt_sigframe: siginfo size");
266_Static_assert(offsetof(ucontext_t, uc_stack) == 16, "uc_stack");
267_Static_assert(offsetof(ucontext_t, uc_sigmask) == 40, "uc_sigmask");
268_Static_assert(offsetof(ucontext_t, uc_mcontext.sp) == 432, "mc sp");
269_Static_assert(offsetof(ucontext_t, uc_mcontext.pc) == 440, "mc pc");
270_Static_assert(offsetof(ucontext_t, uc_mcontext.pstate) == 448, "mc pstate");
271
272#define CORO_A64_PSR_BTYPE 0xC00ull /* PSTATE.BTYPE, bits 10-11 */
273
274static int
275__coro_preempt_redirect_aarch64(void *uctx)
276{
277 extern void __xtc_preempt_trampoline(void);
278 extern void __xtc_preempt_trampoline_end(void);
279 ucontext_t *uc = (ucontext_t *)uctx;
280 struct xtc_coro *c = __xtc_current_coro;
281 unsigned long long pc, frame, lo, hi, *stash;
282
283 if (c == NULL)
284 return 0; /* not in a fiber; leave the tick pending */
285 if (g_in_preempt > 0)
286 return 0; /* inside a switch or a redirect in flight */
287 pc = uc->uc_mcontext.pc;
288 if (pc >= (unsigned long long)(uintptr_t)&__xtc_preempt_trampoline &&
289 pc < (unsigned long long)(uintptr_t)&__xtc_preempt_trampoline_end)
290 return 0;
291 if (!__xtc_preempt_pc_ok((uintptr_t)pc))
292 return 0; /* not a safe point (preempt.c) */
293 /* The frame starts with the siginfo; the ucontext follows it. It
294 * must be the kernel's frame on THIS fiber's stack, below the
295 * interrupted sp (anything else, say a handler on an alternate
296 * stack, declines). */
297 frame = (unsigned long long)(uintptr_t)uctx - sizeof(siginfo_t);
298 lo = (unsigned long long)(uintptr_t)c->stack;
299 hi = lo + c->stack_sz + 65536; /* + the guard: at most a 64 KiB page */
300 if ((frame & 15) != 0 || frame >= uc->uc_mcontext.sp || frame <= lo ||
301 frame >= hi)
302 return 0;
303 stash = (unsigned long long *)(uintptr_t)frame;
304 stash[0] = pc;
305 stash[1] = uc->uc_mcontext.sp;
306 stash[2] = uc->uc_mcontext.pstate;
307
308 g_in_preempt++; /* the trampoline's xtc_yield clears it on resume */
309 uc->uc_mcontext.pstate &= ~CORO_A64_PSR_BTYPE;
310 uc->uc_mcontext.sp = frame;
311 uc->uc_mcontext.pc =
312 (unsigned long long)(uintptr_t)&__xtc_preempt_trampoline;
313 return 1; /* armed: sigreturn lands in the trampoline */
314}
315#endif
316
317#endif /* XTC_CORO_PREEMPT_H */