[PATCH RFC v3 62/90] fs: let a cell set no set-id bits

From: Josef Bacik

Date: Thu Nov 05 2026 - 11:09:32 EST


A file can give a process privilege at exec, through its set-id bits or
its file capabilities. Every task of a cell has no_new_privs, so nothing
that runs in a cell can gain privilege from a file. But that is only half
of the problem.

The ids of a cell usually map to on-disk ids from 0 upwards, because that
is how container images are stored. If the root user of a cell sets the
set-uid bit on a file, the result is a set-uid root file for anybody else
who can reach that filesystem. Only directory permissions, and a nosuid
mount option that the host has to remember, stand in the way.

File capabilities cannot be written without CAP_SETFCAP, which a task of a
cell never has. That leaves the mode bits. Clear S_ISUID, and S_ISGID on
anything that is not a directory, in two places: in vfs_prepare_mode(),
for a file that a task of a cell creates, and in notify_change(), for a
mode that it sets.

The bits are cleared silently instead of failing the call, so that
unpacking an archive keeps working. setattr_prepare() does the same with
S_ISGID for a caller that is not in the group of the file.

Signed-off-by: Josef Bacik <josef@xxxxxxxxxxxxxx>
---
fs/attr.c | 5 +++++
fs/namei.c | 3 ++-
include/linux/cell.h | 28 ++++++++++++++++++++++++++++
3 files changed, 35 insertions(+), 1 deletion(-)

diff --git a/fs/attr.c b/fs/attr.c
index 10cf1b3827c6..946fba92b5c8 100644
--- a/fs/attr.c
+++ b/fs/attr.c
@@ -7,6 +7,7 @@
*/

#include <linux/export.h>
+#include <linux/cell.h>
#include <linux/time.h>
#include <linux/mm.h>
#include <linux/string.h>
@@ -457,6 +458,10 @@ int notify_change(const struct mnt_idmap *idmap, struct dentry *dentry,
if (S_ISLNK(inode->i_mode))
return -EOPNOTSUPP;

+ /* Nothing a cell writes imparts privilege. */
+ attr->ia_mode = cell_strip_setid((attr->ia_mode & ~S_IFMT) |
+ (inode->i_mode & S_IFMT));
+
/* Flag setting protected by i_rwsem */
if (is_sxid(attr->ia_mode))
inode->i_flags &= ~S_NOSEC;
diff --git a/fs/namei.c b/fs/namei.c
index 925aeb63bfa1..49995c353292 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -16,6 +16,7 @@
*/

#include <linux/init.h>
+#include <linux/cell.h>
#include <linux/export.h>
#include <linux/slab.h>
#include <linux/wordpart.h>
@@ -4162,7 +4163,7 @@ static inline umode_t vfs_prepare_mode(const struct mnt_idmap *idmap,
mode &= (mask_perms & ~S_IFMT);
mode |= (type & S_IFMT);

- return mode;
+ return cell_strip_setid(mode);
}

/**
diff --git a/include/linux/cell.h b/include/linux/cell.h
index f204c47ee783..1d0eec34acb9 100644
--- a/include/linux/cell.h
+++ b/include/linux/cell.h
@@ -256,4 +256,32 @@ struct user_namespace *cell_idmap_userns(const struct file *file);
static inline struct user_namespace *cell_idmap_userns(const struct file *f) { return NULL; }
#endif

+/* Nothing a cell writes imparts privilege: new files and new modes. */
+
+/**
+ * cell_strip_setid - take from a mode what a task of a cell may not set
+ * @mode: a file mode, file type included
+ *
+ * Nothing a cell runs can gain from a file, since every task of a cell has
+ * no_new_privs. This is the other half: nothing a cell writes can make anybody
+ * else gain. The ids of a cell commonly map to on-disk ids from 0 up, so a
+ * set-uid bit set from the inside would make a set-uid root file for whoever
+ * else looks at that filesystem.
+ *
+ * The bits are dropped, not refused, as setattr_prepare() drops S_ISGID for a
+ * caller that is not in the group of the file: unpacking an archive keeps
+ * working. On a directory S_ISGID is about inheritance and stays.
+ *
+ * Return: @mode, without S_ISUID and S_ISGID if the caller is in a cell.
+ */
+static inline umode_t cell_strip_setid(umode_t mode)
+{
+ if (likely(!(mode & (S_ISUID | S_ISGID))) || !current_cell())
+ return mode;
+ mode &= ~S_ISUID;
+ if (!S_ISDIR(mode))
+ mode &= ~S_ISGID;
+ return mode;
+}
+
#endif /* _LINUX_CELL_H */

--
2.55.0