| From stable+bounces-172251-greg=kroah.com@vger.kernel.org Fri Aug 22 05:09:26 2025 |
| From: Sasha Levin <sashal@kernel.org> |
| Date: Thu, 21 Aug 2025 23:08:00 -0400 |
| Subject: netfs: Fix unbuffered write error handling |
| To: stable@vger.kernel.org |
| Cc: David Howells <dhowells@redhat.com>, Xiaoli Feng <fengxiaoli0714@gmail.com>, Paulo Alcantara <pc@manguebit.org>, Steve French <sfrench@samba.org>, Shyam Prasad N <sprasad@microsoft.com>, netfs@lists.linux.dev, linux-cifs@vger.kernel.org, linux-fsdevel@vger.kernel.org, Christian Brauner <brauner@kernel.org>, Sasha Levin <sashal@kernel.org> |
| Message-ID: <20250822030800.1054685-1-sashal@kernel.org> |
| |
| From: David Howells <dhowells@redhat.com> |
| |
| [ Upstream commit a3de58b12ce074ec05b8741fa28d62ccb1070468 ] |
| |
| If all the subrequests in an unbuffered write stream fail, the subrequest |
| collector doesn't update the stream->transferred value and it retains its |
| initial LONG_MAX value. Unfortunately, if all active streams fail, then we |
| take the smallest value of { LONG_MAX, LONG_MAX, ... } as the value to set |
| in wreq->transferred - which is then returned from ->write_iter(). |
| |
| LONG_MAX was chosen as the initial value so that all the streams can be |
| quickly assessed by taking the smallest value of all stream->transferred - |
| but this only works if we've set any of them. |
| |
| Fix this by adding a flag to indicate whether the value in |
| stream->transferred is valid and checking that when we integrate the |
| values. stream->transferred can then be initialised to zero. |
| |
| This was found by running the generic/750 xfstest against cifs with |
| cache=none. It splices data to the target file. Once (if) it has used up |
| all the available scratch space, the writes start failing with ENOSPC. |
| This causes ->write_iter() to fail. However, it was returning |
| wreq->transferred, i.e. LONG_MAX, rather than an error (because it thought |
| the amount transferred was non-zero) and iter_file_splice_write() would |
| then try to clean up that amount of pipe bufferage - leading to an oops |
| when it overran. The kernel log showed: |
| |
| CIFS: VFS: Send error in write = -28 |
| |
| followed by: |
| |
| BUG: kernel NULL pointer dereference, address: 0000000000000008 |
| |
| with: |
| |
| RIP: 0010:iter_file_splice_write+0x3a4/0x520 |
| do_splice+0x197/0x4e0 |
| |
| or: |
| |
| RIP: 0010:pipe_buf_release (include/linux/pipe_fs_i.h:282) |
| iter_file_splice_write (fs/splice.c:755) |
| |
| Also put a warning check into splice to announce if ->write_iter() returned |
| that it had written more than it was asked to. |
| |
| Fixes: 288ace2f57c9 ("netfs: New writeback implementation") |
| Reported-by: Xiaoli Feng <fengxiaoli0714@gmail.com> |
| Closes: https://bugzilla.kernel.org/show_bug.cgi?id=220445 |
| Signed-off-by: David Howells <dhowells@redhat.com> |
| Link: https://lore.kernel.org/915443.1755207950@warthog.procyon.org.uk |
| cc: Paulo Alcantara <pc@manguebit.org> |
| cc: Steve French <sfrench@samba.org> |
| cc: Shyam Prasad N <sprasad@microsoft.com> |
| cc: netfs@lists.linux.dev |
| cc: linux-cifs@vger.kernel.org |
| cc: linux-fsdevel@vger.kernel.org |
| cc: stable@vger.kernel.org |
| Signed-off-by: Christian Brauner <brauner@kernel.org> |
| [ Dropped read_collect.c hunk ] |
| Signed-off-by: Sasha Levin <sashal@kernel.org> |
| Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org> |
| --- |
| fs/netfs/write_collect.c | 10 ++++++++-- |
| fs/netfs/write_issue.c | 4 ++-- |
| fs/splice.c | 3 +++ |
| include/linux/netfs.h | 1 + |
| 4 files changed, 14 insertions(+), 4 deletions(-) |
| |
| --- a/fs/netfs/write_collect.c |
| +++ b/fs/netfs/write_collect.c |
| @@ -433,6 +433,7 @@ reassess_streams: |
| if (front->start + front->transferred > stream->collected_to) { |
| stream->collected_to = front->start + front->transferred; |
| stream->transferred = stream->collected_to - wreq->start; |
| + stream->transferred_valid = true; |
| notes |= MADE_PROGRESS; |
| } |
| if (test_bit(NETFS_SREQ_FAILED, &front->flags)) { |
| @@ -538,6 +539,7 @@ void netfs_write_collection_worker(struc |
| struct netfs_io_request *wreq = container_of(work, struct netfs_io_request, work); |
| struct netfs_inode *ictx = netfs_inode(wreq->inode); |
| size_t transferred; |
| + bool transferred_valid = false; |
| int s; |
| |
| _enter("R=%x", wreq->debug_id); |
| @@ -568,12 +570,16 @@ void netfs_write_collection_worker(struc |
| netfs_put_request(wreq, false, netfs_rreq_trace_put_work); |
| return; |
| } |
| - if (stream->transferred < transferred) |
| + if (stream->transferred_valid && |
| + stream->transferred < transferred) { |
| transferred = stream->transferred; |
| + transferred_valid = true; |
| + } |
| } |
| |
| /* Okay, declare that all I/O is complete. */ |
| - wreq->transferred = transferred; |
| + if (transferred_valid) |
| + wreq->transferred = transferred; |
| trace_netfs_rreq(wreq, netfs_rreq_trace_write_done); |
| |
| if (wreq->io_streams[1].active && |
| --- a/fs/netfs/write_issue.c |
| +++ b/fs/netfs/write_issue.c |
| @@ -115,12 +115,12 @@ struct netfs_io_request *netfs_create_wr |
| wreq->io_streams[0].prepare_write = ictx->ops->prepare_write; |
| wreq->io_streams[0].issue_write = ictx->ops->issue_write; |
| wreq->io_streams[0].collected_to = start; |
| - wreq->io_streams[0].transferred = LONG_MAX; |
| + wreq->io_streams[0].transferred = 0; |
| |
| wreq->io_streams[1].stream_nr = 1; |
| wreq->io_streams[1].source = NETFS_WRITE_TO_CACHE; |
| wreq->io_streams[1].collected_to = start; |
| - wreq->io_streams[1].transferred = LONG_MAX; |
| + wreq->io_streams[1].transferred = 0; |
| if (fscache_resources_valid(&wreq->cache_resources)) { |
| wreq->io_streams[1].avail = true; |
| wreq->io_streams[1].active = true; |
| --- a/fs/splice.c |
| +++ b/fs/splice.c |
| @@ -744,6 +744,9 @@ iter_file_splice_write(struct pipe_inode |
| sd.pos = kiocb.ki_pos; |
| if (ret <= 0) |
| break; |
| + WARN_ONCE(ret > sd.total_len - left, |
| + "Splice Exceeded! ret=%zd tot=%zu left=%zu\n", |
| + ret, sd.total_len, left); |
| |
| sd.num_spliced += ret; |
| sd.total_len -= ret; |
| --- a/include/linux/netfs.h |
| +++ b/include/linux/netfs.h |
| @@ -150,6 +150,7 @@ struct netfs_io_stream { |
| bool active; /* T if stream is active */ |
| bool need_retry; /* T if this stream needs retrying */ |
| bool failed; /* T if this stream failed */ |
| + bool transferred_valid; /* T is ->transferred is valid */ |
| }; |
| |
| /* |