GoSuda

HQQ: Mengisolasi Pustaka C dari Go melalui Memori Bersama Cache-Line

By Lemon Mint views ...

gosuda.org/hqq adalah runtime komunikasi antarproses tanpa zero-cgo dan toolchain pembangkitan kode untuk Go dan C11. Runtime ini mengisolasi pustaka C native di dalam proses pekerja OS yang terpisah dan menghubungkannya ke pemanggil Go melalui ring memori bersama yang dipetakan dengan memory-mapped dan diselaraskan pada batas cache line. Proyek ini menggabungkan dua tingkat transport (StandardLink untuk sesi single-producer single-consumer khusus dan Advanced untuk dispatch MPMC multi-pekerja), compiler IDL hqq-bindgen, serta supervisor proses yang terverifikasi integritasnya (secureexec dan launch).

1go get gosuda.org/hqq

Biaya Berbagi Ruang Alamat dengan C

Setiap insinyur sistem Go pada akhirnya menghadapi persimpangan jalan yang sama. Anda sedang membangun layanan Go yang dikompilasi dalam hitungan detik dengan CGO_ENABLED=0, didistribusikan sebagai satu binary statis tunggal, dikompilasi silang dari laptop ke target Linux atau Windows tanpa sysroot, dan berjalan di dalam heap yang aman terhadap memori dan dikelola oleh garbage collector. Kemudian Anda memerlukan pustaka C yang matang: SQLite untuk penyimpanan relasional tersemat, Zstandard (libzstd) untuk kompresi berbasis kamus, atau pustaka media maupun kriptografi yang diakselerasi perangkat keras.

Mengaktifkan cgo untuk menautkan pustaka C tersebut ke dalam proses Go memang menyelesaikan masalah binding secara langsung, tetapi memperkenalkan tiga kelemahan struktural:

  1. Keterkaitan Toolchain dan Kompilasi Silang: CGO_ENABLED=1 memerlukan compiler C, header libc target, dan linker platform untuk setiap OS dan arsitektur target. Membangun artefak linux/arm64 atau windows/arm64 dari workstation darwin/arm64 mengubah satu perintah go build menjadi matriks cross-compiler dan sysroot.
  2. Overhead Penjadwalan Runtime: Setiap panggilan cgo melintasi batas runtime Go, berpindah dari stack goroutine yang dapat tumbuh ke system stack, dan mengunci thread OS (M). Untuk panggilan berbutir halus, biaya transisi (sekitar 50 hingga 150 ns per panggilan) dan kontensi penjadwal terakumulasi dengan cepat.
  3. Runtuhnya Domain Kegagalan: Kode C berjalan dalam ruang alamat virtual yang sama dengan runtime Go. Satu penulisan di luar batas, use-after-free, stack overflow, atau abort() di dalam fungsi C dapat merusak metadata heap Go atau langsung menghentikan seluruh proses server, melewati recover() dan menjatuhkan setiap permintaan HTTP atau RPC yang sedang berjalan.

Ketika tim berupaya menghindari cgo, alternatif yang umum memaksa kompromi yang berbeda. Terjemahan mekanis murni dari basis kode C ke Go mempertahankan CGO_ENABLED=0, tetapi sering tertinggal dari rilis C hulu, membawa lapisan memori virtual yang diemulasi, dan mengalokasikan memori secara intensif di bawah beban. Memindahkan pustaka C ke proses pembantu melalui loopback TCP atau Unix domain socket memulihkan isolasi kesalahan, tetapi setiap panggilan menanggung dua context switch kernel, penyalinan buffer socket, dan serialisasi wire.

Kami membangun HQQ setelah mengajukan pertanyaan langsung: bagaimana jika pustaka C berada dalam proses OS terpisah dengan ruang alamat virtualnya sendiri, sementara Go dan C saling bertukar frame secara langsung melalui cache line CPU bersama tanpa memasuki kernel pada jalur cepat?

Model IntegrasiCGO_ENABLED=0Isolasi KesalahanSyscall Jalur CepatAlokasi Heap per PanggilanFidelitas C11 Hulu
Penautan In-Process cgoTidakTidak ada (heap bersama)00 hingga 1+C hulu persis
Socket / Pipe Subprocess RPCYaBatas proses2 hingga 4Beberapa (framing + I/O)C hulu persis
Translasi Mekanis C-ke-GoYaGo dalam proses0Tinggi pada panggilan kompleksRuntime C yang diemulasi
HQQ Shared-Memory IPCYaBatas proses0 (adaptive spin)0 (StandardLink)C hulu persis

Ikhtisar Arsitektur: Empat Lapisan Melintasi Batas Proses

HQQ memisahkan isolasi proses menjadi empat lapisan yang dapat disusun secara komposabel sehingga mekanika ring memori bersama, pembangkitan kode RPC bertipe, verifikasi integritas biner, dan supervisi proses anak tetap independen.

 1+-------------------------------------------------------------------+
 2| Proses Aplikasi Go (CGO_ENABLED=0)                                |
 3|                                                                   |
 4|  +-----------------------+     +-------------------------------+  |
 5|  | Klien Go yang Dibangkitkan|   | Supervisor Proses             |  |
 6|  | (gosuda.org/hqq/      |     | (gosuda.org/hqq/launch +      |  |
 7|  |  bindgen/rt)          |     |  gosuda.org/hqq/secureexec)   |  |
 8|  +-----------+-----------+     +---------------+---------------+  |
 9|              |                                 |                  |
10|  +-----------v-----------+                     | Anonymous Pipe   |
11|  | Tingkat Transport     |                     | Lifeline         |
12|  | (StandardLink /       |                     | (--parent-stdin) |
13|  |  Advanced MPMC)       |                     |                  |
14+--------------+---------------------------------+------------------+
15               |                                 |
16===============|========= Shared Memory =========|===================
17               |     (mmap / CreateFileMapping)  |
18+--------------v---------------------------------v------------------+
19| Proses Pekerja C11 (Ruang Alamat Terisolasi)                      |
20|                                                                   |
21|  +-----------------------+     +-------------------------------+  |
22|  | Server C11 yang      |---->| Tabel Handle                  |  |
23|  | Dibangkitkan & Shim  |     | Terverifikasi Generasi        |  |
24|  | Batch                |     | (hqq_rpc.c)                   |  |
25|  +-----------+-----------+     +---------------+---------------+  |
26|              |                                 |                  |
27|              +----------------+----------------+                  |
28|                               |                                   |
29|                     +---------v---------+                         |
30|                     | Pustaka C Native  |                         |
31|                     | (SQLite, Zstd...) |                         |
32|                     +-------------------+                         |
33+-------------------------------------------------------------------+

1. Transport Memori Bersama (gosuda.org/hqq dan c/)

Paket dasar gosuda.org/hqq dan padanan C11-nya di c/ mengimplementasikan dua mode transport memori bersama di atas mmap POSIX (Linux dan macOS) serta CreateFileMappingW Win32 (Windows). Setiap segmen bersama dimulai dengan superblock HQQ_STL3 yang terverifikasi, yang mengunci versi protokol, geometri buffer, dan PID endpoint sebelum indeks ring apa pun dibaca.

Seluruh kata kontrol panas dan header slot diselaraskan pada batas cache line 64-byte sehingga core produser dan konsumer tidak pernah berebut cache line yang sama akibat false sharing.

1Tata Letak Slot StandardLink (Diselaraskan pada Cache Line 64-Byte)
2Offset Byte:  0x00                0x08                0x10                          0x40
3              +-------------------+-------------------+-----------------------------+
4Header Line:  | seq (u64 atomic)  | len (u32) | flags | reserved (diisi nol)        |
5              +-------------------+-------------------+-----------------------------+
6Area Payload: | byte payload [0 .. BufferSize - 1] (dikodekan / didekodekan langsung) |
7              +---------------------------------------------------------------------+
FieldOffsetUkuranSemantik
seq0x008 BPenghitung urutan monotonik. Nilai ganjil menandai penulisan yang sedang berlangsung; nilai genap mempublikasikan frame yang telah di-commit melalui store-release.
len0x084 BPanjang byte payload yang ditulis ke dalam slot (0 <= len <= BufferSize).
flags0x0C4 BBit kontrol frame (kelanjutan, status kesalahan, atau pemberitahuan penutupan).
payload0x40BufferSizeArea payload berurutan yang dibaca dan ditulis di tempat tanpa salinan perantara.

Pemanggil memilih di antara dua tingkat transport bergantung pada struktur konkurensinya:

  • StandardLink (Sesi SPSC 1:1): Memasangkan sesi klien dan server melalui slot permintaan dan respons khusus yang dilindungi oleh sequence lock (SeqLock). Pada jalur cepat, penulis melakukan serialisasi langsung ke dalam slot yang dipetakan dan melakukan commit dengan atomic store-release; pembaca melakukan spin singkat di ruang pengguna sebelum beralih ke primitif wake OS (futex di Linux, __ulock_wait di macOS, WaitOnAddress di Windows). Satu round trip frame tunggal selesai dalam sekitar 200 ns dengan 0 allocs/op.
  • Advanced (Ring Dispatch MPMC): Memultipleks M pemanggil Go secara konkuren ke N thread pekerja C menggunakan ring submisi multi-producer multi-consumer tanpa kunci, kumpulan slot bersama, dan tabel penyelesaian berindeks. Pemanggil mencadangkan sebuah slot, mempublikasikan indeks slot ke ring, dan menunggu entri penyelesaiannya sementara thread pekerja mengambil dan menyelesaikan permintaan secara tidak berurutan.

Kedua tingkat secara terus-menerus melacak kelangsungan hidup peer. Setiap endpoint memperbarui timestamp monotonik atomik melalui Heartbeat() dan memeriksa PeerAlive(timeout) selama menunggu. Jika proses pekerja mengalami crash di tengah panggilan, peer segera mendeteksi heartbeat yang usang atau PID yang mati, membatalkan waktu tunggu yang terblokir dengan ErrPeerGone, dan mendukung Takeover sesi secara atomik tanpa meninggalkan berkas memori bersama yang macet.

2. RPC Tanpa Alokasi dan Handle Terverifikasi Generasi (gosuda.org/hqq/bindgen)

Menulis framing memori bersama secara manual untuk pustaka C dengan puluhan fungsi merepotkan dan rentan kesalahan. hqq-bindgen adalah compiler skema yang membaca berkas Interface Definition Language (.idl) (atau membuat berkas tersebut langsung dari header C .h melalui hqq-bindgen init) dan menghasilkan paket klien Go tanpa zero-cgo beserta kerangka server C11.

Kesalahan paling berbahaya dalam binding C mana pun adalah mengekspos pointer C mentah (sqlite3*, sqlite3_stmt*, ZSTD_CCtx*) melintasi batas bahasa. Jika pemanggil meneruskan pointer setelah membebaskannya, meneruskan pointer milik sesi lain, atau merusak alamat integer, pustaka C akan melakukan dereferensi memori yang tidak valid.

hqq-bindgen melarang pointer mentah pada wire. Sebagai gantinya, IDL memodelkan resource C yang stateful sebagai deklarasi handle bertipe:

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}

Pada wire, setiap handle adalah integer 64-bit yang menggabungkan indeks tabel 32-bit dan penghitung generasi 32-bit yang bukan nol: (uint64_t)generation << 32 | index. Runtime C (c/src/hqq_rpc.c) memelihara tabel handle per sesi dengan tiga invarian:

  1. Validasi Generasi: Setiap pencarian memverifikasi bahwa slot terisi, generasi yang tersimpan cocok dengan 32 bit atas handle, dan tag tipe handle-nya cocok dengan tipe parameter (Db vs. Stmt). Menggunakan kembali handle yang sudah ditutup akan gagal seketika dengan HQQRPC_E_BAD_HANDLE sebelum memanggil pustaka C.
  2. Kepemilikan Hierarkis (@owned_by): Karena Stmt dianotasi dengan @owned_by(Db), menutup handle Db secara otomatis menelusuri seluruh handle Stmt anak yang masih hidup dan milik Db tersebut, lalu menjalankan destruktornya dalam urutan alokasi terbalik (sqlite3_finalize sebelum sqlite3_close_v2).
  3. Penyapuan Penghentian Sesi: Jika klien Go terputus atau crash tanpa menutup handle-nya, destruktor sesi server C menyapu seluruh tabel handle sesi dalam urutan terbalik, sehingga menjamin tidak ada alokasi C yang bocor di antara restart klien.

Sebelum sesi memproses panggilan RPC pertamanya, klien dan server saling bertukar digest interface SHA-256 sepanjang 32 byte yang dihitung atas IDL yang telah dikanonikalisasi (dengan awalan HQQ-BINDGEN-IDL-V1\n). Jika klien Go dan worker C dikompilasi dari skema IDL yang tidak kompatibel, handshake menolak koneksi saat bootstrap.

3. Eksekusi Biner Bebas Pemalsuan (gosuda.org/hqq/secureexec)

Memindahkan kode C ke dalam biner worker menimbulkan pertanyaan rantai pasok: bagaimana proses Go mengetahui bahwa biner worker di disk tidak dimodifikasi atau diganti antara verifikasi dan eksekusi?

Memanggil crypto/sha256 pada path berkas lalu os/exec.Command(path).Start() menyisakan jendela Time-of-Check to Time-of-Use (TOCTOU) di mana proses lain dapat menimpa atau menukar berkas sebelum kernel mengeksekusinya. gosuda.org/hqq/secureexec menutup jendela ini:

  • Di Linux, secureexec membuka biner, mengalirkan isinya ke dalam deskriptor berkas memori anonim yang dibuat melalui memfd_create(MFD_CLOEXEC | MFD_ALLOW_SEALING) sambil menghitung hash SHA-256-nya, memverifikasi hash tersebut terhadap digest 32 byte yang dipasang (pinned) atau manifest rilis yang ditandatangani Ed25519 (cmd/hqq-manifest), menerapkan fcntl(F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE) sehingga tidak ada proses di sistem yang dapat memodifikasi halaman tersebut, dan menjalankan proses anak dari deskriptor yang tersegel.
  • Di macOS dan Windows, secureexec memindahkan biner yang telah diverifikasi ke direktori cache dengan izin terkunci yang diindeks berdasarkan digest SHA-256, dan memverifikasi integritas ukuran berkas serta digest sebelum peluncuran.

4. Supervisi Proses Parent-Lifeline (gosuda.org/hqq/launch)

Ketika aplikasi Go menjalankan proses worker C di latar belakang, worker tidak boleh bertahan hidup sebagai proses yatim setelah parent-nya, bahkan jika proses parent Go dihentikan dengan SIGKILL (kill -9) dan tidak dapat menjalankan handler pembersihan defer.

gosuda.org/hqq/launch menegakkan keterkaitan masa hidup melalui pipa OS anonim yang diteruskan ke standard input worker dengan flag --parent-stdin. Parent Go memegang ujung tulis pipa dan tidak pernah menulis ke dalamnya; worker C memantau ujung baca pada thread lifeline khusus. Jika proses Go keluar secara normal, panik, atau dihentikan oleh kernel OS (SIGKILL atau OOM killer), kernel segera menutup deskriptor tulis. Worker C menerima EOF pada pipa dalam hitungan mikrodetik, menginterupsi panggilan aktif apa pun, menjalankan destruktor handle sesinya, menghapus tautan berkas memori bersama, dan keluar dengan bersih.

Panduan Cepat: Memanggil Layanan yang Dibangkitkan

Dengan hqq-bindgen dan launch, memanggil worker C yang terisolasi dari Go terlihat seperti klien Go bertipe yang biasa sambil tetap mempertahankan 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 berjalan pada listener memori bersama %s (opts=%+v)\n", listener, rt.SessionOptions{})
40}

Struktur Repositori

Setiap subsistem dalam gosuda.org/hqq diorganisasikan ke dalam paket-paket terfokus dengan batas yang eksplisit:

 1gosuda.org/hqq
 2├── standard.go / ring.go      # StandardLink (SPSC) dan primitif ring memori bersama tingkat rendah
 3├── advanced.go / slotpool.go  # Dispatcher Advanced (MPMC), kumpulan slot, dan tabel penyelesaian
 4├── shm_*.go / wait_*.go       # mmap/CreateFileMapping spesifik OS dan futex/ulock/WaitOnAddress
 5├── c/                         # Pustaka transport C11 (hqq.c) dan runtime server RPC (hqq_rpc.c)
 6├── bindgen/                   # Parser IDL hqq-bindgen, verifier (V1-V41), dan generator kode Go/C11
 7│   └── rt/                    # Runtime klien Go tanpa alokasi untuk layanan yang dibangkitkan
 8├── secureexec/                # Verifikasi manifest SHA-256 / Ed25519 dan eksekusi tersegel memfd
 9├── launch/                    # Supervisor proses pekerja dengan manajemen lifeline --parent-stdin
10├── cmd/hqq-manifest/          # Alat CLI untuk membangkitkan dan menandatangani manifest biner rilis
11└── spec/                      # Spesifikasi wire yang dapat dieksekusi dan vektor uji emas (BINDGEN.md)

Untuk melihat bagaimana HQQ menyediakan pengganti produksi langsung pakai (drop-in) bagi dua pustaka cgo yang paling banyak digunakan dalam ekosistem Go, lihat paket pendamping yang dibangun di atas tumpukan ini:

  • gosuda.org/sqlite3: Driver database/sql drop-in yang kompatibel dengan github.com/mattn/go-sqlite3, menjalankan SQLite C native 3.54.1 melalui memori bersama dengan prefetch batch yang di-pipeline.
  • gosuda.org/zstd: Pengganti drop-in untuk github.com/DataDog/zstd, menjalankan libzstd 1.5.7 C native dalam worker memori bersama yang terisolasi dengan konteks tanpa alokasi dan batching massal.
  • Repositori GitHub: https://github.com/gosuda/HQQ