[PATCH RFC v3 63/90] fs: drop privileges from what a cell may write to
From: Josef Bacik
Date: Thu Nov 05 2026 - 11:01:23 EST
A write() clears the set-id bits and the file capabilities of a file. A
store through a shared mapping does not. So a task of a cell could replace
the contents of an existing set-uid file while the file keeps the bit.
Clear them as soon as a task of a cell opens a file for writing, and when
pidfd_configure() places a file that is open for writing.
This changes the inode at open time, like O_TRUNC, so it is done in the
same place as the truncation, in do_open(). The directory lock has been
dropped at that point, which is not the case in ->atomic_open(). The
change needs write access to the mount, so it waits if the filesystem is
frozen, but only if there is something to clear.
Signed-off-by: Josef Bacik <josef@xxxxxxxxxxxxxx>
---
fs/inode.c | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++
fs/namei.c | 8 +++++++
include/linux/cell.h | 7 ++++++
kernel/embryo.c | 6 ++++++
4 files changed, 82 insertions(+)
diff --git a/fs/inode.c b/fs/inode.c
index f63535ee3629..d32c14217420 100644
--- a/fs/inode.c
+++ b/fs/inode.c
@@ -4,6 +4,7 @@
* (C) 1999 Andrea Arcangeli <andrea@xxxxxxx> (dynamic inode allocation)
*/
#include <linux/export.h>
+#include <linux/cell.h>
#include <linux/fs.h>
#include <linux/filelock.h>
#include <linux/mm.h>
@@ -2379,6 +2380,66 @@ static int __remove_privs(const struct mnt_idmap *idmap,
return notify_change(idmap, dentry, &newattrs, NULL);
}
+#ifdef CONFIG_CELLS
+static int cell_privs_to_kill(struct dentry *dentry)
+{
+ umode_t mode = d_inode(dentry)->i_mode;
+ int kill = 0, ret;
+
+ if (mode & S_ISUID)
+ kill |= ATTR_KILL_SUID;
+ if (mode & S_ISGID)
+ kill |= ATTR_KILL_SGID;
+ ret = security_inode_need_killpriv(dentry);
+ if (ret < 0)
+ return ret;
+ if (ret)
+ kill |= ATTR_KILL_PRIV;
+ return kill;
+}
+
+/**
+ * cell_file_drop_privs - a task of a cell is about to be able to write @file
+ * @file: a file open for writing
+ *
+ * Nothing a cell writes imparts privilege. A write() drops the set-id bits and
+ * the file capabilities of what it writes to, a store through a shared mapping
+ * does not. So for a cell they go as soon as the file can be written at all,
+ * and they all go, whatever capabilities the caller has.
+ *
+ * This changes the inode, so the caller must hold no inode lock and no write
+ * access to the superblock. It waits for a frozen filesystem, but only if there
+ * is something to drop.
+ *
+ * Return: 0 or a negative error, in which case @file must not be handed out.
+ */
+int cell_file_drop_privs(struct file *file)
+{
+ struct dentry *dentry = file_dentry(file);
+ struct inode *inode = file_inode(file);
+ int kill;
+
+ if (!S_ISREG(inode->i_mode) || IS_NOSEC(inode))
+ return 0;
+
+ /* A look without the lock, as do_truncate() takes one. */
+ kill = cell_privs_to_kill(dentry);
+ if (kill <= 0)
+ return kill;
+
+ kill = mnt_want_write_file(file);
+ if (kill)
+ return kill;
+ inode_lock(inode);
+ kill = cell_privs_to_kill(dentry);
+ if (kill > 0)
+ kill = __remove_privs(file_mnt_idmap(file), dentry, kill);
+ inode_unlock(inode);
+ mnt_drop_write_file(file);
+ return kill;
+}
+#endif
+
static int file_remove_privs_flags(struct file *file, unsigned int flags)
{
struct dentry *dentry = file_dentry(file);
diff --git a/fs/namei.c b/fs/namei.c
index 49995c353292..f36c1a385d80 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -4862,6 +4862,14 @@ static int do_open(struct nameidata *nd,
mnt_drop_write(mnt);
mntput(mnt);
}
+ /*
+ * Nothing a cell writes imparts privilege. Here, like the truncation,
+ * because it changes the inode: the lock of the directory has been
+ * dropped, which is not so in ->atomic_open().
+ */
+ if (!error && unlikely(current_cell()) &&
+ (file->f_mode & (FMODE_WRITE | FMODE_CREATED)) == FMODE_WRITE)
+ error = cell_file_drop_privs(file);
return error;
}
diff --git a/include/linux/cell.h b/include/linux/cell.h
index 1d0eec34acb9..0451055f74fc 100644
--- a/include/linux/cell.h
+++ b/include/linux/cell.h
@@ -284,4 +284,11 @@ static inline umode_t cell_strip_setid(umode_t mode)
return mode;
}
+/* Nothing a cell writes imparts privilege: files that are already there. */
+#ifdef CONFIG_CELLS
+int cell_file_drop_privs(struct file *file);
+#else
+static inline int cell_file_drop_privs(struct file *file) { return 0; }
+#endif
+
#endif /* _LINUX_CELL_H */
diff --git a/kernel/embryo.c b/kernel/embryo.c
index 0a04f39fa37a..657ce07441e0 100644
--- a/kernel/embryo.c
+++ b/kernel/embryo.c
@@ -113,6 +113,12 @@ static int embryo_install_fds(struct embryo *e)
ret = security_file_receive(file);
if (ret)
return ret;
+ /* As if the cell had opened it. */
+ if (file->f_mode & FMODE_WRITE) {
+ ret = cell_file_drop_privs(file);
+ if (ret)
+ return ret;
+ }
ret = replace_fd(e->fds[i].fd, file, 0);
if (ret < 0)
return ret;
--
2.55.0