GoSuda

HQQ: C-kirjastojen eristäminen Go-kielestä välimuistirivillä jaetun muistin avulla

By Lemon Mint views ...

gosuda.org/hqq on cgo-vapaa prosessien välinen kommunikointiajoaika (inter-process communication runtime) ja koodigenerointityökalu Go:lle ja C11:lle. Se eristää natiivit C-kirjastot erillisiin käyttöjärjestelmän worker-prosesseihin ja yhdistää ne Go-kutsujiin muistikartoitettujen, välimuistirivin mukaan kohdistettujen jaettujen muistirenkaiden (ring) kautta. Projekti yhdistää kaksi siirtotasoa (StandardLink yksittäisille SPSC-istunnoille ja Advanced moniworkerisille MPMC-jakelulle), hqq-bindgen IDL-kääntäjän sekä todennetun prosessivalvojan (secureexec ja launch).

1go get gosuda.org/hqq

Jaetun osoiteavaruuden hinta C-koodin kanssa

Jokainen Go-järjestelmäinsinööri kohtaa lopulta saman haarautumiskohdan. Rakennat Go-palvelua, joka kääntyy sekunneissa komennolla CGO_ENABLED=0, toimitetaan yksittäisenä staattisena binäärinä, ristikääntyy kannettavalta tietokoneelta Linux- tai Windows-kohteisiin ilman sysroot-hakemistoa ja ajetaan muistiturvallisen, roskienkeruun hallitseman keon sisällä. Sitten tarvitset kypsän C-kirjaston: SQLiten upotettuun relaatiotietokantatallennukseen, Zstandardin (libzstd) sanakirjapakkaukseen tai laitekiihdytetyn media- tai salauskirjaston.

C-kirjaston linkittäminen Go-prosessiin cgo:n avulla ratkaisee välittömän sidontaongelman, mutta tuo mukanaan kolme rakenteellista riskiä:

  1. Työkaluketjun ja ristikääntämisen kytkentä: CGO_ENABLED=1 edellyttää C-kääntäjää, kohde-libc-otsikkotiedostoja ja alustakohtaisen linkittäjän jokaiselle kohde-käyttöjärjestelmälle ja -arkkitehtuurille. linux/arm64- tai windows/arm64-artefaktien rakentaminen darwin/arm64-työasemalta muuttaa yksittäisen go build -komennon ristikääntäjien ja sysroot-hakemistojen matriisiksi.
  2. Ajonaikaisen ajastuksen ylikuormitus: Jokainen cgo-kutsu ylittää Go-ajonaikaisen rajan, vaihtaa kasvavasta goroutine-pinosta järjestelmäpinoon ja sitoo käyttöjärjestelmän säikeen (M). Hienojakoisissa kutsuissa siirtymäkustannus (noin 50 ja 150 ns kutsua kohden) sekä ajastimen kilpailu kertyvät nopeasti.
  3. Vikaalueen romahtaminen: C-koodi toimii samassa virtuaalisessa osoiteavaruudessa kuin Go-ajonaikainen. Yksi ulkopuolinen kirjoitus, käyttö vapautetun muistin jälkeen, pinon ylivuoto tai abort() C-funktion sisällä korruptoi Go-keon metatiedot tai lopettaa koko palvelinprosessin välittömästi ohittaen recover()-kutsun ja vieden mukanaan jokaisen käsiteltävänä olevan HTTP- tai RPC-pyynnön.

Kun tiimit yrittävät välttää cgo:n, yleiset vaihtoehdot pakottavat toisenlaiseen kompromissiin. Puhtaasti Go-kielisiin mekaanisiin käännöksiin perustuvat C-koodikantojen muunnokset säilyttävät CGO_ENABLED=0:n, mutta jäävät usein jälkeen C-julkaisuista, sisältävät emuloidun virtuaalimuistikerroksen ja varaavat kuormituksen alla runsaasti muistia. C-kirjaston siirtäminen aputtajaan loopback-TCP:n tai Unix-domain-socketien kautta palauttaa vikaeristyksen, mutta jokainen kutsu maksaa kahdesta ytimen kontekstinvaihdosta, socket-puskurikopioinneista ja siirtoformaatin serialisoinnista.

Rakensimme HQQ:n esitettyämme yhden suoran kysymyksen: entä jos C-kirjasto asuisi erillisessä käyttöjärjestelmäprosessissa omalla virtuaalisella osoiteavaruudellaan, ja Go sekä C vaihtaisivat kehyksiä suoraan jaettujen CPU-välimuistirivien kautta ilman ytimeen siirtymistä nopealla polulla?

IntegraatiomalliCGO_ENABLED=0VikaeristysNopean polun järjestelmäkutsutKutsukohtaiset keon allokaatiotUpstream C11 -uskollisuus
cgo-sisäinen linkitysEiEi (jaettu keko)00 tai enemmänTarkka upstream C
Socket / putki -aliprosessi RPCKylläProsessiraja2–4Useita (kehystys + I/O)Tarkka upstream C
Mekaaninen C-to-Go-käännösKylläProsessinsisäinen Go0Korkea monimutkaisissa kutsuissaEmuloitu C-ajonaikainen
HQQ jaetun muistin IPCKylläProsessiraja0 (adaptiivinen odotus)0 (StandardLink)Tarkka upstream C

Arkkitehtuurin yleiskuvaus: neljä kerrosta prosessirajojen yli

HQQ erottaa prosessieristyksen neljään yhdisteltävään kerrokseen, jotta jaetun muistin renkaiden mekaniikka, tyypitetty RPC-koodigenerointi, binäärien eheyden varmistaminen ja lapsiprosessien valvonta pysyvät toisistaan riippumattomina.

 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. Jaetun muistin siirto (gosuda.org/hqq ja c/)

Peruspaketti gosuda.org/hqq ja sen C11-vastine hakemistossa c/ toteuttavat kaksi jaetun muistin siirtotilaa POSIX-mmap:n (Linux ja macOS) sekä Win32-CreateFileMappingW:n (Windows) päällä. Jokainen jaettu segmentti alkaa todennetulla HQQ_STL3-superlohkolla, joka kiinnittää protokollaversion, puskurigeometrian ja päätepisteiden PID-tunnisteet ennen kuin mitään renkaan indeksiä luetaan.

Kaikki aktiiviset ohjaussanat ja slotien otsikot on kohdistettu 64 tavun välimuistirivin rajoille, jotta tuottaja- ja kuluttajaytimet eivät kilpaile samasta välimuistirivistä väärän jakamisen (false sharing) kautta.

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              +---------------------------------------------------------------------+
KenttäSiirtymäKokoMerkitys
seq0x008 BMonotoninen sekvenssilaskuri. Parittomat arvot merkitsevät kesken olevaa kirjoitusta; parilliset arvot julkaisevat vahvistetun kehyksen store-release-operaatiolla.
len0x084 BSlottiin kirjoitetun hyötykuorman tavupituus (0 <= len <= BufferSize).
flags0x0C4 BKehyksen ohjausbitit (jatkuvuus, virhetila tai sammutusilmoitus).
payload0x40BufferSizeYhtenäinen hyötykuormaalue, johon luetaan ja josta kirjoitetaan paikallaan ilman välikopiointeja.

Kutsuja valitsee kahden siirtotason välillä samanaikaisuusrakenteen mukaan:

  • StandardLink (1:1 SPSC-istunto): Muodostaa client- ja server-istuntoparin omistettujen pyyntö- ja vastausslottien kautta, joita suojaa sekvenssilukko (SeqLock). Nopealla polulla kirjoittaja serialisoi tiedot suoraan kartoitettuun slottiin ja vahvistaa ne atomisella store-release-operaatiolla. Lukija odottaa lyhyesti käyttäjätilassa ennen kuin siirtyy käyttöjärjestelmän herätysprimitiiveihin (futex Linuxilla, __ulock_wait macOS:llä, WaitOnAddress Windowsilla). Yhden kehyksen kierros valmistuu noin 200 ns:ssa ja 0 allocs/op.
  • Advanced (MPMC-jakelurengas): Multipleksoi M samanaikaista Go-kutsujaa N C-worker-säikeelle lukkovapaan moniesittäjä-moniottajan lähetysrenkaan, jaetun slottialtaan ja indeksoidun valmistumistaulun avulla. Kutsujat varaavat slotin, julkaisevat slotin indeksin renkaaseen ja odottavat valmistumismerkintäänsä, kun worker-säikeet poistavat pyyntöjä jonosta ja suorittavat ne epäjärjestyksessä.

Molemmat tasot seuraavat vastapuolen elossaoloa jatkuvasti. Jokainen päätepiste päivittää atomisen monotonisen aikaleiman Heartbeat()-kutsulla ja tarkastaa PeerAlive(timeout)-funktiolla odottaessaan. Jos worker-prosessi kaatuu kesken kutsun, vastapuoli havaitsee vanhentuneen sykkeen tai kuolleen PID:n välittömästi, keskeyttää estävät odotukset virheellä ErrPeerGone ja tukee atomista istunnon Takeover-siirtoa jättämättä jäljelle jumiutuneita jaetun muistin tiedostoja.

2. Nollakeon RPC ja sukupolvitarkistetut kahvat (gosuda.org/hqq/bindgen)

Jaetun muistin kehystyksen kirjoittaminen käsin C-kirjastolle, jossa on kymmeniä funktioita, on työlästä ja virhealtista. hqq-bindgen on skeemakääntäjä, joka lukee Interface Definition Language (.idl) -tiedoston (tai alustaa sellaisen suoraan C-.h-otsikkotiedostosta komennolla hqq-bindgen init) ja tuottaa cgo-vapaan Go-client-paketin sekä C11-palvelinrungon.

Yleisin vakava virhe missä tahansa C-sidonnassa on paljastaa raakoja C-osoittimia (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) kielirajan yli. Jos kutsuja välittää osoittimen sen jälkeen, kun se on vapautettu, välittää osoittimen toiselle istunnolle tai korruptoi kokonaislukuosoitteen, C-kirjasto dereferoi virheellisen muistin.

hqq-bindgen kieltää raa'at osoittimet langalla. Sen sijaan IDL mallintaa tilalliset C-resurssit tyypitettyinä handle-määrittelyinä:

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}

Langalla jokainen kahva on 64-bittinen kokonaisluku, joka pakkaa 32-bittisen taulukkoindeksin ja 32-bittisen nollasta poikkeavan sukupolvilaskurin: (uint64_t)generation << 32 | index. C-ajonaikainen (c/src/hqq_rpc.c) ylläpitää istuntokohtaista kahvataulukkoa, jossa on kolme invarianttia:

  1. Sukupolven validointi: Jokainen haku varmistaa, että slotti on varattu, sen tallennettu sukupolvi vastaa kahvan ylempiä 32 bittiä ja sen kahvatyypin tunniste vastaa parametrin tyyppiä (Db vs. Stmt). Suljetun kahvan uudelleenkäyttö epäonnistuu välittömästi virheellä HQQRPC_E_BAD_HANDLE ennen kuin C-kirjastoa kutsutaan.
  2. Hierarkkinen omistajuus (@owned_by): Koska Stmt on merkitty tunnisteella @owned_by(Db), Db-kahvan sulkeminen käy automaattisesti läpi kaikki sen omistamat elävät lapsi-Stmt-kahvat ja suorittaa niiden tuhoajat käänteisessä allokointijärjestyksessä (sqlite3_finalize ennen sqlite3_close_v2:ta).
  3. Istunnon purkupyyhkäisy: Jos Go-client katkeaa tai kaatuu sulkematta kahvojaan, C-palvelimen istunnon tuhoaja pyyhkii koko istuntokahvataulukon käänteisessä järjestyksessä. Tämä takaa, ettei C-allokaatioita vuoda clientin uudelleenkäynnistysten yli.

Ennen kuin istunto käsittelee ensimmäisen RPC-kutsun, client ja palvelin vaihtavat 32 tavun SHA-256-rajapintatiivisteen, joka on laskettu kanonisoidusta IDL:stä (etuliite HQQ-BINDGEN-IDL-V1\n). Jos Go-client ja C-worker on käännetty yhteensopimattomista IDL-skeemoista, kättely hylkää yhteyden jo käynnistyksessä.

3. Väärennyksetön binäärien suoritus (gosuda.org/hqq/secureexec)

C-koodin siirtäminen worker-binääriin nostaa toimitusketjukysymyksen: miten Go-prosessi tietää, ettei levyllä oleva worker-binääri ole muuttunut tai korvattu tarkastuksen ja suorituksen välillä?

Jos crypto/sha256 kutsutaan tiedostopolulle ja sen jälkeen suoritetaan os/exec.Command(path).Start(), jää Time-of-Check to Time-of-Use (TOCTOU) -ikkuna, jonka aikana toinen prosessi voi ylikirjoittaa tai vaihtaa tiedoston ennen kuin ydin suorittaa sen. gosuda.org/hqq/secureexec sulkee tämän ikkunan seuraavasti:

  • Linuxilla secureexec avaa binäärin, virtaa sen sisällön anonyymiin muistitiedostokuvaajaan, joka luodaan kutsulla memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING), ja laskee samalla sen SHA-256-tiivisteen. Se vertaa tiivistettä kiinnitettyyn 32 tavun tiivisteeseen tai Ed25519-allekirjoitettuun julkaisumanifestiin (cmd/hqq-manifest), soveltaa kutsua fcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE), jotta mikään järjestelmän prosessi ei voi muuttaa sivuja, ja käynnistää lapsiprosessin sinetöidystä kuvaajasta.
  • macOS:llä ja Windowsilla secureexec materialisoi todennetut binäärit lukittuihin käyttöoikeuksiin sidottuihin välimuistihakemistoihin, jotka on nimetty SHA-256-tiivisteen mukaan, ja tarkistaa tiedoston koon sekä tiivisteen eheyden ennen käynnistystä.

4. Vanhempi-yhteyden prosessivalvonta (gosuda.org/hqq/launch)

Kun Go-sovellus käynnistää taustalla C-worker-prosessin, worker ei saa koskaan jäädä orvoksi vanhempansa kuoleman jälkeen, vaikka Go-vanhempi lopetettaisiin komennolla SIGKILL (kill -9) eikä se ehtisi suorittaa defer-siivoustoimintoja.

gosuda.org/hqq/launch pakottaa elinkaariyhteyden anonyymin käyttöjärjestelmän putken kautta, joka välitetään workerin vakiosyötteeseen lipulla --parent-stdin. Go-vanhempi pitää putken kirjoituspäätä eikä koskaan kirjoita siihen. C-worker valvoo lukupäätä omalla elinkaarisäikeellaan. Jos Go-prosessi päättyy normaalisti, kaatuu paniikkiin tai käyttöjärjestelmän ydin tappaa sen (SIGKILL tai OOM-killer), ydin sulkee kirjoituskuvaajan välittömästi. C-worker saa EOF-merkin putkesta muutamassa mikrosekunnissa, keskeyttää aktiiviset kutsut, suorittaa istuntokahvojen tuhoajat, poistaa jaetun muistin tiedostot linkityksistä ja poistuu siististi.

Pikaopas: generoidun palvelun kutsuminen

hqq-bindgen:n ja launch:n avulla eristetyn C-workerin kutsuminen Go:sta näyttää tavalliselta tyypitetyltä Go-clientilta säilyttäen samalla CGO_ENABLED=0:

 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}

Repositorion rakenne

Jokainen gosuda.org/hqq:n osajärjestelmä on jäsennelty kohdennettuihin paketteihin, joilla on selkeät rajat:

 1gosuda.org/hqq
 2├── standard.go / ring.go      # StandardLink (SPSC) ja matalan tason jaetun muistin rengasprimitiivit
 3├── advanced.go / slotpool.go  # Advanced (MPMC) -jakelija, slottialtaa ja valmistumistaulu
 4├── shm_*.go / wait_*.go       # Käyttöjärjestelmäkohtainen mmap/CreateFileMapping sekä futex/ulock/WaitOnAddress
 5├── c/                         # C11-siirtokirjasto (hqq.c) ja RPC-palvelimen ajonaikainen (hqq_rpc.c)
 6├── bindgen/                   # hqq-bindgen IDL-jäsennin, verifioija (V1–V41) ja Go/C11-koodigeneraattori
 7│   └── rt/                    # Nollakeon Go-clientin ajonaikainen generoiduille palveluille
 8├── secureexec/                # SHA-256/Ed25519-manifestin verifiointi ja memfd-sinetöity suoritus
 9├── launch/                    # Worker-prosessivalvoja, jossa --parent-stdin-elinkaarihallinta
10├── cmd/hqq-manifest/          # CLI-työkalu julkaisubinäärimanifestien generointiin ja allekirjoittamiseen
11└── spec/                      # Suoritettavat siirtomäärittelyt ja kultaiset testivektorit (BINDGEN.md)

Nähdäksesi, miten HQQ tarjoaa pudotettavat tuotantokorvikkeet kahdelle laajimmin käytetylle cgo-kirjastolle Go-ekosysteemissä, tutustu tähän pinoon rakennettuihin kumppanipaketteihin:

  • gosuda.org/sqlite3: Pudotettava database/sql-ajuri, joka on yhteensopiva github.com/mattn/go-sqlite3:n kanssa ja ajaa natiivia C SQLite 3.54.1:tä jaetun muistin yli putkitetulla eräesihakulla.
  • gosuda.org/zstd: Pudotettava korvike kirjastolle github.com/DataDog/zstd, joka ajaa natiivia C libzstd 1.5.7:ää eristetyssä jaetun muistin worker-prosessissa nollakeon konteksteilla ja eräkäsittelyllä.
  • GitHub-repositorio: https://github.com/gosuda/HQQ