mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-16 23:54:13 +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.
57 lines
2.5 KiB
Go
57 lines
2.5 KiB
Go
package serverless
|
|
|
|
import "context"
|
|
|
|
// invCtxKey is the unexported context-value key used to attach an
|
|
// InvocationContext to a Go context. The empty struct is the standard
|
|
// Go pattern for context keys (avoids string-collision risk).
|
|
type invCtxKey struct{}
|
|
|
|
// WithInvocationContext returns a derived ctx that carries invCtx. Host
|
|
// function accessors check the ctx FIRST and only fall back to the
|
|
// HostFunctions singleton field when nothing is carried on ctx.
|
|
//
|
|
// Why this exists: HostFunctions is a process-wide singleton (one per
|
|
// gateway engine). Its `invCtx` field is shared across all WASM instances.
|
|
// For STATELESS functions the gateway sets/clears that field per-call
|
|
// (executor contextSetter/contextClearer), 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.
|
|
//
|
|
// For PERSISTENT WS functions the race is far worse: the field used to be
|
|
// bound ONCE at instantiation and reused for the connection's lifetime.
|
|
// Two simultaneous persistent WS connections from different users
|
|
// overwrote each other's invCtx, and every subsequent function_invoke /
|
|
// GetCallerJWTSubject / GetSecret call from inside the WASM read whatever
|
|
// was bound LAST — silently leaking identity across tenants.
|
|
//
|
|
// The fix is per-call invCtx propagation through Go's context.Context.
|
|
// wazero passes the ctx given to api.Function.Call all the way through
|
|
// to host function callbacks (engine.go's host-function wrappers receive
|
|
// it), so every WASM-host hop carries its own invCtx and never reads the
|
|
// shared field.
|
|
//
|
|
// Persistent WS uses this exclusively (see persistent.Instance, which
|
|
// wraps every export call's ctx with the per-instance invCtx). Stateless
|
|
// continues to use the singleton-field path for now — its race window
|
|
// is microseconds, has been latent since the host-functions split, and
|
|
// migrating it is a separate scoped change.
|
|
func WithInvocationContext(ctx context.Context, invCtx *InvocationContext) context.Context {
|
|
if invCtx == nil {
|
|
return ctx
|
|
}
|
|
return context.WithValue(ctx, invCtxKey{}, invCtx)
|
|
}
|
|
|
|
// InvocationContextFromCtx extracts the invCtx attached via
|
|
// WithInvocationContext, or nil if none is present. Exported so the
|
|
// hostfunctions package and any other consumer can read it without
|
|
// duplicating the key type.
|
|
func InvocationContextFromCtx(ctx context.Context) *InvocationContext {
|
|
if ctx == nil {
|
|
return nil
|
|
}
|
|
v, _ := ctx.Value(invCtxKey{}).(*InvocationContext)
|
|
return v
|
|
}
|