mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-16 22:54:12 +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.
83 lines
2.5 KiB
Go
83 lines
2.5 KiB
Go
package hostfunctions
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/DeBrosOfficial/network/pkg/serverless"
|
|
)
|
|
|
|
// WSPubSubBridge wires a WS client to a PubSub topic in the function's
|
|
// own namespace. Returns an error if:
|
|
//
|
|
// - bridge is not configured on this gateway
|
|
// - the function has no namespace in its invocation context
|
|
// - the client's namespace (set at WS upgrade) doesn't match the function's
|
|
// - the bridge itself returns an error (e.g. per-client topic cap exceeded)
|
|
//
|
|
// Idempotent: re-bridging the same (client, topic) is a no-op.
|
|
func (h *HostFunctions) WSPubSubBridge(ctx context.Context, clientID, topic string) error {
|
|
if h.wsBridge == nil {
|
|
return &serverless.HostFunctionError{
|
|
Function: "ws_pubsub_bridge",
|
|
Cause: fmt.Errorf("bridge not configured on this gateway"),
|
|
}
|
|
}
|
|
fnNS := h.namespaceFromCtx(ctx)
|
|
if fnNS == "" {
|
|
return &serverless.HostFunctionError{
|
|
Function: "ws_pubsub_bridge",
|
|
Cause: fmt.Errorf("no namespace in invocation context"),
|
|
}
|
|
}
|
|
cliNS, ok := h.wsBridge.GetClientNamespace(clientID)
|
|
if !ok {
|
|
return &serverless.HostFunctionError{
|
|
Function: "ws_pubsub_bridge",
|
|
Cause: fmt.Errorf("unknown client_id %q", clientID),
|
|
}
|
|
}
|
|
if cliNS != fnNS {
|
|
return &serverless.HostFunctionError{
|
|
Function: "ws_pubsub_bridge",
|
|
Cause: fmt.Errorf("namespace mismatch: function=%q client=%q", fnNS, cliNS),
|
|
}
|
|
}
|
|
if err := h.wsBridge.Add(ctx, fnNS, clientID, topic); err != nil {
|
|
return &serverless.HostFunctionError{Function: "ws_pubsub_bridge", Cause: err}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WSPubSubUnbridge removes a (client, topic) bridge. Idempotent.
|
|
func (h *HostFunctions) WSPubSubUnbridge(ctx context.Context, clientID, topic string) error {
|
|
if h.wsBridge == nil {
|
|
return &serverless.HostFunctionError{
|
|
Function: "ws_pubsub_unbridge",
|
|
Cause: fmt.Errorf("bridge not configured on this gateway"),
|
|
}
|
|
}
|
|
fnNS := h.namespaceFromCtx(ctx)
|
|
if fnNS == "" {
|
|
return &serverless.HostFunctionError{
|
|
Function: "ws_pubsub_unbridge",
|
|
Cause: fmt.Errorf("no namespace in invocation context"),
|
|
}
|
|
}
|
|
if err := h.wsBridge.Remove(ctx, fnNS, clientID, topic); err != nil {
|
|
return &serverless.HostFunctionError{Function: "ws_pubsub_unbridge", Cause: err}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// namespaceFromCtx returns the current invocation's namespace, or "" if
|
|
// no context is set. ctx-attached invCtx wins over the singleton (see
|
|
// invocation_context.go).
|
|
func (h *HostFunctions) namespaceFromCtx(ctx context.Context) string {
|
|
cur := h.currentInvocationContext(ctx)
|
|
if cur == nil {
|
|
return ""
|
|
}
|
|
return cur.Namespace
|
|
}
|