GoSuda

HQQ: C-kirjastojen eristäminen Go-kielestä välimuistirivin (Cache-Line) jaetun muistin avulla

By Lemon Mint
views ...

gosuda.org/hqq on nolla-cgo-prosessienvälinen viestintäajonaikainen ympäristö ja koodin genereerointityökaluketju Golle ja C11:lle. Se eristää natiivit C-kirjastot omiin OS-työläisprosesseihinsa ja yhdistää ne Go-kutsujiin muistikartoitettujen, välimuistirivin mukaisten jaetun muistin renkaiden kautta. Projekti yhdistää kaksi siirtotasoa (StandardLink omistetuille yhden tuottajan ja yhden kuluttajan sessioille ja Advanced monityöläiselle MPMC-lähetykselle), hqq-bindgen IDL-kääntäjän sekä peukaloinnilta suojatun prosessin valvojan (secureexec ja launch).

1go get gosuda.org/hqq

Osoiteavaruuden jakamisen kustannukset C:n kanssa

Jokainen Go-järjestelmäsuunnittelija kohtaa lopulta saman risteyksen. Rakennat Go-palvelua, joka kääntyy sekunneissa komenolla CGO_ENABLED=0, jakautuu yhtenäisenä staattisena binäärinä, ristiinkääntyy kannettavasta tietokoneesta Linux- tai Windows-kohteisiin ilman sysrootia ja toimii muistiturvallisessa roskienkeruun kasassa. Sitten tarvitset kypsän C-kirjaston: SQLite sulautettuun relationaaliseen tallennukseen, Zstandard (libzstd) sanakirjapakkaukseen tai laitteistokiihdytetty media- tai kryptografiakirjasto.

cgon ottaminen käyttöön kyseisen C-kirjaston linkittämiseksi Go-prosessiin ratkaisee välittömän sidontaongelman ja tuo samalla mukanaan kolme rakenteellista vastuuta:

  1. Työkaluketjun ja ristiinkäännön sidonnaisuus: CGO_ENABLED=1 vaatii C-kääntäjän, kohde-libc-otsikot ja alustan linkittimen jokaiselle kohdekäyttöjärjestelmälle ja -arkkitehtuurille. linux/arm64- tai windows/arm64-tiedostojen rakentaminen darwin/arm64-työasemalta muuttaa yksittäisen go build -komennon ristiinkääntäjien ja sysrootien matriisiksi.
  2. Ajonaikaisen ajoituksen yleiskustannukset: Jokainen cgo-kutsu ylittää Go-ajon aikaisen rajan, vaihtaa kasvatettavasta gorutiinipinosta järjestelmäpinoon ja kiinnittää OS-säikeen (M). Hienojakoisissa kutsuissa siirtymävero (~50–150 ns kutsua kohden) ja ajoittimen kilpailu kasautuvat nopeasti.
  3. Virhealueen romahdus: C-koodi suoritetaan samassa virtuaalisessa osoiteavaruudessa kuin Go-ajoaika. Yksittäinen alueen ulkopuolinen kirjoitus, vapautuksen jälkeinen käyttö, pinon ylivuoto tai abort() C-funktion sisällä turmelee Go-kasan metatiedot tai lopettaa koko palvelinprosessin välittömästi, ohittaen recover()-funktion ja vieden jokaisen meneillään olevan HTTP- tai RPC-pyynnön mukanaan.

Kun tiimit yrittävät välttää cgo-kutsuja, yleiset vaihtoehdot pakottavat erilaiseen kompromissiin. C-koodikantojen puhtaat Go-pohjaiset mekaaniset käännökset säilyttävät komennon CGO_ENABLED=0, mutta seuraavat usein perässä ylävirran C-julkaisuja, kantavat mukanaan emuloitua virtuaalimuistikerrosta ja varaavat runsaasti muistia kuormituksessa. C-kirjaston siirteminen apuprosessiin loopback-TCP:n tai Unix-domain-socketien kautta palauttaa virheiden eristyksen, mutta jokainen kutsu maksaa kaksi ytimen kontekstinvaihtoa, socket-puskurikopiota ja lankasarjoitusta.

Rakensimme HQQ:n esitettyämme suoran kysymyksen: entä jos C-kirjasto eläisi erillisessä OS-prosessissa omalla virtuaalisella osoiteavaruudellaan, samalla kun Go ja C vaihtaisivat kehyksiä suoraan jaettujen suorittimen välimuistirivien kautta siirtymättä ytimeen nopealla reitillä?

IntegrointimalliCGO_ENABLED=0Virheiden eristysNopean reitin järjestelmäkutsutKutsukohtaiset kasan varauksetYlävirran C11-uskollisuus
cgo In-Process -linkitysEiEi mitään (jaettu kasa)00–1+Tarkka ylävirran C
Socket- / Putki-aliprosessi RPCKylläProsessiraja2–4Useita (kehystys + I/O)Tarkka ylävirran C
Mekaaninen C–Go-käännösKylläIn-process Go0Korkea monimutkaisissa kutsuissaEmuloitu C-ajoaika
HQQ jaetun muistin IPCKylläProsessiraja0 (mukautuva pyörintä)0 (StandardLink)Tarkka ylävirran C

Arkkitehtuurin yleiskatsaus: Neljä kerrosta prosessirajojen yli

HQQ erottaa prosessien eristyksen neljään yhdistettävään kerrokseen, jotta jaetun muistin rengasmekaniikka, tyypitetty RPC-koodin luominen, binäärin eheyden todentaminen ja lapsiprosessin 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 siirtotaso (gosuda.org/hqq ja c/)

Peruspaketti gosuda.org/hqq ja sen C11-vastine kansiossa c/ toteuttavat kaksi jaetun muistin siirtotilaa POSIX mmap -toiminnolla (Linux ja macOS) sekä Win32 CreateFileMappingW -toiminnolla (Windows). Jokainen jaettu segmentti alkaa varmennetulla HQQ_STL3-superlohkolla, joka lukitsee protokollan version, puskurin geometrian ja päätepisteiden PID-tunnukset ennen minkään rengasindeksin lukemista.

Kaikki kuumat ohjaussanat ja paikkatietueet on tasausmitattu 64 tavun välimuistirivin rajoille, jotta tuottajan ja kuluttajan ytimet eivät koskaan kilpaile samasta välimuistirivistä väärän jakamisen vuoksi.

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äKokoSemantiikka
seq0x008 BMonotoninen sekvenssilaskuri. Parittomat arvot merkitsevät meneillään olevaa kirjoitusta; parilliset arvot julkaisevat vahvistetun kehyksen store-release-toiminnon kautta.
len0x084 BPaikkaan kirjoitettu hyötykuorman tavupituus (0 <= len <= BufferSize).
flags0x0C4 BKehyksen ohjausbitti (jatko, virhetila tai sammutusilmoitus).
payload0x40BufferSizeYhtenäinen hyötykuorma-alue, joka luetaan ja kirjoitetaan paikan päällä ilman välikopioita.

Kutsujat valitsevat kahden siirtotason väliltä rinnakkaisuusrakenteen mukaan:

  • StandardLink (1:1 SPSC -sessio): Yhdistää asiakas- ja palvelinsession omistettujen pyyntö- ja vastauspaikkojen yli, joita suojaa sekvenssilukko (SeqLock). Nopealla reitillä kirjoittaja sarjoittaa tiedot suoraan kartoitettuun paikkaan ja tekee vahvistuksen atomisella store-release-operaatiolla; lukija pyörii hetken käyttäjätilassa ennen siirtymistään OS:n herätysprimitiiveihin (futex Linuxissa, __ulock_wait macOS:ssä, WaitOnAddress Windowsissa). Yhden kehyksen meno-paluumatka valmistuu noin 200 nanosekunnissa arvolla 0 allocs/op.
  • Advanced (MPMC-lähetysrengas): Monikanavoi M samanaikaista Go-kutsujaa N C-työläissäikeen yli käyttäen lukkovapaata monen tuottajan ja monen kuluttajan lähetysrengasta, jaettua paikkatelinettä sekä indeksoitua valmistumistaulukkoa. Kutsujat varaavat paikan, julkaisevat paikkatunnuksen renkaalle ja odottavat valmistumistietuettaan samalla, kun työläissäikeet poistavat jonosta ja toteuttavat pyyntöjä epäjärjestyksessä.

Molemmat tasot seuraavat vertaiskoneen elinvoimaa jatkuvasti. Jokainen päätepiste päivittää atomisen monotonisen aikaleiman Heartbeat()-kutsun kautta ja tarkastelee PeerAlive(timeout)-kutsua odottaessaan. Jos työläisprosessisi kaatuu kesken kutsun, vertaiskone havaitsee vanhentuneen sykkeen tai kuolleen PID-tunnuksen välittömästi, keskeyttää estyneet odotukset virheellä ErrPeerGone ja tukee atomista session haltuunottoa (Takeover) jättämättä jumiin jääneitä jaetun muistin tiedostoja jälkeensä.

2. Nolla-allokoinnin RPC ja sukupolvitarkastetut 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 rajapintamäärittelykielen (.idl) tiedoston (tai käynnistää sellaisen suoraan C:n .h-otsikosta komennolla hqq-bindgen init) ja tuottaa nolla-cgo Go-asiakaspaketin yhdessä C11-palvelimen rungon kanssa.

Vaarallisin virhe missä tahansa C-sidonnassa on raakojen C-osoittimien (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) paljastaminen kielirajan yli. Jos kutsuja välittää osoittimen sen vapauttamisen jälkeen, välittää toiseen sessioon kuuluvan osoittimen tai turmelee kokonaislukuosoitteen, C-kirjasto dereferensoi virheellistä muistia.

hqq-bindgen kieltää raa'at osoittimet langassa. 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}

Langassa jokainen kahva on 64-bittinen kokonaisluku, joka pakkaa 32-bittisen taulukkoindeksin ja 32-bittisen nollasta poikkeavan sukupolvilaskurin: (uint64_t)generation << 32 | index. C-ajoaika (c/src/hqq_rpc.c) ylläpitää sessiokohtaista kahvataulukkoa kolmella invariantilla:

  1. Sukupolven vahvistus: Jokainen haku varmistaa, että paikka on varattu, sen tallennettu sukupolvi vastaa kahvan ylempää 32-bittistä osaa 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 C-kirjastoon kutsumista.
  2. Hierarkkinen omistajuus (@owned_by): Koska Stmt on merkitty huomautuksella @owned_by(Db), Db-kahvan sulkeminen käy automaattisesti läpi kaikki kyseisen Db-tietokannan omistamat elävät Stmt-lapsikahvat ja suorittaa niiden tuhoajat päinvastaisessa varausjärjestyksessä (sqlite3_finalize ennen sqlite3_close_v2).
  3. Session alasajon siivous: Jos Go-asiakas katkaisee yhteyden tai kaatuu sulkematta kahvojaan, C-palvelimen session tuhoaja pyyhkäisee koko session kahvataulukon läpi päinvastaisessa järjestyksessä, taaten nolla vuotanutta C-varausta asiakkaan uudelleenkäynnistyksien yli.

Ennen kuin sessio käsittelee ensimmäistä RPC-kutsuaan, asiakas ja palvelin vaihtavat 32-tavuisen SHA-256-rajapintatiivisteen, joka on laskettu kanonisoidusta IDL:stä (HQQ-BINDGEN-IDL-V1\n-etuliite). Jos Go-asiakas ja C-työläinen on käännetty epäyhteensopivista IDL-skeemoista, kättely hylkää yhteyden käynnistyksessä.

3. Peukalointivapaa binäärisuoritus (gosuda.org/hqq/secureexec)

C-koodin siirteminen työläisbinääriin herättää toimitusketjukysymyksen: mistä Go-prosessin tietää, ettei levyllä olevaa työläisbinääriä ole muokattu tai korvattu varmennuksen ja suorituksen välisenä aikana?

Funktion crypto/sha256 kutsuminen tiedostopolulle ja sitä seuraava os/exec.Command(path).Start() jättävät aikaikkunan (TOCTOU, Time-of-Check to Time-of-Use), jossa toinen prosessi voi korvata tai vaihtaa tiedoston ennen kuin ydin suorittaa sen. gosuda.org/hqq/secureexec sulkee tämän ikkunan:

  • Linuxissa secureexec avaa binäärin, suoratoistaa sen sisällön anonyymiin muistitiedostokuvaukseen, joka on luotu funktion memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING) kautta samalla kun se laskee sen SHA-256-tiivisteen, varmistaa tiivisteen sidottuun 32-tavuiseen tiivisteeseen tai Ed25519-allekirjoitettuun julkaisumanifestiin (cmd/hqq-manifest), soveltaa komentoa 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ä kuvauksesta.
  • macOS:ssä ja Windowsissa secureexec toteuttaa varmennetut binäärit käyttöoikeuslukittuihin välimuistihakemistoihin, jotka on indeksoitu SHA-256-tiivisteellä, ja varmistaa tiedoston koon ja tiivisteen eheyden ennen käynnistystä.

4. Vanhemman elämänlangan prosessivalvonta (gosuda.org/hqq/launch)

Kun Go-sovellus käynnistää taustalla olevan C-työläisprosessin, työläinen ei saa koskaan elää vanhempaansa pidempään orpona prosessina, vaikka Go-vanhempi lopetettaisiin komennolla SIGKILL (kill -9) eikä se voi suorittaa defer-puhdistuskäsittelijöitä.

gosuda.org/hqq/launch pakottaa elinkaarisidonnaisuuden työläisen vakiosyötteelle välitetyn anonyymin OS-putken kautta, jossa on lippu --parent-stdin. Go-vanhempi pitää hallussaan putken kirjoituspäätä eikä koskaan kirjoita siihen; C-työläinen valvoo lukupäätä omistetulla elämänlankasäikeellä. Jos Go-prosessi poistuu normaalisti, aiheuttaa vakavan virheen tai OS-ydin tappaa sen (SIGKILL tai OOM killer), ydin sulkee kirjoitustiedostokuvauksen välittömästi. C-työläinen vastaanottaa EOF-merkin putkessa mikrosekunteissa, keskeyttää aktiiviset kutsut, suorittaa sessiokahvan tuhoajansa, poistaa jaetun muistin tiedostojen sidokset ja poistuu siististi.

Pika-aloitus: Generoidun palvelun kutsuminen

Kun käytössä ovat hqq-bindgen ja launch, eristetyn C-työläisen kutsuminen Go:sta näyttää tavalliselta tyypitetyltä Go-asiakkaalta pitäen samalla yllä asetusta 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-kirjaston alijärjestelmä on järjestetty kohdistettuihin paketteihin nimenomaisilla rajoilla:

 1gosuda.org/hqq
 2├── standard.go / ring.go      # StandardLink (SPSC) and low-level shared-memory ring primitives
 3├── advanced.go / slotpool.go  # Advanced (MPMC) dispatcher, slot pool, and completion table
 4├── shm_*.go / wait_*.go       # OS-specific mmap/CreateFileMapping and futex/ulock/WaitOnAddress
 5├── c/                         # C11 transport library (hqq.c) and RPC server runtime (hqq_rpc.c)
 6├── bindgen/                   # hqq-bindgen IDL parser, verifier (V1-V41), and Go/C11 code generator
 7│   └── rt/                    # Zero-allocation Go client runtime for generated services
 8├── secureexec/                # SHA-256 / Ed25519 manifest verification and memfd-sealed execution
 9├── launch/                    # Worker process supervisor with --parent-stdin lifeline management
10├── cmd/hqq-manifest/          # CLI tool for generating and signing release binary manifests
11└── spec/                      # Executable wire specifications and golden test vectors (BINDGEN.md)

Nähdäksesi, kuinka HQQ mahdollistaa suoraan käyttöön otettavat tuotantovaihtoehdot kahdelle Go-ekosysteemin laajimmin käytetylle cgo-kirjastolle, tutustu tähän pinoon rakennettuihin oheispaketteihin:

  • gosuda.org/sqlite3: Suoraan korvaava database/sql-ajuri, joka on yhteensopiva kirjaston github.com/mattn/go-sqlite3 kanssa ja suorittaa natiivia C SQLite 3.54.1 -kirjastoa jaetun muistin yli putkitetulla eräesihakulla.
  • gosuda.org/zstd: Suoraan korvaava toteutus kirjastolle github.com/DataDog/zstd, joka suorittaa natiivia C libzstd 1.5.7 -kirjastoa eristetyssä jaetun muistin työläisessä nolla-allokointikonteksteilla ja massaeräkäsittelyllä.
  • GitHub-repositorio: https://github.com/gosuda/HQQ