cumiddleware: reject unsupported RC token schemes with 501

The RDMA token parser accepts only the cuObject combined-token format,
whose first colon-delimited field is a hex-encoded uint64 base address
(at most 16 hex chars). Fixed-width binary RC token schemes such as AMD
hipObject's (a 44-byte payload hex-encoded to 88 chars, optionally
suffixed with ":addr:size") fail that parse and produce a 400 Bad
Request. hipObject clients treat a 400 as a hard failure: they fall
back to the HTTP data path only when the response carries no
x-amz-rdma-reply header, so the 400 blocks the fallback entirely.

Reject structurally incompatible token schemes with 501 Not
Implemented instead. The first field being longer than 16 hex chars
(and entirely hex) cannot be a valid cuObject base address, so this
never reclassifies well-formed cuObject tokens; non-hex fields still
fall through to the existing malformed-token 400 path. The 501 response
is serialized and sent directly from the middleware because the global
error handler collapses non-fiber errors into 500, and the
x-amz-rdma-reply header is deliberately left unset so RDMA-capable
clients recognize the gateway as RDMA-unsupported for this request and
use the HTTP path.

Signed-off-by: Jihyeon Gim <potatogim@potatogim.net>
This commit is contained in:
Jihyeon Gim
2026-08-23 14:52:48 +09:00
parent 5d9d54802d
commit 05487cf040
2 changed files with 130 additions and 0 deletions
+53
View File
@@ -33,6 +33,9 @@ import (
"github.com/gofiber/fiber/v3"
"github.com/valyala/fasthttp"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
)
// String keys used for fasthttp user-value storage.
@@ -147,6 +150,23 @@ func CuObjMiddleware(ctx fiber.Ctx) error {
// which case Content-Length is 0/absent; fall back to the token's own
// registered-buffer-size field (already part of the documented wire
// format) rather than leaving the backend with no usable size.
//
// Fixed-width binary RC token schemes (e.g. AMD hipObject's 88-hex-char
// token) are structurally incompatible with this gateway's DC transport.
// Reject them with 501 instead of a 400 parse error so such clients can
// fall back to the HTTP data path: they treat a response without
// x-amz-rdma-reply as "RDMA not supported", so the reply header is
// deliberately left unset. The error is serialized and sent directly
// (terminal response) because the global error handler collapses
// non-fiber errors into 500.
if isRCTokenScheme(token) {
requestID, hostID := utils.EnsureRequestIDs(ctx)
err := s3err.GetNotImplementedErr(HeaderRDMAToken, "")
ctx.Response().Header.SetContentType(fiber.MIMEApplicationXML)
ctx.Status(err.HTTPStatusCode)
return ctx.Send(err.XMLBody(requestID, hostID))
}
rctx.SetUserValue(localKeyRDMADescr, token)
remoteStart, err := parseRDMATokenBaseAddr(token)
@@ -164,6 +184,39 @@ func CuObjMiddleware(ctx fiber.Ctx) error {
return ctx.Next()
}
// isHexDigit reports whether c is an ASCII hexadecimal digit.
func isHexDigit(c byte) bool {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F')
}
// isRCTokenScheme reports whether the token's first colon-delimited field
// is longer than any cuObject base address can be: a cuObject base address
// is a hex-encoded uint64 (at most 16 hex chars, see the token layout
// comment above), while fixed-width binary RC token schemes such as AMD
// hipObject's (44-byte payload hex-encoded to 88 chars, optionally
// suffixed ":addr:size") always exceed it. Non-hex first fields are left
// to the regular cuObject parsing, which reports them as malformed.
// Note this deliberately reclassifies 17+-char non-canonical hex values
// (e.g. leading zeros) as unsupported; well-formed cuObject tokens are
// unaffected.
func isRCTokenScheme(token string) bool {
i := strings.IndexByte(token, ':')
first := token
if i >= 0 {
first = token[:i]
}
if len(first) <= 16 {
return false
}
for j := 0; j < len(first); j++ {
if !isHexDigit(first[j]) {
return false
}
}
return true
}
// parseRDMATokenBaseAddr extracts the remote base address — the first
// colon-delimited field, a hex-encoded uint64 — from a cuObj RDMA token.
func parseRDMATokenBaseAddr(token string) (uint64, error) {
+77
View File
@@ -21,6 +21,7 @@ package cumiddleware
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"strconv"
@@ -180,3 +181,79 @@ func TestSetRDMAReplyHeaderNoopForNonFasthttpContext(t *testing.T) {
ctx := InjectRDMAContext(t.Context(), "descr", 10, 0)
SetRDMAReplyHeader(ctx, http.StatusOK, 10)
}
// rcToken builds a fixed-width hex RC token of the hipObject shape:
// 88 lowercase hex chars, optionally suffixed with ":addr:size".
func rcToken(suffix string) string {
tok := ""
for i := 0; i < 88; i++ {
tok += string(rune('0' + i%10))
}
return tok + suffix
}
func TestRCTokenSchemeRejectedWith501(t *testing.T) {
cases := []struct {
name string
token string
}{
{"88 hex chars, no colon", rcToken("")},
{"88 hex chars with addr:size suffix", rcToken(":1234abcd:1000")},
{"88 hex chars uppercase", func() string {
tok := ""
for i := 0; i < 88; i++ {
tok += string(rune('A' + i%6))
}
return tok
}()},
{"17-char leading-zero hex first field", "0ffffffffffffffff:00000001"},
{"suffix contents are not inspected", rcToken(":not-hex!:not-hex2")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
app, reached := newTestApp(t)
req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set(HeaderRDMAToken, tc.token)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusNotImplemented, resp.StatusCode)
assert.Equal(t, fiber.MIMEApplicationXML,
resp.Header.Get("Content-Type"))
assert.Empty(t, resp.Header.Get(HeaderRDMAReply))
body, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(body), "<Code>NotImplemented</Code>")
select {
case <-reached:
t.Fatal("handler should not have been reached for an RC token")
default:
}
})
}
}
func TestRCTokenSchemeBoundaryPassesThrough(t *testing.T) {
// A 16-hex-char first field is a legal cuObject base address; with a
// valid second field the request must reach the downstream handler.
app, reached := newTestApp(t)
req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set(HeaderRDMAToken, "ffffffffffffffff:00000001:01020304:0102:010203:1:0102030405060708090a0b0c0d0e0f10")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
select {
case <-reached:
default:
t.Fatal("handler should have been reached for a cuObject token")
}
}
func TestRCTokenSchemeMalformedStill400(t *testing.T) {
// A >16-char first field that is not hex is not an RC token scheme; it
// falls through to the cuObject parser, which rejects it as malformed.
app, _ := newTestApp(t)
req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set(HeaderRDMAToken, "zzzzzzzzzzzzzzzzzzzz:00000001")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}