mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-16 21:54:14 +00:00
HostFunctions is a process-wide singleton (one per gateway engine). Its `invCtx` field is shared across all WASM instances. For STATELESS execution the executor sets/clears it per-call but the lock is released before WASM runs — two concurrent invocations can race on the field and one's host call can read the other's identity. Window is microseconds. For PERSISTENT WS the bug was much worse: invCtx used to be bound ONCE at instantiation and reused for the connection's lifetime. Two simultaneous persistent WS connections from different namespaces / wallets overwrote each other's invCtx, and EVERY subsequent function_invoke / GetCallerJWTSubject / GetCallerWallet / GetSecret call from inside the WASM read whatever was bound LAST. Result: silent identity leak across tenants for as long as the connections overlapped. Fix: per-call invCtx propagation through Go's context.Context. wazero passes the ctx given to api.Function.Call through to host function callbacks, so every WASM-host hop carries its own invCtx. - pkg/serverless/invocation_context.go (new): WithInvocationContext + InvocationContextFromCtx helpers using an unexported invCtxKey. - pkg/serverless/hostfunctions/invocation_context.go (new): currentInvocationContext(ctx) — ctx-attached invCtx wins over the singleton field. - All host accessors (FunctionInvoke, GetEnv, GetSecret, GetRequestID, GetCallerWallet, GetWSClientID, GetCallerClaim, GetCallerJWTSubject) now route through currentInvocationContext(ctx). - pkg/serverless/persistent/instance.go: every export call's ctx is wrapped with the per-instance invCtx before being passed to wazero. - pkg/gateway/handlers/serverless/ws_persistent_handler.go: invCtx is built per-frame and attached to ctx, not stored on a shared field. - pkg/serverless/engine.go: removed the SetInvocationContext call at InstantiatePersistent (no longer needed; ctx carries it). Stateless still uses the singleton field — its race is latent since the host-functions split and migrating it is a separate scoped change. Tests: - hostfunctions/invocation_context_test.go covers ctx-wins-over-singleton. - gateway/handlers/serverless/ws_persistent_handler_test.go covers the per-frame ctx wiring. - cli/functions/build_test.go is new coverage for the build path touched in this change. VERSION bumped to 0.122.24.
124 lines
4.1 KiB
Go
124 lines
4.1 KiB
Go
package functions
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// tinygoBuildArgs returns the argv (without the leading `tinygo`) used
|
|
// to compile a function. Pure function — extracted from buildFunction
|
|
// so the WS-persistent → `-buildmode=c-shared` policy can be unit
|
|
// tested without invoking TinyGo.
|
|
//
|
|
// Persistent WS functions need the WASI-reactor variant (exports
|
|
// `_initialize`, no `_start`) — see the comment on cfg loading in
|
|
// buildFunction for the full rationale. Stateless (default) functions
|
|
// stay on command mode for back-compat.
|
|
func tinygoBuildArgs(outputPath string, wsPersistent bool) []string {
|
|
args := []string{"build", "-o", outputPath, "-target", "wasi"}
|
|
if wsPersistent {
|
|
args = append(args, "-buildmode=c-shared")
|
|
}
|
|
args = append(args, ".")
|
|
return args
|
|
}
|
|
|
|
// BuildCmd compiles a function to WASM using TinyGo.
|
|
var BuildCmd = &cobra.Command{
|
|
Use: "build [directory]",
|
|
Short: "Build a function to WASM using TinyGo",
|
|
Long: `Compiles function.go in the given directory (or current directory) to a WASM binary.
|
|
Requires TinyGo to be installed (https://tinygo.org/getting-started/install/).`,
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: runBuild,
|
|
}
|
|
|
|
func runBuild(cmd *cobra.Command, args []string) error {
|
|
dir := ""
|
|
if len(args) > 0 {
|
|
dir = args[0]
|
|
}
|
|
_, err := buildFunction(dir)
|
|
return err
|
|
}
|
|
|
|
// buildFunction compiles the function in dir and returns the path to the WASM output.
|
|
func buildFunction(dir string) (string, error) {
|
|
absDir, err := ResolveFunctionDir(dir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Verify function.go exists
|
|
goFile := filepath.Join(absDir, "function.go")
|
|
if _, err := os.Stat(goFile); os.IsNotExist(err) {
|
|
return "", fmt.Errorf("function.go not found in %s", absDir)
|
|
}
|
|
|
|
// Verify function.yaml exists
|
|
if _, err := os.Stat(filepath.Join(absDir, "function.yaml")); os.IsNotExist(err) {
|
|
return "", fmt.Errorf("function.yaml not found in %s", absDir)
|
|
}
|
|
|
|
// Load config so we can pick the right TinyGo build mode based on
|
|
// ws_persistent. Persistent functions need WASI-reactor semantics
|
|
// (`_initialize` export, no `_start`); command-mode functions stay
|
|
// on the default. See bug #240/#249 follow-up #6 for the full
|
|
// rationale — TL;DR: TinyGo command-mode `_start` doesn't set the
|
|
// runtime guard `wasmExportCheckRun` checks, so any export call
|
|
// from the host (e.g. orama_alloc → ws_open payload) traps with
|
|
// "wasm error: unreachable" inside the runtime hashmap path.
|
|
//
|
|
// `-buildmode=c-shared` flips TinyGo to reactor mode: the wasm
|
|
// exports `_initialize` instead of `_start`. The gateway's
|
|
// persistent-instance bootstrap (pkg/serverless/engine.go) calls
|
|
// `_initialize` first if exported, which sets the guard cleanly,
|
|
// and the function's exports become callable from the host loop.
|
|
cfg, cfgErr := LoadConfig(absDir)
|
|
if cfgErr != nil {
|
|
return "", fmt.Errorf("failed to load function.yaml: %w", cfgErr)
|
|
}
|
|
|
|
// Check TinyGo is installed
|
|
tinygoPath, err := exec.LookPath("tinygo")
|
|
if err != nil {
|
|
return "", fmt.Errorf("tinygo not found in PATH. Install it: https://tinygo.org/getting-started/install/")
|
|
}
|
|
|
|
outputPath := filepath.Join(absDir, "function.wasm")
|
|
|
|
fmt.Printf("Building %s...\n", absDir)
|
|
|
|
// Build args. Default = command mode. Persistent WS functions get
|
|
// reactor mode via `-buildmode=c-shared` so TinyGo emits
|
|
// `_initialize` and the runtime guard activates.
|
|
tinygoArgs := tinygoBuildArgs(outputPath, cfg.WSPersistent)
|
|
if cfg.WSPersistent {
|
|
fmt.Printf(" (ws_persistent=true → using -buildmode=c-shared for WASI-reactor semantics)\n")
|
|
}
|
|
|
|
buildCmd := exec.Command(tinygoPath, tinygoArgs...)
|
|
buildCmd.Dir = absDir
|
|
buildCmd.Stdout = os.Stdout
|
|
buildCmd.Stderr = os.Stderr
|
|
|
|
if err := buildCmd.Run(); err != nil {
|
|
return "", fmt.Errorf("tinygo build failed: %w", err)
|
|
}
|
|
|
|
// Validate output
|
|
if err := ValidateWASMFile(outputPath); err != nil {
|
|
os.Remove(outputPath)
|
|
return "", fmt.Errorf("build produced invalid WASM: %w", err)
|
|
}
|
|
|
|
info, _ := os.Stat(outputPath)
|
|
fmt.Printf("Built %s (%d bytes)\n", outputPath, info.Size())
|
|
|
|
return outputPath, nil
|
|
}
|