Threading
An update on the porting progress (Base is now Go 1.27.1)
What works.
On Sculpt 26.08 hardware, an unmodified Go program (main.go and go.mod, no build tags, no _genode.go files, no genode-specific patches) builds with Goa and runs:
- M (OS threads) creation and pooling
- parking and waking on Genode semaphores (
Genode::Semaphore)
- Go scheduling (G - goroutines)
- async preemption (copied implementation from Windows using Genode API)
- CPU profiling (copied implementation from Windows using Genode API)
- GC
- Signalling in both directions (Genode ↔ Go — outward as a client, and inward as a service provider)
- Clocks make no RPC: RDTSC calibrated once against
elapsed_us, computed in-process; thus nanotime costs 8 ns only.
- Entropy is RDRAND, failing if it is missing (
arm_v8).
The Go sources are unmodified; the packaging is ordinary Genode work. The demo declares genodelabs/api/base and api/rtc_session in used_apis, and rm, timer, rtc in its runtime package.
Threading in details
g lives in R14.
Genode has no TLS: no fs_base in Cpu_state, CR4.FSGSBASE never set, __tls_get_addr apparently is a weak stub returning one shared static. The compiler rewrites the runtime TLS accesses to R14 for this target, and the build rejects any artifact containing an %fs:-prefixed instruction, since such an instruction faults on first execution with no output.
Every call into Genode runs on g0.
Thread::myself() derives thread identity from the stack pointer, and the UTCB used to marshal an RPC comes from the same stack slot. And goroutine stack is located not in the PD’s stack area, but in the Go heap. So every call to Genode must execute while running on g0’s stack, not on a goroutine’s stack.
Foreign entry doesn’t require cgo.
Genode calling into Go requires a g for the current thread. It either needs to reuse already existing one for it’s thread or to “create” a new one. On all other supported platforms the threads TLS slot holds g (if it exists). On Genode we have to keep that association between thread and g and look up for it using Thread::myself(), so the stock runtime.cgocallback path works unmodified. Also Go-provided services get a dedicated entrypoint thread: to avoid not-answering it’s clients because of blocking on a channel/mutex or suspending by the GC.
Preemption by stop-and-rewrite.
Genode doesn’t support SIGURG that is used on POSIX platforms to interrupt a goroutine at a point the goroutine didn’t choose, so preemptM uses Cpu_thread::pause to push the resume address of the asyncPreempt to the Cpu_thread::state and then Cpu_thread::resume it (copied from Windows). CPU profiling uses the same trick. A thread stopped inside Genode’s C++ is left alone: isAsyncSafePoint rejects a PC that isn’t in Go code with valid metadata.
Threads are pooled, not reaped.
A thread can’t destroy itself in Genode (Thread’s destructor refuses when myself() == this) and Go’s mexit returns from mstart rather than calling exitThread. So “retired” Ms (treads) leak holding a slot in the PD’s stack area.
Since the stack area has roughly 256 such slots, we have to pool threads parking the “retired” ones to reuse them for to the next M. Threads stacks are zeroed on park.
A note on osyield
Genode doesn’t expose/provide it and usleep(1) a no-op.
But it’s not an issue, because Go barely uses the call: runtime/lock_spinbit.go invokes osyield exactly once per lock acquisition, after the PAUSE rounds, then sleeps on the M’s. Upstream flags the step for possible removal effectively reaching the same conclusions as Genode reached about yield-based spinning in 9.08.
Client isolation in a Go-provided service.
Confidentiality comes from capabilities, not threads: an incoming call is routed by the capability’s badge to the object registered under it, so a client can only invoke the session object it holds and cannot name another’s — regardless of how many entrypoints, Ms or goroutines sit behind it.
So a single-threaded entrypoint doesn’t rise security concerns.
But a single shared entrypoint costs availability: one handler blocking or caught in a stop-the-world stalls every client, which is the real argument for per-session entrypoints.
Practical limitations so far
- 248 OS threads — 256 slots of 1 MiB in the stack area, less a reserve of 8 for the threads Genode itself runs (On most platforms the limit is 10000 threads). Goroutines are mainly unaffected; one may reach this limit through heavy
LockOSThread use or many simultaneous blocking calls. Exceeding this limits fatals the component with thread exhaustion, no degrading.
- No
entersyscall around blocking calls from Go. An M inside one counts as running, so a stop-the-world waits for it. Today it doesn’t bite, but once file and socket reads will be added it will: a goroutine blocked on a slow read stalls every GC cycle for the duration. This fixing planned as part of the next steps.
- Deadlock detection is disabled for components that serve requests, since an idle service has no runnable goroutine. A real deadlock there hangs silently instead of aborting with a traceback.
- Three Timer sessions per component: one for
usleep/elapsed_us, one for deadline timeouts, one opened only if the program asks for timer signals.
thread_local is “broken” component-wide: any C++ linked into a Go component that uses it silently shares state across threads. So porting Go apps to genode one needs to make sure those don’t use it or modify them to avoid using it.
- Traceback through the foreign-entry stub is unverified. If a handler panics, treat frames above the entry point as unreliable.
What’s next
Features, in an approximate order:
- Sockets and
netpoll over genode_c_api/socket.h
entersyscall around blocking calls, with the above.
VFS
args and env from the config ROM.
os/signal.
jitterentropy beside RDRAND: defence in depth, and the only option on arm_v8, which has no RDRAND and so currently gets nothing. Will be revisited once VFS will be in place.
mpreinit gives each M a 32 KiB gsignal stack that is never be used, since no POSIX signal is ever delivered here. The sizing will be also revisited.
- Pooling holds threads for the component’s life resulting in keeping at most peak number of them: if normally component runs on 6 threads, then suddenly peaks to 50, and settles down back to 6 - this results in holding 44 idle threads without much benefit. A trimming logic run from a dedicated thread is needed, to destroy unnecessary threads after they are not used for some time.
Technical debt:
- Bump
api/base to a 26.08, once a matching Sculpt and Goa release exists.
What would help from the Genode side
A timed down() on Semaphore, or any blocking primitive with a deadline. Not a blocker - a One_shot_timeout per M works - but emulating it costs a Timer session per component and an object per M.