Merge tag 'v7.2-rc3-smb3-client-fixes' of git://git.samba.org/sfrench/cifs-2.6

Pull smb client fixes from Steve French:

 - fallocate fixes

 - unit test fixes

 - fix allocation size after duplicate extents

 - fix check for overlapping data areas

* tag 'v7.2-rc3-smb3-client-fixes' of git://git.samba.org/sfrench/cifs-2.6:
  smb/client: flush dirty data before punching a hole
  smb/client: Use EXPORT_SYMBOL_IF_KUNIT() to export symbols in SMB2
  smb/client: Use EXPORT_SYMBOL_IF_KUNIT() to export symbols
  smb: client: reject overlapping data areas in SMB2 responses
  smb/client: refresh allocation after EOF-extending fallocate
  smb/client: emulate small EOF-extending mode 0 fallocate ranges
  smb/client: reduce fallocate zero buffer allocation
  smb/client: handle overlapping allocated ranges in fallocate
  smb/client: refresh allocation size after duplicate extents
  smb: client: use kvzalloc() for megabyte buffer in simple fallocate
This commit is contained in:
Linus Torvalds
2026-07-16 16:46:26 -07:00
11 changed files with 240 additions and 56 deletions

View File

@@ -1490,9 +1490,13 @@ static loff_t cifs_remap_file_range(struct file *src_file, loff_t off,
}
}
/* force revalidate of size and timestamps of target file now
that target is updated on the server */
CIFS_I(target_inode)->time = 0;
/*
* On success, duplicate_extents already updated the target inode attrs
* or marked them stale if the refresh failed. On failure, mark attrs
* stale because EOF may have changed before the clone failed.
*/
if (rc)
CIFS_I(target_inode)->time = 0;
unlock:
/* although unlocking in the reverse order from locking is not
strictly necessary here it is a little cleaner to be consistent */

View File

@@ -10,6 +10,9 @@
*/
#include <linux/bsearch.h>
#include <kunit/visibility.h>
#include "cifsproto.h"
#include "smb1proto.h"
#include "smberr.h"
@@ -239,48 +242,45 @@ int __init smb1_init_maperror(void)
}
#if IS_ENABLED(CONFIG_SMB1_KUNIT_TESTS)
#define EXPORT_SYMBOL_FOR_SMB_TEST(sym) \
EXPORT_SYMBOL_FOR_MODULES(sym, "smb1maperror_test")
const struct ntstatus_to_dos_err *
search_ntstatus_to_dos_map_test(__u32 ntstatus)
{
return search_ntstatus_to_dos_map(ntstatus);
}
EXPORT_SYMBOL_FOR_SMB_TEST(search_ntstatus_to_dos_map_test);
EXPORT_SYMBOL_IF_KUNIT(search_ntstatus_to_dos_map_test);
const struct ntstatus_to_dos_err *
ntstatus_to_dos_map_test = ntstatus_to_dos_map;
EXPORT_SYMBOL_FOR_SMB_TEST(ntstatus_to_dos_map_test);
EXPORT_SYMBOL_IF_KUNIT(ntstatus_to_dos_map_test);
unsigned int ntstatus_to_dos_num = ARRAY_SIZE(ntstatus_to_dos_map);
EXPORT_SYMBOL_FOR_SMB_TEST(ntstatus_to_dos_num);
EXPORT_SYMBOL_IF_KUNIT(ntstatus_to_dos_num);
const struct smb_to_posix_error *
search_mapping_table_ERRDOS_test(__u16 smb_err)
{
return search_mapping_table_ERRDOS(smb_err);
}
EXPORT_SYMBOL_FOR_SMB_TEST(search_mapping_table_ERRDOS_test);
EXPORT_SYMBOL_IF_KUNIT(search_mapping_table_ERRDOS_test);
const struct smb_to_posix_error *
mapping_table_ERRDOS_test = mapping_table_ERRDOS;
EXPORT_SYMBOL_FOR_SMB_TEST(mapping_table_ERRDOS_test);
EXPORT_SYMBOL_IF_KUNIT(mapping_table_ERRDOS_test);
unsigned int mapping_table_ERRDOS_num = ARRAY_SIZE(mapping_table_ERRDOS);
EXPORT_SYMBOL_FOR_SMB_TEST(mapping_table_ERRDOS_num);
EXPORT_SYMBOL_IF_KUNIT(mapping_table_ERRDOS_num);
const struct smb_to_posix_error *
search_mapping_table_ERRSRV_test(__u16 smb_err)
{
return search_mapping_table_ERRSRV(smb_err);
}
EXPORT_SYMBOL_FOR_SMB_TEST(search_mapping_table_ERRSRV_test);
EXPORT_SYMBOL_IF_KUNIT(search_mapping_table_ERRSRV_test);
const struct smb_to_posix_error *
mapping_table_ERRSRV_test = mapping_table_ERRSRV;
EXPORT_SYMBOL_FOR_SMB_TEST(mapping_table_ERRSRV_test);
EXPORT_SYMBOL_IF_KUNIT(mapping_table_ERRSRV_test);
unsigned int mapping_table_ERRSRV_num = ARRAY_SIZE(mapping_table_ERRSRV);
EXPORT_SYMBOL_FOR_SMB_TEST(mapping_table_ERRSRV_num);
EXPORT_SYMBOL_IF_KUNIT(mapping_table_ERRSRV_num);
#endif

View File

@@ -75,3 +75,4 @@ kunit_test_suite(maperror_suite);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("KUnit tests of SMB1 maperror");
MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");

View File

@@ -8,6 +8,9 @@
*
*/
#include <linux/errno.h>
#include <kunit/visibility.h>
#include "cifsproto.h"
#include "cifs_debug.h"
#include "smb2proto.h"
@@ -109,18 +112,15 @@ int __init smb2_init_maperror(void)
}
#if IS_ENABLED(CONFIG_SMB_KUNIT_TESTS)
#define EXPORT_SYMBOL_FOR_SMB_TEST(sym) \
EXPORT_SYMBOL_FOR_MODULES(sym, "smb2maperror_test")
const struct status_to_posix_error *smb2_get_err_map_test(__u32 smb2_status)
{
return smb2_get_err_map(smb2_status);
}
EXPORT_SYMBOL_FOR_SMB_TEST(smb2_get_err_map_test);
EXPORT_SYMBOL_IF_KUNIT(smb2_get_err_map_test);
const struct status_to_posix_error *smb2_error_map_table_test = smb2_error_map_table;
EXPORT_SYMBOL_FOR_SMB_TEST(smb2_error_map_table_test);
EXPORT_SYMBOL_IF_KUNIT(smb2_error_map_table_test);
unsigned int smb2_error_map_num = ARRAY_SIZE(smb2_error_map_table);
EXPORT_SYMBOL_FOR_SMB_TEST(smb2_error_map_num);
EXPORT_SYMBOL_IF_KUNIT(smb2_error_map_num);
#endif

View File

@@ -47,3 +47,4 @@ kunit_test_suite(maperror_suite);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("KUnit tests of SMB2 maperror");
MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");

View File

@@ -19,7 +19,8 @@
#include "nterr.h"
#include "cached_dir.h"
static unsigned int __smb2_calc_size(void *buf, bool *have_data);
static unsigned int __smb2_calc_size(void *buf, bool *have_data,
bool *data_area_overlap);
static int
check_smb2_hdr(struct smb2_hdr *shdr, __u64 mid)
@@ -148,6 +149,7 @@ smb2_check_message(char *buf, unsigned int pdu_len, unsigned int len,
__u32 calc_len; /* calculated length */
__u64 mid;
bool have_data;
bool data_area_overlap;
/* If server is a channel, select the primary channel */
pserver = SERVER_IS_CHAN(server) ? server->primary_server : server;
@@ -232,7 +234,12 @@ smb2_check_message(char *buf, unsigned int pdu_len, unsigned int len,
}
have_data = false;
calc_len = __smb2_calc_size(buf, &have_data);
data_area_overlap = false;
calc_len = __smb2_calc_size(buf, &have_data, &data_area_overlap);
/* Reject responses whose data area overlaps the fixed area. */
if (data_area_overlap)
return 1;
/* For SMB2_IOCTL, OutputOffset and OutputLength are optional, so might
* be 0, and not a real miscalculation */
@@ -416,14 +423,15 @@ smb2_get_data_area_len(int *off, int *len, struct smb2_hdr *shdr)
}
/*
* Calculate the size of the SMB message based on the fixed header
* portion, the number of word parameters and the data portion of the message.
* If have_data is non-NULL, it is set to true when a non-empty data area was
* found (data_length > 0), allowing callers to distinguish the implied bcc[0]
* case (no data area) from an overreported data length.
* Calculate the size of the SMB message based on the fixed header, fixed
* parameter area, and variable data area.
*
* If have_data is not NULL, it is set when a non-empty data area is found.
* If data_area_overlap is not NULL, it is set when the data area overlaps
* the fixed area.
*/
static unsigned int
__smb2_calc_size(void *buf, bool *have_data)
__smb2_calc_size(void *buf, bool *have_data, bool *data_area_overlap)
{
struct smb2_pdu *pdu = buf;
struct smb2_hdr *shdr = &pdu->hdr;
@@ -432,6 +440,11 @@ __smb2_calc_size(void *buf, bool *have_data)
/* Structure Size has already been checked to make sure it is 64 */
int len = le16_to_cpu(shdr->StructureSize);
if (have_data)
*have_data = false;
if (data_area_overlap)
*data_area_overlap = false;
/*
* StructureSize2, ie length of fixed parameter area has already
* been checked to make sure it is the correct length.
@@ -454,7 +467,10 @@ __smb2_calc_size(void *buf, bool *have_data)
if (offset + 1 < len) {
cifs_dbg(VFS, "data area offset %d overlaps SMB2 header %d\n",
offset + 1, len);
if (data_area_overlap)
*data_area_overlap = true;
data_length = 0;
goto calc_size_exit;
} else {
len = offset + data_length;
}
@@ -469,7 +485,7 @@ calc_size_exit:
unsigned int
smb2_calc_size(void *buf)
{
return __smb2_calc_size(buf, NULL);
return __smb2_calc_size(buf, NULL, NULL);
}
/* Note: caller must free return buffer */

View File

@@ -2193,10 +2193,14 @@ smb2_duplicate_extents(const unsigned int xid,
u64 len, u64 dest_off)
{
int rc;
int qrc;
unsigned int ret_data_len;
struct inode *inode;
struct smb2_file_all_info file_inf;
struct duplicate_extents_to_file dup_ext_buf;
struct timespec64 ts;
struct cifs_tcon *tcon = tlink_tcon(trgtfile->tlink);
u64 asize;
/* server fileays advertise duplicate extent support with this flag */
if ((le32_to_cpu(tcon->fsAttrInfo.Attributes) &
@@ -2232,6 +2236,32 @@ smb2_duplicate_extents(const unsigned int xid,
if (ret_data_len > 0)
cifs_dbg(FYI, "Non-zero response length in duplicate extents\n");
if (rc == 0) {
qrc = SMB2_query_info(xid, tcon, trgtfile->fid.persistent_fid,
trgtfile->fid.volatile_fid, &file_inf);
spin_lock(&inode->i_lock);
if (qrc == 0) {
asize = le64_to_cpu(file_inf.AllocationSize);
CIFS_I(inode)->time = jiffies;
if (file_inf.LastWriteTime) {
ts = cifs_NTtimeToUnix(file_inf.LastWriteTime);
inode_set_mtime_to_ts(inode, ts);
}
if (file_inf.ChangeTime) {
ts = cifs_NTtimeToUnix(file_inf.ChangeTime);
inode_set_ctime_to_ts(inode, ts);
}
if (file_inf.LastAccessTime) {
ts = cifs_NTtimeToUnix(file_inf.LastAccessTime);
inode_set_atime_to_ts(inode, ts);
}
inode->i_blocks = CIFS_INO_BLOCKS(asize);
} else {
CIFS_I(inode)->time = 0; /* force reval */
}
spin_unlock(&inode->i_lock);
}
duplicate_extents_out:
if (rc)
trace_smb3_clone_err(xid, srcfile->fid.volatile_fid,
@@ -3489,6 +3519,15 @@ static long smb3_punch_hole(struct file *file, struct cifs_tcon *tcon,
goto out;
filemap_invalidate_lock(inode->i_mapping);
/*
* Flush dirty data first, otherwise a dirty folio spanning the punched
* range may be written back after the ioctl and refill the hole.
*/
rc = filemap_write_and_wait_range(inode->i_mapping, offset,
offset + len - 1);
if (rc < 0)
goto unlock;
/*
* We implement the punch hole through ioctl, so we need remove the page
* caches first, otherwise the data may be inconsistent with the server.
@@ -3543,7 +3582,7 @@ static int smb3_simple_fallocate_write_range(unsigned int xid,
char *buf)
{
struct cifs_io_parms io_parms = {0};
int nbytes;
unsigned int nbytes;
int rc = 0;
struct kvec iov[2];
@@ -3564,9 +3603,10 @@ static int smb3_simple_fallocate_write_range(unsigned int xid,
rc = SMB2_write(xid, &io_parms, &nbytes, iov, 1);
if (rc)
break;
if (!nbytes)
return -EIO;
if (nbytes > len)
return -EINVAL;
buf += nbytes;
off += nbytes;
len -= nbytes;
}
@@ -3579,11 +3619,25 @@ static int smb3_simple_fallocate_range(unsigned int xid,
loff_t off, loff_t len)
{
struct file_allocated_range_buffer in_data, *out_data = NULL, *tmp_data;
struct inode *inode = d_inode(cfile->dentry);
u32 out_data_len;
char *buf = NULL;
u64 range_start, range_len, range_end;
loff_t l;
int rc;
buf = kvzalloc(min_t(loff_t, len, SMB2_MAX_BUFFER_SIZE), GFP_KERNEL);
if (!buf) {
rc = -ENOMEM;
goto out;
}
if (off >= i_size_read(inode)) {
rc = smb3_simple_fallocate_write_range(xid, tcon, cfile,
off, len, buf);
goto out;
}
in_data.file_offset = cpu_to_le64(off);
in_data.length = cpu_to_le64(len);
rc = SMB2_ioctl(xid, tcon, cfile->fid.persistent_fid,
@@ -3595,12 +3649,6 @@ static int smb3_simple_fallocate_range(unsigned int xid,
if (rc)
goto out;
buf = kzalloc(1024 * 1024, GFP_KERNEL);
if (buf == NULL) {
rc = -ENOMEM;
goto out;
}
tmp_data = out_data;
while (len) {
/*
@@ -3617,13 +3665,21 @@ static int smb3_simple_fallocate_range(unsigned int xid,
goto out;
}
if (off < le64_to_cpu(tmp_data->file_offset)) {
range_start = le64_to_cpu(tmp_data->file_offset);
range_len = le64_to_cpu(tmp_data->length);
if (check_add_overflow(range_start, range_len, &range_end) ||
range_end > S64_MAX) {
rc = -EINVAL;
goto out;
}
if (off < range_start) {
/*
* We are at a hole. Write until the end of the region
* or until the next allocated data,
* whichever comes next.
*/
l = le64_to_cpu(tmp_data->file_offset) - off;
l = range_start - off;
if (len < l)
l = len;
rc = smb3_simple_fallocate_write_range(xid, tcon,
@@ -3640,11 +3696,13 @@ static int smb3_simple_fallocate_range(unsigned int xid,
* until the end of the data or the end of the region
* we are supposed to fallocate, whichever comes first.
*/
l = le64_to_cpu(tmp_data->length);
if (len < l)
l = len;
off += l;
len -= l;
if (off < range_end) {
l = range_end - off;
if (len < l)
l = len;
off += l;
len -= l;
}
tmp_data = &tmp_data[1];
out_data_len -= sizeof(struct file_allocated_range_buffer);
@@ -3652,7 +3710,7 @@ static int smb3_simple_fallocate_range(unsigned int xid,
out:
kfree(out_data);
kfree(buf);
kvfree(buf);
return rc;
}
@@ -3665,18 +3723,22 @@ static long smb3_simple_falloc(struct file *file, struct cifs_tcon *tcon,
struct cifsFileInfo *cfile = file->private_data;
long rc = -EOPNOTSUPP;
unsigned int xid;
loff_t new_eof;
loff_t old_eof, new_eof;
struct smb2_file_all_info file_inf;
u64 asize;
int qrc;
xid = get_xid();
inode = d_inode(cfile->dentry);
cifsi = CIFS_I(inode);
old_eof = i_size_read(inode);
trace_smb3_falloc_enter(xid, cfile->fid.persistent_fid, tcon->tid,
tcon->ses->Suid, off, len);
/* if file not oplocked can't be sure whether asking to extend size */
if (!CIFS_CACHE_READ(cifsi))
if (keep_size == false) {
if (!keep_size) {
trace_smb3_falloc_err(xid, cfile->fid.persistent_fid,
tcon->tid, tcon->ses->Suid, off, len, rc);
free_xid(xid);
@@ -3686,21 +3748,98 @@ static long smb3_simple_falloc(struct file *file, struct cifs_tcon *tcon,
/*
* Extending the file
*/
if ((keep_size == false) && i_size_read(inode) < off + len) {
if (!keep_size && old_eof < off + len) {
rc = inode_newsize_ok(inode, off + len);
if (rc)
goto out;
/*
* A small range at or beyond EOF can be allocated by writing
* zeroes. For off > old_eof, this preserves the intervening
* hole instead of allocating from offset 0.
*/
if (off > old_eof ||
(off == old_eof && old_eof != 0 &&
(cifsi->cifsAttrs & FILE_ATTRIBUTE_SPARSE_FILE))) {
if (len > 1024 * 1024) {
rc = -EOPNOTSUPP;
goto out;
}
rc = smb3_simple_fallocate_range(xid, tcon, cfile,
off, len);
if (rc) {
spin_lock(&inode->i_lock);
cifsi->time = 0;
spin_unlock(&inode->i_lock);
goto out;
}
new_eof = off + len;
netfs_resize_file(&cifsi->netfs, new_eof, true);
cifs_setsize(inode, new_eof);
qrc = SMB2_query_info(xid, tcon,
cfile->fid.persistent_fid,
cfile->fid.volatile_fid, &file_inf);
spin_lock(&inode->i_lock);
if (qrc == 0) {
asize = le64_to_cpu(file_inf.AllocationSize);
inode->i_blocks = CIFS_INO_BLOCKS(asize);
} else {
cifsi->time = 0;
}
spin_unlock(&inode->i_lock);
goto out;
}
if (cifsi->cifsAttrs & FILE_ATTRIBUTE_SPARSE_FILE)
smb2_set_sparse(xid, tcon, cfile, inode, false);
new_eof = off + len;
qrc = SMB2_query_info(xid, tcon,
cfile->fid.persistent_fid,
cfile->fid.volatile_fid, &file_inf);
if (qrc == 0)
asize = le64_to_cpu(file_inf.AllocationSize);
/*
* FILE_ALLOCATION_INFORMATION can only describe allocation up to
* new_eof. Some servers may accept it without allocating blocks,
* so refresh AllocationSize before updating i_blocks.
*/
if (off == 0 || off == old_eof) {
if (qrc || asize < new_eof) {
rc = SMB2_set_allocation(xid, tcon,
cfile->fid.persistent_fid,
cfile->fid.volatile_fid,
cfile->pid, new_eof);
if (rc)
goto out;
}
}
rc = SMB2_set_eof(xid, tcon, cfile->fid.persistent_fid,
cfile->fid.volatile_fid, cfile->pid, new_eof);
if (rc == 0) {
netfs_resize_file(&cifsi->netfs, new_eof, true);
cifs_setsize(inode, new_eof);
if (rc)
goto out;
netfs_resize_file(&cifsi->netfs, new_eof, true);
cifs_setsize(inode, new_eof);
qrc = SMB2_query_info(xid, tcon,
cfile->fid.persistent_fid,
cfile->fid.volatile_fid, &file_inf);
spin_lock(&inode->i_lock);
if (qrc == 0) {
asize = le64_to_cpu(file_inf.AllocationSize);
if (asize >= new_eof)
inode->i_blocks = CIFS_INO_BLOCKS(asize);
} else {
cifsi->time = 0;
}
spin_unlock(&inode->i_lock);
goto out;
}

View File

@@ -5949,6 +5949,25 @@ SMB2_set_eof(const unsigned int xid, struct cifs_tcon *tcon, u64 persistent_fid,
0, 1, &data, &size);
}
int
SMB2_set_allocation(const unsigned int xid, struct cifs_tcon *tcon,
u64 persistent_fid, u64 volatile_fid, u32 pid,
loff_t allocation_size)
{
struct smb2_file_alloc_info info;
void *data;
unsigned int size;
info.AllocationSize = cpu_to_le64(allocation_size);
data = &info;
size = sizeof(struct smb2_file_alloc_info);
return send_set_info(xid, tcon, persistent_fid, volatile_fid,
pid, FILE_ALLOCATION_INFORMATION, SMB2_O_INFO_FILE,
0, 1, &data, &size);
}
int
SMB2_set_acl(const unsigned int xid, struct cifs_tcon *tcon,
u64 persistent_fid, u64 volatile_fid,

View File

@@ -204,6 +204,9 @@ void SMB2_query_directory_free(struct smb_rqst *rqst);
int SMB2_set_eof(const unsigned int xid, struct cifs_tcon *tcon,
u64 persistent_fid, u64 volatile_fid, u32 pid,
loff_t new_eof);
int SMB2_set_allocation(const unsigned int xid, struct cifs_tcon *tcon,
u64 persistent_fid, u64 volatile_fid, u32 pid,
loff_t allocation_size);
int SMB2_set_info_init(struct cifs_tcon *tcon, struct TCP_Server_Info *server,
struct smb_rqst *rqst, u64 persistent_fid,
u64 volatile_fid, u32 pid, u8 info_class, u8 info_type,

View File

@@ -283,6 +283,11 @@ struct smb2_file_eof_info { /* encoding of request for level 10 */
__le64 EndOfFile; /* new end of file value */
} __packed; /* level 20 Set */
/* See MS-FSCC 2.4.4 */
struct smb2_file_alloc_info { /* encoding of request for level 19 */
__le64 AllocationSize;
} __packed;
/* See MS-FSCC 2.4.15 */
typedef struct {
__le32 NextEntryOffset;

View File

@@ -212,10 +212,6 @@ struct smb2_file_ea_info {
__le32 EASize;
} __packed;
struct smb2_file_alloc_info {
__le64 AllocationSize;
} __packed;
struct smb2_file_disposition_info {
__u8 DeletePending;
} __packed;