[RFC PATCH v2 31/41] ext4: add the uid_r feature for UUID and GUID owners
From: Jan Koester
Date: Tue Oct 20 2026 - 23:38:27 EST
Add a read-only compatible feature, uid_r, that lets inodes have UUID
and GUID owners and groups. The inode gains i_uid_r_type and
i_gid_r_type (0: the classic i_uid/i_gid apply) and 16 bytes for each
id after i_projid, which grows i_extra_isize from 32 to 68 bytes; only
filesystems with the feature reserve the room in new inodes, others
keep the old size.
For a UUID or GUID the classic fields hold 0xffffffff. Kernels without
the feature may only mount read-only, and see such files as owned by an
id nobody has.
With the feature the superblock gets SB_I_UID_R. Inodes from before it,
or with in-inode xattrs in the way, are expanded when they get such an
owner, as for project ids; -EOVERFLOW if that fails. The feature needs
inodes of at least 196 bytes, so in practice 256.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Jan Koester <jan.koester@xxxxxxxxx>
---
fs/ext4/ext4.h | 26 ++++++++++-
fs/ext4/inode.c | 115 +++++++++++++++++++++++++++++++++++++++++++++++-
fs/ext4/super.c | 16 ++++++-
3 files changed, 152 insertions(+), 5 deletions(-)
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 724a27e8be..1cba42b410 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -850,8 +850,29 @@ struct ext4_inode {
__le32 i_crtime_extra; /* extra FileCreationtime (nsec << 2 | epoch) */
__le32 i_version_hi; /* high 32 bits for 64-bit version */
__le32 i_projid; /* Project ID */
+ /* uid_r feature: a UUID or GUID owner and group, see EXT4_ID_R_* */
+ __u8 i_uid_r_type; /* kind of i_uid_r, EXT4_ID_R_LEGACY: i_uid */
+ __u8 i_gid_r_type; /* kind of i_gid_r, EXT4_ID_R_LEGACY: i_gid */
+ __le16 i_id_r_reserved;
+ __u8 i_uid_r[16]; /* UUID (RFC 9562 order) or GUID owner */
+ __u8 i_gid_r[16]; /* UUID (RFC 9562 order) or GUID group */
};
+/*
+ * Kinds of i_uid_r/i_gid_r. For a UUID or GUID the classic i_uid/i_gid
+ * fields hold 0xffffffff, which kernels without uid_r -- they may only
+ * mount read-only -- see as an id nobody has.
+ */
+#define EXT4_ID_R_LEGACY 0
+#define EXT4_ID_R_UUID 1
+#define EXT4_ID_R_GUID 2
+
+/* i_extra_isize of inodes without and with the uid_r fields */
+#define EXT4_EXTRA_ISIZE_BASE (offsetofend(struct ext4_inode, i_projid) - \
+ EXT4_GOOD_OLD_INODE_SIZE)
+#define EXT4_EXTRA_ISIZE_UID_R (sizeof(struct ext4_inode) - \
+ EXT4_GOOD_OLD_INODE_SIZE)
+
#define EXT4_EPOCH_BITS 2
#define EXT4_EPOCH_MASK ((1 << EXT4_EPOCH_BITS) - 1)
#define EXT4_NSEC_MASK (~0UL << EXT4_EPOCH_BITS)
@@ -2214,6 +2235,7 @@ static inline struct mapping_metadata_bhs *ext4_i_metadata_bhs(
#define EXT4_FEATURE_RO_COMPAT_VERITY 0x8000
#define EXT4_FEATURE_RO_COMPAT_ORPHAN_PRESENT 0x10000 /* Orphan file may be
non-empty */
+#define EXT4_FEATURE_RO_COMPAT_UID_R 0x20000 /* UUID and GUID owners */
#define EXT4_FEATURE_INCOMPAT_COMPRESSION 0x0001
#define EXT4_FEATURE_INCOMPAT_FILETYPE 0x0002
@@ -2313,6 +2335,7 @@ EXT4_FEATURE_RO_COMPAT_FUNCS(readonly, READONLY)
EXT4_FEATURE_RO_COMPAT_FUNCS(project, PROJECT)
EXT4_FEATURE_RO_COMPAT_FUNCS(verity, VERITY)
EXT4_FEATURE_RO_COMPAT_FUNCS(orphan_present, ORPHAN_PRESENT)
+EXT4_FEATURE_RO_COMPAT_FUNCS(uid_r, UID_R)
EXT4_FEATURE_INCOMPAT_FUNCS(compression, COMPRESSION)
EXT4_FEATURE_INCOMPAT_FUNCS(filetype, FILETYPE)
@@ -2373,7 +2396,8 @@ EXT4_FEATURE_INCOMPAT_FUNCS(casefold, CASEFOLD)
EXT4_FEATURE_RO_COMPAT_QUOTA |\
EXT4_FEATURE_RO_COMPAT_PROJECT |\
EXT4_FEATURE_RO_COMPAT_VERITY |\
- EXT4_FEATURE_RO_COMPAT_ORPHAN_PRESENT)
+ EXT4_FEATURE_RO_COMPAT_ORPHAN_PRESENT |\
+ EXT4_FEATURE_RO_COMPAT_UID_R)
#define EXTN_FEATURE_FUNCS(ver) \
static inline bool ext4_has_unknown_ext##ver##_compat_features(struct super_block *sb) \
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 26f0f9714f..bed8d535a6 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4780,6 +4780,75 @@ static int ext4_inode_blocks_set(struct ext4_inode *raw_inode,
return 0;
}
+/*
+ * uid_r: a UUID or GUID owner and group live in i_uid_r and i_gid_r of
+ * inodes with room for them; i_uid and i_gid then hold 0xffffffff.
+ */
+static int ext4_inode_set_id_r(struct inode *inode, struct ext4_inode *raw_inode)
+{
+ struct ext4_inode_info *ei = EXT4_I(inode);
+ bool fits = EXT4_INODE_SIZE(inode->i_sb) > EXT4_GOOD_OLD_INODE_SIZE &&
+ EXT4_FITS_IN_INODE(raw_inode, ei, i_gid_r);
+ bool legacy = uid_is_legacy(inode->i_uid) && gid_is_legacy(inode->i_gid);
+
+ if (!legacy && (!ext4_has_feature_uid_r(inode->i_sb) || !fits))
+ return -EOVERFLOW;
+ if (!fits || !ext4_has_feature_uid_r(inode->i_sb))
+ return 0;
+
+ raw_inode->i_id_r_reserved = 0;
+ if (uid_is_legacy(inode->i_uid)) {
+ raw_inode->i_uid_r_type = EXT4_ID_R_LEGACY;
+ memset(raw_inode->i_uid_r, 0, sizeof(raw_inode->i_uid_r));
+ } else {
+ BUILD_BUG_ON(KID_UUID != EXT4_ID_R_UUID ||
+ KID_GUID != EXT4_ID_R_GUID);
+ raw_inode->i_uid_r_type = inode->i_uid.type;
+ memcpy(raw_inode->i_uid_r, inode->i_uid.uuid.b,
+ sizeof(raw_inode->i_uid_r));
+ }
+ if (gid_is_legacy(inode->i_gid)) {
+ raw_inode->i_gid_r_type = EXT4_ID_R_LEGACY;
+ memset(raw_inode->i_gid_r, 0, sizeof(raw_inode->i_gid_r));
+ } else {
+ raw_inode->i_gid_r_type = inode->i_gid.type;
+ memcpy(raw_inode->i_gid_r, inode->i_gid.uuid.b,
+ sizeof(raw_inode->i_gid_r));
+ }
+ return 0;
+}
+
+static int ext4_inode_get_id_r(struct inode *inode, struct ext4_inode *raw_inode)
+{
+ struct ext4_inode_info *ei = EXT4_I(inode);
+ uuid_t id;
+
+ if (!ext4_has_feature_uid_r(inode->i_sb) ||
+ EXT4_INODE_SIZE(inode->i_sb) <= EXT4_GOOD_OLD_INODE_SIZE ||
+ !EXT4_FITS_IN_INODE(raw_inode, ei, i_gid_r))
+ return 0;
+
+ if (raw_inode->i_uid_r_type > EXT4_ID_R_GUID ||
+ raw_inode->i_gid_r_type > EXT4_ID_R_GUID) {
+ ext4_error_inode(inode, __func__, __LINE__, 0,
+ "iget: bad uid_r kind %u/%u",
+ raw_inode->i_uid_r_type,
+ raw_inode->i_gid_r_type);
+ return -EFSCORRUPTED;
+ }
+ if (raw_inode->i_uid_r_type != EXT4_ID_R_LEGACY) {
+ memcpy(id.b, raw_inode->i_uid_r, sizeof(id.b));
+ inode->i_uid = raw_inode->i_uid_r_type == EXT4_ID_R_UUID ?
+ KUIDT_INIT_UUID(id) : KUIDT_INIT_GUID(*(guid_t *)&id);
+ }
+ if (raw_inode->i_gid_r_type != EXT4_ID_R_LEGACY) {
+ memcpy(id.b, raw_inode->i_gid_r, sizeof(id.b));
+ inode->i_gid = raw_inode->i_gid_r_type == EXT4_ID_R_UUID ?
+ KGIDT_INIT_UUID(id) : KGIDT_INIT_GUID(*(guid_t *)&id);
+ }
+ return 0;
+}
+
static int ext4_fill_raw_inode(struct inode *inode, struct ext4_inode *raw_inode)
{
struct ext4_inode_info *ei = EXT4_I(inode);
@@ -4792,8 +4861,10 @@ static int ext4_fill_raw_inode(struct inode *inode, struct ext4_inode *raw_inode
err = ext4_inode_blocks_set(raw_inode, ei);
raw_inode->i_mode = cpu_to_le16(inode->i_mode);
+ /* a UUID or GUID owner: 0xffffffff here, the id in i_uid_r */
i_uid = i_uid_read(inode);
i_gid = i_gid_read(inode);
+ err = err ?: ext4_inode_set_id_r(inode, raw_inode);
i_projid = from_kprojid(&init_user_ns, ei->i_projid);
if (!(test_opt(inode->i_sb, NO_UID32))) {
raw_inode->i_uid_low = cpu_to_le16(low_16_bits(i_uid));
@@ -5435,6 +5506,9 @@ struct inode *__ext4_iget(struct super_block *sb, unsigned long ino,
}
i_uid_write(inode, i_uid);
i_gid_write(inode, i_gid);
+ ret = ext4_inode_get_id_r(inode, raw_inode);
+ if (ret)
+ goto bad_inode;
ei->i_projid = make_kprojid(&init_user_ns, i_projid);
set_nlink(inode, le16_to_cpu(raw_inode->i_links_count));
@@ -5547,8 +5621,7 @@ struct inode *__ext4_iget(struct super_block *sb, unsigned long ino,
if (ei->i_extra_isize == 0) {
/* The extra space is currently unused. Use it. */
BUILD_BUG_ON(sizeof(struct ext4_inode) & 3);
- ei->i_extra_isize = sizeof(struct ext4_inode) -
- EXT4_GOOD_OLD_INODE_SIZE;
+ ei->i_extra_isize = EXT4_SB(inode->i_sb)->s_want_extra_isize;
} else {
ret = ext4_iget_extra_inode(inode, raw_inode, ei);
if (ret)
@@ -6006,6 +6079,40 @@ static void ext4_wait_for_tail_page_commit(struct inode *inode)
*
* Called with inode->i_rwsem down.
*/
+/*
+ * An inode from before uid_r, or with in-inode xattrs in the way, may
+ * lack room for i_uid_r and i_gid_r: make it, before it gets a UUID or
+ * GUID owner or group.
+ */
+static int ext4_make_room_for_id_r(struct mnt_idmap *idmap,
+ struct inode *inode, struct iattr *attr)
+{
+ struct ext4_iloc iloc;
+ bool legacy = true;
+ int err;
+
+ if (attr->ia_valid & ATTR_UID)
+ legacy = uid_is_legacy(from_vfsuid(idmap, i_user_ns(inode),
+ attr->ia_vfsuid));
+ if (attr->ia_valid & ATTR_GID)
+ legacy &= gid_is_legacy(from_vfsgid(idmap, i_user_ns(inode),
+ attr->ia_vfsgid));
+ if (legacy)
+ return 0;
+ if (!ext4_has_feature_uid_r(inode->i_sb))
+ return -EOVERFLOW;
+
+ err = ext4_get_inode_loc(inode, &iloc);
+ if (err)
+ return err;
+ if (EXT4_FITS_IN_INODE(ext4_raw_inode(&iloc), EXT4_I(inode), i_gid_r)) {
+ brelse(iloc.bh);
+ return 0;
+ }
+ err = ext4_expand_extra_isize(inode, EXT4_EXTRA_ISIZE_UID_R, &iloc);
+ return err ? -EOVERFLOW : 0;
+}
+
int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
struct iattr *attr)
{
@@ -6045,6 +6152,10 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
i_gid_needs_update(idmap, attr, inode)) {
handle_t *handle;
+ error = ext4_make_room_for_id_r(idmap, inode, attr);
+ if (error)
+ return error;
+
/* (user+group)*(old+new) structure, inode write (sb,
* inode block, ? - but truncate inode update has it) */
handle = ext4_journal_start(inode, EXT4_HT_QUOTA,
diff --git a/fs/ext4/super.c b/fs/ext4/super.c
index d636b89090..34ac087c32 100644
--- a/fs/ext4/super.c
+++ b/fs/ext4/super.c
@@ -4613,9 +4613,17 @@ static int ext4_inode_info_init(struct super_block *sb,
sb->s_time_min = EXT4_TIMESTAMP_MIN;
}
+ if (ext4_has_feature_uid_r(sb) &&
+ sbi->s_inode_size < EXT4_GOOD_OLD_INODE_SIZE + EXT4_EXTRA_ISIZE_UID_R) {
+ ext4_msg(sb, KERN_ERR, "uid_r needs inodes of at least %zu bytes",
+ EXT4_GOOD_OLD_INODE_SIZE + EXT4_EXTRA_ISIZE_UID_R);
+ return -EINVAL;
+ }
+
if (sbi->s_inode_size > EXT4_GOOD_OLD_INODE_SIZE) {
- sbi->s_want_extra_isize = sizeof(struct ext4_inode) -
- EXT4_GOOD_OLD_INODE_SIZE;
+ /* room for the uid_r fields in new inodes only if needed */
+ sbi->s_want_extra_isize = ext4_has_feature_uid_r(sb) ?
+ EXT4_EXTRA_ISIZE_UID_R : EXT4_EXTRA_ISIZE_BASE;
if (ext4_has_feature_extra_isize(sb)) {
unsigned v, max = (sbi->s_inode_size -
EXT4_GOOD_OLD_INODE_SIZE);
@@ -5437,6 +5445,10 @@ static int __ext4_fill_super(struct fs_context *fc, struct super_block *sb)
/* HSM events are allowed by default. */
sb->s_iflags |= SB_I_ALLOW_HSM;
+ /* uid_r: inodes can have UUID and GUID owners */
+ if (ext4_has_feature_uid_r(sb) && !test_opt(sb, NO_UID32))
+ sb->s_iflags |= SB_I_UID_R;
+
err = ext4_check_feature_compatibility(sb, es, silent);
if (err)
goto failed_mount;
--
2.56.0