GoSuda

HQQ: Isolering af C-biblioteker fra Go over Cache-Line Shared Memory

By Lemon Mint
views ...

gosuda.org/hqq er en zero-cgo interproceskommunikationsruntime og kodegenereringsværktøjskæde til Go og C11. Den isolerer indfødte C-biblioteker inde i separate OS-arbejdsprocesser og forbinder dem til Go-kaldere over hukommelsesafbildede, cachelinjejusterede delte hukommelsesringe. Projektet kombinerer to transportlag (StandardLink til dedikerede single-producer single-consumer-sessioner og Advanced for multi-worker MPMC-dispatch), hqq-bindgen IDL-kompileren og en manipulationsverificeret procesovervåger (secureexec og launch).

1go get gosuda.org/hqq

Prisen ved at dele et adresserum med C

Enhver Go-systemingeniør rammer uundgåeligt den samme skillevej. Du bygger en Go-tjeneste, der kompilerer på få sekunder med CGO_ENABLED=0, leveres som en enkelt statisk binær fil, krydskompilerer fra en bærbar computer til Linux- eller Windows-mål uden et sysroot og kører inde i en hukommelsessikker affaldsopsamlet heap. Derefter får du brug for et modent C-bibliotek: SQLite til indlejret relationel lagring, Zstandard (libzstd) til ordbogskomprimering eller et hardwareaccelereret medie- eller kryptografibibliotek.

Aktivering af cgo for at linke det pågældende C-bibliotek ind i Go-processen løser det umiddelbare bindingsproblem, samtidig med at der indføres tre strukturelle forpligtelser:

  1. Værktøjskæde- og krydskompileringskobling: CGO_ENABLED=1 kræver en C-kompiler, mål-libc-headerfiler og en platformslinker for hvert målopos og arkitektur. Opbygning af linux/arm64- eller windows/arm64-artikler fra en darwin/arm64-arbejdsstation forvandler en enkelt go build-kommando til en matrix af krydskompilere og sysroots.
  2. Runtime-planlægningsomkostninger: Hvert cgo-kald krydser Go-runtime-grænsen, skifter fra en voksbar goroutine-stak til en systemstak og fastlåser en OS-tråd (M). For finkornede kald ophobes overgangsskatten (~50 til 150 ns pr. kald) og planlæggerkonkurrencen hurtigt.
  3. Fejldomænekollaps: C-kode kører i det samme virtuelle adresserum som Go-runtime. En enkelt grænseoverskridende skrivning, brug-efter-frigivelse, stakoverløb eller abort() inde i en C-funktion korrumperer Go-heap-metadata eller afslutter hele serverprocessen øjeblikkeligt, idet den omgår recover() og tager enhver igangværende HTTP- eller RPC-forespørgsel med sig ned.

Når teams forsøger at undgå cgo, tvinger de almindelige alternativer til et andet kompromis. Rent-Go mekaniske oversættelser af C-kodebaser bevarer CGO_ENABLED=0, men halter ofte efter opstrøms C-udgivelser, bærer et emuleret virtuelt hukommelseslag og allokerer kraftigt under belastning. Flytning af C-biblioteket ind i en hjælpeproces over loopback-TCP eller Unix-domænesokler genopretter feilisolering, men hvert kald betaler for to kernekontekstskift, sokkelbufferkopier og trådeserialisering.

Vi byggede HQQ efter at have stillet 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-cachelinjer uden at gå ind i kernen på hurtigstien?

IntegrationsmodelCGO_ENABLED=0FejlisoleringHurtigsti-systemkaldPr. kald heap-allokeringerOpstrøms C11-trofasthed
cgo In-process LinkingNejIngen (delt heap)00 til 1+Nøjagtig opstrøms C
Socket / Pipe Subprocess RPCJaProcesgrænse2 til 4Flere (indramning + I/O)Nøjagtig opstrøms C
Mekanisk C-til-Go-oversættelseJaIn-process Go0Høj ved komplekse kaldEmuleret C-runtime
HQQ Shared-Memory IPCJaProcesgrænse0 (adaptiv spin)0 (StandardLink)Nøjagtig opstrøms C

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

HQQ opdeler procesisolering i fire sammensættelige lag, så delt hukommelsesring-mekanik, typet RPC-kodegenerering, binær integritetsverifikation og underprocesovervågning 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. Delt hukommelse-transport (gosuda.org/hqq og c/)

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

Alle varme kontrolord og slot-hoveder er justeret til 64-byte cachelintegrænser, så producent- og forbrugerkerner aldrig konkurrerer om den samme cachelinje gennem 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              +---------------------------------------------------------------------+
FeltForskydningStørrelseSemantik
seq0x008 BMonoton sekvenstæller. Ulige værdier markerer en igangværende skrivning; lige værdier offentliggør en committet ramme via store-release.
len0x084 BNyttelastens bтельный længde skrevet ind i slotten (0 <= len <= BufferSize).
flags0x0C4 BRammekontrolbit (fortsættelse, fejlstatus eller lukningsmeddelelse).
payload0x40BufferSizeSammenhængende nyttelastområde læst og skrevet på plads uden mellemliggende kopier.

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

  • StandardLink (1:1 SPSC-session): Parret en klient- og en serversession over dedikerede forespørgsels- og svarslots beskyttet af en sekvenslås (SeqLock). På hurtigstien serialiserer skribenten direkte ind i den afbildede slot og commits med en atomar store-release; læseren spinner kortvarigt i brugerrummet, før den falder tilbage til OS-vågneprimitiver (futex på Linux, __ulock_wait på macOS, WaitOnAddress på Windows). En enkelt-rums rundtur fuldendes på ~200 ns med 0 allocs/op.
  • Advanced (MPMC Dispatch Ring): Multiplexer M samtidige Go-kaldere på tværs af N C-arbejdertråde ved hjælp af en låsefri multi-producer multi-consumer indsendelsesring, en delt slot-pool og en indekseret fuldførelsestabel. Kaldere reserverer en slot, offentliggør slotindekset til ringen og venter på deres fuldførelsesindgang, mens arbejdertråde tager ud af kø og fuldfører forespørgsler ude af rækkefølge.

Begge lag sporer ligemandslevetid kontinuerligt. Hvert slutpunkt opdaterer et atomart monotonisk tidsstempel via Heartbeat() og inspicerer PeerAlive(timeout) under ventetiden. Hvis en arbejdsproces går ned midt i et kald, opdager ligemanden den forældede hjerterytme eller døde PID øjeblikkeligt, afbryder blokerede ventetider med ErrPeerGone og understøtter atomar sessions-Takeover uden at efterlade fastklemte delte hukommelsesfiler.

2. Nul-allokerings-RPC og generationskontrollerede håndtag (gosuda.org/hqq/bindgen)

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

Den farligste fejl i enhver C-binding er at eksponere rå C-pointere (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) på tværs af 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 heltaladresse, derefererer C-biblioteket ugyldig hukommelse.

hqq-bindgen forbyder rå pointere på tråden. I stedet modellerer IDL tilstandsrige 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å tråden er hvert håndtag et 64-bit heltal, der pakker et 32-bit tabelindeks og en 32-bit ikke-nul generationstæller: (uint64_t)generation << 32 | index. C-runtime (c/src/hqq_rpc.c) opretholder en pr. session håndtagstabel med tre invariante egenskaber:

  1. Generationsvalidering: Hvert opslag verificerer, at slotten er optaget, at dens lagrede generation matcher de øverste 32 bit af håndtaget, og at dens håndtagstypetag matcher parametertypen (Db vs. Stmt). Genbrug af et lukket håndtag fejler øjeblikkeligt med HQQRPC_E_BAD_HANDLE inden kald ind i C-biblioteket.
  2. Hierarkisk ejerskab (@owned_by): Fordi Stmt er annoteret med @owned_by(Db), vil lukning af et Db-håndtag automatisk gennemgå alle levende underordnede Stmt-håndtag ejet af det pågældende Db og køre deres destruktorer i omvendt allokeringsrækkefølge (sqlite3_finalize før sqlite3_close_v2).
  3. Sessionsnedlukningssvejsning: Hvis en Go-klient afbryder forbindelsen eller går ned uden at lukke sine håndtag, gennemsøger C-serverens sessionsdestruktor hele sessionshåndtagstablen i omvendt rækkefølge, hvilket garanterer nul lækkede C-allokeringer på tværs af klientgenstart.

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

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

Flytning af C-kode ind i en arbejderbinær rejser et forsyningskædespørgsmål: Hvordan ved Go-processen, at arbejderens binære fil på disken ikke blev ændret eller udskiftet mellem verifikation og eksekvering?

Kald af crypto/sha256 på en filsti efterfulgt af os/exec.Command(path).Start() efterlader et Tids-af-kontrol til Tids-af-brug (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 den binære fil, streamer dens indhold ind i en anonym hukommelsesfildeskriptor oprettet via memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING), mens den beregner sit SHA-256-hash, verificerer hashet mod et fastlåst 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 mutere siderne, og spawner barnet fra den forseglede deskriptor.
  • På macOS og Windows materialiserer secureexec verificerede binære filer i tilladelseslåste cache-mapper nøglet efter SHA-256-digest og verificerer filstørrelse og digest-integritet før opstart.

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

Når en Go-applikation spawner en baggrunds-C-arbejdsproces, 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-rydningshandlinger.

gosuda.org/hqq/launch håndhæver levetidskobling gennem et anonymt OS-rør, der sendes til arbejderens standardinput med --parent-stdin-flaget. Go-forælderen holder skriveenden af røret og skriver aldrig til den; C-arbejderen overvåger læseenden på en dedikeret livlinetråd. Hvis Go-processen afslutter normalt, panikker eller dræbes af OS-kernen (SIGKILL eller OOM killer), lukker kernen øjeblikkeligt skrivedeskriptoren. C-arbejderen modtager EOF på røret inden for mikrosekunder, afbryder eventuelle aktive kald, kører sine sessionshåndtagsdestruktorer, fjerner linkene til de delte hukommelsesfiler og afsluttes rent.

Hurtigstart: Kald af en genereret tjeneste

Med hqq-bindgen og launch ligner kald af en isoleret C-arbejder fra Go en almindelig typet Go-klient, mens 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}

Repositoriestruktur

Hvert undersystem 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 delte hukommelsesring-primitiver
 3├── advanced.go / slotpool.go  # Advanced (MPMC) dispatcher, slot-pool og fuldfø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 til genererede tjenester
 8├── secureexec/                # SHA-256 / Ed25519 manifestverifikation og memfd-forseglet eksekvering
 9├── launch/                    # Arbejdsprocesovervåger med --parent-stdin-livlinestyring
10├── cmd/hqq-manifest/          # CLI-værktøj til generering og signering af udgivelsesbinære manifester
11└── spec/                      # Eksekverbare trådspecifikationer og gyldne testvektorer (BINDGEN.md)

For at se, hvordan HQQ driver drop-in produktionserstatninger for to af de mest brugte cgo-biblioteker i Go-økosystemet, kan du tjekke 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 indfødt C SQLite 3.54.1 over delt hukommelse med pipelined batch-præhentning.
  • gosuda.org/zstd: Drop-in erstatning for github.com/DataDog/zstd, der kører indfødt C libzstd 1.5.7 i en isoleret delt hukommelse-arbejder med nul-allokeringskontekster og bulk-batching.
  • GitHub Repository: https://github.com/gosuda/HQQ