summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Brauner <brauner@kernel.org>2026-10-30 13:40:47 +0100
committerChristian Brauner <brauner@kernel.org>2026-10-30 13:40:47 +0100
commit99f7d39384d6f1967f3dddf27aca47280f63d8d7 (patch)
treeb4abc877d09db63f52991fdf0f478e558ab312ed
parent172658cf600904ff6121e240f69a0ccb36e65d6b (diff)
parenta290a9e5cf5fbba05d6c59b94b36d00f4fc15ef2 (diff)
downloadlinux-99f7d39384d6f1967f3dddf27aca47280f63d8d7.tar.gz
linux-99f7d39384d6f1967f3dddf27aca47280f63d8d7.zip
Merge patch series "fs: fixes for aio rings, backing files and iov_iter_alignment()"
Christian Brauner <brauner@kernel.org> says: The aio bug fixed here is the third way this year to fault on an aio ring after its file was truncated. The ring file has no ->read_folio because all of its folios are supposed to be in the page cache so we jump to NULL. This time vm_munmap() gets interrupted by a fatal signal in io_destroy() and leaves the ring mapped. I didn't want to plug yet another route so the ring now gets a ->read_folio that fails and the fault gets SIGBUS. Making the munmap unkillable would only cover this one route. I rewrote the backing-file one. FUSE passthrough opens the backing file with the flags of the user's open but never runs the may_open() checks against the backing inode. So a passthrough file backed by an append-only file can be overwritten anywhere and one backed by an immutable file can be written to. Chris' version only checked the write paths. That left shared mappings and S_IMMUTABLE open and it failed writes on file descriptors opened before chattr +a which can keep writing on any other filesystem. So the checks are now done at open and mmap time like may_open() and do_mmap() do and writes only fail if append mode was taken away from the user file. FUSE reports EIO instead of EPERM for a refused open because it turns every error from opening the backing file into EIO. I left that alone. Miklos, if you'd rather pass the error through I can do that on top. Amir, for overlayfs this only changes something if the upper inode is changed behind its back. The iov_iter one is a NULL deref that any user can trigger on btrfs with a single zero-length IORING_OP_READ_FIXED on an O_DIRECT file. io_import_fixed() hands out an empty bvec iterator without an array for that and iov_iter_alignment() looks at the first segment before it checks the count. Other places build such iterators as well so I fixed iov_iter_alignment() instead of io_uring. All three are tested in a KASAN VM. Without the patches the aio and iov_iter reproducers oops and the backing-file one overwrites both an append-only and an immutable file through FUSE passthrough. With them none of that happens. * patches from https://patch.msgid.link/20261029-work-fs-ai-fixes-v1-0-88f22792f6f7@kernel.org: iov_iter: fix NULL deref in iov_iter_alignment() backing-file: enforce append-only and immutable backing inodes aio: fix NULL deref when a fault races ring truncation Link: https://patch.msgid.link/20261029-work-fs-ai-fixes-v1-0-88f22792f6f7@kernel.org Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
-rw-r--r--fs/aio.c11
-rw-r--r--fs/backing-file.c28
-rw-r--r--lib/iov_iter.c4
3 files changed, 43 insertions, 0 deletions
diff --git a/fs/aio.c b/fs/aio.c
index 4f32b45cb8a0..893db6961ad3 100644
--- a/fs/aio.c
+++ b/fs/aio.c
@@ -528,7 +528,18 @@ out:
#define aio_migrate_folio NULL
#endif
+/*
+ * filemap_fault() reads a folio that it did not find in the page cache. All
+ * folios of a live ring are there, so the ring was truncated under the fault.
+ */
+static int aio_read_folio(struct file *file, struct folio *folio)
+{
+ folio_unlock(folio);
+ return -EIO;
+}
+
static const struct address_space_operations aio_ctx_aops = {
+ .read_folio = aio_read_folio,
.dirty_folio = noop_dirty_folio,
.migrate_folio = aio_migrate_folio,
};
diff --git a/fs/backing-file.c b/fs/backing-file.c
index 5614cb7801e1..2424dc38d21a 100644
--- a/fs/backing-file.c
+++ b/fs/backing-file.c
@@ -36,9 +36,21 @@ struct file *backing_file_open(const struct file *user_file, int flags,
const struct cred *cred)
{
const struct path *user_path = file_user_path(user_file);
+ struct inode *real_inode = d_inode(real_path->dentry);
struct file *f;
int error;
+ /*
+ * may_open() checked the inode of the user file, which need not carry
+ * S_APPEND and S_IMMUTABLE of the backing inode.
+ */
+ if ((flags & O_ACCMODE) != O_RDONLY) {
+ if (IS_IMMUTABLE(real_inode))
+ return ERR_PTR(-EPERM);
+ if (IS_APPEND(real_inode) && !(flags & O_APPEND))
+ return ERR_PTR(-EPERM);
+ }
+
f = alloc_empty_backing_file(flags, cred, user_file);
if (IS_ERR(f))
return f;
@@ -263,6 +275,14 @@ ssize_t backing_file_write_iter(struct file *file, struct iov_iter *iter,
if (!iov_iter_count(iter))
return 0;
+ /*
+ * F_SETFL and RWF_NOAPPEND can take append mode away from the user
+ * file. They checked its inode, not the backing one.
+ */
+ if ((file->f_flags & O_APPEND) && !(flags & IOCB_APPEND) &&
+ IS_APPEND(file_inode(file)))
+ return -EPERM;
+
ret = file_remove_privs(iocb->ki_filp);
if (ret)
return ret;
@@ -309,6 +329,10 @@ ssize_t backing_file_splice_write(struct pipe_inode_info *pipe,
if (!out->f_op->splice_write)
return -EINVAL;
+ /* splice refuses a user file in append mode, so F_SETFL cleared it */
+ if ((out->f_flags & O_APPEND) && IS_APPEND(file_inode(out)))
+ return -EPERM;
+
ret = file_remove_privs(iocb->ki_filp);
if (ret)
return ret;
@@ -338,6 +362,10 @@ int backing_file_mmap(struct file *file, struct vm_area_struct *vma,
if (!can_mmap_file(file))
return -ENODEV;
+ /* do_mmap() checked the inode of the user file */
+ if ((vma->vm_flags & VM_SHARED) && IS_APPEND(file_inode(file)))
+ return -EACCES;
+
vma_set_file(vma, file);
scoped_with_creds(ctx->cred) {
diff --git a/lib/iov_iter.c b/lib/iov_iter.c
index 2072c04e99d0..56c59a2b589d 100644
--- a/lib/iov_iter.c
+++ b/lib/iov_iter.c
@@ -848,6 +848,10 @@ unsigned long iov_iter_alignment(const struct iov_iter *i)
return 0;
}
+ /* An empty iterator may have no segment array at all. */
+ if (!i->count)
+ return 0;
+
/* iovec and kvec have identical layouts */
if (likely(iter_is_iovec(i) || iov_iter_is_kvec(i)))
return iov_iter_alignment_iovec(i);