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 <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 probe.stack = srn_fiber_stack_alloc(1); // rounds up to one page plus guard
172 srn_fiber_ctx_make(&probe.fiber_ctx, probe.stack, stack_bounds_probe, f);
173 srn_fiber_switch(f, &probe);
175#endif
176#if SRN_TSAN
177 // The loop fiber stands in for this OS thread, so it takes the thread's own
178 // current TSan fiber rather than a freshly created one.
179 f->tsan_fiber = __tsan_get_current_fiber();
180#endif
181}
182
183static void srn_fiber_launcher(void *fiber_ptr) {
184 PANIC_IF_NULL(fiber_ptr);
185
186 srn_fiber_t *fiber = fiber_ptr;
187 // The resumer is the worker loop that switched us in, on_entry records its
188 // stack bounds, and switch_final hands control back to it when the entry
189 // returns.
191 // The worker loop already set the state to RUNNING before switching in, for
192 // both the first run and every resume, so it is not set again here.
193 // A null result is legal, the result is type erased and "no result" is a
194 // reasonable outcome for an entry run for its effects.
195 fiber->result = fiber->entry(fiber->ctx, fiber->arg);
196 fiber->state = SRN_FIBER_DONE;
198}
199
201 srn_context_t *ctx, srn_scheduler_t *sched, const char *name, srn_fiber_entry_t entry, void *arg,
202 size_t stack_size
203) {
204
205 srn_fiber_t *f = ALLOC(ctx, srn_fiber_t);
206 PANIC_IF_NULL(f);
207 memset((void *)f, 0, sizeof(srn_fiber_t));
208
209 // TODO(lxsameer): Make the fiber stack configurable via cli arg or something.
210 // This is the acquire side of the stack-ring TODO in fiber.h: a pooled stack
211 // would be pulled from the per-thread ring here, falling back to a fresh
212 // mapping only on a miss.
213 // A zero `stack_size` falls back to the configured per fiber default.
214 f->stack =
215 srn_fiber_stack_alloc(stack_size != 0 ? stack_size : ctx->engine->config.fiber.stack_size);
216 f->ctx = ctx;
217 f->state = SRN_FIBER_NEW;
218 f->entry = entry;
219 f->arg = arg;
220
221 if (name != nullptr) {
222 (void)snprintf(f->name, sizeof(f->name), "%s", name);
223 } else {
224 srn_fiber_autoname(ctx->engine, f->name, sizeof(f->name));
225 }
226
227 FIBER_TRACEPOINT(fiber_created, f->name, srn_fiber_stack_size(f->stack));
228
229#if SRN_TSAN
230 f->tsan_fiber = __tsan_create_fiber(0);
231#endif
233
234 // Register before enqueuing, the scheduler must know about the fiber for its
235 // whole life, independent of which queue (if any) it currently sits on. The
236 // registry is how a SUSPENDED fiber, off every run queue, stays reachable for
237 // cleanup and cancellation.
238 srn_sched_register(sched, f);
239
240 // The fiber stays NEW. srn_fiber_schedule makes it runnable, so a caller
241 // can build several fibers and wire them up before any of them runs.
242 return f;
243}
244
246 PANIC_IF_NULL(ctx);
247 srn_fiber_t *f = srn_fiber_make(ctx, ctx->engine->scheduler, nullptr, entry, arg, 0);
249 return f;
250}
251
253srn_fiber_spawn_copy(srn_context_t *ctx, srn_fiber_entry_t entry, const void *arg, size_t size) {
254 PANIC_IF_NULL(ctx);
255 PANIC_IF_NULL(arg);
256 // max_align_t suits any argument type, the caller only hands over bytes.
257 void *copy = srn_allocate(ctx, size, alignof(max_align_t));
258 PANIC_IF_NULL(copy);
259 memcpy(copy, arg, size);
260 return srn_fiber_spawn(ctx, entry, copy);
261}
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:253
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:245
static void srn_fiber_launcher(void *fiber_ptr)
Definition fiber.c:183
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:200
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)
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:625
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:236
@ SRN_FIBER_RUNNING
Currently executing.
Definition fiber.h:240
@ SRN_FIBER_READY
On the run queue, eligible to run.
Definition fiber.h:238
@ SRN_FIBER_DONE
Entry returned. The result is final.
Definition fiber.h:244
srn_fiber_result_t(* srn_fiber_entry_t)(srn_context_t *ctx, void *arg)
The function a fiber runs.
Definition fiber.h:255
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 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:202
void * limit
Low end of usable region.
Definition fiber.h:226
void * start
High end, stack pointer initialises to this address.
Definition fiber.h:224
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:333
srn_fiber_entry_t entry
Definition fiber.h:279
_Atomic srn_fiber_state_t state
The lifecycle state.
Definition fiber.h:277
srn_fiber_result_t result
Set when state reaches SRN_FIBER_DONE.
Definition fiber.h:283
srn_context_t * ctx
Definition fiber.h:278
srn_fiber_stack_t stack
Definition fiber.h:271
void * arg
Definition fiber.h:280
srn_fiber_ctx_t fiber_ctx
Saved stack pointer (see srn_fiber_ctx_t).
Definition fiber.h:270
#define PANIC_IF_NULL(ptr)
Definition utils.h:66
#define SHOULD_NOT_HAPPEN
Definition utils.h:83
#define UNUSED(x)
Definition utils.h:45