From 39aae5435cc9e6e07e9b8686470c738a1c6af1c7 Mon Sep 17 00:00:00 2001 From: Jihyeon Gim Date: Sun, 6 Sep 2026 03:15:02 +0900 Subject: [PATCH] rdma: close publication gaps for RC pre-session failures Review of the operational publication found four gaps where the records disagreed with the S3 surface or were missing entirely. Authorization-failure records lost the requester because the pre-session publisher did not carry the authenticated account; the account now flows into the record, so the audit trail names who was denied. Signature failures and malformed PREPARE headers ended the request before any publication point. The auth adapter now publishes authentication failures through the route handler, and header validation failures publish with whatever object identity the headers still carried, matching the S3 surface where every denied request still logs. RC requests carried no region: the custom routes run before the middleware that stores the region local, so event records lacked awsRegion and audit host headers read s3..amazonaws.com. The auth adapter sets the region for every verified RC request. Operational records reported generic 500 statuses for protocol errors that the wire answers with a specific status (a resource limit rejection logged 500 while the client saw 429). Status mapping now reuses the route error mapping, so the recorded status always equals the wire status. --- cmd/vgwrdma/main.go | 8 +++++- rdma/rcroutes/ops_linux.go | 17 +++++-------- rdma/rcroutes/routes_linux.go | 48 ++++++++++++++++++++++++----------- rdma/rcroutes/routes_stub.go | 3 +++ 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/cmd/vgwrdma/main.go b/cmd/vgwrdma/main.go index 4cf78035..bbb9d963 100644 --- a/cmd/vgwrdma/main.go +++ b/cmd/vgwrdma/main.go @@ -1287,13 +1287,19 @@ func runGateway(ctx context.Context, be backend.Backend) error { // nil on success without doing so. Wrap it so a verified // request reaches the route handler, while errors end the // chain as usual. + rcH := rcroutes.New(rcSvc, be, iamSvc, readonly, disableACLs) rcAuth := func(ctx fiber.Ctx) error { + // The RC routes run before the default-values + // middleware sets the request locals; the access + // logger and event schema read the region from + // there, so set it for every verified request. + utils.ContextKeyRegion.Set(ctx, region) if err := rcVerify(ctx); err != nil { + rcH.PublishAuthFailure(ctx, err) return rcroutes.WriteRouteError(ctx, err) } return ctx.Next() } - rcH := rcroutes.New(rcSvc, be, iamSvc, readonly, disableACLs) // The gateway builds the access logger, metrics manager, // and event sender inside RunVersityGW; hand them to the // RC routes as soon as they exist so finished transfers diff --git a/rdma/rcroutes/ops_linux.go b/rdma/rcroutes/ops_linux.go index e89ad6ae..341ec7ad 100644 --- a/rdma/rcroutes/ops_linux.go +++ b/rdma/rcroutes/ops_linux.go @@ -18,7 +18,6 @@ package rcroutes import ( - "errors" "fmt" "sync" "time" @@ -30,7 +29,6 @@ import ( "github.com/versity/versitygw/metrics" "github.com/versity/versitygw/rdma/rcserver" "github.com/versity/versitygw/s3api/utils" - "github.com/versity/versitygw/s3err" "github.com/versity/versitygw/s3event" "github.com/versity/versitygw/s3log" ) @@ -118,16 +116,14 @@ func (e *opsEmitter) publish(err error, bytes int64) { } // httpStatusFromError maps an operation error to the HTTP status -// the S3 surface would have answered with. +// the S3 surface would have answered with, using the same route +// error mapping as the wire response so operational records never +// disagree with what the client saw. func httpStatusFromError(err error) int { if err == nil { return 200 } - var serr s3err.S3Error - if errors.As(err, &serr) { - return serr.StatusCode() - } - return 500 + return routeError(err).HTTPStatusCode } // sessionRecord is one tracked session with its captured context. @@ -248,14 +244,15 @@ func (t *opsTracker) publishClaimed(sessionID string, err error, bytes ...int64) // publishRequest emits an operation record for a request that ended // before any session existed (authentication, authorization, or // header failures): no tracking table entry, single emission. -func (t *opsTracker) publishRequest(ctx fiber.Ctx, err error, - bucket, key string, isPut bool) { +func (t *opsTracker) publishRequest(ctx fiber.Ctx, acct auth.Account, + err error, bucket, key string, isPut bool) { if t == nil { return } emit := &opsEmitter{ ops: t.ops, app: t.app, + acct: acct, region: regionFromCtx(ctx), bucket: bucket, key: key, diff --git a/rdma/rcroutes/routes_linux.go b/rdma/rcroutes/routes_linux.go index b770708b..cb896f2d 100644 --- a/rdma/rcroutes/routes_linux.go +++ b/rdma/rcroutes/routes_linux.go @@ -83,6 +83,14 @@ type Handler struct { ops *opsTracker } +// PublishAuthFailure emits an operation record for a request whose +// authentication failed before any route logic ran. The gateway +// auth adapter calls it so signature failures appear in the access +// log like they do on the S3 surface. +func (h *Handler) PublishAuthFailure(ctx fiber.Ctx, err error) { + h.ops.publishRequest(ctx, auth.Account{}, err, "", "", false) +} + // SetOpsServices injects the operational service instances once the // gateway has created them, and wires the native teardown callback // that publishes sessions no request path ever completed. @@ -142,43 +150,53 @@ func (h *Handler) prepareCore(ctx fiber.Ctx) error { } defer h.svc.Leave() - if proto := ctx.Get(hdrProtocol); proto != protocolValue { - return invalidHeader(hdrProtocol, proto) - } - acct := utils.ContextKeyAccount.Get(ctx).(auth.Account) isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool) + // Header parse failures end the request before authorization; + // publish them as request records too, with whatever object + // identity the malformed headers still carried. + publishHeaderErr := func(err error) error { + target := ctx.Get(hdrTarget) + bucket, key, _ := splitTarget(target) + h.ops.publishRequest(ctx, acct, err, bucket, key, false) + return err + } + + if proto := ctx.Get(hdrProtocol); proto != protocolValue { + return publishHeaderErr(invalidHeader(hdrProtocol, proto)) + } + op := strings.ToUpper(ctx.Get(hdrOp)) if op != "GET" && op != "PUT" { - return invalidHeader(hdrOp, ctx.Get(hdrOp)) + return publishHeaderErr(invalidHeader(hdrOp, ctx.Get(hdrOp))) } + isPut := op == "PUT" target := ctx.Get(hdrTarget) bucket, key, ok := splitTarget(target) if !ok { - return invalidHeader(hdrTarget, target) + return publishHeaderErr(invalidHeader(hdrTarget, target)) } size, err := parseUint(ctx.Get(hdrSize), 10, 64) if err != nil || size == 0 { - return invalidHeader(hdrSize, ctx.Get(hdrSize)) + return publishHeaderErr(invalidHeader(hdrSize, ctx.Get(hdrSize))) } offset, err := parseUint(ctx.Get(hdrOffset), 10, 64) if err != nil { - return invalidHeader(hdrOffset, ctx.Get(hdrOffset)) + return publishHeaderErr(invalidHeader(hdrOffset, ctx.Get(hdrOffset))) } psn, err := parseUint(ctx.Get(hdrPsn), 16, 32) if err != nil || psn == 0 || psn > 0xffffff { - return invalidHeader(hdrPsn, ctx.Get(hdrPsn)) + return publishHeaderErr(invalidHeader(hdrPsn, ctx.Get(hdrPsn))) } cookie, err := parseUint(ctx.Get(hdrCookie), 16, 32) if err != nil || cookie == 0 { - return invalidHeader(hdrCookie, ctx.Get(hdrCookie)) + return publishHeaderErr(invalidHeader(hdrCookie, ctx.Get(hdrCookie))) } - isPut := op == "PUT" // Authorize through the regular object-access chain. if err := h.authorize(ctx, acct, isRoot, bucket, key, isPut); err != nil { - h.ops.publishRequest(ctx, err, bucket, key, isPut) + h.ops.publishRequest(ctx, acct, err, bucket, key, isPut) return err } @@ -193,7 +211,7 @@ func (h *Handler) prepareCore(ctx fiber.Ctx) error { ClientToken: ctx.Get(hdrToken), }) if err != nil { - h.ops.publishRequest(ctx, mapRcError(err), bucket, key, isPut) + h.ops.publishRequest(ctx, acct, mapRcError(err), bucket, key, isPut) return mapRcError(err) } @@ -202,12 +220,12 @@ func (h *Handler) prepareCore(ctx fiber.Ctx) error { if !isPut { if err := h.stageGet(ctx, resp.SessionID, bucket, key, offset, size); err != nil { _ = h.svc.FinishPrepare(resp.SessionID, false) - h.ops.publishRequest(ctx, err, bucket, key, isPut) + h.ops.publishRequest(ctx, acct, err, bucket, key, isPut) return err } } if err := h.svc.FinishPrepare(resp.SessionID, true); err != nil { - h.ops.publishRequest(ctx, mapRcError(err), bucket, key, isPut) + h.ops.publishRequest(ctx, acct, mapRcError(err), bucket, key, isPut) return mapRcError(err) } diff --git a/rdma/rcroutes/routes_stub.go b/rdma/rcroutes/routes_stub.go index bace981b..6a48adab 100644 --- a/rdma/rcroutes/routes_stub.go +++ b/rdma/rcroutes/routes_stub.go @@ -61,3 +61,6 @@ type OpsServices struct { // SetOpsServices is a stub: without RDMA support there is nothing // to publish into. func (h *Handler) SetOpsServices(ops OpsServices) {} + +// PublishAuthFailure is a stub mirror of the linux handler. +func (h *Handler) PublishAuthFailure(ctx fiber.Ctx, err error) {}