GoSuda

HQQ: Isolering af C-biblioteker fra Go via delt hukommelse på cache-linjeniveau

By Lemon Mint views ...

gosuda.org/hqq er en zero-cgo runtime for interproceskommunikation og et kodegenereringsværktøjssæt for Go og C11. Det isolerer native C-biblioteker i separate OS-arbejdsprocesser og forbinder dem med Go-kaldere via hukommelsesafbildede, cache-linje-justerede delte hukommelsesringbuffere. Projektet kombinerer to transportniveauer (StandardLink for dedikerede single-producer single-consumer-sessioner og Advanced for multi-worker MPMC-dispatch), hqq-bindgen IDL-compileren og en manipulationsverificeret procesovervåger (secureexec og launch).

1go get gosuda.org/hqq

Omkostningen ved at dele et adresserum med C

Enhver Go-systemingeniør står før det samme vejvalg. Du bygger en Go-tjeneste, der kompileres på få sekunder med CGO_ENABLED=0, distribueres som en enkelt statisk binærfil, krydskompileres fra en laptop til Linux- eller Windows-mål uden et sysroot og kører i en hukommelsessikker, affaldsindsamlet heap. Derefter har du brug for et modent C-bibliotek: SQLite til indlejret relationel lagring, Zstandard (libzstd) til ordbogsbaseret komprimering eller et hardwareaccelereret medie- eller kryptografibibliotek.

Ved at aktivere cgo for at linke C-biblioteket ind i Go-processen løses det umiddelbare bindingsproblem, men det introducerer tre strukturelle ulemper:

  1. Kobling mellem værktøjskæde og krydskompilering: CGO_ENABLED=1 kræver en C-compiler, C-bibliotekets headere for målet og en platformslinker for hvert mål-OS og hver arkitektur. Opbygning af linux/arm64- eller windows/arm64-artefakter fra en darwin/arm64-arbejdsstation gør et enkelt go build-kald til en matrix af krydskompilere og sysroots.
  2. Overhead ved runtime-planlægning: Hvert cgo-kald krydser grænsen til Go-runtime, skifter fra en voksende goroutine-stak til en systemstak og fastlåser en OS-tråd (M). For finkornede kald akkumuleres overgangsomkostningen (~50 til 150 ns per kald) og planlæggerkonkurrencen hurtigt.
  3. Kollaps af fejldomæne: C-kode kører i samme virtuelle adresserum som Go-runtime. Et enkelt skrivning uden for grænserne, use-after-free, stakoverløb eller abort() inde i en C-funktion korrumperer Go-heap-metadata eller afslutter hele serverprocessen med det samme, under omgåelse af recover() og tager alle igangværende HTTP- eller RPC-anmodninger med sig.

Når teams forsøger at undgå cgo, tvinger de almindelige alternativer til et andet kompromis. Rene Go-mekaniske oversættelser af C-kodebaser bevarer CGO_ENABLED=0, men halter ofte efter upstream C-udgivelser, medfører et emuleret virtuelt hukommelseslag og allokerer kraftigt under belastning. Flytning af C-biblioteket til en hjælpeproces over loopback-TCP eller Unix-domænesockets genskaber fejlisolering, men hvert kald betaler for to kernelkontekstskift, kopiering af socketbuffere og wire-serialisering.

Vi byggede HQQ ud fra et direkte spørgsmål: Hvad hvis C-biblioteket levede i en separat OS-proces med sit eget virtuelle adresserum, mens Go og C udvekslede rammer direkte gennem delte CPU-cache-linjer uden at gå ind i kernen på den hurtige sti?

IntegrationsmodelCGO_ENABLED=0FejlisoleringSyscalls på hurtig stiHeap-allokeringer pr. kaldUpstream C11-troskab
cgo In-Process-linkingNejIngen (delt heap)00 til 1+Præcis upstream C
Socket / Pipe-underprocess-RPCJaProcesgrænse2 til 4Flere (indramning + I/O)Præcis upstream C
Mekanisk C-til-Go-oversættelseJaGo i processen0Høj ved komplekse kaldEmuleret C-runtime
HQQ Shared-Memory IPCJaProcesgrænse0 (adaptiv spin)0 (StandardLink)Præcis upstream C

Arkitektonisk oversigt: Fire lag på tværs af procesgrænser

HQQ adskiller procesisolering i fire sammensættelige lag, så mekanikken for delte hukommelsesringbuffere, typede RPC-kodegenereringer, binær integritetsverifikation og supervision af underprocesser forbliver uafhængige.

 1+-------------------------------------------------------------------+
 2| Go Application Process (CGO_ENABLED=0)                            |
 3|                                                                   |
 4|  +-----------------------+     +-------------------------------+  |
 5|  | Generated Go Client   |     | Process Supervisor            |  |
 6|  | (gosuda.org/hqq/      |     | (gosuda.org/hqq/launch +      |  |
 7|  |  bindgen/rt)          |     |  gosuda.org/hqq/secureexec)   |  |
 8|  +-----------+-----------+     +---------------+---------------+  |
 9|              |                                 |                  |
10|  +-----------v-----------+                     | Anonymous Pipe   |
11|  | Transport Tier        |                     | Lifeline         |
12|  | (StandardLink /       |                     | (--parent-stdin) |
13|  |  Advanced MPMC)       |                     |                  |
14+--------------+---------------------------------+------------------+
15               |                                 |
16===============|========= Shared Memory =========|===================
17               |     (mmap / CreateFileMapping)  |
18+--------------v---------------------------------v------------------+
19| C11 Worker Process (Isolated Address Space)                       |
20|                                                                   |
21|  +-----------------------+     +-------------------------------+  |
22|  | Generated C11 Server  |---->| Generation-Checked Handle     |  |
23|  | & Batch Shims         |     | Table (hqq_rpc.c)             |  |
24|  +-----------+-----------+     +---------------+---------------+  |
25|              |                                 |                  |
26|              +----------------+----------------+                  |
27|                               |                                   |
28|                     +---------v---------+                         |
29|                     | Native C Library  |                         |
30|                     | (SQLite, Zstd...) |                         |
31|                     +-------------------+                         |
32+-------------------------------------------------------------------+

1. Transport via delt hukommelse (gosuda.org/hqq og c/)

Basispakken gosuda.org/hqq og dens C11-modstykke i c/ implementerer to transporttilstande via delt hukommelse over POSIX mmap (Linux og macOS) og Win32 CreateFileMappingW (Windows). Hvert delte segment begynder med en verificeret HQQ_STL3-superblok, der fastlægger protokolversion, bufferGeometri og endpoint-PID'er, før noget ringindeks læses.

Alle varme kontrolord og slot-headere er justeret til 64-byte cache-linjegrænser, så producent- og forbrugerkerner aldrig konkurrerer om samme cache-linje via falsk deling.

1StandardLink Slot Layout (64-Byte Cache-Line Aligned)
2Byte Offset:  0x00                0x08                0x10                          0x40
3              +-------------------+-------------------+-----------------------------+
4Header Line:  | seq (u64 atomic)  | len (u32) | flags | reserved (zero-padded)      |
5              +-------------------+-------------------+-----------------------------+
6Payload Area: | payload bytes [0 .. BufferSize - 1] (directly encoded / decoded)    |
7              +---------------------------------------------------------------------+
FeltOffsetStørrelseSemantik
seq0x008 BMonotont sekvenstæller. Ulige værdier markerer en igangværende skrivning; lige værdier offentliggør en committet ramme via store-release.
len0x084 BPayloadens bytelængde skrevet i slotten (0 <= len <= BufferSize).
flags0x0C4 BRammekontrolbits (fortsættelse, fejlstatus eller lukningsbesked).
payload0x40BufferSizeSammenhængende payloadområde, der læses og skrives in place uden mellemliggende kopier.

Kaldere vælger mellem to transportniveauer afhængigt af samtidighedsstrukturen:

  • StandardLink (1:1 SPSC-session): Parrer en klient- og en serversession over dedikerede anmodnings- og svarslots beskyttet af en sekvenslås (SeqLock). På den hurtige sti serialiserer skriveren direkte ind i den afbildede slot og committer med et atomisk store-release; læseren spinner kort i brugerrummet, før den falder tilbage til OS-vækkeprimitiver (futex på Linux, __ulock_wait på macOS, WaitOnAddress på Windows). En enkelt rammes round trip fuldføres på ~200 ns med 0 allocs/op.
  • Advanced (MPMC-dispatch-ring): Multiplekser M samtidige Go-kaldere over N C-arbejdertråde ved hjælp af en låsefri multi-producer multi-consumer-indsendelsesring, en delt slotpulje og en indekseret færdiggørelsestabel. Kaldere reserverer en slot, offentliggør slotindekset til ringen og venter på deres færdiggørelsespost, mens arbejdertrådene udtager og færdiggør anmodninger uden for rækkefølge.

Begge niveauer overvåger løbende peers levetid. Hvert endpoint opdaterer et atomisk monotont tidsstempel via Heartbeat() og undersøger PeerAlive(timeout) under ventetiden. Hvis en arbejdsproces går ned midt i et kald, registrerer peeren det forældede heartbeat eller den døde PID med det samme, afbryder blokerede ventetider med ErrPeerGone og understøtter atomisk session-Takeover uden at efterlade hængende delte hukommelsesfiler.

2. Nul-allokerings-RPC og generationskontrollerede handles (gosuda.org/hqq/bindgen)

At skrive indramning af delt hukommelse i hånden til et C-bibliotek med snesevis af funktioner er kedeligt og fejlbehæftet. hqq-bindgen er en skemakompiler, der læser en Interface Definition Language-fil (.idl) (eller opretter en direkte fra en C-.h-header via hqq-bindgen init) og udsender en zero-cgo Go-klientpakke sammen med et C11-serverskelet.

Den farligste fejl i enhver C-binding er at eksponere rå C-pointere (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) over sproggrænsen. Hvis en kalder sender en pointer efter at have frigivet den, sender en pointer, der tilhører en anden session, eller korrumperer en heltalsadresse, dereferencer C-biblioteket ugyldig hukommelse.

hqq-bindgen forbyder rå pointere på wiren. I stedet modellerer IDL'en stateful C-ressourcer som typede handle-deklarationer:

1service sqlite3 {
2    handle Db;
3    handle Stmt @owned_by(Db);
4
5    fn open_v2(path: string, flags: i32) -> result<Db, i32>;
6    fn prepare_one(db: Db, sql: string) -> result<Stmt, i32>;
7    fn finalize(stmt: @consume Stmt) -> result<void, i32>;
8    fn close(db: @consume Db) -> result<void, i32>;
9}

På wiren er hver handle et 64-bit heltal, der pakker et 32-bit tabelindeks og en 32-bit generationstæller forskellig fra nul: (uint64_t)generation << 32 | index. C-runtime (c/src/hqq_rpc.c) vedligeholder en handle-tabel pr. session med tre invarianter:

  1. Generationsvalidering: Hvert opslag verificerer, at slotten er optaget, at dens lagrede generation svarer til de øverste 32 bit af handlen, og at dens handle-typetag svarer til parametertypen (Db vs. Stmt). Genbrug af en lukket handle fejler straks med HQQRPC_E_BAD_HANDLE, før der kaldes ind i C-biblioteket.
  2. Hierarkisk ejerskab (@owned_by): Fordi Stmt er annoteret med @owned_by(Db), går lukning af en Db-handle automatisk igennem alle levende børne-Stmt-handles, der ejes af denne Db, og kører deres destruktorer i omvendt allokeringsrækkefølge (sqlite3_finalize før sqlite3_close_v2).
  3. Oprydningsfejning ved session-nedlukning: Hvis en Go-klient afbryder forbindelsen eller går ned uden at lukke sine handles, fejer sessionsdestruktoren på C-serveren hele session-handle-tabellen i omvendt rækkefølge og garanterer nul lækkede C-allokeringer på tværs af klientgenstarter.

Før en session behandler sit første RPC-kald, udveksler klient og server et 32-byte SHA-256-interfacedigest beregnet over den kanoniserede IDL (præfiks HQQ-BINDGEN-IDL-V1\n). Hvis Go-klienten og C-arbejderen er kompileret fra inkompatible IDL-skemaer, afviser håndtrykket forbindelsen ved opstart.

3. Manipulationsfri binær eksekvering (gosuda.org/hqq/secureexec)

At flytte C-kode ind i en arbejderbinærfil rejser et spørgsmål om forsyningskæden: Hvordan ved Go-processen, at arbejderbinærfilen på disken ikke blev ændret eller erstattet mellem verifikation og eksekvering?

Kald af crypto/sha256 på en filsti efterfulgt af os/exec.Command(path).Start() efterlader et Time-of-Check to Time-of-Use (TOCTOU)-vindue, hvor en anden proces kan overskrive eller udskifte filen, før kernen eksekverer den. gosuda.org/hqq/secureexec lukker dette vindue:

  • På Linux åbner secureexec binærfilen og streamer dens indhold ind i en anonym hukommelsesfil-deskriptor oprettet via memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING), mens dens SHA-256-hash beregnes. Den verificerer hashen mod et fastlagt 32-byte digest eller et Ed25519-signeret udgivelsesmanifest (cmd/hqq-manifest), anvender fcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE), så ingen proces på systemet kan ændre siderne, og starter barnet fra den forseglede deskriptor.
  • På macOS og Windows materialiserer secureexec verificerede binærfiler i tilladelseslåste cache-mapper, der indekseres efter SHA-256-digest, og verificerer filstørrelse og digest-integritet før opstart.

4. Forælder-livline-procesovervågning (gosuda.org/hqq/launch)

Når en Go-applikation starter en baggrunds-C-arbejderproces, må arbejderen aldrig overleve sin forælder som en forældreløs proces, selv hvis Go-forælderen afsluttes med SIGKILL (kill -9) og ikke kan køre defer-oprydningshandlere.

gosuda.org/hqq/launch håndhæver livstidskobling via en anonym OS-pipe, der sendes til arbejderens standardinput med flaget --parent-stdin. Go-forælderen holder pipens skrivende ende og skriver aldrig til den; C-arbejderen overvåger den læsende ende på en dedikeret livlinetråd. Hvis Go-processen afslutter normalt, går i panik eller dræbes af OS-kernen (SIGKILL eller OOM-dræberen), lukker kernen straks skrivedeskriptoren. C-arbejderen modtager EOF på pipen inden for mikrosekunder, afbryder eventuelle aktive kald, kører sine session-handle-destruktorer, fjerner de delte hukommelsesfiler (unlink) og afslutter rent.

Hurtigstart: Kald af en genereret tjeneste

Med hqq-bindgen og launch ser kald af en isoleret C-arbejder fra Go ud som en almindelig typet Go-klient, samtidig med at CGO_ENABLED=0 bevares:

 1package main
 2
 3import (
 4	"context"
 5	"fmt"
 6	"log"
 7	"time"
 8
 9	"gosuda.org/hqq/bindgen/rt"
10	"gosuda.org/hqq/launch"
11	"gosuda.org/hqq/secureexec"
12)
13
14func main() {
15	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
16	defer cancel()
17
18	hash, err := secureexec.ParseHash("7649b1b45dacd934cde8149f35f4fcc3937f6486fbac23ce976466719b3752bb")
19	if err != nil {
20		log.Fatal(err)
21	}
22
23	listener, err := launch.NewListenerName("demo")
24	if err != nil {
25		log.Fatal(err)
26	}
27
28	srv, err := launch.Start(ctx, launch.Config{
29		Binary:   "/path/to/verified-worker",
30		SHA256:   hash,
31		Listener: listener,
32		Args:     []string{listener},
33	})
34	if err != nil {
35		log.Fatal(err)
36	}
37	defer srv.Close()
38
39	fmt.Printf("worker running on shared-memory listener %s (opts=%+v)\n", listener, rt.SessionOptions{})
40}

Repository-struktur

Hvert delsystem i gosuda.org/hqq er organiseret i fokuserede pakker med eksplicitte grænser:

 1gosuda.org/hqq
 2├── standard.go / ring.go      # StandardLink (SPSC) og lavniveau-primitiver til delte hukommelsesringbuffere
 3├── advanced.go / slotpool.go  # Advanced (MPMC)-dispatcher, slotpulje og færdiggørelsestabel
 4├── shm_*.go / wait_*.go       # OS-specifik mmap/CreateFileMapping og futex/ulock/WaitOnAddress
 5├── c/                         # C11-transportbibliotek (hqq.c) og RPC-serverruntime (hqq_rpc.c)
 6├── bindgen/                   # hqq-bindgen IDL-parser, verifikator (V1-V41) og Go/C11-kodegenerator
 7│   └── rt/                    # Nul-allokerings Go-klientruntime for genererede tjenester
 8├── secureexec/                # SHA-256 / Ed25519-manifestverifikation og memfd-forseglet eksekvering
 9├── launch/                    # Arbejderprocesovervåger med --parent-stdin-livlinehåndtering
10├── cmd/hqq-manifest/          # CLI-værktøj til generering og signering af manifester for binære udgivelser
11└── spec/                      # Eksekverbare wire-specifikationer og golden test-vektorer (BINDGEN.md)

For at se hvordan HQQ driver drop-in produktionserstatninger for to af de mest udbredte cgo-biblioteker i Go-økosystemet, se de medfølgende pakker bygget på denne stak:

  • gosuda.org/sqlite3: Drop-in database/sql-driver kompatibel med github.com/mattn/go-sqlite3, der kører native C SQLite 3.54.1 over delt hukommelse med pipelinet batch-prefetching.
  • gosuda.org/zstd: Drop-in erstatning for github.com/DataDog/zstd, der kører native C libzstd 1.5.7 i en isoleret arbejder med delt hukommelse, med nul-allokerings-kontekster og bulk-batching.
  • GitHub-repository: https://github.com/gosuda/HQQ