From 7acb1b591d6e42d17972fb7e5fe51529d8649f35 Mon Sep 17 00:00:00 2001 From: Ben Jarvis Date: Sat, 20 Jun 2026 19:17:02 +0000 Subject: [PATCH] rpc2: RPCSEC_GSS privacy service (krb5p) Implement the GSS privacy service (rpc_gss_svc_privacy, sec=krb5p) on top of the existing krb5/krb5i RPCSEC_GSS layer. Call arguments arrive wrapped as rpc_gss_priv_data { opaque databody_priv<> } where databody_priv is the GSS-sealed token of XDR(seq_num) || XDR(proc_args); gss_unwrap recovers the plaintext, the embedded seq is verified against the credential, and the inner proc arguments are repointed for normal dispatch. Replies are wrapped symmetrically: gss_wrap(seq_num || results) emitted as the single opaque of rpc_gss_priv_data. EVPL_RPC2_GSS_SVC_PRIVACY is now accepted in the DATA path rather than rejected with AUTH_TOOWEAK. The GSS wrap/unwrap scratch buffers (the privacy databody_priv token, the integrity databody, and the privacy reply plaintext) need to be contiguous for the crypto calls. They previously came from the request's fixed 128 KiB dbuf arena, so any wrapped call larger than the arena (e.g. a 256 KiB WRITE) failed. Allocate them from the recycling iovec allocator (evpl_iovec_alloc, max_iovecs == 1) instead: contiguous, sized to the message, and recycled rather than malloc/free-churned on every data-path op. Add evpl_buffer_size() to expose the allocator's single-buffer contiguous limit. Bound each length before allocating: it must fit in the bytes actually received (a raw wire u32 reaches ~4 GiB; the message is already capped at EVPL_RPC2_MAX_REASM_LENGTH) and within one recycling buffer. The latter is also a correctness fix -- evpl_iovec_alloc with max_iovecs == 1 cannot satisfy a single-iovec request larger than buffer_size and would spin, so an oversized GSS payload (the existing inner-args allocation included) is now rejected as malformed rather than hanging the rpc2 thread. buffer_size (2 MiB default) sits far above any NFS wsize/rsize. --- include/evpl/evpl_memory.h | 5 + src/core/iovec.c | 6 + src/rpc2/rpc2.c | 308 +++++++++++++++++++++++++++++++++++-- 3 files changed, 303 insertions(+), 16 deletions(-) diff --git a/include/evpl/evpl_memory.h b/include/evpl/evpl_memory.h index 49d1885a..8383a1b5 100644 --- a/include/evpl/evpl_memory.h +++ b/include/evpl/evpl_memory.h @@ -217,6 +217,11 @@ int evpl_iovec_alloc( unsigned int flags, struct evpl_iovec *r_iovec); +/* The configured iovec buffer size: the largest contiguous region a single + * evpl_iovec_alloc (max_iovecs == 1) can satisfy. */ +unsigned int evpl_buffer_size( + void); + int evpl_iovec_reserve( struct evpl *evpl, unsigned int length, diff --git a/src/core/iovec.c b/src/core/iovec.c index 8f382a77..864064e8 100644 --- a/src/core/iovec.c +++ b/src/core/iovec.c @@ -176,6 +176,12 @@ evpl_iovec_alloc( return niovs; } /* evpl_iovec_alloc */ +SYMBOL_EXPORT unsigned int +evpl_buffer_size(void) +{ + return evpl_shared->config->buffer_size; +} /* evpl_buffer_size */ + void evpl_iovec_alloc_whole( struct evpl *evpl, diff --git a/src/rpc2/rpc2.c b/src/rpc2/rpc2.c index 133e715e..87801395 100644 --- a/src/rpc2/rpc2.c +++ b/src/rpc2/rpc2.c @@ -611,6 +611,19 @@ evpl_rpc2_gss_wrap_reply_integrity( struct evpl_iovec *out_iov, int *out_length); +/* Defined in the RPCSEC_GSS section below; used by evpl_rpc2_send_reply to + * wrap a privacy-service (krb5p) reply body as rpc_gss_priv_data. */ +static int +evpl_rpc2_gss_wrap_reply_privacy( + struct evpl *evpl, + struct evpl_rpc2_request *request, + struct evpl_iovec *msg_iov, + int msg_niov, + int length, + int reserve, + struct evpl_iovec *out_iov, + int *out_length); + static int evpl_rpc2_send_reply( struct evpl *evpl, @@ -676,6 +689,23 @@ evpl_rpc2_send_reply( length = wrapped_len; } } + + /* Privacy service (krb5p): GSS-wrap the proc results as the single + * opaque of rpc_gss_priv_data. Same non-RDMA / SUCCESS constraints and + * safe-fallthrough semantics as the integrity path above. */ + if (!rdma && error_stat == SUCCESS && + request->gss_service == EVPL_RPC2_GSS_SVC_PRIVACY) { + int wrapped_len; + + if (evpl_rpc2_gss_wrap_reply_privacy(evpl, request, msg_iov, + msg_niov, length, reserve, + &gss_wrapped_iov, + &wrapped_len) == 0) { + msg_iov = &gss_wrapped_iov; + msg_niov = 1; + length = wrapped_len; + } + } } else { /* MSG_DENIED - currently only AUTH_ERROR is supported */ rpc_reply.body.rbody.rreply.stat = AUTH_ERROR; @@ -941,9 +971,9 @@ evpl_rpc2_send_reply_success( * context-establishment handshake (carried on the program NULL procedure), * the per-request call verifier and sequence-number replay window, and the * reply verifier. A registered provider (evpl_rpc2_set_gss_provider) wraps - * the actual GSS mechanism (Kerberos). This first cut implements the - * authentication service (rpc_gss_svc_none, i.e. sec=krb5); integrity and - * privacy are rejected with AUTH_TOOWEAK until wrapping is wired up. + * the actual GSS mechanism (Kerberos). All three services are implemented: + * authentication (rpc_gss_svc_none, sec=krb5), integrity (sec=krb5i, + * rpc_gss_integ_data) and privacy (sec=krb5p, rpc_gss_priv_data). */ /* rpc_gss_proc_t */ @@ -1281,9 +1311,10 @@ evpl_rpc2_gss_unwrap_integrity( uint8_t lenbuf[4]; const uint8_t *lp; uint32_t db_len, ck_off, ck_len, embedded, inner_len; - uint8_t *databody, *checksum; + uint8_t *databody = NULL, *checksum; + struct evpl_iovec databody_iov; struct evpl_iovec *inner; - int ninner; + int ninner, rc = -1, have_databody = 0; /* databody length prefix */ if (evpl_rpc2_iov_gather(iov, niov, 0, lenbuf, 4)) { @@ -1294,28 +1325,47 @@ evpl_rpc2_gss_unwrap_integrity( return -1; } - databody = xdr_dbuf_alloc_space(db_len, dbuf); - if (!databody || evpl_rpc2_iov_gather(iov, niov, 4, databody, db_len)) { + /* Bound the claimed length before allocating. It must fit in the bytes + * actually received (a raw wire u32 reaches ~4 GiB; the message itself is + * already capped at EVPL_RPC2_MAX_REASM_LENGTH) and within one recycling + * buffer: the databody must be contiguous for verify_mic, and a single-iovec + * request larger than buffer_size cannot be satisfied. buffer_size sits far + * above any NFS wsize, so a larger databody is treated as malformed. */ + if (4 + (uint64_t) db_len > (uint64_t) (uint32_t) *len_p || + db_len > evpl_buffer_size()) { return -1; } + /* Gather the databody (seq || args, can be a full WRITE payload) into a + * contiguous, recycled iovec (freed via the out: label); the recycling + * allocator avoids per-request malloc churn. The small checksum stays in + * the dbuf. */ + if (evpl_iovec_alloc(evpl, db_len, 8, 1, 0, &databody_iov) != 1) { + return -1; + } + have_databody = 1; + databody = databody_iov.data; + if (evpl_rpc2_iov_gather(iov, niov, 4, databody, db_len)) { + goto out; + } + /* checksum opaque follows the (padded) databody */ ck_off = 4 + evpl_rpc2_gss_pad4(db_len); if (evpl_rpc2_iov_gather(iov, niov, ck_off, lenbuf, 4)) { - return -1; + goto out; } lp = lenbuf; evpl_rpc2_gss_rd_u32(&lp, lenbuf + 4, &ck_len); checksum = xdr_dbuf_alloc_space(ck_len ? ck_len : 1, dbuf); if (!checksum || evpl_rpc2_iov_gather(iov, niov, ck_off + 4, checksum, ck_len)) { - return -1; + goto out; } if (thread->gss_provider->verify_mic(thread->gss_provider_arg, ctx->gss_ctx, databody, db_len, checksum, ck_len)) { evpl_rpc2_debug("rpcsec_gss: integ databody MIC failed db_len=%u " "ck_len=%u niov=%d", db_len, ck_len, niov); - return -1; + goto out; } /* The seq embedded in databody must match the credential seq. */ @@ -1324,7 +1374,7 @@ evpl_rpc2_gss_unwrap_integrity( if (embedded != seq) { evpl_rpc2_debug("rpcsec_gss: integ seq mismatch embedded=%u cred=%u", embedded, seq); - return -1; + goto out; } /* The inner proc arguments are databody after the 4-byte seq. Copy them @@ -1335,16 +1385,142 @@ evpl_rpc2_gss_unwrap_integrity( inner_len = db_len - 4; inner = xdr_dbuf_alloc_space(sizeof(*inner), dbuf); if (!inner) { + goto out; + } + ninner = evpl_iovec_alloc(evpl, inner_len ? inner_len : 1, 8, 1, 0, inner); + if (ninner != 1) { + goto out; + } + inner->length = inner_len; + if (inner_len) { + memcpy(inner->data, databody + 4, inner_len); + } + + evpl_iovecs_release(evpl, request->msg->req_iov, request->msg->req_niov); + request->msg->req_iov = inner; + request->msg->req_niov = 1; + + *iov_p = inner; + *niov_p = 1; + *len_p = (int) inner_len; + rc = 0; + + out: + if (have_databody) { + evpl_iovec_release(evpl, &databody_iov); + } + return rc; +} /* evpl_rpc2_gss_unwrap_integrity */ + +/* + * Privacy service (krb5p): the call arguments are wrapped as + * rpc_gss_priv_data { opaque databody_priv<>; } + * where databody_priv is the GSS-wrapped (encrypted+sealed) token of + * XDR(seq_num) || XDR(proc_args). gss_unwrap recovers the plaintext; verify + * the embedded seq, then repoint (*iov_p, *niov_p, *len_p) to the inner proc + * arguments (plaintext after the 4-byte seq) so normal dispatch sees the + * unwrapped call. + * + * Must be called with evpl_rpc2_gss_lock held (it touches ctx->gss_ctx). + * Returns 0 on success, -1 on a framing/decryption failure. + */ +static int +evpl_rpc2_gss_unwrap_privacy( + struct evpl_rpc2_request *request, + struct evpl_rpc2_gss_context *ctx, + uint32_t seq, + struct evpl_iovec **iov_p, + int *niov_p, + int *len_p) +{ + struct evpl_rpc2_thread *thread = request->thread; + struct evpl *evpl = thread->evpl; + struct xdr_dbuf *dbuf = &request->msg->dbuf; + struct evpl_iovec *iov = *iov_p; + int niov = *niov_p; + uint8_t lenbuf[4]; + const uint8_t *lp; + uint32_t wrapped_len, embedded, inner_len; + struct evpl_iovec wrapped_iov; + void *plain = NULL; + size_t plain_len = 0; + struct evpl_iovec *inner; + int ninner, rc; + + /* databody_priv opaque length prefix */ + if (evpl_rpc2_iov_gather(iov, niov, 0, lenbuf, 4)) { + return -1; + } + lp = lenbuf; + if (evpl_rpc2_gss_rd_u32(&lp, lenbuf + 4, &wrapped_len) || wrapped_len == 0) { + return -1; + } + + /* Bound the claimed length before allocating. It must fit in the bytes + * actually received (a raw wire u32 reaches ~4 GiB; the message itself is + * already capped at EVPL_RPC2_MAX_REASM_LENGTH) and within one recycling + * buffer: gss_unwrap needs a single contiguous scratch, and a single-iovec + * request larger than buffer_size cannot be satisfied. buffer_size sits + * far above any NFS wsize, so a larger token is treated as malformed. */ + if (4 + (uint64_t) wrapped_len > (uint64_t) (uint32_t) *len_p || + wrapped_len > evpl_buffer_size()) { + return -1; + } + + /* Gather the ciphertext into a contiguous, recycled iovec for gss_unwrap + * (the recycling allocator avoids per-request malloc churn on the data + * path; the wrapped token can be a full WRITE payload + GSS overhead). */ + if (evpl_iovec_alloc(evpl, wrapped_len, 8, 1, 0, &wrapped_iov) != 1) { + return -1; + } + if (evpl_rpc2_iov_gather(iov, niov, 4, wrapped_iov.data, wrapped_len)) { + evpl_iovec_release(evpl, &wrapped_iov); + return -1; + } + + /* gss_unwrap (decrypt + verify seal) -> seq || proc_args */ + rc = thread->gss_provider->unwrap(thread->gss_provider_arg, ctx->gss_ctx, + wrapped_iov.data, wrapped_len, &plain, + &plain_len); + evpl_iovec_release(evpl, &wrapped_iov); + if (rc || !plain || plain_len < 4) { + evpl_rpc2_debug("rpcsec_gss: privacy unwrap failed wrapped_len=%u " + "plain_len=%zu", wrapped_len, plain_len); + if (plain) { + free(plain); + } + return -1; + } + + /* The seq embedded in the plaintext must match the credential seq. */ + lp = plain; + evpl_rpc2_gss_rd_u32(&lp, (uint8_t *) plain + 4, &embedded); + if (embedded != seq) { + evpl_rpc2_debug("rpcsec_gss: privacy seq mismatch embedded=%u cred=%u", + embedded, seq); + free(plain); + return -1; + } + + /* Copy the inner proc arguments into a fresh, ref-counted iovec (see the + * integrity path for why a dbuf view is insufficient for the zero-copy + * unmarshallers) and swap it into the msg for teardown. */ + inner_len = (uint32_t) plain_len - 4; + inner = xdr_dbuf_alloc_space(sizeof(*inner), dbuf); + if (!inner) { + free(plain); return -1; } ninner = evpl_iovec_alloc(evpl, inner_len ? inner_len : 1, 8, 1, 0, inner); if (ninner != 1) { + free(plain); return -1; } inner->length = inner_len; if (inner_len) { - memcpy(inner->data, databody + 4, inner_len); + memcpy(inner->data, (uint8_t *) plain + 4, inner_len); } + free(plain); evpl_iovecs_release(evpl, request->msg->req_iov, request->msg->req_niov); request->msg->req_iov = inner; @@ -1354,7 +1530,7 @@ evpl_rpc2_gss_unwrap_integrity( *niov_p = 1; *len_p = (int) inner_len; return 0; -} /* evpl_rpc2_gss_unwrap_integrity */ +} /* evpl_rpc2_gss_unwrap_privacy */ /* * Integrity service reply wrap: reframe the marshalled proc results as @@ -1452,6 +1628,96 @@ evpl_rpc2_gss_wrap_reply_integrity( return -1; } /* evpl_rpc2_gss_wrap_reply_integrity */ +/* + * Privacy service reply wrap: GSS-wrap (encrypt + seal) seq_num || results and + * emit it as the single opaque of rpc_gss_priv_data, into a fresh buffer that + * keeps `reserve` bytes of RPC-header headroom at the front. The original + * reply iovecs are released. Returns 0 and fills out_iov/out_length, or -1. + */ +static int +evpl_rpc2_gss_wrap_reply_privacy( + struct evpl *evpl, + struct evpl_rpc2_request *request, + struct evpl_iovec *msg_iov, + int msg_niov, + int length, + int reserve, + struct evpl_iovec *out_iov, + int *out_length) +{ + struct evpl_rpc2_thread *thread = request->thread; + struct evpl_rpc2_gss_context *ctx; + uint32_t bodylen = length - reserve; + uint32_t plain_len = 4 + bodylen; /* seq || results */ + struct evpl_iovec plain_iov; + uint8_t *plain, *p, *pp; + void *wrapped = NULL; + size_t wrapped_len = 0; + uint32_t cap, newbodylen; + int niov, ok = 0; + + /* Assemble seq || results in a contiguous, recycled iovec so gss_wrap can + * seal it in one call (the recycling allocator avoids per-reply malloc + * churn). Bounded by one buffer: a reply above buffer_size cannot be + * wrapped, so the caller falls through and sends it unwrapped -- the client + * then rejects the missing privacy, which is the safe outcome. */ + if (plain_len > evpl_buffer_size() || + evpl_iovec_alloc(evpl, plain_len, 8, 1, 0, &plain_iov) != 1) { + return -1; + } + plain = plain_iov.data; + pp = plain; + if (evpl_rpc2_gss_wr_u32(&pp, plain + 4, request->gss_seq) || + (bodylen && + evpl_rpc2_iov_gather(msg_iov, msg_niov, reserve, plain + 4, bodylen))) { + evpl_iovec_release(evpl, &plain_iov); + return -1; + } + + /* gss_wrap(seq || results) under the global context lock */ + pthread_mutex_lock(&evpl_rpc2_gss_lock); + ctx = evpl_rpc2_gss_ctx_lookup(request->gss_handle); + if (ctx) { + ok = (thread->gss_provider->wrap(thread->gss_provider_arg, ctx->gss_ctx, + plain, plain_len, &wrapped, + &wrapped_len) == 0); + } + pthread_mutex_unlock(&evpl_rpc2_gss_lock); + evpl_iovec_release(evpl, &plain_iov); + + if (!ok || !wrapped) { + if (wrapped) { + free(wrapped); + } + return -1; + } + + /* reserve headroom + opaque (4-byte length prefix + padded token) */ + cap = reserve + 4 + evpl_rpc2_gss_pad4(wrapped_len); + niov = evpl_iovec_alloc(evpl, cap, 8, 1, 0, out_iov); + if (niov != 1) { + free(wrapped); + return -1; + } + + p = (uint8_t *) out_iov->data + reserve; + pp = p; + if (evpl_rpc2_gss_wr_opaque(&pp, p + 4 + evpl_rpc2_gss_pad4(wrapped_len), + wrapped, wrapped_len)) { + free(wrapped); + evpl_iovec_release(evpl, out_iov); + return -1; + } + free(wrapped); + + newbodylen = 4 + evpl_rpc2_gss_pad4(wrapped_len); + out_iov->length = reserve + newbodylen; + *out_length = reserve + newbodylen; + + evpl_iovecs_release(evpl, msg_iov, msg_niov); + return 0; +} /* evpl_rpc2_gss_wrap_reply_privacy */ + /* * Emit an rpc_gss_init_res reply for the context-establishment handshake. * The response body (handle, major, minor, seq_window, token) is marshalled @@ -1702,10 +1968,11 @@ evpl_rpc2_gss_handle_call( return 0; } /* switch */ - /* DATA: authentication (krb5) and integrity (krb5i) are supported. - * Privacy (krb5p) wrapping is not yet implemented. */ + /* DATA: authentication (krb5), integrity (krb5i) and privacy (krb5p) are + * all supported. */ if (cred.service != EVPL_RPC2_GSS_SVC_NONE && - cred.service != EVPL_RPC2_GSS_SVC_INTEGRITY) { + cred.service != EVPL_RPC2_GSS_SVC_INTEGRITY && + cred.service != EVPL_RPC2_GSS_SVC_PRIVACY) { evpl_rpc2_debug("rpcsec_gss: DATA proc=%u service=%u unsupported " "-> AUTH_TOOWEAK", cred.proc, cred.service); evpl_rpc2_send_reply_denied(evpl, request, AUTH_TOOWEAK); @@ -1770,6 +2037,15 @@ evpl_rpc2_gss_handle_call( evpl_rpc2_send_reply_denied(evpl, request, RPCSEC_GSS_CTXPROBLEM); return 0; } + } else if (cred.service == EVPL_RPC2_GSS_SVC_PRIVACY) { + if (evpl_rpc2_gss_unwrap_privacy(request, ctx, cred.seq, req_iov_p, + req_niov_p, request_length_p)) { + pthread_mutex_unlock(&evpl_rpc2_gss_lock); + evpl_rpc2_debug("rpcsec_gss: privacy UNWRAP failed proc=%u seq=%u " + "reqlen=%d", cred.proc, cred.seq, *request_length_p); + evpl_rpc2_send_reply_denied(evpl, request, RPCSEC_GSS_CTXPROBLEM); + return 0; + } } /* Pre-compute the reply verifier: a GSS MIC over the request seq_num. */