mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
OSS public chains module must not reference the private GPU plugin
implementation by repo name or filesystem path. Two leak types fixed:
1. Hard compile-time dependency in quantumvm:
`#cgo CFLAGS: -I${SRCDIR}/../../../lux-private/gpu-kernels/include`
Vendor the public ABI v14 header (backend_plugin.h + crypto.h, 1475
LOC total) into chains/internal/luxgpu/include/lux/gpu/ and point
CFLAGS at the in-tree path. Plugin DSO is still dlopen'd at runtime
— only the public C ABI surface is vendored.
2. Soft leakage — `LUX_PRIVATE_GPU_KERNELS_DIR` env vocabulary, dev-tree
probe paths to ~/work/lux-private/gpu-kernels/build/..., prose
mentions of "lux-private/gpu-kernels" in package comments.
All replaced with `LUX_GPU_PLUGIN_DIR` (universal env) and generic
"GPU plugin" prose.
Bridges affected: aivm, bridgevm, dexvm, quantumvm, thresholdvm.
All 139 tests PASS post-scrub under both build tags. No functional
behavior change — only path / env / prose.
919 lines
29 KiB
Go
919 lines
29 KiB
Go
//go:build cgo
|
|
|
|
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package quantumvm GPU backend — runtime-loaded plugin bridge via the
|
|
// ABI v14 vtbl entry point.
|
|
//
|
|
// Unlike cevm/cevm_cgo.go (which links libevm + libevm-gpu via pkg-config
|
|
// and reaches the C ABI through extern "C" gpu_* helpers), Q-Chain
|
|
// resolves its GPU substrate at PROCESS START via dlopen / dlsym against
|
|
// the lux-gpu-kernels plugin DSO and reaches the per-op kernel through
|
|
// function pointers inside a vtbl populated by the plugin's
|
|
// `lux_gpu_backend_init` entry point. This keeps the chains module
|
|
// compilable without the lux GPU plugin present in the build tree —
|
|
// the plugin is fully optional. When no libluxgpu_backend_*.{so,dylib}
|
|
// is findable on the dlopen search path, AutoBackend() returns
|
|
// BackendNone, every GPUBackend method returns ErrGPUNotAvailable, and
|
|
// the existing CPU verify path stays unchanged.
|
|
//
|
|
// Probe order (init()): cuda → hip → metal → vulkan → webgpu. First
|
|
// plugin that resolves the entry point AND reports a v14 ABI matching
|
|
// the header we compiled against AND exposes the three Q-Chain slots
|
|
// (op_mldsa_verify_batch, op_mldsa_sign_batch,
|
|
// op_slhdsa_verify_batch[_shake192f]) wins; remaining probes are
|
|
// skipped. The cookie checks (abi_version + vtbl_size) defend against
|
|
// a plugin that lies about its ABI version while shipping a truncated
|
|
// vtable (see backend_plugin.h §"Backend Descriptor" comment).
|
|
//
|
|
// Search path: the dlopen library name is the bare basename
|
|
// (libluxgpu_backend_<x>.{so,dylib}); the loader's standard
|
|
// LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / rpath resolution finds it.
|
|
// LUX_GPU_PLUGIN_DIR overrides — if set, every probe joins it to the
|
|
// basename before calling dlopen.
|
|
package quantumvm
|
|
|
|
/*
|
|
#cgo darwin LDFLAGS: -ldl
|
|
#cgo linux LDFLAGS: -ldl
|
|
|
|
// The plugin DSO is dlopen'd at runtime; the only compile-time
|
|
// dependency is the C struct layout in backend_plugin.h, which is
|
|
// vendored under chains/internal/luxgpu/include/lux/gpu/ as the public
|
|
// ABI surface. No link against any external library.
|
|
#cgo CFLAGS: -I${SRCDIR}/../internal/luxgpu/include
|
|
|
|
#include <dlfcn.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "lux/gpu/backend_plugin.h"
|
|
|
|
// ABI v14 vtbl entry-point thunk. dlsym returns void*; this struct lets
|
|
// Go pass the function pointer back through the bridge without losing
|
|
// type information at the cgo boundary.
|
|
typedef bool (*qvm_backend_init_fn)(lux_gpu_backend_desc* out);
|
|
|
|
// qvm_backend_init invokes the plugin's `lux_gpu_backend_init` symbol
|
|
// and returns 0 on success, non-zero (1..5) on a specific cookie /
|
|
// slot failure. The cookies (abi_version + vtbl_size) detect plugins
|
|
// built against a different header — fail fast at process start
|
|
// instead of UB at the first vtbl slot call (a stale plugin's
|
|
// op_mldsa_verify_batch at the wrong offset would be a junk function
|
|
// pointer dispatch otherwise).
|
|
//
|
|
// ret == 0 — desc populated AND all three Q-Chain slots non-NULL.
|
|
// ret == 1 — init function returned false (no runtime available on
|
|
// this host: missing driver, ICD, etc.). Caller treats
|
|
// as SKIP, not FAIL.
|
|
// ret == 2 — abi_version mismatch (plugin is from a different ABI
|
|
// than the Go bindings were compiled against).
|
|
// ret == 3 — vtbl_size mismatch (plugin shipped a truncated vtbl).
|
|
// ret == 4 — vtbl pointer is NULL (malformed descriptor).
|
|
// ret == 5 — vtbl is missing one of the three Q-Chain slots.
|
|
static int qvm_backend_init(void* sym, lux_gpu_backend_desc* desc) {
|
|
if (!sym || !desc) return 4;
|
|
memset(desc, 0, sizeof(*desc));
|
|
qvm_backend_init_fn fn = (qvm_backend_init_fn)sym;
|
|
if (!fn(desc)) return 1;
|
|
if (desc->abi_version != LUX_GPU_BACKEND_ABI_VERSION) return 2;
|
|
if (desc->vtbl_size != sizeof(lux_gpu_backend_vtbl)) return 3;
|
|
if (!desc->vtbl) return 4;
|
|
if (!desc->vtbl->create_context || !desc->vtbl->destroy_context) return 5;
|
|
// ML-DSA verify is the canonical Q-Chain slot. ML-DSA sign and
|
|
// SLH-DSA verify may be nullptr on backends that haven't wired
|
|
// their orchestrator yet — those methods then return
|
|
// LUX_BACKEND_ERROR_NOT_SUPPORTED at call time, which the Go
|
|
// wrapper translates to ErrGPUNotAvailable. The plugin is still
|
|
// viable if at least op_mldsa_verify_batch is wired (ML-DSA
|
|
// verify is the load-bearing Q-Chain op).
|
|
if (!desc->vtbl->op_mldsa_verify_batch) return 5;
|
|
return 0;
|
|
}
|
|
|
|
// Lifecycle helpers — exported so the Go side can open / close a
|
|
// LuxBackendContext per probe-result backend exactly once. device_index
|
|
// = 0 is the canonical "first available device" sentinel — backends
|
|
// like Metal that don't expose multi-GPU selection assume 0 and would
|
|
// throw on -1 (NSRangeException on Apple Silicon, observed).
|
|
static LuxBackendContext* qvm_create_context(const lux_gpu_backend_vtbl* vtbl) {
|
|
return vtbl->create_context(0);
|
|
}
|
|
static void qvm_destroy_context(const lux_gpu_backend_vtbl* vtbl, LuxBackendContext* ctx) {
|
|
if (ctx) vtbl->destroy_context(ctx);
|
|
}
|
|
|
|
// Per-slot wrappers. The trampolines unpack Go's flat parameter slices
|
|
// and dispatch through the vtbl function pointer. Each returns the raw
|
|
// LuxBackendError code — the Go side maps NOT_SUPPORTED (3) →
|
|
// ErrGPUNotAvailable and other non-zero codes → fmt.Errorf("rc=%d").
|
|
|
|
// op_mldsa_verify_batch — see backend_plugin.h v14 block. msg_lens is
|
|
// optional (NULL → use msg_width_hint for every element).
|
|
static int qvm_op_mldsa_verify(
|
|
const lux_gpu_backend_vtbl* vtbl,
|
|
LuxBackendContext* ctx,
|
|
const uint8_t* const* pubkeys,
|
|
const uint8_t* const* messages,
|
|
const size_t* msg_lens,
|
|
uint32_t msg_width_hint,
|
|
const uint8_t* const* signatures,
|
|
bool* results,
|
|
size_t count) {
|
|
if (!vtbl->op_mldsa_verify_batch) return LUX_BACKEND_ERROR_NOT_SUPPORTED;
|
|
return (int)vtbl->op_mldsa_verify_batch(
|
|
ctx, pubkeys, messages, msg_lens,
|
|
msg_width_hint, signatures, results, count);
|
|
}
|
|
|
|
// op_mldsa_sign_batch — see backend_plugin.h v14 block. sig_lens_out is
|
|
// optional (NULL is fine — caller falls back to per-element verify if
|
|
// it needs accept / reject feedback).
|
|
static int qvm_op_mldsa_sign(
|
|
const lux_gpu_backend_vtbl* vtbl,
|
|
LuxBackendContext* ctx,
|
|
const uint8_t* skeys,
|
|
size_t sk_stride,
|
|
const uint8_t* msgs,
|
|
const size_t* msg_lens,
|
|
uint32_t msg_width_hint,
|
|
size_t count,
|
|
uint8_t* sigs_out,
|
|
uint32_t* sig_lens_out) {
|
|
if (!vtbl->op_mldsa_sign_batch) return LUX_BACKEND_ERROR_NOT_SUPPORTED;
|
|
return (int)vtbl->op_mldsa_sign_batch(
|
|
ctx, skeys, sk_stride, msgs, msg_lens,
|
|
msg_width_hint, count, sigs_out, sig_lens_out);
|
|
}
|
|
|
|
// op_slhdsa_verify_batch — SHAKE-128f. See backend_plugin.h ABI v13.
|
|
static int qvm_op_slhdsa_verify_128f(
|
|
const lux_gpu_backend_vtbl* vtbl,
|
|
LuxBackendContext* ctx,
|
|
const uint8_t* const* pubkeys,
|
|
const uint8_t* const* messages,
|
|
const size_t* msg_lens,
|
|
const uint8_t* const* signatures,
|
|
bool* results,
|
|
size_t count) {
|
|
if (!vtbl->op_slhdsa_verify_batch) return LUX_BACKEND_ERROR_NOT_SUPPORTED;
|
|
return (int)vtbl->op_slhdsa_verify_batch(
|
|
ctx, pubkeys, messages, msg_lens, signatures, results, count);
|
|
}
|
|
|
|
// op_slhdsa_verify_batch_shake192f — SHAKE-192f. See backend_plugin.h
|
|
// ABI v13.
|
|
static int qvm_op_slhdsa_verify_192f(
|
|
const lux_gpu_backend_vtbl* vtbl,
|
|
LuxBackendContext* ctx,
|
|
const uint8_t* const* pubkeys,
|
|
const uint8_t* const* messages,
|
|
const size_t* msg_lens,
|
|
const uint8_t* const* signatures,
|
|
bool* results,
|
|
size_t count) {
|
|
if (!vtbl->op_slhdsa_verify_batch_shake192f) return LUX_BACKEND_ERROR_NOT_SUPPORTED;
|
|
return (int)vtbl->op_slhdsa_verify_batch_shake192f(
|
|
ctx, pubkeys, messages, msg_lens, signatures, results, count);
|
|
}
|
|
|
|
// dlopen / dlsym wrappers — kept here so backend.go stays pure Go.
|
|
static void* qvm_dlopen(const char* path) {
|
|
return dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
|
}
|
|
static void* qvm_dlsym(void* handle, const char* sym) {
|
|
return dlsym(handle, sym);
|
|
}
|
|
static const char* qvm_dlerror(void) {
|
|
return dlerror();
|
|
}
|
|
static void qvm_dlclose(void* handle) {
|
|
if (handle) dlclose(handle);
|
|
}
|
|
|
|
// Allocates a lux_gpu_backend_desc the Go side keeps for the lifetime
|
|
// of the GPUBackend. Heap-allocated so the Go object's address can be
|
|
// shared across cgo calls without escape-analysis surprises.
|
|
static lux_gpu_backend_desc* qvm_alloc_desc(void) {
|
|
lux_gpu_backend_desc* d = (lux_gpu_backend_desc*)calloc(1, sizeof(*d));
|
|
return d;
|
|
}
|
|
static void qvm_free_desc(lux_gpu_backend_desc* d) {
|
|
free(d);
|
|
}
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sync"
|
|
"unsafe"
|
|
)
|
|
|
|
// pluginEntrySymbol is the canonical ABI v14 entry point exported by every
|
|
// libluxgpu_backend_<x>.{so,dylib} plugin. Resolving this symbol yields a
|
|
// function pointer that populates a lux_gpu_backend_desc with abi_version,
|
|
// vtbl_size cookies, and the vtbl pointer the rest of this file walks.
|
|
const pluginEntrySymbol = "lux_gpu_backend_init"
|
|
|
|
// pluginEnv overrides the dlopen search path. When set, every probe joins
|
|
// the env value to the bare basename before calling dlopen — useful for
|
|
// in-tree development against a freshly-built plugin under
|
|
// the GPU plugin build tree without installing it.
|
|
const pluginEnv = "LUX_GPU_PLUGIN_DIR"
|
|
|
|
// probeOrder is the dlopen probe sequence fixed by the spec:
|
|
// cuda → hip → metal → vulkan → webgpu. First plugin that satisfies the
|
|
// cookie checks AND exposes the load-bearing op_mldsa_verify_batch slot
|
|
// wins; the remaining probes are skipped. Order is preserved at struct
|
|
// definition (not encoded as a map) so the iteration order is the same
|
|
// on every platform.
|
|
var probeOrder = []struct {
|
|
kind Backend
|
|
name string // basename without lib prefix or .so/.dylib suffix
|
|
}{
|
|
{BackendCUDA, "luxgpu_backend_cuda"},
|
|
{BackendHIP, "luxgpu_backend_hip"},
|
|
{BackendMetal, "luxgpu_backend_metal"},
|
|
{BackendVulkan, "luxgpu_backend_vulkan"},
|
|
{BackendWebGPU, "luxgpu_backend_webgpu"},
|
|
}
|
|
|
|
// pluginBasenames returns the platform-specific DSO basenames to attempt
|
|
// for a given backend. On darwin we try both .dylib (native) and .so
|
|
// (cross-tooling fallback). On linux / other we try .so first. The
|
|
// order within each entry's slice matches the lookup order used by
|
|
// dlopen — first hit wins.
|
|
func pluginBasenames(name string) []string {
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
return []string{
|
|
"lib" + name + ".dylib",
|
|
"lib" + name + ".so",
|
|
}
|
|
case "windows":
|
|
return []string{
|
|
name + ".dll",
|
|
}
|
|
default:
|
|
return []string{
|
|
"lib" + name + ".so",
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// gpuBackend — the cgo implementation of the GPUBackend interface.
|
|
//
|
|
// One struct per loaded plugin; pinned at package level by init()'s probe.
|
|
// All vtbl invocations route through C trampolines (qvm_op_*) defined in
|
|
// the cgo preamble above. mu serialises Close() against concurrent op
|
|
// dispatch — every vtbl call increments / decrements an in-flight counter
|
|
// so Close() can wait for outstanding calls before destroying the context.
|
|
// =============================================================================
|
|
|
|
type gpuBackend struct {
|
|
mu sync.Mutex
|
|
kind Backend
|
|
handle unsafe.Pointer // dlopen result (void*)
|
|
desc *C.lux_gpu_backend_desc // heap-allocated descriptor
|
|
ctx *C.LuxBackendContext // backend-managed opaque context
|
|
path string
|
|
}
|
|
|
|
// init runs the dlopen probe exactly once at process start. The probe
|
|
// walks probeOrder, attempts each candidate basename, and stops at the
|
|
// first plugin that:
|
|
//
|
|
// 1. Successfully dlopens.
|
|
// 2. Exposes the lux_gpu_backend_init symbol.
|
|
// 3. Reports abi_version == LUX_GPU_BACKEND_ABI_VERSION (14).
|
|
// 4. Reports vtbl_size == sizeof(lux_gpu_backend_vtbl) on our header.
|
|
// 5. Has op_mldsa_verify_batch wired (the load-bearing Q-Chain slot).
|
|
// 6. Successfully opens a LuxBackendContext via vtbl->create_context.
|
|
//
|
|
// On success the package-level activeBackend atomic is set to the
|
|
// matching Backend enum value. Failure at any stage is silent — we move
|
|
// to the next candidate. If no candidate succeeds, activeBackend stays
|
|
// BackendNone and ActiveGPUBackend() returns a noGPUBackend stub.
|
|
func init() {
|
|
if b, ok := tryProbe(); ok {
|
|
setActiveBackend(b.kind)
|
|
setActiveGPU(b)
|
|
return
|
|
}
|
|
setActiveBackend(BackendNone)
|
|
setActiveGPU(nil)
|
|
}
|
|
|
|
// tryProbe runs the probeOrder dlopen loop. Returns the loaded gpuBackend
|
|
// on success, or (nil, false) if no candidate satisfied all six checks.
|
|
// Errors are deliberately not surfaced — this is a probe, not a hard
|
|
// requirement. Callers see BackendNone and route to the CPU path.
|
|
func tryProbe() (*gpuBackend, bool) {
|
|
dir := os.Getenv(pluginEnv)
|
|
for _, p := range probeOrder {
|
|
for _, bn := range pluginBasenames(p.name) {
|
|
path := bn
|
|
if dir != "" {
|
|
path = filepath.Join(dir, bn)
|
|
}
|
|
b, err := openPlugin(p.kind, path)
|
|
if err == nil {
|
|
return b, true
|
|
}
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// openPlugin attempts to dlopen `path`, resolve lux_gpu_backend_init,
|
|
// run the cookie checks, and create a backend context. Returns the
|
|
// loaded gpuBackend on success, or (nil, err) with the offending stage
|
|
// documented in the error. The dlopen handle is closed on any failure
|
|
// past the dlopen point — we never leak a half-bound plugin.
|
|
func openPlugin(kind Backend, path string) (*gpuBackend, error) {
|
|
cpath := C.CString(path)
|
|
defer C.free(unsafe.Pointer(cpath))
|
|
|
|
// Clear pending dlerror so a stale message from a prior failed call
|
|
// doesn't get mis-attributed to this dlopen.
|
|
C.qvm_dlerror()
|
|
|
|
handle := C.qvm_dlopen(cpath)
|
|
if handle == nil {
|
|
return nil, fmt.Errorf("quantumvm: dlopen(%s): %s",
|
|
path, C.GoString(C.qvm_dlerror()))
|
|
}
|
|
|
|
csym := C.CString(pluginEntrySymbol)
|
|
defer C.free(unsafe.Pointer(csym))
|
|
C.qvm_dlerror()
|
|
sym := C.qvm_dlsym(handle, csym)
|
|
if sym == nil {
|
|
C.qvm_dlclose(handle)
|
|
return nil, fmt.Errorf("quantumvm: dlsym(%s, %s): %s",
|
|
path, pluginEntrySymbol, C.GoString(C.qvm_dlerror()))
|
|
}
|
|
|
|
desc := C.qvm_alloc_desc()
|
|
if desc == nil {
|
|
C.qvm_dlclose(handle)
|
|
return nil, fmt.Errorf("quantumvm: calloc(lux_gpu_backend_desc) failed")
|
|
}
|
|
if rc := C.qvm_backend_init(sym, desc); rc != 0 {
|
|
C.qvm_free_desc(desc)
|
|
C.qvm_dlclose(handle)
|
|
return nil, fmt.Errorf(
|
|
"quantumvm: %s plugin init failed (rc=%d, path=%s)",
|
|
kind, int(rc), path)
|
|
}
|
|
|
|
ctx := C.qvm_create_context(desc.vtbl)
|
|
if ctx == nil {
|
|
C.qvm_free_desc(desc)
|
|
C.qvm_dlclose(handle)
|
|
return nil, fmt.Errorf(
|
|
"quantumvm: %s create_context returned NULL (no runtime on this host?)",
|
|
kind)
|
|
}
|
|
|
|
return &gpuBackend{
|
|
kind: kind,
|
|
handle: handle,
|
|
desc: desc,
|
|
ctx: ctx,
|
|
path: path,
|
|
}, nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Active-backend handle plumbing.
|
|
//
|
|
// activeGPU stores the *gpuBackend resolved by init()'s probe (or nil
|
|
// when no plugin loaded). Read via ActiveGPUBackend() — which returns
|
|
// the GPUBackend interface (gpuBackend implements it). The pointer is
|
|
// pinned at init time and never mutated thereafter; we still serialise
|
|
// access through a Mutex for safety against a future caller that might
|
|
// add an explicit re-probe path.
|
|
// =============================================================================
|
|
|
|
var (
|
|
activeGPUMu sync.RWMutex
|
|
activeGPU *gpuBackend
|
|
)
|
|
|
|
func setActiveGPU(b *gpuBackend) {
|
|
activeGPUMu.Lock()
|
|
activeGPU = b
|
|
activeGPUMu.Unlock()
|
|
}
|
|
|
|
// ActiveGPUBackend returns the package-level GPUBackend handle. Under
|
|
// cgo this is the *gpuBackend chosen by init()'s probe, or a sentinel
|
|
// noGPUBackend stub when no plugin loaded. The returned handle's
|
|
// methods are safe to call from any goroutine — every vtbl call
|
|
// holds the backend mutex.
|
|
func ActiveGPUBackend() GPUBackend {
|
|
activeGPUMu.RLock()
|
|
b := activeGPU
|
|
activeGPUMu.RUnlock()
|
|
if b == nil {
|
|
return noGPUBackend{}
|
|
}
|
|
return b
|
|
}
|
|
|
|
// noGPUBackend is the cgo build's stub for the "no plugin loaded" case.
|
|
// Distinct from the !cgo noGPUBackend so the !cgo file can ship without
|
|
// any cgo machinery; same method semantics.
|
|
type noGPUBackend struct{}
|
|
|
|
func (noGPUBackend) Backend() Backend { return BackendNone }
|
|
|
|
func (noGPUBackend) Close() error { return nil }
|
|
|
|
func (noGPUBackend) MLDSAVerifyBatch(
|
|
mode MLDSAMode,
|
|
pubkeys [][]byte,
|
|
messages [][]byte,
|
|
msgLens []int,
|
|
msgWidthHint uint32,
|
|
signatures [][]byte,
|
|
results []bool,
|
|
) error {
|
|
_ = mode
|
|
_ = messages
|
|
_ = msgLens
|
|
_ = msgWidthHint
|
|
_ = signatures
|
|
_ = results
|
|
if len(pubkeys) == 0 {
|
|
return nil
|
|
}
|
|
return ErrGPUNotAvailable
|
|
}
|
|
|
|
func (noGPUBackend) MLDSASignBatch(
|
|
mode MLDSAMode,
|
|
skeys []byte,
|
|
msgs []byte,
|
|
msgLens []int,
|
|
msgWidthHint uint32,
|
|
count int,
|
|
sigsOut []byte,
|
|
sigLensOut []uint32,
|
|
) error {
|
|
_ = mode
|
|
_ = skeys
|
|
_ = msgs
|
|
_ = msgLens
|
|
_ = msgWidthHint
|
|
_ = sigsOut
|
|
_ = sigLensOut
|
|
if count == 0 {
|
|
return nil
|
|
}
|
|
return ErrGPUNotAvailable
|
|
}
|
|
|
|
func (noGPUBackend) SLHDSAVerifyBatch(
|
|
variant SLHDSAVariant,
|
|
pubkeys [][]byte,
|
|
messages [][]byte,
|
|
msgLens []int,
|
|
signatures [][]byte,
|
|
results []bool,
|
|
) error {
|
|
_ = variant
|
|
_ = messages
|
|
_ = msgLens
|
|
_ = signatures
|
|
_ = results
|
|
if len(pubkeys) == 0 {
|
|
return nil
|
|
}
|
|
return ErrGPUNotAvailable
|
|
}
|
|
|
|
// =============================================================================
|
|
// gpuBackend method implementations.
|
|
// =============================================================================
|
|
|
|
func (b *gpuBackend) Backend() Backend {
|
|
if b == nil {
|
|
return BackendNone
|
|
}
|
|
return b.kind
|
|
}
|
|
|
|
// Close destroys the LuxBackendContext, dlcloses the plugin, and frees
|
|
// the descriptor. Safe on a nil receiver and idempotent — a second call
|
|
// is a no-op once the handle has been cleared.
|
|
func (b *gpuBackend) Close() error {
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if b.handle == nil {
|
|
return nil
|
|
}
|
|
if b.ctx != nil {
|
|
C.qvm_destroy_context(b.desc.vtbl, b.ctx)
|
|
b.ctx = nil
|
|
}
|
|
if b.desc != nil {
|
|
C.qvm_free_desc(b.desc)
|
|
b.desc = nil
|
|
}
|
|
C.qvm_dlclose(b.handle)
|
|
b.handle = nil
|
|
return nil
|
|
}
|
|
|
|
// MLDSAVerifyBatch dispatches op_mldsa_verify_batch from the loaded
|
|
// vtbl. See the GPUBackend interface doc + backend_plugin.h v14 block
|
|
// for the contract.
|
|
func (b *gpuBackend) MLDSAVerifyBatch(
|
|
mode MLDSAMode,
|
|
pubkeys [][]byte,
|
|
messages [][]byte,
|
|
msgLens []int,
|
|
msgWidthHint uint32,
|
|
signatures [][]byte,
|
|
results []bool,
|
|
) error {
|
|
if mode != MLDSAMode65 {
|
|
// v14 vtbl only exposes ML-DSA-65 at op_mldsa_verify_batch.
|
|
// Modes 44 / 87 will land at sibling slots in a future ABI
|
|
// bump (see backend_plugin.h v14 block).
|
|
return ErrGPUNotAvailable
|
|
}
|
|
n := len(pubkeys)
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
if len(messages) != n || len(signatures) != n || len(results) != n {
|
|
return fmt.Errorf(
|
|
"quantumvm: MLDSAVerifyBatch: length mismatch (pubkeys=%d messages=%d signatures=%d results=%d)",
|
|
n, len(messages), len(signatures), len(results))
|
|
}
|
|
if msgLens != nil && len(msgLens) != n {
|
|
return fmt.Errorf(
|
|
"quantumvm: MLDSAVerifyBatch: msgLens length %d != batch %d",
|
|
len(msgLens), n)
|
|
}
|
|
if b == nil || b.ctx == nil || b.desc == nil {
|
|
return ErrGPUNotAvailable
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
// Pin every Go-side byte slice for the duration of the C call.
|
|
// runtime.Pinner guarantees the GC won't move (or collect) the
|
|
// backing arrays while the kernel reads them.
|
|
var pinner runtime.Pinner
|
|
defer pinner.Unpin()
|
|
|
|
pkPtrs := make([]*C.uint8_t, n)
|
|
msgPtrs := make([]*C.uint8_t, n)
|
|
sigPtrs := make([]*C.uint8_t, n)
|
|
var lensBuf []C.size_t
|
|
for i := 0; i < n; i++ {
|
|
if len(pubkeys[i]) != MLDSA65PublicKeySize {
|
|
return fmt.Errorf(
|
|
"quantumvm: MLDSAVerifyBatch[%d]: pubkey len %d != %d",
|
|
i, len(pubkeys[i]), MLDSA65PublicKeySize)
|
|
}
|
|
if len(signatures[i]) > MLDSA65SignatureSize {
|
|
return fmt.Errorf(
|
|
"quantumvm: MLDSAVerifyBatch[%d]: signature len %d > max %d",
|
|
i, len(signatures[i]), MLDSA65SignatureSize)
|
|
}
|
|
pinner.Pin(&pubkeys[i][0])
|
|
pkPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&pubkeys[i][0]))
|
|
// Allow zero-length message (legal — empty-message verify is
|
|
// a defined SHAKE256 absorb-zero operation per FIPS 204).
|
|
if len(messages[i]) > 0 {
|
|
pinner.Pin(&messages[i][0])
|
|
msgPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&messages[i][0]))
|
|
}
|
|
pinner.Pin(&signatures[i][0])
|
|
sigPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&signatures[i][0]))
|
|
}
|
|
if msgLens != nil {
|
|
lensBuf = make([]C.size_t, n)
|
|
for i := 0; i < n; i++ {
|
|
if msgLens[i] < 0 {
|
|
return fmt.Errorf(
|
|
"quantumvm: MLDSAVerifyBatch[%d]: negative msgLen %d",
|
|
i, msgLens[i])
|
|
}
|
|
lensBuf[i] = C.size_t(msgLens[i])
|
|
}
|
|
}
|
|
|
|
// bool layout in cgo: C.bool is one byte. Stage results in a
|
|
// scratch buffer; copy back to the caller's []bool after the call.
|
|
cresults := make([]C.bool, n)
|
|
pkPtrsPtr := (**C.uint8_t)(unsafe.Pointer(&pkPtrs[0]))
|
|
msgPtrsPtr := (**C.uint8_t)(unsafe.Pointer(&msgPtrs[0]))
|
|
sigPtrsPtr := (**C.uint8_t)(unsafe.Pointer(&sigPtrs[0]))
|
|
var lensPtr *C.size_t
|
|
if lensBuf != nil {
|
|
lensPtr = (*C.size_t)(unsafe.Pointer(&lensBuf[0]))
|
|
}
|
|
|
|
rc := C.qvm_op_mldsa_verify(
|
|
b.desc.vtbl,
|
|
b.ctx,
|
|
pkPtrsPtr,
|
|
msgPtrsPtr,
|
|
lensPtr,
|
|
C.uint32_t(msgWidthHint),
|
|
sigPtrsPtr,
|
|
(*C.bool)(unsafe.Pointer(&cresults[0])),
|
|
C.size_t(n),
|
|
)
|
|
runtime.KeepAlive(pkPtrs)
|
|
runtime.KeepAlive(msgPtrs)
|
|
runtime.KeepAlive(sigPtrs)
|
|
runtime.KeepAlive(lensBuf)
|
|
runtime.KeepAlive(cresults)
|
|
|
|
if rc != 0 {
|
|
return mapBackendError(int(rc), "op_mldsa_verify_batch")
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
results[i] = bool(cresults[i])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MLDSASignBatch dispatches op_mldsa_sign_batch from the loaded vtbl.
|
|
// See the GPUBackend interface doc + backend_plugin.h v14 block.
|
|
func (b *gpuBackend) MLDSASignBatch(
|
|
mode MLDSAMode,
|
|
skeys []byte,
|
|
msgs []byte,
|
|
msgLens []int,
|
|
msgWidthHint uint32,
|
|
count int,
|
|
sigsOut []byte,
|
|
sigLensOut []uint32,
|
|
) error {
|
|
if mode != MLDSAMode65 {
|
|
return ErrGPUNotAvailable
|
|
}
|
|
if count == 0 {
|
|
return nil
|
|
}
|
|
const skStride = MLDSA65SecretKeySize
|
|
const sigStride = MLDSA65SignatureSize
|
|
if len(skeys) != count*skStride {
|
|
return fmt.Errorf(
|
|
"quantumvm: MLDSASignBatch: skeys len %d != count*%d (%d)",
|
|
len(skeys), skStride, count*skStride)
|
|
}
|
|
if len(sigsOut) != count*sigStride {
|
|
return fmt.Errorf(
|
|
"quantumvm: MLDSASignBatch: sigsOut len %d != count*%d (%d)",
|
|
len(sigsOut), sigStride, count*sigStride)
|
|
}
|
|
if msgLens != nil && len(msgLens) != count {
|
|
return fmt.Errorf(
|
|
"quantumvm: MLDSASignBatch: msgLens length %d != count %d",
|
|
len(msgLens), count)
|
|
}
|
|
if sigLensOut != nil && len(sigLensOut) != count {
|
|
return fmt.Errorf(
|
|
"quantumvm: MLDSASignBatch: sigLensOut length %d != count %d",
|
|
len(sigLensOut), count)
|
|
}
|
|
if b == nil || b.ctx == nil || b.desc == nil {
|
|
return ErrGPUNotAvailable
|
|
}
|
|
|
|
// Validate the msgs pool is large enough to cover the per-element
|
|
// lengths. NULL msgLens uses msgWidthHint uniformly — same check
|
|
// against count * msgWidthHint.
|
|
var total int
|
|
if msgLens != nil {
|
|
for i := 0; i < count; i++ {
|
|
if msgLens[i] < 0 {
|
|
return fmt.Errorf(
|
|
"quantumvm: MLDSASignBatch[%d]: negative msgLen %d",
|
|
i, msgLens[i])
|
|
}
|
|
total += msgLens[i]
|
|
}
|
|
} else {
|
|
total = int(msgWidthHint) * count
|
|
}
|
|
if len(msgs) < total {
|
|
return fmt.Errorf(
|
|
"quantumvm: MLDSASignBatch: msgs pool len %d < total %d",
|
|
len(msgs), total)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
var pinner runtime.Pinner
|
|
defer pinner.Unpin()
|
|
pinner.Pin(&skeys[0])
|
|
if total > 0 {
|
|
pinner.Pin(&msgs[0])
|
|
}
|
|
pinner.Pin(&sigsOut[0])
|
|
var lensBuf []C.size_t
|
|
var lensPtr *C.size_t
|
|
if msgLens != nil {
|
|
lensBuf = make([]C.size_t, count)
|
|
for i := 0; i < count; i++ {
|
|
lensBuf[i] = C.size_t(msgLens[i])
|
|
}
|
|
lensPtr = (*C.size_t)(unsafe.Pointer(&lensBuf[0]))
|
|
}
|
|
var sigLensPtr *C.uint32_t
|
|
if sigLensOut != nil {
|
|
sigLensPtr = (*C.uint32_t)(unsafe.Pointer(&sigLensOut[0]))
|
|
}
|
|
var msgsPtr *C.uint8_t
|
|
if total > 0 {
|
|
msgsPtr = (*C.uint8_t)(unsafe.Pointer(&msgs[0]))
|
|
}
|
|
|
|
rc := C.qvm_op_mldsa_sign(
|
|
b.desc.vtbl,
|
|
b.ctx,
|
|
(*C.uint8_t)(unsafe.Pointer(&skeys[0])),
|
|
C.size_t(skStride),
|
|
msgsPtr,
|
|
lensPtr,
|
|
C.uint32_t(msgWidthHint),
|
|
C.size_t(count),
|
|
(*C.uint8_t)(unsafe.Pointer(&sigsOut[0])),
|
|
sigLensPtr,
|
|
)
|
|
runtime.KeepAlive(lensBuf)
|
|
|
|
if rc != 0 {
|
|
return mapBackendError(int(rc), "op_mldsa_sign_batch")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SLHDSAVerifyBatch dispatches the SHAKE-128f or SHAKE-192f variant of
|
|
// op_slhdsa_verify_batch from the loaded vtbl. The 10 other FIPS 205
|
|
// variants are not exposed at v14 — they return ErrGPUNotAvailable.
|
|
func (b *gpuBackend) SLHDSAVerifyBatch(
|
|
variant SLHDSAVariant,
|
|
pubkeys [][]byte,
|
|
messages [][]byte,
|
|
msgLens []int,
|
|
signatures [][]byte,
|
|
results []bool,
|
|
) error {
|
|
is128f := variant == SLHDSAShake128f
|
|
is192f := variant == SLHDSAShake192f
|
|
if !is128f && !is192f {
|
|
return ErrGPUNotAvailable
|
|
}
|
|
n := len(pubkeys)
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
if len(messages) != n || len(signatures) != n || len(results) != n {
|
|
return fmt.Errorf(
|
|
"quantumvm: SLHDSAVerifyBatch: length mismatch (pubkeys=%d messages=%d signatures=%d results=%d)",
|
|
n, len(messages), len(signatures), len(results))
|
|
}
|
|
if msgLens != nil && len(msgLens) != n {
|
|
return fmt.Errorf(
|
|
"quantumvm: SLHDSAVerifyBatch: msgLens length %d != batch %d",
|
|
len(msgLens), n)
|
|
}
|
|
pkSize := variant.PublicKeySize()
|
|
sigSize := variant.SignatureSize()
|
|
if b == nil || b.ctx == nil || b.desc == nil {
|
|
return ErrGPUNotAvailable
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
var pinner runtime.Pinner
|
|
defer pinner.Unpin()
|
|
pkPtrs := make([]*C.uint8_t, n)
|
|
msgPtrs := make([]*C.uint8_t, n)
|
|
sigPtrs := make([]*C.uint8_t, n)
|
|
lensBuf := make([]C.size_t, n)
|
|
for i := 0; i < n; i++ {
|
|
if len(pubkeys[i]) != pkSize {
|
|
return fmt.Errorf(
|
|
"quantumvm: SLHDSAVerifyBatch[%d]: pubkey len %d != %d",
|
|
i, len(pubkeys[i]), pkSize)
|
|
}
|
|
if len(signatures[i]) != sigSize {
|
|
return fmt.Errorf(
|
|
"quantumvm: SLHDSAVerifyBatch[%d]: signature len %d != %d",
|
|
i, len(signatures[i]), sigSize)
|
|
}
|
|
pinner.Pin(&pubkeys[i][0])
|
|
pkPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&pubkeys[i][0]))
|
|
if len(messages[i]) > 0 {
|
|
pinner.Pin(&messages[i][0])
|
|
msgPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&messages[i][0]))
|
|
}
|
|
pinner.Pin(&signatures[i][0])
|
|
sigPtrs[i] = (*C.uint8_t)(unsafe.Pointer(&signatures[i][0]))
|
|
// SLH-DSA has no msg_width_hint surface — msg_lens is
|
|
// mandatory at the vtbl boundary. Fill lensBuf from either
|
|
// msgLens or the message slice length.
|
|
if msgLens != nil {
|
|
if msgLens[i] < 0 {
|
|
return fmt.Errorf(
|
|
"quantumvm: SLHDSAVerifyBatch[%d]: negative msgLen %d",
|
|
i, msgLens[i])
|
|
}
|
|
lensBuf[i] = C.size_t(msgLens[i])
|
|
} else {
|
|
lensBuf[i] = C.size_t(len(messages[i]))
|
|
}
|
|
}
|
|
cresults := make([]C.bool, n)
|
|
pkPtrsPtr := (**C.uint8_t)(unsafe.Pointer(&pkPtrs[0]))
|
|
msgPtrsPtr := (**C.uint8_t)(unsafe.Pointer(&msgPtrs[0]))
|
|
sigPtrsPtr := (**C.uint8_t)(unsafe.Pointer(&sigPtrs[0]))
|
|
lensPtr := (*C.size_t)(unsafe.Pointer(&lensBuf[0]))
|
|
|
|
var rc C.int
|
|
if is128f {
|
|
rc = C.qvm_op_slhdsa_verify_128f(
|
|
b.desc.vtbl,
|
|
b.ctx,
|
|
pkPtrsPtr,
|
|
msgPtrsPtr,
|
|
lensPtr,
|
|
sigPtrsPtr,
|
|
(*C.bool)(unsafe.Pointer(&cresults[0])),
|
|
C.size_t(n),
|
|
)
|
|
} else {
|
|
rc = C.qvm_op_slhdsa_verify_192f(
|
|
b.desc.vtbl,
|
|
b.ctx,
|
|
pkPtrsPtr,
|
|
msgPtrsPtr,
|
|
lensPtr,
|
|
sigPtrsPtr,
|
|
(*C.bool)(unsafe.Pointer(&cresults[0])),
|
|
C.size_t(n),
|
|
)
|
|
}
|
|
runtime.KeepAlive(pkPtrs)
|
|
runtime.KeepAlive(msgPtrs)
|
|
runtime.KeepAlive(sigPtrs)
|
|
runtime.KeepAlive(lensBuf)
|
|
runtime.KeepAlive(cresults)
|
|
|
|
if rc != 0 {
|
|
return mapBackendError(int(rc), "op_slhdsa_verify_batch")
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
results[i] = bool(cresults[i])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// mapBackendError translates a LuxBackendError code to a Go error.
|
|
// NOT_SUPPORTED (3) maps to the package sentinel so callers can route
|
|
// to the CPU oracle via errors.Is. Other non-zero codes return a
|
|
// formatted error including the numeric code for diagnostics.
|
|
func mapBackendError(rc int, op string) error {
|
|
switch rc {
|
|
case 0:
|
|
return nil
|
|
case int(C.LUX_BACKEND_ERROR_NOT_SUPPORTED):
|
|
return ErrGPUNotAvailable
|
|
case int(C.LUX_BACKEND_ERROR_INVALID_ARGUMENT):
|
|
return fmt.Errorf("quantumvm: %s: invalid argument (rc=1)", op)
|
|
case int(C.LUX_BACKEND_ERROR_OUT_OF_MEMORY):
|
|
return fmt.Errorf("quantumvm: %s: out of memory (rc=2)", op)
|
|
case int(C.LUX_BACKEND_ERROR_DEVICE_LOST):
|
|
return fmt.Errorf("quantumvm: %s: device lost (rc=4)", op)
|
|
case int(C.LUX_BACKEND_ERROR_INTERNAL):
|
|
return fmt.Errorf("quantumvm: %s: internal error (rc=5)", op)
|
|
default:
|
|
return fmt.Errorf("quantumvm: %s: rc=%d", op, rc)
|
|
}
|
|
}
|