Gramine's fast path runs through libc, so Go programs trap on every syscall

Gramine lets unmodified Linux programs run inside an Intel SGX enclave. We ran into this debugging why Geth, the Go Ethereum client, ran so slowly under it. Start a Go program in Gramine and it warns: "Emulating a raw syscall instruction. This degrades performance, consider patching your application to use Gramine syscall API." A dynamically linked C or Python program making the same system calls gets no warning. Why would it matter which instruction asks for a syscall, when the same syscall happens either way?

Because the fast path lives in libc. Gramine is a library OS: it implements the Linux system-call interface inside the enclave, since the host kernel is untrusted and SGX forbids the syscall instruction in enclave mode anyway. Gramine leaves the enclave only when the host must act, such as reading a file or a socket. Each enclave exit or entry costs roughly 8,000–12,000 cycles, against about 100 for a native syscall, per the Gramine performance docs.

Gramine ships patched glibc and musl whose wrappers replace syscall with an ordinary function call into the library OS. Most programs reach the kernel only through libc, so each of their syscalls starts as a function call inside the enclave, and one Gramine can answer from its own state, like getpid, never leaves.

Go on Linux skips libc: its runtime executes syscall itself, even in a binary built with cgo. Inside the enclave that instruction faults. The fault forces an asynchronous enclave exit; the host kernel delivers SIGILL to Gramine's untrusted runtime, which enters the enclave to run a first-stage handler, exits again, and resumes the enclave in Gramine's exception handler. That handler decodes the faulting instruction and only then runs the syscall. Every syscall pays two exits, two entries and a trip through the host's signal delivery before its own work begins, including the ones the fast path would have answered without leaving.

Swapping in Gramine's libc can't help: a pure-Go binary is statically linked with no libc at all, and a cgo binary's runtime bypasses the one it has. The fix has to reach the Go toolchain, patching it to emit Gramine's call-based syscall ABI; rewriting the syscall instructions in the built binary was rejected as unreliable and hard to maintain in discussion #2008, where Gramine's developers lay out the Go case.