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.