ksmbd: implement the command sequence window

ksmbd tracked only credit counts (total_credits/outstanding_credits) and
never validated the MessageId of an incoming request. As a result a
request carrying a MessageId outside the granted range was accepted, a
MessageId could be replayed, and a 64-bit sequence wrap was not detected.

Maintain a command sequence window per connection:

  - [seq_low, seq_high) is the range of granted sequence numbers and
    seq_bitmap records which of them have been granted but not yet
    consumed. The window starts as { 0 } at connection setup.
  - smb2_set_rsp_credits() extends seq_high by the number of credits it
    grants (setting the corresponding bits), capped so the window never
    spans more than KSMBD_CMD_SEQ_WINDOW (== SMB2_MAX_CREDITS) sequence
    numbers. This implements the "limit the range of acceptable
    sequence numbers" allowance and keeps seq_bitmap usable as a ring.
  - smb2_check_sequence_number(), run for every SMB2 request from
    ksmbd_smb2_check_message(), verifies that the CreditCharge
    consecutive sequence numbers starting at MessageId lie within the
    window and have not already been consumed, then removes them and
    slides seq_low forward. CANCEL consumes nothing. A violation
    (out of window, replay, or wrap) tears the connection down.

The legacy SMB1 multi-protocol negotiate occupies sequence number 0 but
does not pass through ksmbd_smb2_check_message(), so it consumes that
sequence number explicitly; otherwise seq_low would stay pinned at 0
after the upgrade to SMB2 and eventually stall credit grants.

For an in-order client seq_high - seq_low equals total_credits, so the
window-room cap never reduces the number of credits granted. it only
engages for a client that withholds low sequence numbers.
init_smb2_max_credits() now clamps the configured maximum to
SMB2_MAX_CREDITS so the window (and its bitmap) can always represent
every outstanding sequence number.

Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
This commit is contained in:
Namjae Jeon 2026-07-13 21:55:58 +09:00
parent 7b46161088
commit fd8c97d7c1
6 changed files with 146 additions and 1 deletions

View File

@ -291,6 +291,14 @@ struct ksmbd_conn *ksmbd_conn_alloc(void)
conn->total_credits = 1;
conn->outstanding_credits = 0;
/*
* The command sequence window starts as the set { 0 } when the
* connection is established.
*/
conn->seq_low = 0;
conn->seq_high = 1;
__set_bit(0, conn->seq_bitmap);
init_waitqueue_head(&conn->req_running_q);
init_waitqueue_head(&conn->r_count_q);
INIT_LIST_HEAD(&conn->requests);

View File

@ -17,6 +17,7 @@
#include <linux/nls.h>
#include <linux/unicode.h>
#include <linux/workqueue.h>
#include <linux/bitmap.h>
#include "smb_common.h"
#include "ksmbd_work.h"
@ -25,6 +26,15 @@ struct smbdirect_buffer_descriptor_v1;
#define KSMBD_SOCKET_BACKLOG 16
/*
* Size of the per-connection SMB2 command sequence window. This mirrors
* SMB2_MAX_CREDITS, the maximum number of credits (and therefore the
* maximum number of outstanding sequence numbers) that can be granted on
* a connection. It must be a power of two so the window can be indexed as
* a ring.
*/
#define KSMBD_CMD_SEQ_WINDOW 8192
enum {
KSMBD_SESS_NEW = 0,
KSMBD_SESS_GOOD,
@ -74,6 +84,16 @@ struct ksmbd_conn {
unsigned int total_credits;
unsigned int outstanding_credits;
spinlock_t credits_lock;
/*
* Connection command sequence window. [seq_low, seq_high) is the
* range of granted sequence numbers (message IDs). seq_bitmap marks
* the ones in that range that have been granted but
* not yet consumed by a received request. All three are protected by
* credits_lock.
*/
u64 seq_low;
u64 seq_high;
DECLARE_BITMAP(seq_bitmap, KSMBD_CMD_SEQ_WINDOW);
wait_queue_head_t req_running_q;
wait_queue_head_t r_count_q;
/* Lock to protect requests list*/

View File

@ -372,6 +372,75 @@ static int smb2_validate_credit_charge(struct ksmbd_work *work,
return ret;
}
/*
* Verify that the sequence number(s) consumed by an incoming request fall
* within the connection's command sequence window and are not a replay, then
* remove them from the window. Returns 0 if the request
* may proceed, or 1 if it is invalid and the connection must be torn down.
*/
static int smb2_check_sequence_number(struct ksmbd_work *work,
struct smb2_hdr *hdr)
{
struct ksmbd_conn *conn = work->conn;
u64 mid = le64_to_cpu(hdr->MessageId);
unsigned short charge;
u64 i;
int ret = 0;
/* An SMB2 CANCEL consumes no sequence number. */
if (hdr->Command == SMB2_CANCEL)
return 0;
/*
* A multi-credit request consumes CreditCharge consecutive sequence
* numbers; every other request consumes exactly one.
*/
charge = le16_to_cpu(hdr->CreditCharge);
if (!(conn->vals->req_capabilities & SMB2_GLOBAL_CAP_LARGE_MTU) ||
charge == 0)
charge = 1;
/* The 64-bit sequence number space must not wrap. */
if (mid + charge < mid) {
pr_err("SMB2 sequence number wrapped (mid %llu charge %u)\n",
mid, charge);
return 1;
}
spin_lock(&conn->credits_lock);
/* The whole range must lie within the granted window... */
if (mid < conn->seq_low || mid + charge > conn->seq_high) {
ksmbd_debug(SMB,
"MessageId %llu (charge %u) outside command window [%llu, %llu)\n",
mid, charge, conn->seq_low, conn->seq_high);
ret = 1;
goto out;
}
/* ...and none of it may have been consumed already (replay). */
for (i = mid; i < mid + charge; i++) {
if (!test_bit(i & (KSMBD_CMD_SEQ_WINDOW - 1), conn->seq_bitmap)) {
ksmbd_debug(SMB,
"replayed sequence number %llu (mid %llu charge %u)\n",
i, mid, charge);
ret = 1;
goto out;
}
}
/* Consume the sequence numbers and slide the low edge forward. */
for (i = mid; i < mid + charge; i++)
__clear_bit(i & (KSMBD_CMD_SEQ_WINDOW - 1), conn->seq_bitmap);
while (conn->seq_low < conn->seq_high &&
!test_bit(conn->seq_low & (KSMBD_CMD_SEQ_WINDOW - 1),
conn->seq_bitmap))
conn->seq_low++;
out:
spin_unlock(&conn->credits_lock);
return ret;
}
int ksmbd_smb2_check_message(struct ksmbd_work *work)
{
struct smb2_pdu *pdu = ksmbd_req_buf_next(work);
@ -476,6 +545,16 @@ int ksmbd_smb2_check_message(struct ksmbd_work *work)
smb2_validate_credit_charge(work, hdr))
return 1;
/*
* A sequence number violation (out of window or a replay) is a
* protocol error. tear the connection down rather than
* keep accepting requests on it.
*/
if (smb2_check_sequence_number(work, hdr)) {
ksmbd_conn_set_exiting(work->conn);
return 1;
}
return 0;
}

View File

@ -331,6 +331,13 @@ void init_smb2_max_trans_size(unsigned int sz)
void init_smb2_max_credits(unsigned int sz)
{
/*
* The command sequence window (and its backing bitmap) can track at
* most SMB2_MAX_CREDITS outstanding sequence numbers, so the number of
* credits granted on a connection must not exceed that.
*/
if (sz > SMB2_MAX_CREDITS)
sz = SMB2_MAX_CREDITS;
smb21_server_values.max_credits = sz;
smb30_server_values.max_credits = sz;
smb302_server_values.max_credits = sz;

View File

@ -388,6 +388,7 @@ int smb2_set_rsp_credits(struct ksmbd_work *work)
struct ksmbd_conn *conn = work->conn;
unsigned short credits_requested, aux_max;
unsigned short credit_charge, credits_granted = 0;
u64 window_room, i;
if (work->send_no_response)
return 0;
@ -424,11 +425,26 @@ int smb2_set_rsp_credits(struct ksmbd_work *work)
aux_max = 1;
else
aux_max = conn->vals->max_credits - conn->total_credits;
/*
* The command sequence window must not grow beyond
* KSMBD_CMD_SEQ_WINDOW sequence numbers ahead of the oldest one still
* outstanding. Cap the grant by the room left in the window so that
* credits are withheld until the client consumes the low end (and so
* that seq_bitmap stays usable as a ring).
*/
window_room = conn->seq_low + KSMBD_CMD_SEQ_WINDOW - conn->seq_high;
aux_max = min_t(unsigned short, aux_max, window_room);
credits_granted = min_t(unsigned short, credits_requested, aux_max);
conn->total_credits += credits_granted;
work->credits_granted += credits_granted;
/* Extend the sequence window to cover the newly granted credits. */
for (i = conn->seq_high; i < conn->seq_high + credits_granted; i++)
__set_bit(i & (KSMBD_CMD_SEQ_WINDOW - 1), conn->seq_bitmap);
conn->seq_high += credits_granted;
if (!req_hdr->NextCommand) {
/* Update CreditRequest in last request */
hdr->CreditRequest = cpu_to_le16(work->credits_granted);

View File

@ -164,7 +164,22 @@ int ksmbd_verify_smb_message(struct ksmbd_work *work)
hdr = smb_get_msg(work->request_buf);
if (*(__le32 *)hdr->Protocol == SMB1_PROTO_NUMBER &&
hdr->Command == SMB_COM_NEGOTIATE) {
work->conn->outstanding_credits++;
struct ksmbd_conn *conn = work->conn;
conn->outstanding_credits++;
/*
* A legacy SMB1 multi-protocol negotiate occupies sequence
* number 0 but does not pass through
* ksmbd_smb2_check_message(). Consume it here so that, after
* the connection is upgraded to SMB2, the command sequence
* window can advance instead of staying pinned at 0.
*/
spin_lock(&conn->credits_lock);
if (conn->seq_low == 0) {
__clear_bit(0, conn->seq_bitmap);
conn->seq_low = 1;
}
spin_unlock(&conn->credits_lock);
return 0;
}