ksmbd: serialize oplock close with pending break ownership

close may abort an in-flight oplock break while another breaker already
holds an opinfo reference. Releasing pending_break wakes that waiter, but
without serializing the close transition with bit acquisition it can become
a new break owner through the test_and_set_bit() fast path. It can then
overwrite OPLOCK_CLOSING with OPLOCK_ACK_WAIT and continue a break for
a dying opinfo.

Make OPLOCK_CLOSING terminal once the opinfo is removed from the inode
list. Serialize that transition, pending_break acquisition, and
OPLOCK_ACK_WAIT setup with an opinfo state lock. A breaker which loses
the race releases its ownership and returns -ENOENT. Explicitly wake
pending_break waiters during close so they can observe the terminal state.

Also prevent ACK and timeout paths from replacing OPLOCK_CLOSING with
OPLOCK_STATE_NONE.

Fixes: e2f34481b2 ("cifsd: add server-side procedures for SMB3")
Co-developed-by: Yunseong Kim <yunseong.kim@est.tech>
Signed-off-by: Yunseong Kim <yunseong.kim@est.tech>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
This commit is contained in:
Namjae Jeon 2026-07-28 22:17:14 +09:00
parent f39ec312c3
commit b0148dc562
3 changed files with 81 additions and 18 deletions

View File

@ -90,6 +90,7 @@ static struct oplock_info *alloc_opinfo(struct ksmbd_work *work,
opinfo->conn = ksmbd_conn_get(work->conn);
opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
opinfo->op_state = OPLOCK_STATE_NONE;
spin_lock_init(&opinfo->state_lock);
opinfo->pending_break = 0;
opinfo->fid = id;
opinfo->Tid = Tid;
@ -546,14 +547,23 @@ void close_id_del_oplock(struct ksmbd_file *fp)
opinfo_del(opinfo);
rcu_assign_pointer(fp->f_opinfo, NULL);
if (opinfo->op_state == OPLOCK_ACK_WAIT) {
opinfo->op_state = OPLOCK_CLOSING;
wake_up_interruptible_all(&opinfo->oplock_q);
if (opinfo->is_lease) {
atomic_set(&opinfo->breaking_cnt, 0);
wake_up_interruptible_all(&opinfo->oplock_brk);
}
}
spin_lock(&opinfo->state_lock);
if (opinfo->op_state == OPLOCK_ACK_WAIT && opinfo->is_lease)
atomic_set(&opinfo->breaking_cnt, 0);
/*
* An opinfo that has been removed from the inode list is terminal. Keep
* this transition and releasing pending_break under state_lock. a breaker
* takes the same lock before it acquires pending_break or sets ACK_WAIT.
*/
opinfo->op_state = OPLOCK_CLOSING;
clear_bit_unlock(0, &opinfo->pending_break);
spin_unlock(&opinfo->state_lock);
wake_up_interruptible_all(&opinfo->oplock_q);
if (opinfo->is_lease)
wake_up_interruptible_all(&opinfo->oplock_brk);
/* memory barrier is needed for wake_up_bit() */
smp_mb__after_atomic();
wake_up_bit(&opinfo->pending_break, 0);
opinfo_count_dec(fp);
atomic_dec(&opinfo->refcount);
@ -735,12 +745,18 @@ static bool wait_for_break_ack(struct oplock_info *opinfo)
/* is this a timeout ? */
if (!rc) {
spin_lock(&opinfo->state_lock);
if (opinfo->op_state == OPLOCK_CLOSING) {
spin_unlock(&opinfo->state_lock);
return false;
}
if (opinfo->is_lease) {
opinfo->o_lease->state = SMB2_LEASE_NONE_LE;
lease_update_oplock_levels(opinfo->o_lease);
}
opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
opinfo->op_state = OPLOCK_STATE_NONE;
spin_unlock(&opinfo->state_lock);
return true;
}
@ -755,9 +771,35 @@ static void wake_up_oplock_break(struct oplock_info *opinfo)
wake_up_bit(&opinfo->pending_break, 0);
}
static bool oplock_break_set_ack_wait(struct oplock_info *opinfo)
{
bool ret = false;
spin_lock(&opinfo->state_lock);
if (opinfo->op_state != OPLOCK_CLOSING) {
opinfo->op_state = OPLOCK_ACK_WAIT;
ret = true;
}
spin_unlock(&opinfo->state_lock);
return ret;
}
static int oplock_break_pending(struct oplock_info *opinfo, int req_op_level)
{
while (test_and_set_bit(0, &opinfo->pending_break)) {
for (;;) {
bool closing;
spin_lock(&opinfo->state_lock);
closing = opinfo->op_state == OPLOCK_CLOSING;
if (!closing && !test_and_set_bit(0, &opinfo->pending_break)) {
spin_unlock(&opinfo->state_lock);
break;
}
spin_unlock(&opinfo->state_lock);
if (closing)
return -ENOENT;
if (opinfo->is_lease)
opinfo->o_lease->reuse_epoch = true;
@ -766,9 +808,12 @@ static int oplock_break_pending(struct oplock_info *opinfo, int req_op_level)
/* Not immediately break to none. */
opinfo->open_trunc = 0;
if (opinfo->op_state == OPLOCK_CLOSING)
spin_lock(&opinfo->state_lock);
closing = opinfo->op_state == OPLOCK_CLOSING;
spin_unlock(&opinfo->state_lock);
if (closing)
return -ENOENT;
else if (opinfo->level <= req_op_level) {
if (opinfo->level <= req_op_level) {
if (opinfo->is_lease == false)
return 1;
@ -1146,7 +1191,11 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level,
if (lease->state & (SMB2_LEASE_WRITE_CACHING_LE |
SMB2_LEASE_HANDLE_CACHING_LE)) {
brk_opinfo->op_state = OPLOCK_ACK_WAIT;
if (!oplock_break_set_ack_wait(brk_opinfo)) {
atomic_dec_if_positive(&brk_opinfo->breaking_cnt);
wake_up_oplock_break(brk_opinfo);
return -ENOENT;
}
} else
atomic_dec(&brk_opinfo->breaking_cnt);
@ -1201,8 +1250,12 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level,
return err < 0 ? err : 0;
if (brk_opinfo->level == SMB2_OPLOCK_LEVEL_BATCH ||
brk_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE)
brk_opinfo->op_state = OPLOCK_ACK_WAIT;
brk_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) {
if (!oplock_break_set_ack_wait(brk_opinfo)) {
wake_up_oplock_break(brk_opinfo);
return -ENOENT;
}
}
/*
* Keep a conflicting CREATE asynchronous while waiting for an
@ -1776,7 +1829,10 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work,
if (!brk_op->is_lease && !send_oplock_break) {
brk_op->level = SMB2_OPLOCK_LEVEL_NONE;
brk_op->op_state = OPLOCK_STATE_NONE;
spin_lock(&brk_op->state_lock);
if (brk_op->op_state != OPLOCK_CLOSING)
brk_op->op_state = OPLOCK_STATE_NONE;
spin_unlock(&brk_op->state_lock);
} else {
oplock_break(brk_op,
brk_op->is_lease && !is_trunc ?

View File

@ -66,6 +66,7 @@ struct oplock_info {
struct ksmbd_file *o_fp;
int level;
int op_state;
spinlock_t state_lock;
unsigned long pending_break;
u64 fid;
atomic_t breaking_cnt;

View File

@ -10824,7 +10824,10 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work)
smb2_set_err_rsp(work);
out:
opinfo->op_state = OPLOCK_STATE_NONE;
spin_lock(&opinfo->state_lock);
if (opinfo->op_state != OPLOCK_CLOSING)
opinfo->op_state = OPLOCK_STATE_NONE;
spin_unlock(&opinfo->state_lock);
wake_up_interruptible_all(&opinfo->oplock_q);
out_no_state_change:
opinfo_put(opinfo);
@ -10915,9 +10918,12 @@ static void smb21_lease_break_ack(struct ksmbd_work *work)
if (ret)
goto err_out;
opinfo->op_state = OPLOCK_STATE_NONE;
spin_lock(&opinfo->state_lock);
if (opinfo->op_state != OPLOCK_CLOSING)
opinfo->op_state = OPLOCK_STATE_NONE;
spin_unlock(&opinfo->state_lock);
wake_up_interruptible_all(&opinfo->oplock_q);
atomic_dec(&opinfo->breaking_cnt);
atomic_dec_if_positive(&opinfo->breaking_cnt);
wake_up_interruptible_all(&opinfo->oplock_brk);
opinfo_put(opinfo);
return;