Porting GO to Genode

I’d like to run Syncthing on my Sculpt natively to use it as a NAS.
The Syncthing is ans awesome file sharing solution, but unfortunately it written in GO :pensive_face:

So in order to port it to Genode I have to port GO first.
And this is what I’m playing around currently.

I plan t use this topic to report my progress and to leave some useful notes and hints.

I use Claude to learn, investigate and try things, so some vibe coding is used.
Will see were this journey will lead me. :rocket:

1 Like

First findings to note so far:

  1. Apparently there’s outdated and stale gccgo, that is stayed behind the official GO specification some years ago and is currently in a maintenance mode. I don’t see a feasibility to update it and reimplement all the new features to just trying to keep up with the GO development. Not an option :cross_mark:
  2. Another option is to extend GO to the genode platform leveraging it’s POSIX layer. I don’t see a value to implement and support hacks and workarounds to make it fully POSIX (unix=true) compatible. Too much efforts for something that is not natural in Genode :cross_mark:
  3. And finally an option is to extend GO to the genode platform utilizing Genode API directly + reusing some POSIX (unix=true) implementaions. This is the most promising and least effort way so far :check_box_with_check:

So I’m currently trying to implement the option 3 :+1:

1 Like

I managed to build some minimal executable and run it in goa testbead with many issue so far, currently trying to resolve them.

One of the issue was connected with the necessity of RM session for GO.
And apparently the Goa testbed is not configured to provide RM sessions out of the box.

So in order to make it work I did this runtime patching:

  1. Start the goa_testbed Preset
  2. On default FS in /depot/jschlatov/pkg/goa_testbed/2026-04-16/runtime add + rm node as a child of + requires node
  3. In /model/deploy add + rm | + parent node as a child of + connect node under + child goa_testbed node.
  4. Restart goa_testbed component

Now all your test runs will be able to obtain RM sessions.

1 Like

Apparently there’s a mismatch in maximum threads count.
Go allows to have 10000 threads, while Genode is capped in 250 per component.

This is not an issue for the most common case of FS operations, since Genode VFS is asynchronous there’s no need to park a thread for an otherwise synchronous file operation.

But this puts a limitation on CGO usage, meaning apps requiring C would not build. And the ones that have CGO as optional might run a bit slower and heavier in some cases without it.

Implementing CGO support would require a POSIX libc implemented, that I’m trying to avoid for now (too much efforts to implement it and to make it fully compatible with Genode)

Other possible thread-heavy use cases are either irrelevant for Genode or very rare to happen.

So I’m proceeding with CGO_ENABLED=0 for now leaving CGO_ENABLED=1 challenge for future…

That’s an exciting project. I’d be very interested in using a few Go-based applications on Genode as well.

I’m not familiar with Go at all and thus am curious to learn more about it. The cgo part raised my interest in particular. If I understand it correctly, Go is independent from a C runtime but requires it to link against C-libraries. Have you had a look at existing Go projects and their dependence on 3rd-party C-libraries (and therefore their dependence on cgo)? If it turns out that most interesting Go projects require cgo anyway, it might be easier and more maintainable to directly use Genode’s libc and improving its POSIX compliance where needed.

I’m not an expert in GO either, but what I learned while researching this topic on a high level is:

  • GO compiles into a native platform statically linked binary using the supported platform implementations. E.g. libc for POSIX, something custom for plan9, Windows (and I’m trying to add Genode now)
  • Most of the GO projects stay “pure GO” and use only GO libraries dependencies.
  • CGO is optional for use when someone wants to squeeze out max performance by directly calling libc (or other c libs) functions. So portable development way is to use CGO when it’s available and fallback to pure GO when it’s not.
  • Though some projects are built without fallbacks and require CGO.
  • Gemini estimates that 87% of all projects written in pure GO, 8.1% with light CGO usage for some isolated tasks, and 3.2% with heavy CGO usage (massive, infrastructure-level software or heavy database/graphics wrappers)

In particular Syncthing is able to compile and run without CGO, and this is my first goal with GO port at the moment :slight_smile:

1 Like

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.

Would pthread_cond_timedwait be an option?

1 Like