Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
Loading...
Searching...
No Matches
fiber.c
Go to the documentation of this file.
1/* -*- C -*-
2 * Serene programming language
3 * Copyright (C) 2019-2026 Sameer Rahmani <lxsameer@lxsameer.com>
4 *
5 * This library is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Lesser General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with this library. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19#include "serene/rt/fiber.h"
20
21#include <stdio.h>
22#include <string.h>
23
24#include "serene/rt/context.h"
25#include "serene/rt/engine.h"
26#include "serene/utils.h"
27
28#if SRN_ASAN
29/// Declared directly rather than via <sanitizer/common_interface_defs.h> to
30/// avoid a hard dependency on the sanitizer headers. These tell ASan when
31/// execution moves between stacks, so it does not mistake a fiber switch for
32/// corruption.
33// NOLINTBEGIN(bugprone-*, cert-dcl*)
34extern void __sanitizer_start_switch_fiber(void **fake_save, const void *bottom, size_t size);
35extern void
36__sanitizer_finish_switch_fiber(void *fake_save, const void **bottom_old, size_t *size_old);
37// NOLINTEND(bugprone-*, cert-dcl*)
38#endif
39
40#if SRN_TSAN
41/// Declared directly rather than via <sanitizer/tsan_interface.h> to avoid a
42/// hard dependency on the sanitizer headers. TSan models each fiber as its own
43/// execution context. Switching tells it which one is now running, so once
44/// fibers migrate between threads it does not read one fiber's stack accesses
45/// as another's.
46// NOLINTBEGIN(bugprone-*, cert-dcl*)
47extern void *__tsan_get_current_fiber(void);
48extern void *__tsan_create_fiber(unsigned flags);
49extern void __tsan_destroy_fiber(void *fiber);
50extern void __tsan_switch_to_fiber(void *fiber, unsigned flags);
51// NOLINTEND(bugprone-*, cert-dcl*)
52#endif
53
54// -----------------------------------------------------------------------------
55// Fiber management
56// -----------------------------------------------------------------------------
57
58/// Compiled without AddressSanitizer instrumentation, in stack-use-after-return
59/// mode ASan would place `from`/`to` on a fake stack that
60/// __sanitizer_start_switch_fiber releases before srn_fiber_swap reads them.
61/// Not instrumented by either sanitizer, the stack swaps mid-function, which
62/// confuses ASan's fake stack and TSan's shadow stack. The explicit annotations
63/// keep each sanitizer's fiber tracking correct across the swap instead.
64[[gnu::no_sanitize_address]] [[gnu::no_sanitize_thread]]
66
67#if SRN_ASAN
68 const size_t size = srn_fiber_stack_size(to->stack);
69 __sanitizer_start_switch_fiber(&from->fake_stack, to->stack.limit, size);
70#endif
71#if SRN_TSAN
72 // Move TSan's notion of the running fiber to `to` before the stack swaps. A
73 // fiber built outside srn_fiber_make / srn_fiber_init_thread has no handle
74 // and is simply not tracked. Only the low-level switch tests build such a
75 // fiber. Every fiber the scheduler runs has one.
76 if (to->tsan_fiber != nullptr) {
77 __tsan_switch_to_fiber(to->tsan_fiber, 0);
78 }
79#endif
81#if SRN_ASAN
82 __sanitizer_finish_switch_fiber(from->fake_stack, nullptr, nullptr);
83#endif
84}
85
86[[gnu::no_sanitize_address]] [[gnu::no_sanitize_thread]]
88#if SRN_ASAN
89 // A nullptr fake-stack tells ASan the current fiber is finished and will not
90 // resume, so it can discard the bookkeeping rather than leak it.
91 const size_t size = srn_fiber_stack_size(to->stack);
92 __sanitizer_start_switch_fiber(nullptr, to->stack.limit, size);
93#endif
94#if SRN_TSAN
95 // The finished fiber's handle is released later, at reap. Here just move
96 // TSan to `to` before the swap, when `to` has a handle (see
97 // srn_fiber_switch).
98 if (to->tsan_fiber != nullptr) {
99 __tsan_switch_to_fiber(to->tsan_fiber, 0);
100 }
101#endif
102 // srn_fiber_swap always writes the outgoing sp somewhere. This fiber is done,
103 // so discard it. Control loads `to` and never comes back.
104 srn_fiber_ctx_t discard;
105 srn_fiber_swap(&discard, &to->fiber_ctx);
107}
108
109[[gnu::no_sanitize_address]]
111#if SRN_ASAN
112 // A nullptr fake-stack, a fresh fiber has no previously saved bookkeeping.
113 // The out-params report the stack this fiber was started from -- this is the
114 // one chance to learn `from`'s bounds (there is no portable way to query a
115 // thread's own stack), and a later switch back to `from` needs them, so
116 // record them. They flow through the sanitizer rather than libc.
117 const void *bottom = nullptr;
118 size_t size = 0;
119 __sanitizer_finish_switch_fiber(nullptr, &bottom, &size);
120
121 if (from != nullptr) {
122 // ASan reports the came-from stack as (bottom, size). The struct keeps the
123 // low and high boundaries, so limit = bottom and start = bottom + size.
124 from->stack.limit = (void *)bottom;
125 from->stack.start = (char *)bottom + size;
126 }
127#else
128 UNUSED(from);
129#endif
130}
131
133#if SRN_TSAN
134 __tsan_destroy_fiber(fiber->tsan_fiber);
135#else
136 UNUSED(fiber);
137#endif
138}
139
140#if SRN_ASAN
141/**
142 * Entry of the throwaway fiber srn_fiber_init_thread runs once per thread.
143 * srn_fiber_on_entry makes ASan report the stack this fiber was started
144 * from, the loop's own stack, and records its bounds on the loop fiber.
145 */
146static void stack_bounds_probe(void *loop_ptr) {
147 srn_fiber_t *loop = loop_ptr;
148 srn_fiber_on_entry(loop);
150}
151#endif
152
154 // Represent the calling thread as the running fiber. The saved context
155 // (fiber_ctx) stays empty, and the first switch away from the thread fills
156 // it.
157 memset(f, 0, sizeof(*f));
159 (void)snprintf(f->name, sizeof(f->name), "thread");
160#if SRN_ASAN
161 // ASan needs this loop's stack bounds on every switch back to it, and a
162 // worker that only ever resumes stolen fibers never launches a fresh
163 // fiber, so srn_fiber_on_entry would never report them. Run a throwaway
164 // fiber once, its entry records the bounds through the same protocol
165 // every real fiber uses. The probe finishes before the switch back, so
166 // srn_fiber_switch_final already has real bounds to announce.
167 srn_fiber_t probe;
168 memset(&probe, 0, sizeof(probe));
169 probe.state = SRN_FIBER_READY;
170 (void)snprintf(probe.name, sizeof(probe.name), "stack-bounds-probe");
171 // One usable page and a one page guard. The probe only measures stack bounds
172 // and is freed at once, so it does not need the configured guard band.
173 probe.stack = srn_fiber_stack_alloc(1, 1);
174 srn_fiber_ctx_make(&probe.fiber_ctx, probe.stack, stack_bounds_probe, f);
175 srn_fiber_switch(f, &probe);
177#endif
178#if SRN_TSAN
179 // The loop fiber stands in for this OS thread, so it takes the thread's own
180 // current TSan fiber rather than a freshly created one.
181 f->tsan_fiber = __tsan_get_current_fiber();
182#endif
183}
184
185static void srn_fiber_launcher(void *fiber_ptr) {
186 PANIC_IF_NULL(fiber_ptr);
187
188 srn_fiber_t *fiber = fiber_ptr;
189 // The resumer is the worker loop that switched us in, on_entry records its
190 // stack bounds, and switch_final hands control back to it when the entry
191 // returns.
193 // The worker loop already set the state to RUNNING before switching in, for
194 // both the first run and every resume, so it is not set again here.
195 // A null result is legal, the result is type erased and "no result" is a
196 // reasonable outcome for an entry run for its effects.
197 fiber->result = fiber->entry(fiber->ctx, fiber->arg);
198 fiber->state = SRN_FIBER_DONE;
200}
201
203 srn_context_t *ctx, srn_scheduler_t *sched, const char *name, srn_fiber_entry_t entry, void *arg,
204 size_t stack_size
205) {
206
207 srn_fiber_t *f = ALLOC(ctx, srn_fiber_t);
208 PANIC_IF_NULL(f);
209 memset((void *)f, 0, sizeof(srn_fiber_t));
210
211 // TODO(lxsameer): Make the fiber stack configurable via cli arg or something.
212 // This is the acquire side of the stack-ring TODO in fiber.h: a pooled stack
213 // would be pulled from the per-thread ring here, falling back to a fresh
214 // mapping only on a miss.
215 // A zero `stack_size` falls back to the configured per fiber default. The
216 // guard band is engine wide, always taken from the configuration.
218 stack_size != 0 ? stack_size : ctx->engine->config.fiber.stack_size,
220 );
221 f->ctx = ctx;
222 f->state = SRN_FIBER_NEW;
223 f->entry = entry;
224 f->arg = arg;
225
226 if (name != nullptr) {
227 (void)snprintf(f->name, sizeof(f->name), "%s", name);
228 } else {
229 srn_fiber_autoname(ctx->engine, f->name, sizeof(f->name));
230 }
231
232 FIBER_TRACEPOINT(fiber_created, f->name, srn_fiber_stack_size(f->stack));
233
234#if SRN_TSAN
235 f->tsan_fiber = __tsan_create_fiber(0);
236#endif
238
239 // Register before enqueuing, the scheduler must know about the fiber for its
240 // whole life, independent of which queue (if any) it currently sits on. The
241 // registry is how a SUSPENDED fiber, off every run queue, stays reachable for
242 // cleanup and cancellation.
243 srn_sched_register(sched, f);
244
245 // The fiber stays NEW. srn_fiber_schedule makes it runnable, so a caller
246 // can build several fibers and wire them up before any of them runs.
247 return f;
248}
249
251 PANIC_IF_NULL(ctx);
252 srn_fiber_t *f = srn_fiber_make(ctx, ctx->engine->scheduler, nullptr, entry, arg, 0);
254 return f;
255}
256
258srn_fiber_spawn_copy(srn_context_t *ctx, srn_fiber_entry_t entry, const void *arg, size_t size) {
259 PANIC_IF_NULL(ctx);
260 PANIC_IF_NULL(arg);
261 // max_align_t suits any argument type, the caller only hands over bytes.
262 void *copy = srn_allocate(ctx, size, alignof(max_align_t));
263 PANIC_IF_NULL(copy);
264 memcpy(copy, arg, size);
265 return srn_fiber_spawn(ctx, entry, copy);
266}
void * srn_allocate(const srn_context_t *ctx, size_t size, size_t alignment)
Definition context.c:73
#define ALLOC(ctx, T)
Definition context.h:84
void srn_fiber_switch_final(srn_fiber_t *to)
Like srn_fiber_switch, but for a fiber that has finished and must not be resumed, control transfers t...
Definition fiber.c:87
srn_fiber_t * srn_fiber_spawn_copy(srn_context_t *ctx, srn_fiber_entry_t entry, const void *arg, size_t size)
srn_fiber_spawn with size bytes of *arg copied into ctx first, so the fiber owns its argument and the...
Definition fiber.c:258
srn_fiber_t * srn_fiber_spawn(srn_context_t *ctx, srn_fiber_entry_t entry, void *arg)
Make and schedule a fiber with every default, the engine's scheduler, the configured stack size,...
Definition fiber.c:250
static void srn_fiber_launcher(void *fiber_ptr)
Definition fiber.c:185
void srn_fiber_init_thread(srn_fiber_t *f)
Represent the calling OS thread as the running fiber ("#0"), so the scheduler or a test can switch aw...
Definition fiber.c:153
void srn_fiber_switch(srn_fiber_t *from, srn_fiber_t *to)
Compiled without AddressSanitizer instrumentation, in stack-use-after-return mode ASan would place fr...
Definition fiber.c:65
srn_fiber_t * srn_fiber_make(srn_context_t *ctx, srn_scheduler_t *sched, const char *name, srn_fiber_entry_t entry, void *arg, size_t stack_size)
Create a fiber that will run entry(ctx, arg), registered with sched but NOT scheduled.
Definition fiber.c:202
void srn_fiber_on_entry(srn_fiber_t *from)
Call as the first action inside a fresh fiber's entry.
Definition fiber.c:110
void srn_fiber_on_reap(srn_fiber_t *fiber)
Call when a finished fiber is reaped, after it has switched away for the last time.
Definition fiber.c:132
AI Generated (🤦) Fiber subsystem overview.
void srn_fiber_swap(srn_fiber_ctx_t *from, srn_fiber_ctx_t *to)
Save the current execution context into from, restore to, and resume on to's stack.
#define FIBER_TRACEPOINT(...)
Definition fiber.h:146
srn_fiber_stack_t srn_fiber_stack_alloc(size_t size, size_t guard_pages)
Allocate a stack of at least size usable bytes, or SRN_CONFIG_DEFAULT_FIBER_STACK_SIZE when size is 0...
static size_t srn_fiber_stack_size(srn_fiber_stack_t s)
Definition fiber.h:612
void srn_fiber_ctx_make(srn_fiber_ctx_t *fiber_ctx, srn_fiber_stack_t stack, void(*fn)(void *), void *arg)
Initialise a fresh fiber context so the first srn_fiber_swap into it begins executing fn(arg) on stac...
void srn_fiber_stack_free(srn_fiber_stack_t stack)
@ SRN_FIBER_NEW
Created, stack mapped, never resumed.
Definition fiber.h:221
@ SRN_FIBER_RUNNING
Currently executing.
Definition fiber.h:225
@ SRN_FIBER_READY
On the run queue, eligible to run.
Definition fiber.h:223
@ SRN_FIBER_DONE
Entry returned. The result is final.
Definition fiber.h:229
srn_fiber_result_t(* srn_fiber_entry_t)(srn_context_t *ctx, void *arg)
The function a fiber runs.
Definition fiber.h:240
void srn_sched_register(srn_scheduler_t *sched, srn_fiber_t *fiber)
Record a fiber in the scheduler's registry of live fibers, where it stays until it is reaped.
Definition scheduler.c:324
srn_fiber_t * srn_fiber_worker_loop(void)
The worker's loop of the worker running on the calling os thread.
Definition scheduler.c:1101
void srn_fiber_schedule(srn_fiber_t *fiber)
Schedule a NEW fiber, making it eligible to run.
Definition scheduler.c:638
void srn_fiber_autoname(srn_engine_t *engine, char *dst, size_t size)
Write the autogenerated debug name for a new fiber into dst.
Definition scheduler.c:1141
srn_fiber_config_t fiber
srn_engine_t * engine
Long term state of the compiler.
Definition context.h:49
srn_configuration_t config
The runtime's tunable knobs, the single source for every configurable value (see configuration....
Definition engine.h:62
srn_scheduler_t * scheduler
The fiber scheduler, that is the entry point of the fiber subsystem.
Definition engine.h:75
size_t guard_pages
Pages in the guard band below every fiber stack.
size_t stack_size
Size of every fiber stack, in bytes.
The saved context of a suspended fiber is a single word, its stack pointer at the moment it was switc...
Definition fiber.h:187
void * limit
Low end of usable region.
Definition fiber.h:211
void * start
High end, stack pointer initialises to this address.
Definition fiber.h:209
char name[SRN_FIBER_NAME_MAX]
Debug name, the caller's choice copied at creation, or autogenerated when the caller passed none (see...
Definition fiber.h:318
srn_fiber_entry_t entry
Definition fiber.h:264
_Atomic srn_fiber_state_t state
The lifecycle state.
Definition fiber.h:262
srn_fiber_result_t result
Set when state reaches SRN_FIBER_DONE.
Definition fiber.h:268
srn_context_t * ctx
Definition fiber.h:263
srn_fiber_stack_t stack
Definition fiber.h:256
void * arg
Definition fiber.h:265
srn_fiber_ctx_t fiber_ctx
Saved stack pointer (see srn_fiber_ctx_t).
Definition fiber.h:255
#define PANIC_IF_NULL(ptr)
Definition utils.h:66
#define SHOULD_NOT_HAPPEN
Definition utils.h:83
#define UNUSED(x)
Definition utils.h:45