Viewing: sec_gssiam.c
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2026, Google
*
* GSSIAM Security Policy for Lustre
*
* Overview:
* ---------
* The GSSIAM (Identity Access Management) security flavor provides a
* high-performance, identity-based authentication and authorization mechanism
* for Lustre. It utilizes a Hybrid RPC Model designed to balance strong
* initial authentication with minimal overhead for data traffic.
*
* Hybrid RPC Model:
* -----------------
* 1. Handshake (GSS Framing): The initial security context establishment
* (SEC_CTX_INIT) uses GSS framing to securely transport the client's GSSIAM
* identity (Token and Subdirectory/Fileset).
* 2. Data RPCs (Null Framing): Once the context is established and pinned to
* the server-side export, all subsequent RPCs (including metadata and bulk
* data) use standard Null security framing. This eliminates GSS header
* (gss_svc_accept()) and signing overhead (gss_alloc_reqbuf_intg(),
* gss_cli_ctx_sign()) for every request, achieving performance parity with
* unauthenticated traffic while maintaining session-based security.
*
* Nodemap Integration:
* --------------------
* GSSIAM integrates with the Lustre Nodemap framework to provide
* access control (UID/GID/PROJID squashing, RBAC, and Fileset
* restrictions) by mapping verified client identities to server-side
* nodemaps.
*
* Security Context Lifecycle:
* ---------------------------
* - Mount/Connect: The client triggers an upcall to retrieve the GSSIAM token
* and subdirectory from its local environment.
* - Context Init: The client sends a SEC_CTX_INIT RPC. The server executes
* an external upcall (l_gssiam_upcall) to verify the token and permissions.
* - Installation: Upon success, the GSSIAM info is installed onto the
* server-side export.
* - Verification: Subsequent RPCs are verified against the established export
* context.
*/
#define DEBUG_SUBSYSTEM S_SEC
#include <linux/module.h>
#include <linux/string.h>
#include <linux/atomic.h>
#include <linux/random.h>
#include <linux/user_namespace.h>
#include <linux/uidgid.h>
#include <obd.h>
#include <obd_class.h>
#include <lustre_net.h>
#include <lustre_sec.h>
#include <lustre_mds.h>
#include <lustre_req_layout.h>
#include <upcall_cache.h>
#include <lustre_gssiam.h>
#include "../ptlrpc_internal.h"
#include "gss_internal.h"
#include "gss_err.h"
#include <lprocfs_status.h>
static struct ptlrpc_sec_policy sptlrpc_gssiam_policy;
static struct upcall_cache *gssiam_upcall_cache;
static inline __u64 gssiam_sec_cache_key(struct ptlrpc_sec *sec)
{
if (sec && sec->ps_gssiam) {
const char *p = sec->ps_gssiam->lgmi_principal;
if (p && *p)
return lustre_hash_fnv_1a_64(p, strlen(p));
return (__u64)sec->ps_gssiam->lgmi_loginuid;
}
return 0;
}
static struct kobject *gssiam_kobj;
struct gssiam_sec {
struct ptlrpc_sec gis_base;
spinlock_t gis_lock;
struct mutex gis_mutex;
struct gssiam_cli_ctx __rcu *gis_ctx;
};
struct gssiam_cli_ctx {
struct ptlrpc_cli_ctx gicc_base;
struct lustre_gssiam_desc gicc_lid;
struct rawobj_s gicc_ich_handle;
};
static inline struct gssiam_sec *gss_sec2gssiam(struct ptlrpc_sec *sec)
{
return container_of(sec, struct gssiam_sec, gis_base);
}
static inline struct gssiam_cli_ctx *gss_ctx2gssiam(struct ptlrpc_cli_ctx *ctx)
{
return container_of(ctx, struct gssiam_cli_ctx, gicc_base);
}
/* Upcall Cache Operations */
static void gssiam_entry_init(struct upcall_cache_entry *entry, void *args)
{
struct ptlrpc_sec *sec = args;
entry->u.gssiam.gd_uc_entry = entry;
entry->u.gssiam.gd_token = NULL;
entry->u.gssiam.gd_token_len = 0;
entry->u.gssiam.gd_subdir = NULL;
entry->u.gssiam.gd_options = 0;
entry->u.gssiam.gd_auth_permission = GSSIAM_AUTH_DENY;
if (sec && sec->ps_gssiam)
entry->u.gssiam.gd_loginuid = sec->ps_gssiam->lgmi_loginuid;
else
entry->u.gssiam.gd_loginuid = from_kuid(&init_user_ns,
INVALID_UID);
if (sec && sec->ps_gssiam && sec->ps_gssiam->lgmi_principal) {
OBD_STRDUP(entry->u.gssiam.gd_principal,
sec->ps_gssiam->lgmi_principal);
if (!entry->u.gssiam.gd_principal)
UC_CACHE_SET_INVALID(entry);
} else {
entry->u.gssiam.gd_principal = NULL;
}
}
static void gssiam_entry_free(struct upcall_cache *cache,
struct upcall_cache_entry *entry)
{
OBD_FREE_STR(entry->u.gssiam.gd_principal);
OBD_FREE(entry->u.gssiam.gd_token,
entry->u.gssiam.gd_token_len);
OBD_FREE_STR(entry->u.gssiam.gd_subdir);
}
static int gssiam_upcall_compare(struct upcall_cache *cache,
struct upcall_cache_entry *entry,
__u64 key, void *args)
{
struct ptlrpc_sec *sec = args;
const char *principal;
const char *p1, *p2;
if (entry->ue_key != key)
return -1;
if (!sec)
return 0;
if (!sec->ps_gssiam) {
/* sec is not NULL, but ps_gssiam is not yet
* populated: no identity to match
*/
return -1;
}
if (entry->u.gssiam.gd_loginuid != sec->ps_gssiam->lgmi_loginuid)
return -1;
principal = sec->ps_gssiam->lgmi_principal;
p1 = entry->u.gssiam.gd_principal ? entry->u.gssiam.gd_principal : "";
p2 = principal ? principal : "";
if (strcmp(p1, p2) != 0)
return -1;
return 0;
}
/*
* gssiam_principal_match() - Compare a kernel principal string (p1) with a
* downcall principal buffer (p2, of length len2).
*
* Note: len2 (data->idd_principal_len) includes the '\0' null terminator
* (and any trailing '\0' padding), so we strip trailing '\0' bytes
* before comparing lengths and content.
*/
static bool gssiam_principal_match(const char *p1, const char *p2,
size_t len2)
{
size_t len1 = p1 ? strlen(p1) : 0;
while (len2 > 0 && p2 && p2[len2 - 1] == '\0')
len2--;
return len1 == len2 && (len1 == 0 || memcmp(p1, p2, len1) == 0);
}
static int gssiam_downcall_compare(struct upcall_cache *cache,
struct upcall_cache_entry *entry,
__u64 key, void *args)
{
struct gssiam_downcall_data *data = args;
const char *p1, *p2;
if (!data)
return 0;
if (entry->u.gssiam.gd_loginuid != data->idd_loginuid)
return -1;
p1 = entry->u.gssiam.gd_principal;
p2 = (data->idd_principal_len > 0) ? idd_principal(data) : NULL;
if (!gssiam_principal_match(p1, p2, data->idd_principal_len))
return -1;
return 0;
}
static int gssiam_do_upcall(struct upcall_cache *cache,
struct upcall_cache_entry *entry)
{
char *argv[7];
char *envp[] = {
"HOME=/",
"PATH=/sbin:/usr/sbin",
NULL
};
char buf[32];
char lbuf[32];
int rc, argc = 0;
snprintf(buf, sizeof(buf), "%llu", entry->ue_key);
down_read(&cache->uc_upcall_rwsem);
if (cache->uc_upcall[0] == '\0') {
rc = -EINVAL;
CERROR("%s: upcall path is empty: rc = %d\n",
cache->uc_name, rc);
GOTO(out, rc);
}
argv[argc++] = cache->uc_upcall;
if (entry->u.gssiam.gd_principal &&
strlen(entry->u.gssiam.gd_principal) > 0) {
argv[argc++] = "-p";
argv[argc++] = entry->u.gssiam.gd_principal;
}
snprintf(lbuf, sizeof(lbuf), "%u", entry->u.gssiam.gd_loginuid);
argv[argc++] = "-l";
argv[argc++] = lbuf;
argv[argc++] = buf;
argv[argc] = NULL;
rc = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
if (rc < 0)
CERROR("%s: upcall %s %s failed: rc = %d\n",
cache->uc_name, argv[0], buf, rc);
out:
up_read(&cache->uc_upcall_rwsem);
return rc;
}
static int gssiam_parse_downcall(struct upcall_cache *cache,
struct upcall_cache_entry *entry,
void *args)
{
struct gssiam_downcall_data *data = args;
if (data->idd_token_len > GSSIAM_MAX_TOKEN_LEN)
return -EINVAL;
if (data->idd_token_len > 0) {
OBD_ALLOC(entry->u.gssiam.gd_token, data->idd_token_len);
if (!entry->u.gssiam.gd_token)
return -ENOMEM;
entry->u.gssiam.gd_token_len = data->idd_token_len;
memcpy(entry->u.gssiam.gd_token, idd_token(data),
entry->u.gssiam.gd_token_len);
entry->u.gssiam.gd_auth_permission = GSSIAM_AUTH_RW;
} else {
entry->u.gssiam.gd_auth_permission = GSSIAM_AUTH_DENY;
}
return 0;
}
static struct upcall_cache_ops gssiam_upcall_ops = {
.do_upcall = gssiam_do_upcall,
.downcall_compare = gssiam_downcall_compare,
.free_entry = gssiam_entry_free,
.init_entry = gssiam_entry_init,
.parse_downcall = gssiam_parse_downcall,
.upcall_compare = gssiam_upcall_compare,
};
static inline bool gssiam_is_connect(struct ptlrpc_request *req)
{
return req->rq_pill.rc_fmt == &RQF_CONNECT ||
req->rq_pill.rc_fmt == &RQF_MDS_CONNECT ||
req->rq_pill.rc_fmt == &RQF_OST_CONNECT;
}
/* client operations */
static void gssiam_ctx_die(struct ptlrpc_cli_ctx *ctx, int grace)
{
struct gssiam_sec *gssiam = gss_sec2gssiam(ctx->cc_sec);
bool put = false;
spin_lock(&gssiam->gis_lock);
if (rcu_dereference_protected(gssiam->gis_ctx,
lockdep_is_held(&gssiam->gis_lock)) ==
gss_ctx2gssiam(ctx)) {
RCU_INIT_POINTER(gssiam->gis_ctx, NULL);
put = true;
}
spin_unlock(&gssiam->gis_lock);
if (put) {
synchronize_rcu();
sptlrpc_cli_ctx_put(ctx, 1);
}
}
static
int gssiam_ctx_sign(struct ptlrpc_cli_ctx *ctx, struct ptlrpc_request *req)
{
struct gssiam_cli_ctx *ictx = gss_ctx2gssiam(ctx);
struct gss_header *ghdr;
__u32 flags = 0;
/* init ctx RPC does not need sign */
if (req->rq_ctx_init) {
req->rq_gss_framed = 1;
return 0;
}
if (gssiam_is_connect(req))
req->rq_gss_framed = 1;
/* Non-connect RPC shares the same sign with NULL security */
if (!req->rq_gss_framed)
return null_ctx_sign_common(ctx, req, SPTLRPC_FLVR_GSSIAM);
if (req->rq_pack_bulk)
flags |= LUSTRE_GSS_PACK_BULK;
if (req->rq_pack_udesc)
flags |= LUSTRE_GSS_PACK_USER;
ghdr = lustre_msg_buf(req->rq_reqbuf, 0, sizeof(*ghdr));
if (!ghdr)
return -EFAULT;
ghdr->gh_version = PTLRPC_GSS_VERSION;
ghdr->gh_sp = (__u8) ctx->cc_sec->ps_part;
ghdr->gh_flags = flags;
ghdr->gh_proc = PTLRPC_GSS_PROC_DATA;
ghdr->gh_seq = 0;
ghdr->gh_svc = SPTLRPC_SVC_NULL;
ghdr->gh_handle.len = ictx->gicc_ich_handle.len;
if (ictx->gicc_ich_handle.len)
memcpy(ghdr->gh_handle.data, ictx->gicc_ich_handle.data,
ictx->gicc_ich_handle.len);
req->rq_reqdata_len = lustre_shrink_msg(req->rq_reqbuf, 0,
sizeof(*ghdr) + ghdr->gh_handle.len, 1);
return 0;
}
static
int gssiam_ctx_verify(struct ptlrpc_cli_ctx *ctx, struct ptlrpc_request *req)
{
struct lustre_msg *msg = req->rq_repdata;
struct gss_rep_header *ghdr;
int swabbed;
int rc = 0;
ENTRY;
if (!req->rq_gss_framed)
RETURN(null_ctx_verify(ctx, req));
if (msg->lm_bufcount < 2 || msg->lm_bufcount > 4)
RETURN(-EPROTO);
swabbed = req_capsule_rep_need_swab(&req->rq_pill);
ghdr = (struct gss_rep_header *)gss_swab_header(msg, 0, swabbed);
if (!ghdr) {
rc = -EPROTO;
CERROR("%s: can't decode gss header: rc = %d\n",
ctx->cc_sec->ps_import->imp_obd->obd_name, rc);
RETURN(rc);
}
if (ghdr->gh_handle.len > PTLRPC_GSS_MAX_HANDLE_SIZE) {
rc = -EPROTO;
CERROR("%s: GSSIAM handle len %u too large: rc = %d\n",
ctx->cc_sec->ps_import->imp_obd->obd_name,
ghdr->gh_handle.len, rc);
RETURN(rc);
}
if (ghdr->gh_version != PTLRPC_GSS_VERSION) {
rc = -EPROTO;
CERROR("%s: gss version %u mismatch, expect %u: rc = %d\n",
ctx->cc_sec->ps_import->imp_obd->obd_name,
ghdr->gh_version, PTLRPC_GSS_VERSION, rc);
RETURN(rc);
}
if (ghdr->gh_proc == PTLRPC_GSS_PROC_ERR) {
struct gss_err_header *errhdr = (struct gss_err_header *)ghdr;
if (req->rq_ctx_init) {
if ((errhdr->gh_major == GSS_S_UNAVAILABLE ||
errhdr->gh_major == GSS_S_CREDENTIALS_EXPIRED ||
errhdr->gh_major == GSS_S_NO_CONTEXT)) {
sptlrpc_cli_ctx_expire(ctx);
RETURN(-EAGAIN);
}
rc = -EPERM;
CERROR("%s: GSSIAM init failed: %u/%u: rc = %d\n",
ctx->cc_sec->ps_import->imp_obd->obd_name,
errhdr->gh_major, errhdr->gh_minor, rc);
sptlrpc_cli_ctx_expire(ctx);
RETURN(rc);
}
if (errhdr->gh_major == GSS_S_NO_CONTEXT) {
CWARN("%s: ctxt from %s (uid %u) unknown, retrying: rc = %d\n",
ctx->cc_sec->ps_import->imp_obd->obd_name,
req->rq_import ?
obd_import_nid2str(req->rq_import) : "0@<0:0>",
ctx->cc_vcred.vc_uid, -EAGAIN);
sptlrpc_cli_ctx_expire(ctx);
if (req->rq_import) {
spin_lock(&req->rq_import->imp_lock);
set_bit(IMPF_FORCE_VERIFY,
req->rq_import->imp_flags);
spin_unlock(&req->rq_import->imp_lock);
}
RETURN(-EAGAIN);
}
RETURN(-EACCES);
}
if (req->rq_ctx_init && !req->rq_early) {
if (ghdr->gh_proc != PTLRPC_GSS_PROC_INIT) {
rc = -EPROTO;
CERROR("%s: unexpected proc %u: rc = %d\n",
ctx->cc_sec->ps_import->imp_obd->obd_name,
ghdr->gh_proc, rc);
RETURN(rc);
}
if (ghdr->gh_major != GSS_S_COMPLETE) {
rc = -EPERM;
CERROR("%s: GSSIAM init failed: %u/%u: rc = %d\n",
ctx->cc_sec->ps_import->imp_obd->obd_name,
ghdr->gh_major, ghdr->gh_minor, rc);
sptlrpc_cli_ctx_expire(ctx);
RETURN(rc);
}
if (ghdr->gh_handle.len > 0) {
struct gssiam_cli_ctx *ictx = gss_ctx2gssiam(ctx);
if (rawobj_from_netobj_alloc(&ictx->gicc_ich_handle,
&ghdr->gh_handle))
RETURN(-ENOMEM);
}
} else if (ghdr->gh_proc != PTLRPC_GSS_PROC_DATA) {
rc = -EPROTO;
CERROR("%s: unexpected proc %u: rc = %d\n",
ctx->cc_sec->ps_import->imp_obd->obd_name,
ghdr->gh_proc, rc);
RETURN(rc);
}
req->rq_repmsg = lustre_msg_buf(msg, 1, 0);
req->rq_replen = msg->lm_buflens[1];
RETURN(rc);
}
static struct ptlrpc_ctx_ops gssiam_ctx_ops = {
.die = gssiam_ctx_die,
.sign = gssiam_ctx_sign,
.verify = gssiam_ctx_verify,
};
static void gssiam_destroy_sec(struct ptlrpc_sec *sec)
{
struct gssiam_sec *gssiam = gss_sec2gssiam(sec);
LASSERT(atomic_read(&sec->ps_nctx) == 0);
LASSERT(atomic_read(&sec->ps_refcount) == 0);
LASSERT(rcu_access_pointer(gssiam->gis_ctx) == NULL);
if (sec->ps_import) {
class_import_put(sec->ps_import);
sec->ps_import = NULL;
}
if (sec->ps_gssiam) {
OBD_FREE_STR(sec->ps_gssiam->lgmi_subdir);
OBD_FREE_STR(sec->ps_gssiam->lgmi_principal);
OBD_FREE_PTR(sec->ps_gssiam);
}
mutex_destroy(&gssiam->gis_mutex);
OBD_FREE_PTR(gssiam);
}
static struct ptlrpc_sec *gssiam_create_sec(struct obd_import *imp,
struct ptlrpc_svc_ctx *svc_ctx,
struct sptlrpc_flavor *sf)
{
struct gssiam_sec *gssiam;
struct ptlrpc_sec *sec;
OBD_ALLOC_PTR(gssiam);
if (!gssiam)
return NULL;
sec = &gssiam->gis_base;
sec->ps_policy = &sptlrpc_gssiam_policy;
atomic_set(&sec->ps_refcount, 0);
sec->ps_id = sptlrpc_get_next_secid();
sec->ps_import = class_import_get(imp);
sec->ps_flvr = *sf;
sec->ps_dying = 0;
spin_lock_init(&sec->ps_lock);
atomic_set(&sec->ps_nctx, 0);
INIT_LIST_HEAD(&sec->ps_gc_list);
sec->ps_gc_interval = 0;
sec->ps_gc_next = 0;
spin_lock_init(&gssiam->gis_lock);
mutex_init(&gssiam->gis_mutex);
gssiam->gis_ctx = NULL;
return sec;
}
/**
* gssiam_init_pack_request() - Pack SEC_CTX_INIT request in GSS format.
* @imp: OBD import associated with the request.
* @req: The RPC request being packed.
* @ctx: The GSSIAM client context containing the token and subdir.
*
* This function is strictly used during the initial security context
* establishment (e.g., at mount time or context refresh), NOT for every
* subsequent data/metadata RPC.
*
* It packs the user's GSSIAM token and the requested mount subdirectory
* into a SEC_CTX_INIT RPC. The server uses this information to perform
* an external upcall to verify the user's authorization for the specific
* subdirectory. Upon success, the verified context is attached to the
* client's export, which enforces access boundaries for all future
* lightweight (Null-framed) RPCs.
*
* Specifically, the packed SEC_CTX_INIT request payload contains:
* 1. Lustre service type (e.g., MGS, MDS, or OSS).
* 2. Target OBD UUID.
* 3. Reverse context handle (dummy/empty for GSSIAM).
* 4. Client GSSIAM token representing authentication credentials.
* 5. Requested target mount subdirectory (fileset).
* 6. Connection/mount options (recently added to support enhanced
* properties).
*
* On the server side, the SEC_CTX_INIT request is processed and verified
* by executing an external upcall (e.g., l_gssiam_upcall) to authorize
* the client's request. A later patch adds the server-side verification
* (e.g., via tgt_verify_gssiam() and external upcall).
*
* Return: 0 on success, negative errno on failure.
*/
static int gssiam_init_pack_request(struct obd_import *imp,
struct ptlrpc_request *req,
struct gssiam_cli_ctx *ctx)
{
struct lustre_msg *msg = req->rq_reqbuf;
struct gss_header *ghdr;
__u32 *p, size, offset = 2;
rawobj_t obj;
__u32 lustre_svc;
__u32 le_options;
void *token = ctx->gicc_lid.lid_token;
int token_size = ctx->gicc_lid.lid_token_len;
int rc;
msg->lm_secflvr = req->rq_flvr.sf_rpc;
req->rq_gss_framed = 1;
ghdr = lustre_msg_buf(msg, 0, sizeof(*ghdr));
ghdr->gh_version = PTLRPC_GSS_VERSION;
ghdr->gh_sp = (__u8) imp->imp_sec->ps_part;
ghdr->gh_flags = 0;
ghdr->gh_proc = PTLRPC_GSS_PROC_INIT;
ghdr->gh_seq = 0;
ghdr->gh_svc = SPTLRPC_SVC_NULL;
ghdr->gh_handle.len = 0;
if (req->rq_pack_udesc)
offset++;
/* security payload */
p = lustre_msg_buf(msg, offset, 0);
if (!p)
return -EINVAL;
size = msg->lm_buflens[offset];
if (size < 4)
return -EINVAL;
/* 1. lustre svc type */
lustre_svc = import_to_gss_svc(imp);
*p++ = cpu_to_le32(lustre_svc);
size -= 4;
/* 2. target uuid */
obj.len = strlen(imp->imp_obd->u.cli.cl_target_uuid.uuid) + 1;
obj.data = imp->imp_obd->u.cli.cl_target_uuid.uuid;
rc = rawobj_serialize(&obj, &p, &size);
if (rc)
return rc;
/* 3. reverse context handle (dummy for GSSIAM) */
obj.len = 0;
obj.data = NULL;
rc = rawobj_serialize(&obj, &p, &size);
if (rc)
return rc;
/* 4. token */
obj.len = token_size;
obj.data = token;
rc = rawobj_serialize(&obj, &p, &size);
if (rc)
return rc;
/* 5. subdir */
if (ctx->gicc_lid.lid_subdir)
obj.data = ctx->gicc_lid.lid_subdir;
else
obj.data = "";
obj.len = strlen((char *)obj.data) + 1;
rc = rawobj_serialize(&obj, &p, &size);
if (rc)
return rc;
/* 6. options */
le_options = cpu_to_le32(ctx->gicc_lid.lid_options);
obj.data = (__u8 *)&le_options;
obj.len = sizeof(le_options);
rc = rawobj_serialize(&obj, &p, &size);
if (rc)
return rc;
req->rq_reqdata_len = lustre_shrink_msg(req->rq_reqbuf, offset,
msg->lm_buflens[offset] - size, 0);
return 0;
}
static int gssiam_refresh_token(struct gssiam_cli_ctx *ctx)
{
struct ptlrpc_sec *sec = ctx->gicc_base.cc_sec;
struct obd_import *imp = sec->ps_import;
struct upcall_cache_entry *entry;
int rc = 0;
/* Always use key gssiam_sec_cache_key(sec) to fetch mount token */
entry = upcall_cache_get_entry(gssiam_upcall_cache,
gssiam_sec_cache_key(sec), sec);
if (IS_ERR(entry)) {
rc = PTR_ERR(entry);
CERROR("%s: Failed to get shared GSSIAM token: rc = %d\n",
imp->imp_obd->obd_name, rc);
return rc;
}
if (!UC_CACHE_IS_VALID(entry) ||
entry->u.gssiam.gd_auth_permission == GSSIAM_AUTH_DENY)
GOTO(out, rc = -EACCES);
/* Free old token if it exists */
if (ctx->gicc_lid.lid_token) {
OBD_FREE(ctx->gicc_lid.lid_token, ctx->gicc_lid.lid_token_len);
ctx->gicc_lid.lid_token = NULL;
ctx->gicc_lid.lid_token_len = 0;
}
/* Copy new token */
if (entry->u.gssiam.gd_token) {
OBD_ALLOC(ctx->gicc_lid.lid_token,
entry->u.gssiam.gd_token_len);
if (!ctx->gicc_lid.lid_token)
GOTO(out, rc = -ENOMEM);
ctx->gicc_lid.lid_token_len = entry->u.gssiam.gd_token_len;
memcpy(ctx->gicc_lid.lid_token, entry->u.gssiam.gd_token,
ctx->gicc_lid.lid_token_len);
}
out:
upcall_cache_put_entry(gssiam_upcall_cache, entry);
return rc;
}
static int gssiam_send_init_rpc(struct gssiam_cli_ctx *gssiam_ctx)
{
struct ptlrpc_cli_ctx *ctx = &gssiam_ctx->gicc_base;
struct obd_import *imp = gssiam_ctx->gicc_base.cc_sec->ps_import;
struct ptlrpc_request *req;
bool retried = false;
int rc;
retry:
rc = gssiam_refresh_token(gssiam_ctx);
if (rc)
GOTO(out, rc);
/* Prepare SEC_CTX_INIT request */
req = ptlrpc_request_alloc(imp, &RQF_SEC_CTX);
if (!req)
GOTO(out, rc = -ENOMEM);
rc = ptlrpc_request_bufs_pack(req, LUSTRE_OBD_VERSION, SEC_CTX_INIT,
NULL, ctx);
if (rc) {
ptlrpc_request_free(req);
GOTO(out, rc);
}
/* req->rq_cli_ctx is already set by ptlrpc_request_bufs_pack */
LASSERT(req->rq_ctx_init == 1);
rc = gssiam_init_pack_request(imp, req, gssiam_ctx);
if (rc) {
ptlrpc_req_put(req);
GOTO(out, rc);
}
ptlrpc_request_set_replen(req);
DEBUG_REQ(D_SEC, req, "send init GSSIAM req");
rc = ptlrpc_queue_wait(req);
ptlrpc_req_put(req);
if (rc) {
if ((rc == -EAGAIN || rc == -EKEYEXPIRED ||
rc == -ESTALE || rc == -EACCES || rc == -ETIMEDOUT) &&
!retried) {
retried = true;
CDEBUG(D_SEC, "%s: retry SEC_CTX_INIT (%d)\n",
imp->imp_obd->obd_name, rc);
if (gssiam_upcall_cache)
upcall_cache_invalidate_one(
gssiam_upcall_cache,
gssiam_sec_cache_key(ctx->cc_sec),
ctx->cc_sec);
rawobj_free(&gss_ctx2gssiam(ctx)->gicc_ich_handle);
goto retry;
}
CERROR("%s: GSSIAM SEC_CTX_INIT failed: rc = %d\n",
imp->imp_obd->obd_name, rc);
GOTO(out, rc);
}
set_bit(PTLRPC_CTX_UPTODATE_BIT, &ctx->cc_flags);
out:
return rc;
}
static void
gssiam_ctx_free(struct gssiam_cli_ctx *ctx)
{
rawobj_free(&ctx->gicc_ich_handle);
OBD_FREE(ctx->gicc_lid.lid_token, ctx->gicc_lid.lid_token_len);
OBD_FREE_STR(ctx->gicc_lid.lid_subdir);
OBD_FREE_PTR(ctx);
}
static struct gssiam_cli_ctx *gssiam_ctx_alloc(struct ptlrpc_sec *sec)
{
struct gssiam_cli_ctx *ctx;
/* Create new context */
OBD_ALLOC_PTR(ctx);
if (!ctx)
return NULL;
atomic_set(&ctx->gicc_base.cc_refcount, 1); /* caller ref */
ctx->gicc_base.cc_sec = sec;
ctx->gicc_base.cc_ops = &gssiam_ctx_ops;
ctx->gicc_base.cc_expire = 0;
ctx->gicc_base.cc_flags = PTLRPC_CTX_CACHED | PTLRPC_CTX_ETERNAL;
ctx->gicc_base.cc_vcred.vc_uid = 0;
spin_lock_init(&ctx->gicc_base.cc_lock);
INIT_LIST_HEAD(&ctx->gicc_base.cc_req_list);
INIT_LIST_HEAD(&ctx->gicc_base.cc_gc_chain);
if (sec->ps_gssiam && !sec_is_reverse(sec)) {
OBD_STRDUP(ctx->gicc_lid.lid_subdir,
sec->ps_gssiam->lgmi_subdir);
if (!ctx->gicc_lid.lid_subdir) {
OBD_FREE_PTR(ctx);
return NULL;
}
ctx->gicc_lid.lid_options = sec->ps_gssiam->lgmi_options;
}
atomic_inc(&sec->ps_refcount);
return ctx;
}
static struct ptlrpc_cli_ctx *gssiam_lookup_ctx(struct ptlrpc_sec *sec,
struct vfs_cred *vcred,
int create, int remove_dead)
{
struct obd_import *imp = sec->ps_import;
struct gssiam_sec *gssiam = gss_sec2gssiam(sec);
struct gssiam_cli_ctx *ctx;
int rc;
/* Check existing contexts locklessly via RCU */
rcu_read_lock();
ctx = rcu_dereference(gssiam->gis_ctx);
if (ctx && !atomic_inc_not_zero(&ctx->gicc_base.cc_refcount))
ctx = NULL;
rcu_read_unlock();
if (ctx)
return &ctx->gicc_base;
/* For reverse import, just install a null ctx */
if (unlikely(sec_is_reverse(sec))) {
struct gssiam_cli_ctx *ctx_new;
ctx_new = gssiam_ctx_alloc(sec);
if (!ctx_new)
return ERR_PTR(-ENOMEM);
set_bit(PTLRPC_CTX_UPTODATE_BIT, &ctx_new->gicc_base.cc_flags);
spin_lock(&gssiam->gis_lock);
ctx = rcu_dereference_protected(gssiam->gis_ctx,
lockdep_is_held(&gssiam->gis_lock));
if (ctx && atomic_inc_not_zero(&ctx->gicc_base.cc_refcount)) {
spin_unlock(&gssiam->gis_lock);
gssiam_ctx_free(ctx_new);
sptlrpc_sec_put(sec);
return &ctx->gicc_base;
}
atomic_inc(&ctx_new->gicc_base.cc_refcount); /* list ref */
rcu_assign_pointer(gssiam->gis_ctx, ctx_new);
atomic_inc(&sec->ps_nctx);
spin_unlock(&gssiam->gis_lock);
return &ctx_new->gicc_base;
}
if (!create)
return ERR_PTR(-ENODATA);
/* Serialize context creation to ensure single-shot SEC_CTX_INIT */
mutex_lock(&gssiam->gis_mutex);
/* Re-check under lock in case another thread initialized it */
spin_lock(&gssiam->gis_lock);
ctx = rcu_dereference_protected(gssiam->gis_ctx,
lockdep_is_held(&gssiam->gis_lock));
if (ctx && !atomic_inc_not_zero(&ctx->gicc_base.cc_refcount))
ctx = NULL;
spin_unlock(&gssiam->gis_lock);
if (ctx) {
mutex_unlock(&gssiam->gis_mutex);
return &ctx->gicc_base;
}
/* Get other GSSIAM information from llite before getting entry */
rc = obd_notify_observer(imp->imp_obd, imp->imp_obd, OBD_NOTIFY_GSSIAM);
if (rc) {
CERROR("%s: GSSIAM upcall notify failed: rc = %d\n",
imp->imp_obd->obd_name, rc);
mutex_unlock(&gssiam->gis_mutex);
return ERR_PTR(rc);
}
/* Create new context */
ctx = gssiam_ctx_alloc(sec);
if (!ctx) {
mutex_unlock(&gssiam->gis_mutex);
return ERR_PTR(-ENOMEM);
}
/* Send SEC_CTX_INIT RPC */
rc = gssiam_send_init_rpc(ctx);
if (rc)
GOTO(out_err, rc);
/* Cache context */
spin_lock(&gssiam->gis_lock);
atomic_inc(&ctx->gicc_base.cc_refcount); /* list ref */
rcu_assign_pointer(gssiam->gis_ctx, ctx);
atomic_inc(&sec->ps_nctx);
spin_unlock(&gssiam->gis_lock);
mutex_unlock(&gssiam->gis_mutex);
return &ctx->gicc_base;
out_err:
mutex_unlock(&gssiam->gis_mutex);
gssiam_ctx_free(ctx);
sptlrpc_sec_put(sec);
return ERR_PTR(rc);
}
static void gssiam_release_ctx(struct ptlrpc_sec *sec,
struct ptlrpc_cli_ctx *ctx, int sync)
{
struct gssiam_cli_ctx *gssiam_ctx = gss_ctx2gssiam(ctx);
LASSERT(atomic_read(&ctx->cc_refcount) == 0);
gssiam_ctx_free(gssiam_ctx);
atomic_dec(&sec->ps_nctx);
sptlrpc_sec_put(sec);
}
static int gssiam_flush_ctx_cache(struct ptlrpc_sec *sec, uid_t uid, int grace,
int force)
{
struct gssiam_sec *gssiam = gss_sec2gssiam(sec);
struct gssiam_cli_ctx *ctx = NULL;
spin_lock(&gssiam->gis_lock);
ctx = rcu_dereference_protected(gssiam->gis_ctx,
lockdep_is_held(&gssiam->gis_lock));
if (ctx)
RCU_INIT_POINTER(gssiam->gis_ctx, NULL);
spin_unlock(&gssiam->gis_lock);
if (ctx) {
synchronize_rcu();
sptlrpc_cli_ctx_put(&ctx->gicc_base, 1);
}
/* Also flush upcall cache */
if (gssiam_upcall_cache)
upcall_cache_invalidate_one(gssiam_upcall_cache,
gssiam_sec_cache_key(sec), sec);
return 0;
}
static int gssiam_alloc_reqbuf(struct ptlrpc_sec *sec,
struct ptlrpc_request *req,
int msgsize)
{
if (req->rq_ctx_init || gssiam_is_connect(req)) {
req->rq_gss_framed = 1;
return gss_alloc_reqbuf(sec, req, msgsize);
}
return null_alloc_reqbuf(sec, req, msgsize);
}
static
void gssiam_free_reqbuf(struct ptlrpc_sec *sec, struct ptlrpc_request *req)
{
if (req->rq_gss_framed)
gss_free_reqbuf(sec, req);
else
null_free_reqbuf(sec, req);
}
static int gssiam_alloc_repbuf(struct ptlrpc_sec *sec,
struct ptlrpc_request *req,
int msgsize)
{
if (req->rq_ctx_init || gssiam_is_connect(req)) {
req->rq_gss_framed = 1;
return gss_alloc_repbuf(sec, req, msgsize);
}
return null_alloc_repbuf(sec, req, msgsize);
}
static void gssiam_free_repbuf(struct ptlrpc_sec *sec,
struct ptlrpc_request *req)
{
if (req->rq_gss_framed)
gss_free_repbuf(sec, req);
else
null_free_repbuf(sec, req);
}
static int gssiam_enlarge_reqbuf(struct ptlrpc_sec *sec,
struct ptlrpc_request *req,
int segment, int newsize)
{
if (req->rq_gss_framed)
return gss_enlarge_reqbuf(sec, req, segment, newsize);
return null_enlarge_reqbuf(sec, req, segment, newsize);
}
static void gssiam_kill_sec(struct ptlrpc_sec *sec)
{
sec->ps_dying = 1;
}
static struct ptlrpc_sec_cops gssiam_sec_cops = {
.alloc_repbuf = gssiam_alloc_repbuf,
.alloc_reqbuf = gssiam_alloc_reqbuf,
.create_sec = gssiam_create_sec,
.destroy_sec = gssiam_destroy_sec,
.enlarge_reqbuf = gssiam_enlarge_reqbuf,
.flush_ctx_cache = gssiam_flush_ctx_cache,
.free_repbuf = gssiam_free_repbuf,
.free_reqbuf = gssiam_free_reqbuf,
.kill_sec = gssiam_kill_sec,
.lookup_ctx = gssiam_lookup_ctx,
.release_ctx = gssiam_release_ctx,
};
static struct ptlrpc_svc_ctx gssiam_svc_ctx = {
.sc_refcount = REFCOUNT_INIT(1),
.sc_policy = &sptlrpc_gssiam_policy,
};
/**
* gssiam_is_null_svc_ctx() - Check if a service context uses Null framing
* @ctx: service context to check
*
* In GSSIAM's Hybrid RPC Model, regular Data/Metadata RPCs use
* standard Null framing for performance and are assigned the static,
* shared service context (&gssiam_svc_ctx) by null_accept_common().
*
* Only GSS-framed handshake requests (SEC_CTX_INIT and Connect RPCs) are
* assigned a dynamically allocated struct gss_svc_reqctx by gss_svc_accept().
*
* This helper returns true if @ctx points to the shared &gssiam_svc_ctx
* (Null-framed), allowing callers to distinguish it from a real GSS
* service context before attempting container_of() / gss_svc_ctx2reqctx().
*
* Return: true if @ctx is the static Null-framed service context
*/
bool gssiam_is_null_svc_ctx(struct ptlrpc_svc_ctx *ctx)
{
return ctx == &gssiam_svc_ctx;
}
static int gssiam_alloc_rs(struct ptlrpc_request *req, int msgsize)
{
if (req->rq_svc_ctx != &gssiam_svc_ctx)
return gss_svc_alloc_rs(req, msgsize);
else
return null_alloc_rs(req, msgsize);
}
static void gssiam_free_rs(struct ptlrpc_reply_state *rs)
{
if (rs->rs_svc_ctx == &gssiam_svc_ctx) {
null_free_rs(rs);
return;
}
gss_svc_free_rs(rs);
}
static int gssiam_authorize(struct ptlrpc_request *req)
{
/* Check if we are using generic context (Null flavor) */
if (req->rq_svc_ctx == &gssiam_svc_ctx) {
null_authorize_common(req, req->rq_flvr.sf_rpc);
return 0;
}
/* GSS context (Connect requests) */
return gss_svc_authorize(req);
}
static void gssiam_free_ctx(struct ptlrpc_svc_ctx *ctx)
{
if (ctx == &gssiam_svc_ctx)
return;
gss_svc_free_ctx(ctx);
}
static void gssiam_invalidate_ctx(struct ptlrpc_svc_ctx *ctx)
{
if (ctx == &gssiam_svc_ctx)
return;
gss_svc_invalidate_ctx(ctx);
}
/**
* gssiam_check_nodemap() - Verify that the nodemap assigned to context exists
* @req: The incoming request being processed.
*
* Return:
* * %SECSVC_OK Nodemap exists, or check is not applicable.
* * %SECSVC_COMPLETE Nodemap is missing; an error reply has been
* packed to force client re-initialization.
* * %SECSVC_DROP Failed to pack error reply.
*/
static int gssiam_check_nodemap(struct ptlrpc_request *req)
{
#ifdef CONFIG_LUSTRE_FS_SERVER
struct lu_nodemap *nm;
struct ptlrpc_svc_ctx *ctx = req->rq_svc_ctx;
if (!ctx || !ctx->sc_nodemap)
return SECSVC_OK;
nm = nodemap_lookup_unlocked(ctx->sc_nodemap);
if (IS_ERR(nm)) {
CWARN("gssiam: nodemap %s missing: rc = %ld\n",
ctx->sc_nodemap, PTR_ERR(nm));
ptlrpc_req_drop_rs(req);
if (gss_pack_err_notify(req, GSS_S_NO_CONTEXT, 0))
return SECSVC_DROP;
return SECSVC_COMPLETE;
}
nodemap_putref(nm);
#endif
return SECSVC_OK;
}
/* server operations */
static int gssiam_accept(struct ptlrpc_request *req)
{
if (req->rq_flvr.sf_rpc != SPTLRPC_FLVR_GSSIAM) {
CERROR("%s: rpc flavor 0x%x: rc = %d\n",
libcfs_nidstr(&req->rq_peer.nid),
req->rq_flvr.sf_rpc, -EPROTO);
return SECSVC_DROP;
}
/*
* Under GSS framing, rq_reqbuf is the wrapper message and the embedded
* lustre_msg with the opcode is in segment 1. Check buflens[0] to
* distinguish GSS-framed requests (SEC_CTX_INIT and Connect) from
* Null-framed data RPCs.
*/
if (req->rq_reqbuf->lm_buflens[0] == PTLRPC_GSS_HEADER_SIZE) {
struct gss_header *ghdr;
ghdr = lustre_msg_buf(req->rq_reqbuf, 0, sizeof(*ghdr));
if (ghdr && ghdr->gh_version == PTLRPC_GSS_VERSION) {
int rc;
if (ghdr->gh_svc != SPTLRPC_SVC_NULL) {
CERROR("%s: invalid gssiam svc %u: rc = %d\n",
libcfs_nidstr(&req->rq_peer.nid),
ghdr->gh_svc, -EPROTO);
return SECSVC_DROP;
}
rc = gss_svc_accept(&sptlrpc_gssiam_policy, req);
if (rc == SECSVC_OK && req->rq_svc_ctx)
rc = gssiam_check_nodemap(req);
return rc;
}
}
null_accept_common(req, &gssiam_svc_ctx);
return SECSVC_OK;
}
static struct ptlrpc_sec_sops gssiam_sops = {
.accept = gssiam_accept,
.alloc_rs = gssiam_alloc_rs,
.authorize = gssiam_authorize,
.free_ctx = gssiam_free_ctx,
.free_rs = gssiam_free_rs,
#ifdef CONFIG_LUSTRE_FS_SERVER
.install_export_ctx = gssiam_install_ctx,
#endif
.invalidate_ctx = gssiam_invalidate_ctx,
};
static struct ptlrpc_sec_policy sptlrpc_gssiam_policy = {
.sp_owner = THIS_MODULE,
.sp_name = "gssiam",
.sp_policy = SPTLRPC_POLICY_GSSIAM,
.sp_cops = &gssiam_sec_cops,
.sp_sops = &gssiam_sops,
};
static ssize_t gssiam_upcall_show(struct kobject *kobj, struct attribute *attr,
char *buf)
{
ssize_t len;
down_read(&gssiam_upcall_cache->uc_upcall_rwsem);
len = scnprintf(buf, PAGE_SIZE, "%s\n",
gssiam_upcall_cache->uc_upcall);
up_read(&gssiam_upcall_cache->uc_upcall_rwsem);
return len;
}
static
ssize_t gssiam_upcall_store(struct kobject *kobj, struct attribute *attr,
const char *buf, size_t count)
{
int rc;
rc = upcall_cache_set_upcall(gssiam_upcall_cache, buf, count, true);
if (rc)
CERROR("%s: incorrect gssiam upcall %.*s: rc = %d\n",
gssiam_upcall_cache->uc_name, (int)count, buf, rc);
return rc ?: count;
}
LUSTRE_RW_ATTR(gssiam_upcall);
static
ssize_t gssiam_downcall_store(struct kobject *kobj, struct attribute *attr,
const char *buffer, size_t count)
{
const struct gssiam_downcall_data *param;
size_t size = sizeof(*param);
int rc;
if (count < size) {
rc = -EINVAL;
CERROR("%s: gssiam downcall data too small: %zu: rc = %d\n",
gssiam_upcall_cache->uc_name, count, rc);
return rc;
}
param = (const struct gssiam_downcall_data *)buffer;
if (param->idd_magic != GSSIAM_DOWNCALL_MAGIC) {
rc = -EINVAL;
CERROR("%s: gssiam downcall bad magic: %08x: rc = %d\n",
gssiam_upcall_cache->uc_name, param->idd_magic, rc);
return rc;
}
if (param->idd_token_len > count - size ||
param->idd_principal_len > count - size ||
(param->idd_principal_len > 0 &&
__ALIGN_KERNEL(param->idd_token_len, 8) +
(size_t)param->idd_principal_len > count - size)) {
rc = -EINVAL;
CERROR("%s: gssiam downcall invalid string lengths: token_len=%u principal_len=%u count=%zu: rc = %d\n",
gssiam_upcall_cache->uc_name, param->idd_token_len,
param->idd_principal_len, count, rc);
return rc;
}
rc = upcall_cache_downcall(gssiam_upcall_cache, param->idd_err,
param->idd_key, (void *)param);
return rc ? rc : count;
}
LUSTRE_WO_ATTR(gssiam_downcall);
static
ssize_t gssiam_flush_store(struct kobject *kobj, struct attribute *attr,
const char *buffer, size_t count)
{
unsigned long long key;
int rc;
/*
* gssiam_sec_cache_key() produces 64-bit fnv_1a_64() hash keys that
* span the full unsigned 64-bit range. Use kstrtoull() to avoid
* -ERANGE errors when keys exceed LLONG_MAX.
*/
rc = kstrtoull(buffer, 0, &key);
if (rc) {
long long sval;
/*
* kstrtoull() rejects negative numbers. If -1 is passed as
* the flush-all sentinel, parse it via kstrtoll().
*/
rc = kstrtoll(buffer, 0, &sval);
if (rc || sval != -1)
return rc ? rc : -EINVAL;
/* Invalidate all upcall cache entries */
key = ULLONG_MAX;
}
if (key == ULLONG_MAX) {
/* Invalidate all upcall cache entries */
upcall_cache_invalidate_all(gssiam_upcall_cache);
} else {
/* Invalidate the entry indicated by the key */
upcall_cache_invalidate_one(gssiam_upcall_cache,
(__u64)key, NULL);
}
return count;
}
LUSTRE_WO_ATTR(gssiam_flush);
static struct attribute *gssiam_attrs[] = {
&lustre_attr_gssiam_downcall.attr,
&lustre_attr_gssiam_flush.attr,
&lustre_attr_gssiam_upcall.attr,
NULL
};
static struct attribute_group gssiam_attr_group = {
.attrs = gssiam_attrs,
};
static int gssiam_tunables_init(void)
{
int rc;
gssiam_kobj = kobject_create_and_add("gssiam", sptlrpc_kobj);
if (!gssiam_kobj)
return -ENOMEM;
rc = sysfs_create_group(gssiam_kobj, &gssiam_attr_group);
if (rc) {
kobject_put(gssiam_kobj);
gssiam_kobj = NULL;
}
return rc;
}
static void gssiam_tunables_fini(void)
{
if (gssiam_kobj) {
sysfs_remove_group(gssiam_kobj, &gssiam_attr_group);
kobject_put(gssiam_kobj);
gssiam_kobj = NULL;
}
}
#define UC_GSSIAM_HASH_SIZE 128
#define GSSIAM_UPCALL_PATH "/usr/sbin/l_gssiam_upcall"
#define GSSIAM_CACHE_NAME "gssiam"
int __init sptlrpc_gssiam_init(void)
{
int rc;
gssiam_upcall_cache = upcall_cache_init(GSSIAM_CACHE_NAME,
GSSIAM_UPCALL_PATH,
UC_GSSIAM_HASH_SIZE,
3600, /* entry expire */
30, /* acquire expire */
false, /* replay */
&gssiam_upcall_ops);
if (IS_ERR(gssiam_upcall_cache)) {
int rc = PTR_ERR(gssiam_upcall_cache);
gssiam_upcall_cache = NULL;
CERROR("%s: Failed to init GSSIAM upcall cache: rc = %d\n",
GSSIAM_CACHE_NAME, rc);
return rc;
}
/* XXX register GSSIAM policy once all patches are landed */
rc = gssiam_tunables_init();
if (rc)
upcall_cache_cleanup(gssiam_upcall_cache);
return rc;
}
void sptlrpc_gssiam_exit(void)
{
gssiam_tunables_fini();
upcall_cache_cleanup(gssiam_upcall_cache);
}