[RFC PATCH v2 40/41] nfsd, nfs: carry UUID and GUID owners as their text form

From: Jan Koester

Date: Tue Oct 20 2026 - 23:36:44 EST


NFSv4 owners and ACL principals are strings. Classic ids go through
idmapd or, with id mapping disabled, as decimal numbers; a UUID or GUID
user or group (Documentation/userspace-api/uid_r.rst) has neither a
number nor an idmapd entry and showed as the overflow id.

Encode such an id as its text form -- "xxxxxxxx-xxxx-xxxx-xxxx-
xxxxxxxxxxxx" for a UUID, the same in braces for a GUID -- in both
directions on the server and the client, and take such a string (it
has no '@', so it can't be a user@domain name) as that id again. NFSv4
superblocks set SB_I_UID_R, so the client can chown to such owners.

RPC credentials stay classic: AUTH_SYS has a 32 bit uid, so a UUID or
GUID user on a client still reaches the server as the overflow id.

Assisted-by: Claude:claude-opus-5-5

Signed-off-by: Jan Koester <jan.koester@xxxxxxxxx>
---
fs/nfs/nfs4idmap.c | 53 +++++++++++++++++++++++++++++++++++
fs/nfs/super.c | 2 ++
fs/nfsd/nfs4idmap.c | 68 +++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 121 insertions(+), 2 deletions(-)

diff --git a/fs/nfs/nfs4idmap.c b/fs/nfs/nfs4idmap.c
index bc397110d9..0ca50bf8f3 100644
--- a/fs/nfs/nfs4idmap.c
+++ b/fs/nfs/nfs4idmap.c
@@ -729,11 +729,44 @@ idmap_release_pipe(struct inode *inode)
nfs_idmap_complete_pipe_upcall(data, -EPIPE);
}

+/*
+ * UUID and GUID owners (Documentation/userspace-api/uid_r.rst) travel as
+ * the id's text form, which has no '@' and no number for idmapd.
+ */
+static bool nfs_id_r_name(const char *name, size_t namelen, char *buf)
+{
+ if (namelen != 36 && !(namelen == 38 && name[0] == '{'))
+ return false;
+ memcpy(buf, name, namelen);
+ buf[namelen] = '\0';
+ return true;
+}
+
+static int nfs_id_r_to_string(const char *str, char *buf, size_t buflen)
+{
+ size_t len = strlen(str);
+
+ if (len > buflen)
+ return -ERANGE;
+ memcpy(buf, str, len);
+ return len;
+}
+
int nfs_map_name_to_uid(const struct nfs_server *server, const char *name, size_t namelen, kuid_t *uid)
{
struct idmap *idmap = server->nfs_client->cl_idmap;
__u32 id = -1;
int ret = 0;
+ char idbuf[KID_STR_LEN];
+
+ if (nfs_id_r_name(name, namelen, idbuf)) {
+ kuid_t k = make_kuid_str(idmap_userns(idmap), idbuf);
+
+ if (uid_valid(k) && !uid_is_legacy(k)) {
+ *uid = k;
+ return 0;
+ }
+ }

if (!nfs_map_string_to_numeric(name, namelen, &id))
ret = nfs_idmap_lookup_id(name, namelen, "uid", &id, idmap);
@@ -751,6 +784,16 @@ int nfs_map_group_to_gid(const struct nfs_server *server, const char *name, size
struct idmap *idmap = server->nfs_client->cl_idmap;
__u32 id = -1;
int ret = 0;
+ char idbuf[KID_STR_LEN];
+
+ if (nfs_id_r_name(name, namelen, idbuf)) {
+ kgid_t k = make_kgid_str(idmap_userns(idmap), idbuf);
+
+ if (gid_valid(k) && !gid_is_legacy(k)) {
+ *gid = k;
+ return 0;
+ }
+ }

if (!nfs_map_string_to_numeric(name, namelen, &id))
ret = nfs_idmap_lookup_id(name, namelen, "gid", &id, idmap);
@@ -769,6 +812,11 @@ int nfs_map_uid_to_name(const struct nfs_server *server, kuid_t uid, char *buf,
int ret = -EINVAL;
__u32 id;

+ if (!uid_is_legacy(uid) && kuid_has_mapping(idmap_userns(idmap), uid)) {
+ char str[KID_STR_LEN];
+
+ return nfs_id_r_to_string(kuid_str(uid, str), buf, buflen);
+ }
id = from_kuid_munged(idmap_userns(idmap), uid);
if (!(server->caps & NFS_CAP_UIDGID_NOMAP))
ret = nfs_idmap_lookup_name(id, "user", buf, buflen, idmap);
@@ -783,6 +831,11 @@ int nfs_map_gid_to_group(const struct nfs_server *server, kgid_t gid, char *buf,
int ret = -EINVAL;
__u32 id;

+ if (!gid_is_legacy(gid) && kgid_has_mapping(idmap_userns(idmap), gid)) {
+ char str[KID_STR_LEN];
+
+ return nfs_id_r_to_string(kgid_str(gid, str), buf, buflen);
+ }
id = from_kgid_munged(idmap_userns(idmap), gid);
if (!(server->caps & NFS_CAP_UIDGID_NOMAP))
ret = nfs_idmap_lookup_name(id, "group", buf, buflen, idmap);
diff --git a/fs/nfs/super.c b/fs/nfs/super.c
index cb19f1540d..2d292e4418 100644
--- a/fs/nfs/super.c
+++ b/fs/nfs/super.c
@@ -1100,6 +1100,8 @@ static void nfs_fill_super(struct super_block *sb, struct nfs_fs_context *ctx)
break;
case 4:
sb->s_iflags |= SB_I_NOUMASK;
+ /* owners are strings: UUIDs and GUIDs in their text form */
+ sb->s_iflags |= SB_I_UID_R;
sb->s_time_gran = 1;
sb->s_time_min = S64_MIN;
sb->s_time_max = S64_MAX;
diff --git a/fs/nfsd/nfs4idmap.c b/fs/nfsd/nfs4idmap.c
index e9faf8b78f..8c3e908b32 100644
--- a/fs/nfsd/nfs4idmap.c
+++ b/fs/nfsd/nfs4idmap.c
@@ -644,6 +644,33 @@ static __be32 encode_name_from_id(struct xdr_stream *xdr,
return idmap_id_to_name(xdr, rqstp, type, id);
}

+/*
+ * UUID and GUID users and groups (Documentation/userspace-api/uid_r.rst)
+ * have no number for idmapd or for numeric owner strings: their owner
+ * string is the id's text form, "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" or
+ * "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}", in either mode. Such a string
+ * (it has no '@') is taken as that id again; classic ids are unchanged.
+ */
+static bool id_r_name(const char *name, size_t namelen, char *buf)
+{
+ if (namelen != 36 && !(namelen == 38 && name[0] == '{'))
+ return false;
+ memcpy(buf, name, namelen);
+ buf[namelen] = '\0';
+ return true;
+}
+
+static __be32 encode_id_r(struct xdr_stream *xdr, const char *str)
+{
+ size_t len = strlen(str);
+ __be32 *p = xdr_reserve_space(xdr, len + 4);
+
+ if (!p)
+ return nfserr_resource;
+ xdr_encode_opaque(p, str, len);
+ return 0;
+}
+
/**
* nfsd_map_name_to_uid - Map user@domain to local UID
* @rqstp: RPC execution context
@@ -672,6 +699,16 @@ __be32 nfsd_map_name_to_uid(struct svc_rqst *rqstp, const char *name,
if (name == NULL || namelen == 0)
return nfserr_inval;

+ {
+ char buf[KID_STR_LEN];
+
+ if (id_r_name(name, namelen, buf)) {
+ *uid = make_kuid_str(nfsd_user_namespace(rqstp), buf);
+ if (uid_valid(*uid) && !uid_is_legacy(*uid))
+ return nfs_ok;
+ }
+ }
+
status = do_name_to_id(rqstp, IDMAP_TYPE_USER, name, namelen, &id);
if (status)
return status;
@@ -709,6 +746,16 @@ __be32 nfsd_map_name_to_gid(struct svc_rqst *rqstp, const char *name,
if (name == NULL || namelen == 0)
return nfserr_inval;

+ {
+ char buf[KID_STR_LEN];
+
+ if (id_r_name(name, namelen, buf)) {
+ *gid = make_kgid_str(nfsd_user_namespace(rqstp), buf);
+ if (gid_valid(*gid) && !gid_is_legacy(*gid))
+ return nfs_ok;
+ }
+ }
+
status = do_name_to_id(rqstp, IDMAP_TYPE_GROUP, name, namelen, &id);
if (status)
return status;
@@ -721,13 +768,30 @@ __be32 nfsd_map_name_to_gid(struct svc_rqst *rqstp, const char *name,
__be32 nfsd4_encode_user(struct xdr_stream *xdr, struct svc_rqst *rqstp,
kuid_t uid)
{
- u32 id = from_kuid_munged(nfsd_user_namespace(rqstp), uid);
+ u32 id;
+
+ /* (outside the initial user namespace: the overflow id below) */
+ if (!uid_is_legacy(uid) &&
+ kuid_has_mapping(nfsd_user_namespace(rqstp), uid)) {
+ char buf[KID_STR_LEN];
+
+ return encode_id_r(xdr, kuid_str(uid, buf));
+ }
+ id = from_kuid_munged(nfsd_user_namespace(rqstp), uid);
return encode_name_from_id(xdr, rqstp, IDMAP_TYPE_USER, id);
}

__be32 nfsd4_encode_group(struct xdr_stream *xdr, struct svc_rqst *rqstp,
kgid_t gid)
{
- u32 id = from_kgid_munged(nfsd_user_namespace(rqstp), gid);
+ u32 id;
+
+ if (!gid_is_legacy(gid) &&
+ kgid_has_mapping(nfsd_user_namespace(rqstp), gid)) {
+ char buf[KID_STR_LEN];
+
+ return encode_id_r(xdr, kgid_str(gid, buf));
+ }
+ id = from_kgid_munged(nfsd_user_namespace(rqstp), gid);
return encode_name_from_id(xdr, rqstp, IDMAP_TYPE_GROUP, id);
}
--
2.56.0