- Fix a tree connection use-after-free in smb2_tree_connect() by

balancing references across concurrent connect, disconnect, and
    session logoff paths.
 
  - Validate source and target ranges in COPYCHUNK requests before range
    locking and copy operations.
 
  - Fix an oplock break notification UAF by acquiring a connection
    reference under ksmbd_inode lock and releasing it after the
    notification work completes.
 
  - Fix the sparc build by using an unsigned int for the atomic work
    state, ensuring xchg() uses a supported four-byte operation.
 -----BEGIN PGP SIGNATURE-----
 
 iQJKBAABCgA0FiEE6NzKS6Uv/XAAGHgyZwv7A1FEIQgFAmqan48WHGxpbmtpbmpl
 b25Aa2VybmVsLm9yZwAKCRBnC/sDUUQhCP41D/4gNSDDjbGg/Du5nNlNfd7x0/Ql
 ARgdajcnTT/2rUrBkXb3hfNi7BvSHz8jShb7acnwcs9VbxF7cWMk0r+tkjpsONI9
 hwUAXiqOQNkiUJZez+29WgiVuIqNjWSB9WKDGcA7J364Vnwm4M5a8y9wZHfV9ReQ
 aXmVlNlWP6zosrXv4Ex2Eb1bUaYnB822ZrsMQBdiZireUlVUyi/MeWOIrjxt7xPJ
 /nMTJcyDNIFjJJIQMZ/LjzIzvD82QO4LP3F8rlvHD2UIMQik6m0UXF3wPUZGPMbp
 X3o/sPoZHF1KrVpOG4SR5Lvy6KHLtGDSP7bVVhw4ahdtUnyeAeHx/xo9+OKbsiBO
 E4Sji35E8ZyIZ/xHMtOfSfA74W9ia0A0olWyG/mviptJ0RD8unddJP+D/L+EAD0I
 2pQyt9YfwsXm/7FoQXzfbtyi8Z2gl5Jp+xNr6DOyzkIulsBxFOBjgqrUEaTV2/Dc
 abqnVPLo4X9zqiE0HdSNS/go2STx9iox02blZ2wBmYNxEy4X22IheXbyN+wdtn8Q
 Ts8PrE1W0DO8PjWL9TG8okRYY3tRd0AcAVffi6P+QqkRsQNRCduv0mnAsSY1EuU0
 r/M2i5RpQ+JIBLQc4C53zOaAbBCyY1MSXUCgAs1BVyyUdqtjdPJbzyzNn6FDYVPA
 HrPxSl+fmtBsH0C25A==
 =2ZL4
 -----END PGP SIGNATURE-----

Merge tag 'ksmbd-for-7.3-rc2-part2' of git://git.kernel.org/pub/scm/linux/kernel/git/linkinjeon/smb

Pull smb server fixes from Namjae Jeon:

 - Fix a tree connection use-after-free in smb2_tree_connect() by
   balancing references across concurrent connect, disconnect, and
   session logoff paths.

 - Validate source and target ranges in COPYCHUNK requests before range
   locking and copy operations.

 - Fix an oplock break notification UAF by acquiring a connection
   reference under ksmbd_inode lock and releasing it after the
   notification work completes.

 - Fix the sparc build by using an unsigned int for the atomic work
   state, ensuring xchg() uses a supported four-byte operation.

* tag 'ksmbd-for-7.3-rc2-part2' of git://git.kernel.org/pub/scm/linux/kernel/git/linkinjeon/smb:
  ksmbd: fix tree connection use-after-free in smb2_tree_connect()
  ksmbd: validate COPYCHUNK source and target ranges
  ksmbd: fix use-after-free in oplock break notification
  ksmbd: fix sparc build with atomic work state
This commit is contained in:
Linus Torvalds 2026-09-04 08:42:14 -07:00
commit 58f93a4b73
5 changed files with 90 additions and 33 deletions

View File

@ -82,7 +82,7 @@ struct ksmbd_work {
/* Contiguous SMB2 compression transform owned by this work item. */
void *compress_buf;
unsigned char state;
unsigned int state;
/* No response for cancelled request */
bool send_no_response:1;
/* Request is encrypted */

View File

@ -82,6 +82,8 @@ ksmbd_tree_conn_connect(struct ksmbd_work *work, const char *share_name)
down_write(&sess->tree_conns_lock);
ret = xa_err(xa_store(&sess->tree_conns, tree_conn->id, tree_conn,
KSMBD_DEFAULT_GFP));
if (!ret)
atomic_inc(&tree_conn->refcount);
up_write(&sess->tree_conns_lock);
if (ret) {
status.ret = -ENOMEM;
@ -129,6 +131,12 @@ int ksmbd_tree_conn_disconnect(struct ksmbd_session *sess,
struct ksmbd_tree_connect *tree_conn)
{
down_write(&sess->tree_conns_lock);
if (tree_conn->t_state == TREE_DISCONNECTED ||
xa_load(&sess->tree_conns, tree_conn->id) != tree_conn) {
up_write(&sess->tree_conns_lock);
return -ENOENT;
}
tree_conn->t_state = TREE_DISCONNECTED;
xa_erase(&sess->tree_conns, tree_conn->id);
up_write(&sess->tree_conns_lock);

View File

@ -924,31 +924,69 @@ static void __smb2_oplock_break_noti(struct work_struct *wk)
ksmbd_conn_put(conn);
}
/*
* Select and pin the connection used for an oplock break before doing any
* allocations which may sleep. The caller of oplock_break() holds a live
* reference on ci (a file being opened, a file being operated on, or an
* explicit ksmbd_inode_lookup_lock() reference in the parent lease break
* paths), so the inode cannot be freed during the call and its lock is
* reachable without dereferencing opinfo->o_fp, which is not pinned by
* the oplock reference and may be freed by a concurrent close.
*
* opinfo->conn is cleared under ci->m_lock by session_fd_check() when the
* durable handle owning the oplock is disconnected, reassigned by
* ksmbd_reopen_durable_fd() under the same lock, and the last
* ksmbd_conn_put() of the old connection frees it. Holding the read lock
* excludes both writers, so the connection cannot be freed while it is
* selected.
*/
static struct ksmbd_conn *smb2_oplock_break_conn_get(struct oplock_info *opinfo,
struct ksmbd_inode *ci)
{
struct ksmbd_conn *conn;
down_read(&ci->m_lock);
conn = READ_ONCE(opinfo->conn);
if (conn && !ksmbd_conn_releasing(conn))
conn = ksmbd_conn_get(conn);
else
conn = NULL;
up_read(&ci->m_lock);
return conn;
}
/**
* smb2_oplock_break_noti() - send smb2 exclusive/batch to level2 oplock
* break command from server to client
* @opinfo: oplock info object
* @ci: inode owning the break target's oplock list, pinned by
* the caller
*
* Return: 0 on success, otherwise error
*/
static int smb2_oplock_break_noti(struct oplock_info *opinfo)
static int smb2_oplock_break_noti(struct oplock_info *opinfo,
struct ksmbd_inode *ci)
{
struct ksmbd_conn *conn;
struct oplock_break_info *br_info;
int ret = 0;
struct ksmbd_work *work;
conn = READ_ONCE(opinfo->conn);
conn = smb2_oplock_break_conn_get(opinfo, ci);
if (!conn)
return ksmbd_invalidate_durable_fd(opinfo->fid);
work = ksmbd_alloc_work_struct();
if (!work)
if (!work) {
ksmbd_conn_put(conn);
return -ENOMEM;
}
br_info = kmalloc_obj(struct oplock_break_info, KSMBD_DEFAULT_GFP);
if (!br_info) {
ksmbd_free_work_struct(work);
ksmbd_conn_put(conn);
return -ENOMEM;
}
@ -957,7 +995,8 @@ static int smb2_oplock_break_noti(struct oplock_info *opinfo)
br_info->open_trunc = opinfo->open_trunc;
work->request_buf = (char *)br_info;
work->conn = ksmbd_conn_get(conn);
/* Transfer the reference acquired by smb2_oplock_break_conn_get(). */
work->conn = conn;
work->sess = opinfo->sess;
ksmbd_conn_r_count_inc(conn);
@ -1154,9 +1193,9 @@ static void wait_lease_breaking(struct oplock_info *opinfo)
}
}
static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level,
struct ksmbd_work *in_work, bool share_break,
bool sync_lease_break)
static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
int req_op_level, struct ksmbd_work *in_work,
bool share_break, bool sync_lease_break)
{
int err = 0;
bool sent_interim = false;
@ -1298,7 +1337,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level,
}
}
err = smb2_oplock_break_noti(brk_opinfo);
err = smb2_oplock_break_noti(brk_opinfo, ci);
ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level);
if (brk_opinfo->op_state == OPLOCK_CLOSING)
@ -1326,13 +1365,14 @@ static int oplock_break_add(struct list_head *head, struct oplock_info *opinfo)
return 0;
}
static void oplock_break_drain_none(struct list_head *head)
static void oplock_break_drain_none(struct list_head *head,
struct ksmbd_inode *ci)
{
struct oplock_break_entry *ent, *tmp;
list_for_each_entry_safe(ent, tmp, head, list) {
oplock_break(ent->opinfo, SMB2_OPLOCK_LEVEL_NONE, NULL, false,
false);
oplock_break(ent->opinfo, ci, SMB2_OPLOCK_LEVEL_NONE, NULL,
false, false);
list_del(&ent->list);
opinfo_put(ent->opinfo);
kfree(ent);
@ -1481,7 +1521,7 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp,
}
up_read(&p_ci->m_lock);
oplock_break_drain_none(&brk_list);
oplock_break_drain_none(&brk_list, p_ci);
ksmbd_inode_put(p_ci);
}
@ -1525,7 +1565,7 @@ void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp)
}
up_read(&p_ci->m_lock);
oplock_break_drain_none(&brk_list);
oplock_break_drain_none(&brk_list, p_ci);
ksmbd_inode_put(p_ci);
}
@ -1665,7 +1705,7 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid,
prev_durable_detached = prev_op_snapshot.durable_detached;
prev_fid = prev_op_snapshot.fid;
err = oplock_break(prev_opinfo, break_level, work,
err = oplock_break(prev_opinfo, ci, break_level, work,
share_ret < 0 && prev_opinfo->is_lease, false);
if (prev_durable_detached || (prev_durable_open && err == -ENOENT))
ksmbd_invalidate_durable_fd(prev_fid);
@ -1771,7 +1811,8 @@ static bool smb_break_all_write_oplock(struct ksmbd_work *work,
}
brk_opinfo->open_trunc = is_trunc;
oplock_break(brk_opinfo, SMB2_OPLOCK_LEVEL_II, work, false, false);
oplock_break(brk_opinfo, fp->f_ci, SMB2_OPLOCK_LEVEL_II, work, false,
false);
sent_break = true;
opinfo_put(brk_opinfo);
@ -1863,7 +1904,7 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work,
brk_op->op_state = OPLOCK_STATE_NONE;
spin_unlock(&brk_op->state_lock);
} else {
oplock_break(brk_op,
oplock_break(brk_op, ci,
brk_op->is_lease && !is_trunc ?
SMB2_OPLOCK_LEVEL_II : SMB2_OPLOCK_LEVEL_NONE,
send_interim && !sent_interim ? work : NULL,

View File

@ -2790,6 +2790,7 @@ int smb2_tree_connect(struct ksmbd_work *work)
struct ksmbd_session *sess = work->sess;
char *treename = NULL, *name = NULL;
struct ksmbd_tree_conn_status status;
struct ksmbd_tree_connect *tree_conn = NULL;
struct ksmbd_share_config *share = NULL;
int rc = -EINVAL;
@ -2817,6 +2818,7 @@ int smb2_tree_connect(struct ksmbd_work *work)
status = ksmbd_tree_conn_connect(work, name);
if (status.ret == KSMBD_TREE_CONN_STATUS_OK) {
tree_conn = status.tree_conn;
rsp->hdr.Id.SyncId.TreeId = cpu_to_le32(status.tree_conn->id);
share = status.tree_conn->share_conf;
@ -2860,8 +2862,15 @@ int smb2_tree_connect(struct ksmbd_work *work)
status.tree_conn->posix_extensions = true;
down_write(&sess->tree_conns_lock);
status.tree_conn->t_state = TREE_CONNECTED;
if (status.tree_conn->t_state == TREE_DISCONNECTED) {
status.ret = KSMBD_TREE_CONN_STATUS_ERROR;
share = NULL;
} else {
status.tree_conn->t_state = TREE_CONNECTED;
}
up_write(&sess->tree_conns_lock);
if (status.ret != KSMBD_TREE_CONN_STATUS_OK)
goto out_err1;
rsp->StructureSize = cpu_to_le16(16);
out_err1:
/*
@ -2888,9 +2897,6 @@ int smb2_tree_connect(struct ksmbd_work *work)
rc = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_tree_connect_rsp));
if (rc) {
if (status.ret == KSMBD_TREE_CONN_STATUS_OK) {
down_write(&sess->tree_conns_lock);
status.tree_conn->t_state = TREE_DISCONNECTED;
up_write(&sess->tree_conns_lock);
ksmbd_tree_conn_disconnect(sess, status.tree_conn);
status.tree_conn = NULL;
}
@ -2931,6 +2937,9 @@ int smb2_tree_connect(struct ksmbd_work *work)
if (status.ret != KSMBD_TREE_CONN_STATUS_OK)
smb2_set_err_rsp(work);
if (tree_conn)
ksmbd_tree_connect_put(tree_conn);
return rc;
}
@ -3034,17 +3043,6 @@ int smb2_tree_disconnect(struct ksmbd_work *work)
ksmbd_close_tree_conn_fds(work);
down_write(&sess->tree_conns_lock);
if (tcon->t_state == TREE_DISCONNECTED) {
up_write(&sess->tree_conns_lock);
rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED;
err = -ENOENT;
goto err_out;
}
tcon->t_state = TREE_DISCONNECTED;
up_write(&sess->tree_conns_lock);
err = ksmbd_tree_conn_disconnect(sess, tcon);
if (err) {
rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED;

View File

@ -2007,6 +2007,11 @@ static ssize_t ksmbd_vfs_copy_file_range_buffered(struct ksmbd_work *work,
return ret;
}
static bool ksmbd_vfs_copy_range_valid(loff_t offset, size_t len)
{
return offset >= 0 && (loff_t)len <= MAX_LFS_FILESIZE - offset;
}
int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work,
struct ksmbd_file *src_fp,
struct ksmbd_file *dst_fp,
@ -2042,6 +2047,10 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work,
dst_off = le64_to_cpu(chunks[i].TargetOffset);
len = le32_to_cpu(chunks[i].Length);
if (!ksmbd_vfs_copy_range_valid(src_off, len) ||
!ksmbd_vfs_copy_range_valid(dst_off, len))
return -E2BIG;
if (check_lock_range(src_fp->filp, src_off,
src_off + len - 1, READ))
return -EAGAIN;
@ -2134,7 +2143,8 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work,
len = le32_to_cpu(chunks[i].Length);
copy_len = len;
if (src_off < 0)
if (!ksmbd_vfs_copy_range_valid(src_off, len) ||
!ksmbd_vfs_copy_range_valid(dst_off, len))
return -E2BIG;
if (src_off > src_file_size || len > src_file_size - src_off) {