ksmbd: align SMB2 oplock break ack handling

Handle SMB2 oplock break acknowledgments according to the server-side
validation rules in MS-SMB2.

Return STATUS_INVALID_DEVICE_STATE when an ACK arrives while the open is
not breaking, reject SMB2_OPLOCK_LEVEL_LEASE with
STATUS_INVALID_PARAMETER, allow BATCH acknowledgments to EXCLUSIVE, and
make invalid ACK levels fail with STATUS_INVALID_OPLOCK_PROTOCOL after
lowering the oplock to NONE.

Update the successful response from the final granted oplock level instead
of relying on the oplock transition helpers, which could turn invalid ACKs
into successful responses.

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-18 10:35:42 +09:00 committed by Steve French
parent 079927f5fd
commit 80a56d4a82

View File

@ -8900,11 +8900,10 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work)
struct smb2_oplock_break *rsp;
struct ksmbd_file *fp;
struct oplock_info *opinfo = NULL;
__le32 err = 0;
int ret = 0;
__le32 status = STATUS_SUCCESS;
int ret;
u64 volatile_id, persistent_id;
char req_oplevel = 0, rsp_oplevel = 0;
unsigned int oplock_change_type;
WORK_BUFFERS(work, req, rsp);
@ -8930,70 +8929,54 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work)
return;
}
if (opinfo->op_state != OPLOCK_ACK_WAIT) {
ksmbd_debug(SMB, "unexpected oplock state 0x%x\n",
opinfo->op_state);
status = STATUS_INVALID_DEVICE_STATE;
goto err_out;
}
if (req_oplevel == SMB2_OPLOCK_LEVEL_LEASE) {
opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
status = STATUS_INVALID_PARAMETER;
goto err_out;
}
if (opinfo->level == SMB2_OPLOCK_LEVEL_NONE) {
rsp->hdr.Status = STATUS_INVALID_OPLOCK_PROTOCOL;
status = STATUS_INVALID_OPLOCK_PROTOCOL;
goto err_out;
}
if (opinfo->op_state == OPLOCK_STATE_NONE) {
ksmbd_debug(SMB, "unexpected oplock state 0x%x\n", opinfo->op_state);
rsp->hdr.Status = STATUS_UNSUCCESSFUL;
if (opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE &&
req_oplevel != SMB2_OPLOCK_LEVEL_II &&
req_oplevel != SMB2_OPLOCK_LEVEL_NONE) {
opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
status = STATUS_INVALID_OPLOCK_PROTOCOL;
goto err_out;
}
if ((opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE ||
opinfo->level == SMB2_OPLOCK_LEVEL_BATCH) &&
(req_oplevel != SMB2_OPLOCK_LEVEL_II &&
req_oplevel != SMB2_OPLOCK_LEVEL_NONE)) {
err = STATUS_INVALID_OPLOCK_PROTOCOL;
oplock_change_type = OPLOCK_WRITE_TO_NONE;
} else if (opinfo->level == SMB2_OPLOCK_LEVEL_II &&
req_oplevel != SMB2_OPLOCK_LEVEL_NONE) {
err = STATUS_INVALID_OPLOCK_PROTOCOL;
oplock_change_type = OPLOCK_READ_TO_NONE;
} else if (req_oplevel == SMB2_OPLOCK_LEVEL_II ||
req_oplevel == SMB2_OPLOCK_LEVEL_NONE) {
err = STATUS_INVALID_DEVICE_STATE;
if ((opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE ||
opinfo->level == SMB2_OPLOCK_LEVEL_BATCH) &&
req_oplevel == SMB2_OPLOCK_LEVEL_II) {
oplock_change_type = OPLOCK_WRITE_TO_READ;
} else if ((opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE ||
opinfo->level == SMB2_OPLOCK_LEVEL_BATCH) &&
req_oplevel == SMB2_OPLOCK_LEVEL_NONE) {
oplock_change_type = OPLOCK_WRITE_TO_NONE;
} else if (opinfo->level == SMB2_OPLOCK_LEVEL_II &&
req_oplevel == SMB2_OPLOCK_LEVEL_NONE) {
oplock_change_type = OPLOCK_READ_TO_NONE;
} else {
oplock_change_type = 0;
}
} else {
oplock_change_type = 0;
if (opinfo->level == SMB2_OPLOCK_LEVEL_BATCH &&
req_oplevel != SMB2_OPLOCK_LEVEL_II &&
req_oplevel != SMB2_OPLOCK_LEVEL_NONE &&
req_oplevel != SMB2_OPLOCK_LEVEL_EXCLUSIVE) {
opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
status = STATUS_INVALID_OPLOCK_PROTOCOL;
goto err_out;
}
switch (oplock_change_type) {
case OPLOCK_WRITE_TO_READ:
ret = opinfo_write_to_read(opinfo);
rsp_oplevel = SMB2_OPLOCK_LEVEL_II;
break;
case OPLOCK_WRITE_TO_NONE:
ret = opinfo_write_to_none(opinfo);
if (opinfo->level == SMB2_OPLOCK_LEVEL_II &&
req_oplevel != SMB2_OPLOCK_LEVEL_NONE) {
opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
status = STATUS_INVALID_OPLOCK_PROTOCOL;
goto err_out;
}
if (req_oplevel == SMB2_OPLOCK_LEVEL_EXCLUSIVE)
rsp_oplevel = SMB2_OPLOCK_LEVEL_NONE;
break;
case OPLOCK_READ_TO_NONE:
ret = opinfo_read_to_none(opinfo);
rsp_oplevel = SMB2_OPLOCK_LEVEL_NONE;
break;
default:
pr_err("unknown oplock change 0x%x -> 0x%x\n",
opinfo->level, rsp_oplevel);
}
else
rsp_oplevel = req_oplevel;
if (ret < 0) {
rsp->hdr.Status = err;
goto err_out;
}
opinfo->level = rsp_oplevel;
rsp->StructureSize = cpu_to_le16(24);
rsp->OplockLevel = rsp_oplevel;
@ -9002,11 +8985,16 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work)
rsp->VolatileFid = volatile_id;
rsp->PersistentFid = persistent_id;
ret = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_oplock_break));
if (ret) {
err_out:
smb2_set_err_rsp(work);
}
if (ret)
ksmbd_debug(SMB, "failed to pin oplock break response: %d\n",
ret);
goto out;
err_out:
rsp->hdr.Status = status;
smb2_set_err_rsp(work);
out:
opinfo->op_state = OPLOCK_STATE_NONE;
wake_up_interruptible_all(&opinfo->oplock_q);
opinfo_put(opinfo);