btrfs: fix corruption after buffer fault in during direct IO append write
Subsystems (2)
A message that is not in the archive (sent off-list or not yet received): [email protected]
PATCH 1/1 btrfs: fix corruption after buffer fault in during direct IO append write
From: Filipe Manana <[email protected]>
commit 939b656bc8ab203fdbde26ccac22bcb7f0985be5 upstream.
During an append (O_APPEND write flag) direct IO write if the input buffer was not previously faulted in, we can corrupt the file in a way that the final size is unexpected and it includes an unexpected hole.
The problem happens like this:
1) We have an empty file, with size 0, for example;
2) We do an O_APPEND direct IO with a length of 4096 bytes and the input buffer is not currently faulted in;
3) We enter btrfs_direct_write(), lock the inode and call generic_write_checks(), which calls generic_write_checks_count(), and that function sets the iocb position to 0 with the following code:
if (iocb->ki_flags & IOCB_APPEND)
iocb->ki_pos = i_size_read(inode);4) We call btrfs_dio_write() and enter into iomap, which will end up calling btrfs_dio_iomap_begin() and that calls btrfs_get_blocks_direct_write(), where we update the i_size of the inode to 4096 bytes;
5) After btrfs_dio_iomap_begin() returns, iomap will attempt to access the page of the write input buffer (at iomap_dio_bio_iter(), with a call to bio_iov_iter_get_pages()) and fail with -EFAULT, which gets returned to btrfs at btrfs_direct_write() via btrfs_dio_write();
6) At btrfs_direct_write() we get the -EFAULT error, unlock the inode, fault in the write buffer and then goto to the label 'relock';
7) We lock again the inode, do all the necessary checks again and call again generic_write_checks(), which calls generic_write_checks_count() again, and there we set the iocb's position to 4K, which is the current i_size of the inode, with the following code pointed above:
if (iocb->ki_flags & IOCB_APPEND)
iocb->ki_pos = i_size_read(inode);8) Then we go again to btrfs_dio_write() and enter iomap and the write succeeds, but it wrote to the file range [4K, 8K[, leaving a hole in the [0, 4K[ range and an i_size of 8K, which goes against the expections of having the data written to the range [0, 4K[ and get an i_size of 4K.
Fix this by not unlocking the inode before faulting in the input buffer, in case we get -EFAULT or an incomplete write, and not jumping to the 'relock' label after faulting in the buffer - instead jump to a location immediately before calling iomap, skipping all the write checks and relocking. This solves this problem and it's fine even in case the input buffer is memory mapped to the same file range, since only holding the range locked in the inode's io tree can cause a deadlock, it's safe to keep the inode lock (VFS lock), as was fixed and described in commit 51bd9563b678 ("btrfs: fix deadlock due to page faults during direct IO reads and writes").
A sample reproducer provided by a reporter is the following:
$ cat test.c #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "Usage: %s <test file>\n", argv[0]);
return 1;
} int fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC | O_DIRECT |
O_APPEND, 0644);
if (fd < 0) {
perror("creating test file");
return 1;
}
char *buf = mmap(NULL, 4096, PROT_READ,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
ssize_t ret = write(fd, buf, 4096);
if (ret < 0) {
perror("pwritev2");
return 1;
}
struct stat stbuf;
ret = fstat(fd, &stbuf);
if (ret < 0) {
perror("stat");
return 1;
}
printf("size: %llu\n", (unsigned long long)stbuf.st_size);
return stbuf.st_size == 4096 ? 0 : 1;}
A test case for fstests will be sent soon.
--- fs/btrfs/ctree.h | 1 + fs/btrfs/file.c | 55 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 13 deletions(-)
fs/btrfs/ctree.h +1 -0
@@ -1383,6 +1383,7 @@ struct btrfs_drop_extents_args {struct btrfs_file_private {void *filldir_buf;u64 last_index; +bool fsync_skip_inode_lock; };
fs/btrfs/file.c +42 -13
107 lines not on this page · raw message
Signature · 1 line
2.43.0
Greg KH
maintainerQuoted 104 lines · On Wed, Aug 07, 2024 at 05:06:17PM +0100, [email protected] wrote:
From: Filipe Manana <[email protected]>
commit
939b656bc8ab203fdbde26ccac22bcb7f0985be5upstream.During an append (
O_APPENDwrite flag) direct IO write if the input buffer was not previously faulted in, we can corrupt the file in a way that the final size is unexpected and it includes an unexpected hole.The problem happens like this:
1) We have an empty file, with size 0, for example;
2) We do an
O_APPENDdirect IO with a length of 4096 bytes and the input buffer is not currently faulted in;3) We enter
btrfs_direct_write(), lock the inode and callgeneric_write_checks(), which callsgeneric_write_checks_count(), and that function sets the iocb position to 0 with the following code:if (iocb->ki_flags & IOCB_APPEND) iocb->ki_pos = i_size_read(inode);4) We call
btrfs_dio_write()and enter into iomap, which will end up callingbtrfs_dio_iomap_begin()and that callsbtrfs_get_blocks_direct_write(), where we update thei_sizeof the inode to 4096 bytes;5) After
btrfs_dio_iomap_begin()returns, iomap will attempt to access the page of the write input buffer (atiomap_dio_bio_iter(), with a call tobio_iov_iter_get_pages()) and fail with-EFAULT, which gets returned to btrfs atbtrfs_direct_write()viabtrfs_dio_write();6) At
btrfs_direct_write()we get the-EFAULTerror, unlock the inode, fault in the write buffer and then goto to the label 'relock';7) We lock again the inode, do all the necessary checks again and call again
generic_write_checks(), which callsgeneric_write_checks_count()again, and there we set the iocb's position to 4K, which is the currenti_sizeof the inode, with the following code pointed above:if (iocb->ki_flags & IOCB_APPEND) iocb->ki_pos = i_size_read(inode);8) Then we go again to
btrfs_dio_write()and enter iomap and the write succeeds, but it wrote to the file range [4K, 8K[, leaving a hole in the [0, 4K[ range and ani_sizeof 8K, which goes against the expections of having the data written to the range [0, 4K[ and get ani_sizeof 4K.Fix this by not unlocking the inode before faulting in the input buffer, in case we get
-EFAULTor an incomplete write, and not jumping to the 'relock' label after faulting in the buffer - instead jump to a location immediately before calling iomap, skipping all the write checks and relocking. This solves this problem and it's fine even in case the input buffer is memory mapped to the same file range, since only holding the range locked in the inode's io tree can cause a deadlock, it's safe to keep the inode lock (VFS lock), as was fixed and described in commit51bd9563b678("btrfs: fix deadlock due to page faults during direct IO reads and writes").A sample reproducer provided by a reporter is the following:
$ cat test.c #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif#include <fcntl.h> #include <stdio.h> #include <sys/mman.h> #include <sys/stat.h> #include <unistd.h> int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s <test file>\n", argv[0]); return 1; }int fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC | O_DIRECT | O_APPEND, 0644); if (fd < 0) { perror("creating test file"); return 1; } char *buf = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); ssize_t ret = write(fd, buf, 4096); if (ret < 0) { perror("pwritev2"); return 1; } struct stat stbuf; ret = fstat(fd, &stbuf); if (ret < 0) { perror("stat"); return 1; } printf("size: %llu\n", (unsigned long long)stbuf.st_size); return stbuf.st_size == 4096 ? 0 : 1;
Quoted 8 lines
}
A test case for fstests will be sent soon.
Reported-by Hanna Czenczek <[email protected]>Fixes 8184620ae212 ("btrfs: fix lost file sync on direct IO write with nowait and dsync iocb")Signed-off-by Filipe Manana <[email protected]>
All now queued up, thanks.
greg k-h
PATCH 1/1 btrfs: fix double inode unlock for direct IO sync writes
In reply to a message not in the archiveFrom: Filipe Manana <[email protected]>
commit e0391e92f9ab4fb3dbdeb139c967dcfa7ac4b115 upstream.
If we do a direct IO sync write, at btrfs_sync_file(), and we need to skip inode logging or we get an error starting a transaction or an error when flushing delalloc, we end up unlocking the inode when we shouldn't under the 'out_release_extents' label, and then unlock it again at btrfs_direct_write().
Fix that by checking if we have to skip inode unlocking under that label.
--- fs/btrfs/file.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-)
fs/btrfs/file.c +4 -1
@@ -2433,7 +2433,10 @@ int btrfs_sync_file(struct file *file, loff_t start, loff_t end, int datasync) out_release_extents:btrfs_release_log_ctx_extents (&ctx); -btrfs_inode_unlock (inode,BTRFS_ILOCK_MMAP ); +if (skip_ilock) +up_write (&BTRFS_I (inode)->i_mmap_lock); +else +btrfs_inode_unlock (inode,BTRFS_ILOCK_MMAP ); goto out; }
Signature · 1 line
2.43.0
Greg KH
maintainerRe: [PATCH for 5.15 stable] btrfs: fix double inode unlock for direct IO sync writes
Quoted 18 lines · On Mon, Aug 12, 2024 at 04:27:33PM +0100, [email protected] wrote:
From: Filipe Manana <[email protected]>
commit
e0391e92f9ab4fb3dbdeb139c967dcfa7ac4b115upstream.If we do a direct IO sync write, at
btrfs_sync_file(), and we need to skip inode logging or we get an error starting a transaction or an error when flushing delalloc, we end up unlocking the inode when we shouldn't under the 'out_release_extents' label, and then unlock it again atbtrfs_direct_write().Fix that by checking if we have to skip inode unlocking under that label.
Reported-by [email protected]Fixes 939b656bc8ab ("btrfs: fix corruption after buffer fault in during direct IO append write")Reviewed-by Josef Bacik <[email protected]>Signed-off-by Filipe Manana <[email protected]>Signed-off-by David Sterba <[email protected]>
Quoted 3 lines
--- fs/btrfs/file.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-)
All now queued up, thanks.
greg k-h