smb: client: fix data corruption with concurrent writes and O_TRUNC

cifs_do_truncate() flushes dirty pages with filemap_write_and_wait()
and truncates the file on the server, but in the old code both
operations ran without holding i_rwsem or invalidate_lock.  A
concurrent buffered write via netfs_perform_write() -- which only
needs i_rwsem shared -- could dirty new pages after the flush but
before the local truncation, and those pages would be silently
discarded by cifs_setsize() -> truncate_pagecache().

Fix by acquiring inode_lock (exclusive i_rwsem) and
filemap_invalidate_lock at the top of cifs_do_truncate(), so the
entire flush-truncate-resize sequence is atomic with respect to:

  - buffered writes (blocked by exclusive i_rwsem, since
    netfs_start_io_write takes i_rwsem shared),
  - read page faults (blocked by exclusive invalidate_lock, since
    filemap_fault takes it shared),
  - writeback collection (blocked by netfs_wb_begin/netfs_wb_end
    around the server truncate and local resize, since
    netfs_writepages also acquires the wb lock).

Fixes: 110fee6b9b ("smb: client: fix missing timestamp updates with O_TRUNC")
Signed-off-by: Paulo Alcantara <pc@manguebit.org>
Reviewed-by: Namjae Jeon <linkinjeon@kernel.org>
Cc: Ronnie Sahlberg <ronniesahlberg@gmail.com>
Cc: Shyam Prasad N <sprasad@microsoft.com>
Cc: Tom Talpey <tom@talpey.com>
Cc: Bharath SM <bharathsm@microsoft.com>
Cc: stable@vger.kernel.org
This commit is contained in:
Paulo Alcantara 2026-08-28 19:08:09 -03:00
parent fe39cd9d48
commit a8603b52b3

View File

@ -999,42 +999,50 @@ static int cifs_do_truncate(const unsigned int xid, struct dentry *dentry)
struct cifs_tcon *tcon;
int rc;
rc = filemap_write_and_wait(inode->i_mapping);
if (is_interrupt_error(rc))
rc = inode_lock_killable(inode);
if (rc)
return -ERESTARTSYS;
filemap_invalidate_lock(inode->i_mapping);
rc = filemap_write_and_wait(inode->i_mapping);
if (is_interrupt_error(rc)) {
rc = -ERESTARTSYS;
goto out;
}
mapping_set_error(inode->i_mapping, rc);
cfile = find_writable_file(cinode, FIND_FSUID_ONLY);
rc = cifs_file_flush(xid, inode, cfile);
if (!rc) {
if (cfile) {
struct netfs_inode *ictx = netfs_inode(inode);
tcon = tlink_tcon(cfile->tlink);
server = tcon->ses->server;
netfs_wb_begin(ictx, false);
rc = server->ops->set_file_size(xid, tcon,
cfile, 0, false);
if (!rc) {
inode_lock(inode);
filemap_invalidate_lock(inode->i_mapping);
netfs_resize_file(&cinode->netfs, 0, true);
cifs_setsize(inode, 0);
filemap_invalidate_unlock(inode->i_mapping);
inode_unlock(inode);
cifs_invalidate_cache(inode, 0);
}
netfs_wb_end(ictx);
} else {
/*
* No cached handle; evict stale pages so they can't
* be served after the file is later extended; let
* the server's O_TRUNC open response set the i_size
*/
inode_lock(inode);
filemap_invalidate_lock(inode->i_mapping);
truncate_inode_pages(inode->i_mapping, 0);
filemap_invalidate_unlock(inode->i_mapping);
inode_unlock(inode);
cifs_invalidate_cache(inode, 0);
}
}
out:
filemap_invalidate_unlock(inode->i_mapping);
inode_unlock(inode);
if (cfile)
cifsFileInfo_put(cfile);
return rc;