diff options
| author | Christian Brauner <brauner@kernel.org> | 2026-10-30 13:40:47 +0100 |
|---|---|---|
| committer | Christian Brauner <brauner@kernel.org> | 2026-10-30 13:40:47 +0100 |
| commit | 99f7d39384d6f1967f3dddf27aca47280f63d8d7 (patch) | |
| tree | b4abc877d09db63f52991fdf0f478e558ab312ed | |
| parent | 172658cf600904ff6121e240f69a0ccb36e65d6b (diff) | |
| parent | a290a9e5cf5fbba05d6c59b94b36d00f4fc15ef2 (diff) | |
| download | linux-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.c | 11 | ||||
| -rw-r--r-- | fs/backing-file.c | 28 | ||||
| -rw-r--r-- | lib/iov_iter.c | 4 |
3 files changed, 43 insertions, 0 deletions
@@ -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); |
