ksmbd: snapshot previous oplock state before durable checks

smb_grant_oplock() checks the previous oplock holder's o_fp to decide
whether a durable handle should be invalidated when the oplock break
cannot be delivered. prev_opinfo is obtained with opinfo_get_list(),
which pins only the oplock_info. It does not pin the ksmbd_file stored
in opinfo->o_fp.

A concurrent last close can unlink the opinfo from ci->m_op_list under
ci->m_lock and then free the ksmbd_file. The oplock_info can still be
kept alive by the refcount taken by opinfo_get_list(), but o_fp may
already point at freed memory by the time smb_grant_oplock() reads
is_durable, conn, or tcon.

Snapshot the previous holder's durable state while ci->m_lock is held,
then use only the copied values after dropping the lock. This keeps the
o_fp lifetime tied to the inode lock without taking an extra ksmbd_file
reference. Taking such a reference is unsafe here because smb_grant_oplock()
does not necessarily have the previous holder's session work, and dropping
the temporary reference can otherwise become the final putter.

Fixes: 26fa88dc87 ("ksmbd: invalidate durable handles on oplock break")
Reported-by: Gil Portnoy <dddhkts1@gmail.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
Signed-off-by: Steve French <stfrench@microsoft.com>
This commit is contained in:
Namjae Jeon 2026-06-28 09:30:00 +09:00 committed by Steve French
parent c706195e5e
commit 5138c84dbb

View File

@ -278,10 +278,24 @@ struct oplock_info *opinfo_get(struct ksmbd_file *fp)
return opinfo;
}
static struct oplock_info *opinfo_get_list(struct ksmbd_inode *ci)
struct oplock_snapshot {
bool durable_open;
bool durable_detached;
unsigned long long fid;
};
static struct oplock_info *opinfo_get_list(struct ksmbd_inode *ci,
struct ksmbd_file *skip_fp,
struct oplock_snapshot *snapshot)
{
struct oplock_info *opinfo;
if (snapshot) {
snapshot->durable_open = false;
snapshot->durable_detached = false;
snapshot->fid = KSMBD_NO_FID;
}
down_read(&ci->m_lock);
opinfo = list_first_entry_or_null(&ci->m_op_list, struct oplock_info,
op_entry);
@ -295,6 +309,16 @@ static struct oplock_info *opinfo_get_list(struct ksmbd_inode *ci)
opinfo = NULL;
}
}
if (opinfo && snapshot && opinfo->o_fp &&
opinfo->o_fp != skip_fp &&
READ_ONCE(opinfo->o_fp->is_durable)) {
snapshot->durable_open = true;
snapshot->durable_detached =
!READ_ONCE(opinfo->o_fp->conn) ||
!READ_ONCE(opinfo->o_fp->tcon);
snapshot->fid = opinfo->fid;
}
}
up_read(&ci->m_lock);
@ -314,7 +338,7 @@ void opinfo_put(struct oplock_info *opinfo)
static bool ksmbd_inode_has_lease(struct ksmbd_inode *ci)
{
struct oplock_info *opinfo = opinfo_get_list(ci);
struct oplock_info *opinfo = opinfo_get_list(ci, NULL, NULL);
bool is_lease;
if (!opinfo)
@ -1421,6 +1445,7 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid,
struct oplock_info *opinfo = NULL, *prev_opinfo = NULL;
struct ksmbd_inode *ci = fp->f_ci;
struct lease_table *new_lb = NULL;
struct oplock_snapshot prev_op_snapshot;
bool prev_op_has_lease;
bool prev_durable_open = false;
bool prev_durable_detached = false;
@ -1488,7 +1513,7 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid,
goto out;
}
}
prev_opinfo = opinfo_get_list(ci);
prev_opinfo = opinfo_get_list(ci, fp, &prev_op_snapshot);
if (!prev_opinfo ||
(prev_opinfo->level == SMB2_OPLOCK_LEVEL_NONE && lctx)) {
opinfo_put(prev_opinfo);
@ -1510,13 +1535,9 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid,
goto op_break_not_needed;
}
if (prev_opinfo->o_fp && prev_opinfo->o_fp != fp &&
prev_opinfo->o_fp->is_durable) {
prev_durable_open = true;
prev_durable_detached = !prev_opinfo->o_fp->conn ||
!prev_opinfo->o_fp->tcon;
prev_fid = prev_opinfo->fid;
}
prev_durable_open = prev_op_snapshot.durable_open;
prev_durable_detached = prev_op_snapshot.durable_detached;
prev_fid = prev_op_snapshot.fid;
err = oplock_break(prev_opinfo, break_level, work,
share_ret < 0 && prev_opinfo->is_lease);
@ -1607,7 +1628,7 @@ static bool smb_break_all_write_oplock(struct ksmbd_work *work,
struct oplock_info *brk_opinfo;
bool sent_break = false;
brk_opinfo = opinfo_get_list(fp->f_ci);
brk_opinfo = opinfo_get_list(fp->f_ci, NULL, NULL);
if (!brk_opinfo)
return false;
if (brk_opinfo->level != SMB2_OPLOCK_LEVEL_BATCH &&