Identify a GPU plugin before trusting it, and load only from a named place

A GPU plugin replaces a VM's state transitions wholesale. Three of the four
seams reached one by dlsym'ing launcher names out of a library nothing had
identified, and all four would fall back to a bare filename — which makes the
dynamic loader's default search path, and in bridgevm's case the working
directory of whoever started the node, the thing that decides which code
computes consensus state.

A library exporting the five bridgevm launcher names and nothing else,
compiled from twenty lines of C that return 0:

  before, LUX_GPU_PLUGIN_DIR=/tmp/rogue   AutoBackend = hip
  before, no env, cwd holds the .so       AutoBackend = hip
  after,  either                          AutoBackend = none

The plugin header says a loader must check abi_version and vtbl_size. aivm,
bridgevm and mpcvm now do so before resolving a single launcher: a name was
the only thing being matched, and a name is not a contract.

Bare filenames and the CWD entry are gone. What is left is LUX_GPU_PLUGIN_DIR
and LUXCPP_PREFIX — places an operator named. Unset means no plugin, which is
the configuration every box without a GPU is already in.

Cherry-picked from 634df5e, whose quantumvm hunk is dropped: that seam's file
was deleted in 409d0e4 and the chain no longer loads a plugin.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-01 01:36:45 -07:00
co-authored by Hanzo Dev
parent 1eb35500ef
commit 6aa5b77a44
5 changed files with 118 additions and 23 deletions
+32
View File
@@ -34,9 +34,12 @@ package aivm
#cgo darwin LDFLAGS: -ldl
#cgo linux LDFLAGS: -ldl
#cgo CFLAGS: -I${SRCDIR}/../internal/luxgpu/include
#include <dlfcn.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// Six host-launcher trampolines — invoked by Go via cgo. The function
// pointer is opaque (void*); the cgo bridge casts it to the expected
@@ -136,6 +139,27 @@ static int call_aivm_proof_verify(void* fn, const void* ops, void* results,
return ((aivm_proof_verify_fn)fn)(ops, results, op_count, NULL);
}
#include "lux/gpu/backend_plugin.h"
// aivm_plugin_trusted resolves lux_gpu_backend_init in an already-dlopen'd
// library, runs it, and checks the two cookies the plugin header requires a
// loader to check: abi_version must be the one this build was compiled
// against, and vtbl_size must be our sizeof. Without it, dlsym'ing a launcher
// by name trusts a library nothing has identified — the name is the only thing
// being matched, and a name is not a contract. quantumvm already does this.
//
// 0 trusted | 1 no init symbol | 2 init false | 3 abi | 4 vtbl_size
static int aivm_plugin_trusted(void* h) {
void* sym = dlsym(h, LUX_GPU_BACKEND_INIT_SYMBOL);
if (sym == NULL) return 1;
lux_gpu_backend_desc desc;
memset(&desc, 0, sizeof desc);
if (!((lux_gpu_backend_init_fn)sym)(&desc)) return 2;
if (desc.abi_version != LUX_GPU_BACKEND_ABI_VERSION) return 3;
if (desc.vtbl_size != (uint32_t)sizeof(lux_gpu_backend_vtbl)) return 4;
return 0;
}
// dlopen / dlsym wrappers — kept here so backend.go can stay pure Go.
static void* lux_dlopen(const char* path) {
return dlopen(path, RTLD_NOW | RTLD_LOCAL);
@@ -508,6 +532,14 @@ func openGPUBackend(kind BackendKind, path string) (*GPUBackend, error) {
return nil, fmt.Errorf("aivm: dlopen(%s): %s", path, C.GoString(C.lux_dlerror()))
}
// Identify the library before trusting any symbol in it.
if rc := C.aivm_plugin_trusted(handle); rc != 0 {
C.lux_dlclose(handle)
return nil, fmt.Errorf(
"aivm: %s is not a lux GPU plugin this build can use (check %d)",
path, int(rc))
}
backendName := kind.String() // cuda / hip / metal / vulkan / webgpu
resolve := func(suffix string) (unsafe.Pointer, error) {
+4 -5
View File
@@ -82,9 +82,11 @@ func platformCandidates() []backendCandidate {
// - LUX_GPU_PLUGIN_DIR/build/metal-only/backends/<subdir>/<filename>
// - LUX_GPU_PLUGIN_DIR/build/vulkan-m1/backends/<subdir>/<filename>
// - LUXCPP_PREFIX/lib/<filename>
// - bare filename (DT_RUNPATH / DT_RPATH / system search)
//
// Bare last so an explicit env override always wins.
// Every entry is somewhere an operator named. There is deliberately no bare
// filename: with one the dynamic loader searches its own default path, and a
// GPU plugin replaces aivm's state transitions wholesale, so a library found
// rather than named would be deciding consensus state.
func candidatePaths(c backendCandidate) []string {
var paths []string
if dir := os.Getenv("LUX_GPU_PLUGIN_DIR"); dir != "" {
@@ -107,9 +109,6 @@ func candidatePaths(c backendCandidate) []string {
if dir := os.Getenv("LUXCPP_PREFIX"); dir != "" {
paths = append(paths, filepath.Join(dir, "lib", c.filename))
}
// Bare name — fall back to the loader's default search (DT_RPATH on Linux,
// @rpath on darwin, LD_LIBRARY_PATH / DYLD_LIBRARY_PATH set by user).
paths = append(paths, c.filename)
return paths
}
+49 -15
View File
@@ -15,14 +15,16 @@ package bridgevm
// public build to a private dep; dlopen + dlsym lets us probe at runtime
// and degrade cleanly to ErrGPUNotAvailable when the plugin is absent.
//
// Search path for the plugin:
// Search path for the plugin — every entry is somewhere an operator named:
//
// 1. $LUX_GPU_PLUGIN_DIR (explicit override, useful in tests / CI).
// 1. $LUX_GPU_PLUGIN_DIR/ and $LUX_GPU_PLUGIN_DIR/<bk>/ (explicit override).
// 2. $LUXCPP_PREFIX/lib/lux-gpu/ (the install tree).
// 3. $LUXCPP_PREFIX/lib/ (legacy install tree).
// 4. $HOME/work/the lux GPU plugin build/backends/<bk>/ (dev tree).
// 5. The current working directory.
// 6. The system loader's default path (dlopen with bare name).
//
// Neither variable set means no plugin, which is the configuration every box
// without a GPU is already in. There is deliberately no working-directory
// entry and no bare name: a plugin replaces bridgevm's state transitions
// wholesale, so a library the loader found would be deciding consensus state.
//
// Probe order — cuda → hip → metal → vulkan → webgpu. The first plugin that
// resolves all five symbols wins and sets activeBackend. Subsequent probes
@@ -38,10 +40,13 @@ package bridgevm
/*
#cgo LDFLAGS: -ldl
#cgo CFLAGS: -I${SRCDIR}/../internal/luxgpu/include
#include <dlfcn.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <string.h>
// Trampolines — we cannot call function pointers from Go directly through cgo.
// The trampoline takes the function pointer (resolved via dlsym from Go) and
@@ -122,6 +127,34 @@ static int call_transition(void* fn,
(void*)0);
}
#include "lux/gpu/backend_plugin.h"
// bvm_plugin_trusted resolves lux_gpu_backend_init in an already-dlopen'd
// library, runs it, and checks the two cookies the plugin header requires a
// loader to check: abi_version must be the one this build was compiled
// against, and vtbl_size must be our sizeof. Without this, dlsym'ing a
// launcher by name trusts a library nothing has identified — the name is the
// only thing being matched, and a name is not a contract.
//
// quantumvm already does exactly this; bridgevm, aivm and mpcvm dlsym'd
// blind. One way, and this is it.
//
// 0 — trusted
// 1 — no lux_gpu_backend_init (not one of our plugins)
// 2 — init returned false
// 3 — abi_version mismatch
// 4 — vtbl_size mismatch
static int bvm_plugin_trusted(void* h) {
void* sym = dlsym(h, LUX_GPU_BACKEND_INIT_SYMBOL);
if (sym == NULL) return 1;
lux_gpu_backend_desc desc;
memset(&desc, 0, sizeof desc);
if (!((lux_gpu_backend_init_fn)sym)(&desc)) return 2;
if (desc.abi_version != LUX_GPU_BACKEND_ABI_VERSION) return 3;
if (desc.vtbl_size != (uint32_t)sizeof(lux_gpu_backend_vtbl)) return 4;
return 0;
}
// dl helpers — wrap dlopen / dlsym / dlerror so the Go side doesn't have to
// deal with the C string lifetimes.
@@ -198,11 +231,6 @@ func probePlugin() {
setActiveBackend(bk)
return
}
// Bare-name fallback — let the dynamic loader use its default path.
if tryLoad(bk, dsoBareName(bk)) {
setActiveBackend(bk)
return
}
}
}
@@ -225,11 +253,12 @@ func candidatePluginPaths(bk Backend) []string {
paths = append(paths, filepath.Join(prefix, "lib", name))
}
// 4) CWD — last resort before falling back to the loader default.
if cwd, err := os.Getwd(); err == nil {
paths = append(paths, filepath.Join(cwd, name))
}
// Deliberately no CWD entry and no bare-name fallback. A GPU plugin
// replaces bridgevm's state transitions wholesale, so where it comes from
// has to be something an operator said, not something the loader found:
// with a bare name the dynamic loader searches its default path, and with
// CWD the working directory of whoever started the node decides which
// code computes consensus state. Both are named above or nothing loads.
return paths
}
@@ -274,6 +303,11 @@ func tryLoad(bk Backend, path string) bool {
return false
}
// Identify the library before trusting any symbol in it.
if rc := C.bvm_plugin_trusted(h); rc != 0 {
return false
}
prefix := "lux_" + bk.String() + "_bridgevm_"
syms := map[string]*unsafe.Pointer{
prefix + "signer_apply": &fnSignerApply,
+5 -3
View File
@@ -87,9 +87,11 @@ func candidatesFor(kind GPUBackendKind) []string {
}
}
// Bare leaf names — dynamic-linker default search picks them up if
// LD_LIBRARY_PATH / DYLD_LIBRARY_PATH or /usr/local/lib has them.
out = append(out, leaves...)
// Deliberately no bare leaf names. With one the dynamic linker searches
// its own default path, and a GPU plugin replaces mpcvm's ceremony and
// key-share transitions wholesale — a library found rather than named
// would be minting threshold-signing custody. Every entry above is
// somewhere an operator said.
return out
}
+28
View File
@@ -49,6 +49,8 @@ package mpcvm
#cgo darwin LDFLAGS: -ldl
#cgo linux LDFLAGS: -ldl
#cgo CFLAGS: -I${SRCDIR}/../internal/luxgpu/include
#include <dlfcn.h>
#include <stdint.h>
#include <stdlib.h>
@@ -190,6 +192,27 @@ static void* lux_dlsym(void* handle, const char* name) {
// if every candidate fails. The dlopen mode is RTLD_NOW|RTLD_LOCAL: NOW so
// any missing symbol fails fast at load time, LOCAL so the plugin's
// internal symbols don't pollute the global namespace.
#include "lux/gpu/backend_plugin.h"
// mpcvm_plugin_trusted resolves lux_gpu_backend_init in an already-dlopen'd
// library, runs it, and checks the two cookies the plugin header requires a
// loader to check: abi_version must be the one this build was compiled
// against, and vtbl_size must be our sizeof. Without it, dlsym'ing a launcher
// by name trusts a library nothing has identified — the name is the only thing
// being matched, and a name is not a contract. quantumvm already does this.
//
// 0 trusted | 1 no init symbol | 2 init false | 3 abi | 4 vtbl_size
static int mpcvm_plugin_trusted(void* h) {
void* sym = dlsym(h, LUX_GPU_BACKEND_INIT_SYMBOL);
if (sym == NULL) return 1;
lux_gpu_backend_desc desc;
memset(&desc, 0, sizeof desc);
if (!((lux_gpu_backend_init_fn)sym)(&desc)) return 2;
if (desc.abi_version != LUX_GPU_BACKEND_ABI_VERSION) return 3;
if (desc.vtbl_size != (uint32_t)sizeof(lux_gpu_backend_vtbl)) return 4;
return 0;
}
static void* lux_dlopen_first(const char* a, const char* b, const char* c) {
void* h = NULL;
if (a && *a) { h = dlopen(a, RTLD_NOW|RTLD_LOCAL); if (h) return h; }
@@ -790,6 +813,11 @@ func tryLoadPlugin(kind GPUBackendKind, candidates ...string) *GPUBackend {
return nil
}
// Identify the library before trusting any symbol in it.
if rc := C.mpcvm_plugin_trusted(h); rc != 0 {
return nil
}
resolve := func(name string) unsafe.Pointer {
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))