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 <[email protected]>
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 <inttypes.h>
22#include <stdio.h>
23#include <string.h>
24
25#include "serene/rt/context.h"
26#include "serene/rt/engine.h"
27#include "serene/utils.h"
28
29#if SRN_ASAN
30/// Declared directly rather than via <sanitizer/common_interface_defs.h> to
31/// avoid a hard dependency on the sanitizer headers. These tell ASan when
32/// execution moves between stacks, so it does not mistake a fiber switch for
33/// corruption.
34// NOLINTBEGIN(bugprone-*, cert-dcl*)
35extern void __sanitizer_start_switch_fiber(void **fake_save, const void *bottom, size_t size);
36extern void
37__sanitizer_finish_switch_fiber(void *fake_save, const void **bottom_old, size_t *size_old);
38// NOLINTEND(bugprone-*, cert-dcl*)
39#endif
40
41#if SRN_TSAN
42/// Declared directly rather than via <sanitizer/tsan_interface.h> to avoid a
43/// hard dependency on the sanitizer headers. TSan models each fiber as its own
44/// execution context. Switching tells it which one is now running, so once
45/// fibers migrate between threads it does not read one fiber's stack accesses
46/// as another's.
47// NOLINTBEGIN(bugprone-*, cert-dcl*)
48extern void *__tsan_get_current_fiber(void);
49extern void *__tsan_create_fiber(unsigned flags);
50extern void __tsan_destroy_fiber(void *fiber);
51extern void __tsan_switch_to_fiber(void *fiber, unsigned flags);
52// NOLINTEND(bugprone-*, cert-dcl*)
53#endif
54
55// -----------------------------------------------------------------------------
56// Fiber management
57// -----------------------------------------------------------------------------
58
59/// Compiled without AddressSanitizer instrumentation, in stack-use-after-return
60/// mode ASan would place `from`/`to` on a fake stack that
61/// __sanitizer_start_switch_fiber releases before srn_fiber_swap reads them.
62/// Not instrumented by either sanitizer, the stack swaps mid-function, which
63/// confuses ASan's fake stack and TSan's shadow stack. The explicit annotations
64/// keep each sanitizer's fiber tracking correct across the swap instead.
65[[gnu::no_sanitize_address]] [[gnu::no_sanitize_thread]]
67
68#if SRN_ASAN
69 const size_t size = srn_fiber_stack_size(to->stack);
70 __sanitizer_start_switch_fiber(&from->fake_stack, to->stack.limit, size);
71#endif
72#if SRN_TSAN
73 // Move TSan's notion of the running fiber to `to` before the stack swaps. A
74 // fiber built outside srn_fiber_make / srn_fiber_init_thread has no handle
75 // and is simply not tracked. Only the low-level switch tests build such a
76 // fiber. Every fiber the scheduler runs has one.
77 if (to->tsan_fiber != nullptr) {
78 __tsan_switch_to_fiber(to->tsan_fiber, 0);
79 }
80#endif
82#if SRN_ASAN
83 __sanitizer_finish_switch_fiber(from->fake_stack, nullptr, nullptr);
84#endif
85}
86
87[[gnu::no_sanitize_address]] [[gnu::no_sanitize_thread]]
89#if SRN_ASAN
90 // A nullptr fake-stack tells ASan the current fiber is finished and will not
91 // resume, so it can discard the bookkeeping rather than leak it.
92 const size_t size = srn_fiber_stack_size(to->stack);
93 __sanitizer_start_switch_fiber(nullptr, to->stack.limit, size);
94#endif
95#if SRN_TSAN
96 // The finished fiber's handle is released later, at reap. Here just move
97 // TSan to `to` before the swap, when `to` has a handle (see
98 // srn_fiber_switch).
99 if (to->tsan_fiber != nullptr) {
100 __tsan_switch_to_fiber(to->tsan_fiber, 0);
101 }
102#endif
103 // srn_fiber_swap always writes the outgoing sp somewhere. This fiber is done,
104 // so discard it. Control loads `to` and never comes back.
105 srn_fiber_ctx_t discard;
106 srn_fiber_swap(&discard, &to->fiber_ctx);
108}
109
110[[gnu::no_sanitize_address]]
112#if SRN_ASAN
113 // A nullptr fake-stack, a fresh fiber has no previously saved bookkeeping.
114 // The out-params report the stack this fiber was started from -- this is the
115 // one chance to learn `from`'s bounds (there is no portable way to query a
116 // thread's own stack), and a later switch back to `from` needs them, so
117 // record them. They flow through the sanitizer rather than libc.
118 const void *bottom = nullptr;
119 size_t size = 0;
120 __sanitizer_finish_switch_fiber(nullptr, &bottom, &size);
121
122 if (from != nullptr) {
123 // ASan reports the came-from stack as (bottom, size). The struct keeps the
124 // low and high boundaries, so limit = bottom and start = bottom + size.
125 from->stack.limit = (void *)bottom;
126 from->stack.start = (char *)bottom + size;
127 }
128#else
129 UNUSED(from);
130#endif
131}
132
134#if SRN_TSAN
135 __tsan_destroy_fiber(fiber->tsan_fiber);
136#else
137 UNUSED(fiber);
138#endif
139}
140
141#if SRN_ASAN
142/**
143 * Entry of the throwaway fiber srn_fiber_init_thread runs once per thread.
144 * srn_fiber_on_entry makes ASan report the stack this fiber was started
145 * from, the loop's own stack, and records its bounds on the loop fiber.
146 */
147static void stack_bounds_probe(void *loop_ptr) {
148 srn_fiber_t *loop = loop_ptr;
149 srn_fiber_on_entry(loop);
151}
152#endif
153
155 // Represent the calling thread as the running fiber. The saved context
156 // (fiber_ctx) stays empty, and the first switch away from the thread fills
157 // it.
158 memset(f, 0, sizeof(*f));
160 (void)snprintf(f->name, sizeof(f->name), "thread");
161#if SRN_ASAN
162 // ASan needs this loop's stack bounds on every switch back to it, and a
163 // worker that only ever resumes stolen fibers never launches a fresh
164 // fiber, so srn_fiber_on_entry would never report them. Run a throwaway
165 // fiber once, its entry records the bounds through the same protocol
166 // every real fiber uses. The probe finishes before the switch back, so
167 // srn_fiber_switch_final already has real bounds to announce.
168 srn_fiber_t probe;
169 memset(&probe, 0, sizeof(probe));
170 probe.state = SRN_FIBER_READY;
171 (void)snprintf(probe.name, sizeof(probe.name), "stack-bounds-probe");
172 probe.stack = srn_fiber_stack_alloc(1); // rounds up to one page plus guard
173 srn_fiber_ctx_make(&probe.fiber_ctx, probe.stack, stack_bounds_probe, f);
174 srn_fiber_switch(f, &probe);
176#endif
177#if SRN_TSAN
178 // The loop fiber stands in for this OS thread, so it takes the thread's own
179 // current TSan fiber rather than a freshly created one.
180 f->tsan_fiber = __tsan_get_current_fiber();
181#endif
182}
183
184static void srn_fiber_launcher(void *fiber_ptr) {
185 PANIC_IF_NULL(fiber_ptr);
186
187 srn_fiber_t *fiber = fiber_ptr;
188 // The resumer is the worker loop that switched us in, on_entry records its
189 // stack bounds, and switch_final hands control back to it when the entry
190 // returns.
192 // The worker loop already set the state to RUNNING before switching in, for
193 // both the first run and every resume, so it is not set again here.
194 // A null result is legal, the result is type erased and "no result" is a
195 // reasonable outcome for an entry run for its effects.
196 fiber->result = fiber->entry(fiber->ctx, fiber->arg);
197 fiber->state = SRN_FIBER_DONE;
199}
200
202 srn_context_t *ctx, srn_scheduler_t *sched, const char *name, srn_fiber_entry_t entry, void *arg,
203 size_t stack_size
204) {
205
206 srn_fiber_t *f = ALLOC(ctx, srn_fiber_t);
207 PANIC_IF_NULL(f);
208 memset((void *)f, 0, sizeof(srn_fiber_t));
209
210 // TODO(lxsameer): Make the fiber stack configurable via cli arg or something.
211 // This is the acquire side of the stack-ring TODO in fiber.h: a pooled stack
212 // would be pulled from the per-thread ring here, falling back to a fresh
213 // mapping only on a miss.
214 // A zero `stack_size` falls back to the configured per fiber default.
215 f->stack =
216 srn_fiber_stack_alloc(stack_size != 0 ? stack_size : ctx->engine->config.fiber.stack_size);
217 f->ctx = ctx;
218 f->state = SRN_FIBER_NEW;
219 f->entry = entry;
220 f->arg = arg;
221
222 if (name != nullptr) {
223 (void)snprintf(f->name, sizeof(f->name), "%s", name);
224 } else {
225 // The autogenerated name makes logs tell fibers apart with no caller
226 // input. Ids come from the engine wide object id counter, so a fiber
227 // never shares its number with another runtime object.
228 (void)snprintf(f->name, sizeof(f->name), "f#%" PRIu64, srn_allocate_object_id(ctx->engine));
229 }
230
231#if SRN_TSAN
232 f->tsan_fiber = __tsan_create_fiber(0);
233#endif
235
236 // Register before enqueuing, the scheduler must know about the fiber for its
237 // whole life, independent of which queue (if any) it currently sits on. The
238 // registry is how a SUSPENDED fiber, off every run queue, stays reachable for
239 // cleanup and cancellation.
240 srn_sched_register(sched, f);
241
242 // The fiber stays NEW. srn_fiber_schedule makes it runnable, so a caller
243 // can build several fibers and wire them up before any of them runs.
244 return f;
245}
246
248 PANIC_IF_NULL(ctx);
249 srn_fiber_t *f = srn_fiber_make(ctx, ctx->engine->scheduler, nullptr, entry, arg, 0);
251 return f;
252}
253
255srn_fiber_spawn_copy(srn_context_t *ctx, srn_fiber_entry_t entry, const void *arg, size_t size) {
256 PANIC_IF_NULL(ctx);
257 PANIC_IF_NULL(arg);
258 // max_align_t suits any argument type, the caller only hands over bytes.
259 void *copy = srn_allocate(ctx, size, alignof(max_align_t));
260 PANIC_IF_NULL(copy);
261 memcpy(copy, arg, size);
262 return srn_fiber_spawn(ctx, entry, copy);
263}
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
srn_object_id_t srn_allocate_object_id(srn_engine_t *engine)
Definition engine.c:172
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:88
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:255
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:247
static void srn_fiber_launcher(void *fiber_ptr)
Definition fiber.c:184
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:154
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:66
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:201
void srn_fiber_on_entry(srn_fiber_t *from)
Call as the first action inside a fresh fiber's entry.
Definition fiber.c:111
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:133
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.
srn_fiber_stack_t srn_fiber_stack_alloc(size_t size)
Allocate a stack of at least size usable bytes plus a guard page, or SRN_FIBER_DEFAULT_STACK_SIZE whe...
static size_t srn_fiber_stack_size(srn_fiber_stack_t s)
Definition fiber.h:611
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:232
@ SRN_FIBER_RUNNING
Currently executing.
Definition fiber.h:236
@ SRN_FIBER_READY
On the run queue, eligible to run.
Definition fiber.h:234
@ SRN_FIBER_DONE
Entry returned. The result is final.
Definition fiber.h:240
srn_fiber_result_t(* srn_fiber_entry_t)(srn_context_t *ctx, void *arg)
The function a fiber runs.
Definition fiber.h:251
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:318
srn_fiber_t * srn_fiber_worker_loop(void)
The worker's loop of the worker running on the calling os thread.
Definition scheduler.c:1062
void srn_fiber_schedule(srn_fiber_t *fiber)
Schedule a NEW fiber, making it eligible to run.
Definition scheduler.c:630
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 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:198
void * limit
Low end of usable region.
Definition fiber.h:222
void * start
High end, stack pointer initialises to this address.
Definition fiber.h:220
char name[SRN_FIBER_NAME_MAX]
Debug name, the caller's choice copied at creation, or the autogenerated f#<id> from the engine wide ...
Definition fiber.h:330
srn_fiber_entry_t entry
Definition fiber.h:275
_Atomic srn_fiber_state_t state
The lifecycle state.
Definition fiber.h:273
srn_fiber_result_t result
Set when state reaches SRN_FIBER_DONE.
Definition fiber.h:279
srn_context_t * ctx
Definition fiber.h:274
srn_fiber_stack_t stack
Definition fiber.h:267
void * arg
Definition fiber.h:276
srn_fiber_ctx_t fiber_ctx
Saved stack pointer (see srn_fiber_ctx_t).
Definition fiber.h:266
#define PANIC_IF_NULL(ptr)
Definition utils.h:66
#define SHOULD_NOT_HAPPEN
Definition utils.h:83
#define UNUSED(x)
Definition utils.h:45