* [PATCH 01/22] ksmbd: release connection lock while draining old session
@ 2026-10-17 1:03 Namjae Jeon
2026-10-17 1:03 ` [PATCH 02/22] ksmbd: serialize oplock break ACK state updates Namjae Jeon
` (20 more replies)
0 siblings, 21 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon, Pan Guihua
Previous-session teardown waits for requests on the old channels. Holding
srv_mutex during that wait can block their responses and prevent the drain
from completing.
Record the previous session ID during authentication, finish using the
request's authentication state, and release srv_mutex while the old
session drains. Keep the response binding decision in the work item because
signing and preauthentication hashing run after the drain.
Reported-by: Pan Guihua <pan.guihua@datatom.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/ksmbd_work.h | 2 ++
fs/smb/server/mgmt/user_session.c | 15 +++++++++
fs/smb/server/smb2pdu.c | 56 ++++++++++++++++++-------------
3 files changed, 49 insertions(+), 24 deletions(-)
diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h
index 3a14e4d69aa1..40f70a5d08b5 100644
--- a/fs/smb/server/ksmbd_work.h
+++ b/fs/smb/server/ksmbd_work.h
@@ -94,6 +94,8 @@ struct ksmbd_work {
bool need_invalidate_rkey:1;
bool request_open_chseq_tracked:1;
bool session_setup_reauth:1;
+ /* Retain the binding decision for response signing and preauth hashing. */
+ bool session_setup_binding:1;
unsigned int remote_key;
/* cancel works */
diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c
index 1321cd6c84e7..d814a99ff519 100644
--- a/fs/smb/server/mgmt/user_session.c
+++ b/fs/smb/server/mgmt/user_session.c
@@ -759,6 +759,7 @@ void destroy_previous_session(struct ksmbd_conn *conn,
{
struct ksmbd_session *prev_sess;
struct ksmbd_user *prev_user;
+ bool pinned = false;
int err;
down_write(&sessions_table_lock);
@@ -781,9 +782,21 @@ void destroy_previous_session(struct ksmbd_conn *conn,
}
prev_sess->tearing_down = true;
up_write(&prev_sess->chann_lock);
+ ksmbd_user_session_get(prev_sess);
+ pinned = true;
+ up_write(&conn->session_lock);
+ up_write(&sessions_table_lock);
ksmbd_all_conn_set_status(prev_sess, KSMBD_SESS_NEED_RECONNECT);
+ /*
+ * The caller has finished using the connection's authentication state.
+ * Drop srv_mutex so drained requests can write their responses.
+ */
+ ksmbd_conn_unlock(conn);
err = ksmbd_conn_wait_idle_sess(conn, prev_sess);
+ ksmbd_conn_lock(conn);
+ down_write(&sessions_table_lock);
+ down_write(&conn->session_lock);
if (err) {
down_write(&prev_sess->chann_lock);
prev_sess->tearing_down = false;
@@ -800,6 +813,8 @@ void destroy_previous_session(struct ksmbd_conn *conn,
out:
up_write(&conn->session_lock);
up_write(&sessions_table_lock);
+ if (pinned)
+ ksmbd_user_session_put(prev_sess);
}
static bool ksmbd_preauth_session_id_match(struct preauth_session *sess,
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 2da6163ee776..aca51f54a38d 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -2206,14 +2206,14 @@ static struct ksmbd_user *session_user(struct ksmbd_conn *conn,
static int ntlm_authenticate(struct ksmbd_work *work,
struct smb2_sess_setup_req *req,
- struct smb2_sess_setup_rsp *rsp)
+ struct smb2_sess_setup_rsp *rsp,
+ u64 *previous_session_id)
{
struct ksmbd_conn *conn = work->conn;
struct ksmbd_session *sess = work->sess;
struct ksmbd_user *user;
char channel_key[CIFS_KEY_SIZE] = {};
char *auth_key = conn->binding ? channel_key : sess->sess_key;
- u64 prev_id;
bool binding = conn->binding;
int sz, rc;
@@ -2278,9 +2278,7 @@ static int ntlm_authenticate(struct ksmbd_work *work,
}
}
- prev_id = le64_to_cpu(req->PreviousSessionId);
- if (prev_id && prev_id != sess->id)
- destroy_previous_session(conn, sess->user, prev_id);
+ *previous_session_id = le64_to_cpu(req->PreviousSessionId);
/*
* If session state is SMB2_SESSION_VALID, We can assume
@@ -2342,7 +2340,8 @@ static int ntlm_authenticate(struct ksmbd_work *work,
#ifdef CONFIG_SMB_SERVER_KERBEROS5
static int krb5_authenticate(struct ksmbd_work *work,
struct smb2_sess_setup_req *req,
- struct smb2_sess_setup_rsp *rsp)
+ struct smb2_sess_setup_rsp *rsp,
+ u64 *previous_session_id)
{
struct ksmbd_conn *conn = work->conn;
struct ksmbd_session *sess = work->sess;
@@ -2351,7 +2350,6 @@ static int krb5_authenticate(struct ksmbd_work *work,
char reauth_key[CIFS_KEY_SIZE] = {};
char *auth_key = conn->binding ? channel_key :
(work->session_setup_reauth ? reauth_key : sess->sess_key);
- u64 prev_sess_id;
bool binding = conn->binding;
int in_len, out_len;
int retval;
@@ -2373,10 +2371,7 @@ static int krb5_authenticate(struct ksmbd_work *work,
goto out;
}
- /* Check previous session */
- prev_sess_id = le64_to_cpu(req->PreviousSessionId);
- if (prev_sess_id && prev_sess_id != sess->id)
- destroy_previous_session(conn, sess->user, prev_sess_id);
+ *previous_session_id = le64_to_cpu(req->PreviousSessionId);
rsp->SecurityBufferLength = cpu_to_le16(out_len);
@@ -2445,7 +2440,8 @@ static int krb5_authenticate(struct ksmbd_work *work,
#else
static int krb5_authenticate(struct ksmbd_work *work,
struct smb2_sess_setup_req *req,
- struct smb2_sess_setup_rsp *rsp)
+ struct smb2_sess_setup_rsp *rsp,
+ u64 *previous_session_id)
{
return -EOPNOTSUPP;
}
@@ -2459,6 +2455,8 @@ int smb2_sess_setup(struct ksmbd_work *work)
struct ksmbd_session *sess = NULL;
struct negotiate_message *negblob;
unsigned int negblob_len, negblob_off;
+ u64 previous_session_id = 0;
+ bool authenticated = false;
int rc = 0;
ksmbd_debug(SMB, "Received smb2 session setup request\n");
@@ -2648,14 +2646,11 @@ int smb2_sess_setup(struct ksmbd_work *work)
if (conn->preferred_auth_mech &
(KSMBD_AUTH_KRB5 | KSMBD_AUTH_MSKRB5)) {
- rc = krb5_authenticate(work, req, rsp);
+ rc = krb5_authenticate(work, req, rsp, &previous_session_id);
if (rc)
goto out_err;
- if (!ksmbd_conn_need_reconnect(conn)) {
- ksmbd_conn_set_good(conn);
- sess->state = SMB2_SESSION_VALID;
- }
+ authenticated = true;
} else if (conn->preferred_auth_mech == KSMBD_AUTH_NTLMSSP) {
if (negblob->MessageType == NtLmNegotiate) {
rc = ntlm_negotiate(work, negblob, negblob_len, rsp);
@@ -2664,14 +2659,12 @@ int smb2_sess_setup(struct ksmbd_work *work)
rsp->hdr.Status =
STATUS_MORE_PROCESSING_REQUIRED;
} else if (negblob->MessageType == NtLmAuthenticate) {
- rc = ntlm_authenticate(work, req, rsp);
+ rc = ntlm_authenticate(work, req, rsp,
+ &previous_session_id);
if (rc)
goto out_err;
- if (!ksmbd_conn_need_reconnect(conn)) {
- ksmbd_conn_set_good(conn);
- sess->state = SMB2_SESSION_VALID;
- }
+ authenticated = true;
if (conn->binding) {
struct preauth_session *preauth_sess;
@@ -2804,6 +2797,21 @@ int smb2_sess_setup(struct ksmbd_work *work)
rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES;
}
+ work->session_setup_binding = conn->binding;
+ if (authenticated) {
+ /*
+ * Draining the previous session drops srv_mutex. Finish using the
+ * connection's authentication state and free mechToken first, and
+ * retain this request's binding decision for response processing.
+ */
+ if (previous_session_id && previous_session_id != sess->id)
+ destroy_previous_session(conn, sess->user, previous_session_id);
+ if (!ksmbd_conn_need_reconnect(conn)) {
+ ksmbd_conn_set_good(conn);
+ sess->state = SMB2_SESSION_VALID;
+ }
+ }
+
ksmbd_conn_unlock(conn);
return rc;
}
@@ -12478,7 +12486,7 @@ void smb3_set_sign_rsp(struct ksmbd_work *work)
command = le16_to_cpu(hdr->Command);
if (command == SMB2_SESSION_SETUP_HE &&
- (!conn->binding || hdr->Status != STATUS_SUCCESS)) {
+ (!work->session_setup_binding || hdr->Status != STATUS_SUCCESS)) {
signing_key = work->sess->smb3signingkey;
} else {
chann = lookup_chann_list(work->sess, work->conn);
@@ -12527,7 +12535,7 @@ void smb3_preauth_hash_rsp(struct ksmbd_work *work)
if (le16_to_cpu(rsp->Command) == SMB2_SESSION_SETUP_HE && sess) {
ksmbd_conn_lock(conn);
- if (conn->binding) {
+ if (work->session_setup_binding) {
struct preauth_session *preauth_sess;
preauth_sess = ksmbd_preauth_session_lookup(conn, sess->id);
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 02/22] ksmbd: serialize oplock break ACK state updates
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 03/22] ksmbd: track delivery of queued TCP writes Namjae Jeon
` (19 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon, Pan Guihua
ACK handling updates lease or oplock levels and wakes break waiters.
Without state_lock across validation and publication, timeout or break
completion can observe a partially updated state.
Serialize ACK validation and state changes under state_lock. Publish NONE
after lease levels are updated, then wake waiters after unlocking.
Reported-by: Pan Guihua <pan.guihua@datatom.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/smb2pdu.c | 64 +++++++++++++++++++----------------------
1 file changed, 29 insertions(+), 35 deletions(-)
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index aca51f54a38d..b45453a29fbb 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -11919,6 +11919,8 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work)
return;
}
+ /* Serialize the level update with break completion and timeout. */
+ spin_lock(&opinfo->state_lock);
if (opinfo->op_state != OPLOCK_ACK_WAIT) {
ksmbd_debug(SMB, "unexpected oplock state 0x%x\n",
opinfo->op_state);
@@ -11930,6 +11932,7 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work)
rsp->Reserved2 = 0;
rsp->VolatileFid = volatile_id;
rsp->PersistentFid = persistent_id;
+ spin_unlock(&opinfo->state_lock);
ret = ksmbd_iov_pin_rsp(work, rsp,
sizeof(struct smb2_oplock_break));
if (ret)
@@ -11986,6 +11989,9 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work)
rsp_oplevel = req_oplevel;
opinfo->level = rsp_oplevel;
+ opinfo->op_state = OPLOCK_STATE_NONE;
+ spin_unlock(&opinfo->state_lock);
+ wake_up_interruptible_all(&opinfo->oplock_q);
rsp->StructureSize = cpu_to_le16(24);
rsp->OplockLevel = rsp_oplevel;
@@ -11997,18 +12003,15 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work)
if (ret)
ksmbd_debug(SMB, "failed to pin oplock break response: %d\n",
ret);
- goto out;
+ goto out_no_state_change;
err_out:
- rsp->hdr.Status = status;
- smb2_set_err_rsp(work);
-
-out:
- 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);
+ rsp->hdr.Status = status;
+ smb2_set_err_rsp(work);
out_no_state_change:
opinfo_put(opinfo);
ksmbd_fd_put(work, fp);
@@ -12042,7 +12045,7 @@ static void smb21_lease_break_ack(struct ksmbd_work *work)
struct smb2_lease_ack *req;
struct smb2_lease_ack *rsp;
struct oplock_info *opinfo;
- int ret = 0;
+ int ret;
__le32 lease_state;
struct lease *lease;
@@ -12053,26 +12056,27 @@ static void smb21_lease_break_ack(struct ksmbd_work *work)
opinfo = lookup_lease_in_table(conn, req->LeaseKey);
if (!opinfo) {
ksmbd_debug(OPLOCK, "file not opened\n");
- smb2_set_err_rsp(work);
rsp->hdr.Status = STATUS_UNSUCCESSFUL;
+ smb2_set_err_rsp(work);
return;
}
lease = opinfo->o_lease;
- if (opinfo->op_state == OPLOCK_STATE_NONE) {
- pr_err("unexpected lease break state 0x%x\n",
- opinfo->op_state);
- if (smb3_hdr_replay(&req->hdr))
+ /* Publish NONE only after the lease state and all open levels are updated. */
+ spin_lock(&opinfo->state_lock);
+ if (opinfo->op_state != OPLOCK_ACK_WAIT) {
+ if (opinfo->op_state == OPLOCK_STATE_NONE &&
+ smb3_hdr_replay(&req->hdr))
goto replay_rsp;
rsp->hdr.Status = STATUS_UNSUCCESSFUL;
- goto err_out;
+ goto err_unlock;
}
if (!atomic_read(&opinfo->breaking_cnt)) {
if (smb3_hdr_replay(&req->hdr))
goto replay_rsp;
rsp->hdr.Status = STATUS_UNSUCCESSFUL;
- goto err_out;
+ goto err_unlock;
}
if (check_lease_state(lease, req->LeaseState)) {
@@ -12080,40 +12084,29 @@ static void smb21_lease_break_ack(struct ksmbd_work *work)
ksmbd_debug(OPLOCK,
"req lease state: 0x%x, expected state: 0x%x\n",
req->LeaseState, lease->new_state);
- goto err_out;
+ goto err_unlock;
}
lease_state = req->LeaseState;
lease->state = lease_state;
lease->new_state = SMB2_LEASE_NONE_LE;
lease_update_oplock_levels(lease);
-
- rsp->StructureSize = cpu_to_le16(36);
- rsp->Reserved = 0;
- rsp->Flags = 0;
- memcpy(rsp->LeaseKey, req->LeaseKey, 16);
- rsp->LeaseState = lease_state;
- rsp->LeaseDuration = 0;
- ret = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_lease_ack));
- if (ret)
- goto err_out;
-
- spin_lock(&opinfo->state_lock);
- if (opinfo->op_state != OPLOCK_CLOSING)
- opinfo->op_state = OPLOCK_STATE_NONE;
+ opinfo->op_state = OPLOCK_STATE_NONE;
+ atomic_dec_if_positive(&opinfo->breaking_cnt);
spin_unlock(&opinfo->state_lock);
wake_up_interruptible_all(&opinfo->oplock_q);
- atomic_dec_if_positive(&opinfo->breaking_cnt);
wake_up_interruptible_all(&opinfo->oplock_brk);
- opinfo_put(opinfo);
- return;
+ goto response;
replay_rsp:
+ lease_state = lease->state;
+ spin_unlock(&opinfo->state_lock);
+response:
rsp->StructureSize = cpu_to_le16(36);
rsp->Reserved = 0;
rsp->Flags = 0;
- memcpy(rsp->LeaseKey, req->LeaseKey, 16);
- rsp->LeaseState = lease->state;
+ memcpy(rsp->LeaseKey, req->LeaseKey, SMB2_LEASE_KEY_SIZE);
+ rsp->LeaseState = lease_state;
rsp->LeaseDuration = 0;
ret = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_lease_ack));
if (ret)
@@ -12121,10 +12114,11 @@ static void smb21_lease_break_ack(struct ksmbd_work *work)
opinfo_put(opinfo);
return;
+err_unlock:
+ spin_unlock(&opinfo->state_lock);
err_out:
smb2_set_err_rsp(work);
opinfo_put(opinfo);
- return;
}
/**
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 03/22] ksmbd: track delivery of queued TCP writes
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
2026-10-17 1:03 ` [PATCH 02/22] ksmbd: serialize oplock break ACK state updates Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 04/22] ksmbd: retry break notifications on another connection Namjae Jeon
` (18 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon, Pan Guihua
A nonblocking break send must distinguish a PDU accepted by TCP from one
still queued in ksmbd. Retrying an accepted PDU can send duplicates, while
abandoning a partial send can corrupt the stream.
Queue owned PDU bytes when TCP cannot accept the full message. Track the
stream position through socket delivery and TCP ACK, and expose it to break
retries. Bound retained data and stop queued writes during teardown.
Reported-by: Pan Guihua <pan.guihua@datatom.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/connection.c | 52 +++++-
fs/smb/server/connection.h | 24 ++-
fs/smb/server/transport_tcp.c | 299 +++++++++++++++++++++++++++++++++-
3 files changed, 356 insertions(+), 19 deletions(-)
diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c
index d1cc725eeb06..19d32681bb92 100644
--- a/fs/smb/server/connection.c
+++ b/fs/smb/server/connection.c
@@ -457,17 +457,13 @@ void ksmbd_all_conn_set_status(struct ksmbd_session *sess, u32 status)
void ksmbd_conn_abort(struct ksmbd_conn *conn)
{
- bool shutdown = false;
-
spin_lock(&conn->request_lock);
- if (!ksmbd_conn_exiting(conn) && !ksmbd_conn_releasing(conn)) {
+ if (!ksmbd_conn_exiting(conn) && !ksmbd_conn_releasing(conn))
ksmbd_conn_set_exiting(conn);
- shutdown = true;
- }
spin_unlock(&conn->request_lock);
wake_up_all(&conn->req_running_q);
- if (shutdown && conn->transport->ops->shutdown)
+ if (conn->transport->ops->shutdown)
conn->transport->ops->shutdown(conn->transport);
}
@@ -534,12 +530,19 @@ static int __ksmbd_conn_write(struct ksmbd_work *work,
tx->need_invalidate_rkey = work->need_invalidate_rkey;
tx->remote_key = work->remote_key;
- ksmbd_conn_lock(conn);
+ if (tx->tracking) {
+ /* A busy writer must not consume the break's retry budget. */
+ if (!mutex_trylock(&conn->srv_mutex))
+ return -EAGAIN;
+ } else {
+ ksmbd_conn_lock(conn);
+ }
sent = conn->transport->ops->writev(conn->transport, tx);
ksmbd_conn_unlock(conn);
if (sent < 0) {
- pr_err("Failed to send message: %d\n", sent);
+ if (!tx->tracking || sent != -EAGAIN)
+ pr_err("Failed to send message: %d\n", sent);
return sent;
}
@@ -553,6 +556,20 @@ int ksmbd_conn_write(struct ksmbd_work *work)
return __ksmbd_conn_write(work, &tx);
}
+int ksmbd_conn_write_tracked(struct ksmbd_work *work,
+ struct ksmbd_tx_tracking *tracking)
+{
+ struct ksmbd_transport_write tx = {};
+
+ tracking->valid = false;
+ /* Transports without delivery tracking keep their synchronous send path. */
+ if (work->conn->transport->ops->is_acked) {
+ tx.msg_flags = MSG_DONTWAIT;
+ tx.tracking = tracking;
+ }
+ return __ksmbd_conn_write(work, &tx);
+}
+
int ksmbd_conn_write_eor(struct ksmbd_work *work)
{
struct ksmbd_transport_write tx = {
@@ -805,7 +822,12 @@ int ksmbd_conn_handler_loop(void *p)
}
}
+ /* Exclude new break senders before draining their work references. */
+ spin_lock(&conn->request_lock);
ksmbd_conn_set_releasing(conn);
+ spin_unlock(&conn->request_lock);
+ /* TCP writers waiting for queue space must release their work references. */
+ wake_up_all(&conn->req_running_q);
ksmbd_conn_cancel_async_requests(conn);
/* Wait till all reference dropped to the Server object*/
ksmbd_debug(CONN, "Wait for all pending requests(%d)\n", atomic_read(&conn->r_count));
@@ -832,6 +854,20 @@ void ksmbd_conn_r_count_inc(struct ksmbd_conn *conn)
atomic_inc(&conn->r_count);
}
+/* The caller already owns a connection reference. */
+bool ksmbd_conn_r_count_inc_if_good(struct ksmbd_conn *conn)
+{
+ bool ret = false;
+
+ spin_lock(&conn->request_lock);
+ if (ksmbd_conn_good(conn)) {
+ atomic_inc(&conn->r_count);
+ ret = true;
+ }
+ spin_unlock(&conn->request_lock);
+ return ret;
+}
+
void ksmbd_conn_r_count_dec(struct ksmbd_conn *conn)
{
/*
diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h
index 2026a2afa5e4..85e2d2073cd2 100644
--- a/fs/smb/server/connection.h
+++ b/fs/smb/server/connection.h
@@ -103,6 +103,8 @@ struct ksmbd_conn {
wait_queue_head_t r_count_q;
/* Lock to protect requests list*/
spinlock_t request_lock;
+ /* Set under request_lock at teardown start and kept set thereafter. */
+ bool releasing;
struct list_head requests;
struct list_head async_requests;
int connection_type;
@@ -162,6 +164,13 @@ struct ksmbd_conn_ops {
int (*terminate_fn)(struct ksmbd_conn *conn);
};
+struct ksmbd_tx_tracking {
+ /* Required TCP byte count, including transport and socket queues. */
+ u64 end;
+ unsigned long retry_after;
+ bool valid;
+};
+
struct ksmbd_transport_write {
struct kvec *iov;
int iov_cnt;
@@ -169,6 +178,7 @@ struct ksmbd_transport_write {
bool need_invalidate_rkey;
unsigned int remote_key;
int msg_flags;
+ struct ksmbd_tx_tracking *tracking;
};
struct ksmbd_transport_ops {
@@ -176,8 +186,12 @@ struct ksmbd_transport_ops {
void (*shutdown)(struct ksmbd_transport *t);
int (*read)(struct ksmbd_transport *t, char *buf,
unsigned int size, int max_retries);
+ /* Retained bytes must be privately owned before a full result is returned. */
int (*writev)(struct ksmbd_transport *t,
const struct ksmbd_transport_write *tx);
+ /* Optional delivery tracking for nonblocking break sends. */
+ bool (*is_acked)(struct ksmbd_transport *t,
+ const struct ksmbd_tx_tracking *tracking);
int (*rdma_read)(struct ksmbd_transport *t,
void *buf, unsigned int len,
struct smbdirect_buffer_descriptor_v1 *desc,
@@ -217,6 +231,8 @@ int ksmbd_conn_wq_init(void);
void ksmbd_conn_wq_destroy(void);
bool ksmbd_conn_lookup_dialect(struct ksmbd_conn *c);
int ksmbd_conn_write(struct ksmbd_work *work);
+int ksmbd_conn_write_tracked(struct ksmbd_work *work,
+ struct ksmbd_tx_tracking *tracking);
int ksmbd_conn_write_eor(struct ksmbd_work *work);
int ksmbd_conn_rdma_read(struct ksmbd_conn *conn,
void *buf, unsigned int buflen,
@@ -235,6 +251,7 @@ void ksmbd_conn_transport_destroy(void);
void ksmbd_conn_lock(struct ksmbd_conn *conn);
void ksmbd_conn_unlock(struct ksmbd_conn *conn);
void ksmbd_conn_r_count_inc(struct ksmbd_conn *conn);
+bool ksmbd_conn_r_count_inc_if_good(struct ksmbd_conn *conn);
void ksmbd_conn_r_count_dec(struct ksmbd_conn *conn);
/*
@@ -250,7 +267,8 @@ static inline bool ksmbd_conn_new(struct ksmbd_conn *conn)
static inline bool ksmbd_conn_good(struct ksmbd_conn *conn)
{
- return READ_ONCE(conn->status) == KSMBD_SESS_GOOD;
+ return !READ_ONCE(conn->releasing) &&
+ READ_ONCE(conn->status) == KSMBD_SESS_GOOD;
}
static inline unsigned int
@@ -284,7 +302,8 @@ static inline bool ksmbd_conn_exiting(struct ksmbd_conn *conn)
static inline bool ksmbd_conn_releasing(struct ksmbd_conn *conn)
{
- return READ_ONCE(conn->status) == KSMBD_SESS_RELEASING;
+ return READ_ONCE(conn->releasing) ||
+ READ_ONCE(conn->status) == KSMBD_SESS_RELEASING;
}
static inline void ksmbd_conn_set_new(struct ksmbd_conn *conn)
@@ -319,6 +338,7 @@ static inline void ksmbd_conn_set_exiting(struct ksmbd_conn *conn)
static inline void ksmbd_conn_set_releasing(struct ksmbd_conn *conn)
{
+ WRITE_ONCE(conn->releasing, true);
WRITE_ONCE(conn->status, KSMBD_SESS_RELEASING);
}
diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c
index 488ca113b431..9c6ca878d30f 100644
--- a/fs/smb/server/transport_tcp.c
+++ b/fs/smb/server/transport_tcp.c
@@ -5,6 +5,7 @@
*/
#include <linux/freezer.h>
+#include <linux/uio.h>
#include "smb_common.h"
#include "server.h"
@@ -29,11 +30,30 @@ static LIST_HEAD(iface_list);
static int bind_additional_ifaces;
+struct tcp_tx {
+ struct list_head list;
+ size_t len;
+ size_t offset;
+ unsigned long deadline;
+ int msg_flags;
+ u8 data[];
+};
+
struct tcp_transport {
struct ksmbd_transport transport;
struct socket *sock;
struct kvec *iov;
unsigned int nr_iov;
+ /* Serializes queue access and socket writes. Never taken by callbacks. */
+ struct mutex tx_lock;
+ struct list_head tx_queue;
+ struct delayed_work tx_work;
+ size_t tx_queued;
+ /* TCP byte positions for accepted PDUs and bytes passed to the socket. */
+ u64 tx_end;
+ u64 tx_sent;
+ bool tx_stopped;
+ void (*write_space)(struct sock *sk);
};
static const struct ksmbd_transport_ops ksmbd_tcp_transport_ops;
@@ -42,6 +62,10 @@ static void tcp_stop_kthread(struct task_struct *kthread);
static void ksmbd_tcp_stop_listener(struct interface *iface);
static struct interface *alloc_iface(char *ifname);
static void ksmbd_tcp_disconnect(struct ksmbd_transport *t);
+static void ksmbd_tcp_shutdown(struct ksmbd_transport *t);
+static void ksmbd_tcp_tx_work(struct work_struct *work);
+static void ksmbd_tcp_write_space(struct sock *sk);
+static void ksmbd_tcp_stop_tx(struct tcp_transport *t);
#define KSMBD_TRANS(t) (&(t)->transport)
#define TCP_TRANS(t) ((struct tcp_transport *)container_of(t, \
@@ -66,6 +90,9 @@ static struct tcp_transport *alloc_transport(struct socket *client_sk)
if (!t)
return NULL;
t->sock = client_sk;
+ mutex_init(&t->tx_lock);
+ INIT_LIST_HEAD(&t->tx_queue);
+ INIT_DELAYED_WORK(&t->tx_work, ksmbd_tcp_tx_work);
conn = ksmbd_conn_alloc();
if (!conn) {
@@ -86,13 +113,26 @@ static struct tcp_transport *alloc_transport(struct socket *client_sk)
conn->inet_addr = inet_sk(client_sk->sk)->inet_daddr;
conn->inet_hash = ipv4_addr_hash(inet_sk(client_sk->sk)->inet_daddr);
#endif
- down_write(&conn_list_lock);
- hash_add(conn_list, &conn->hlist, conn->inet_hash);
- up_write(&conn_list_lock);
-
conn->transport = KSMBD_TRANS(t);
KSMBD_TRANS(t)->conn = conn;
KSMBD_TRANS(t)->ops = &ksmbd_tcp_transport_ops;
+
+ lock_sock(client_sk->sk);
+ t->tx_end = tcp_sk(client_sk->sk)->bytes_acked +
+ (u32)(tcp_sk(client_sk->sk)->write_seq -
+ tcp_sk(client_sk->sk)->snd_una);
+ t->tx_sent = t->tx_end;
+ release_sock(client_sk->sk);
+
+ write_lock_bh(&client_sk->sk->sk_callback_lock);
+ t->write_space = client_sk->sk->sk_write_space;
+ client_sk->sk->sk_user_data = t;
+ client_sk->sk->sk_write_space = ksmbd_tcp_write_space;
+ write_unlock_bh(&client_sk->sk->sk_callback_lock);
+
+ down_write(&conn_list_lock);
+ hash_add(conn_list, &conn->hlist, conn->inet_hash);
+ up_write(&conn_list_lock);
return t;
}
@@ -100,6 +140,7 @@ static void ksmbd_tcp_free_transport(struct ksmbd_transport *kt)
{
struct tcp_transport *t = TCP_TRANS(kt);
+ ksmbd_tcp_stop_tx(t);
sock_release(t->sock);
kfree(t->iov);
kfree(t);
@@ -107,7 +148,8 @@ static void ksmbd_tcp_free_transport(struct ksmbd_transport *kt)
static void free_transport(struct tcp_transport *t)
{
- kernel_sock_shutdown(t->sock, SHUT_RDWR);
+ ksmbd_tcp_shutdown(KSMBD_TRANS(t));
+ ksmbd_tcp_stop_tx(t);
ksmbd_conn_free(KSMBD_TRANS(t)->conn);
}
@@ -432,15 +474,251 @@ static int ksmbd_tcp_read(struct ksmbd_transport *t, char *buf,
return ksmbd_tcp_readv(TCP_TRANS(t), &iov, 1, to_read, max_retries);
}
+static bool ksmbd_tcp_tx_has_space(struct tcp_transport *t, size_t size)
+{
+ size_t limit = max_t(size_t, READ_ONCE(t->sock->sk->sk_sndbuf), size);
+
+ /* Allow one large response, but bound the retained response buffers. */
+ return READ_ONCE(t->tx_queued) <= limit - size;
+}
+
+static struct tcp_tx *ksmbd_tcp_alloc_tx(const struct ksmbd_transport_write *tx,
+ size_t offset)
+{
+ struct tcp_tx *msg;
+ struct iov_iter iter;
+ size_t remaining = tx->size - offset;
+
+ msg = kvmalloc(struct_size(msg, data, remaining), KSMBD_DEFAULT_GFP);
+ if (!msg)
+ return NULL;
+
+ iov_iter_kvec(&iter, ITER_SOURCE, tx->iov, tx->iov_cnt, tx->size);
+ iov_iter_advance(&iter, offset);
+ if (!copy_from_iter_full(msg->data, remaining, &iter)) {
+ kvfree(msg);
+ return NULL;
+ }
+ msg->len = remaining;
+ msg->offset = 0;
+ msg->msg_flags = tx->msg_flags;
+ return msg;
+}
+
+static void ksmbd_tcp_purge_tx(struct tcp_transport *t)
+{
+ struct tcp_tx *msg, *next;
+
+ list_for_each_entry_safe(msg, next, &t->tx_queue, list) {
+ list_del(&msg->list);
+ kvfree(msg);
+ }
+ WRITE_ONCE(t->tx_queued, 0);
+}
+
+static bool ksmbd_tcp_send_retry(int ret)
+{
+ return ret == -EAGAIN || ret == -EINTR || ret == -ERESTARTSYS ||
+ ret == -ENOMEM;
+}
+
+static void ksmbd_tcp_tx_work(struct work_struct *work)
+{
+ struct tcp_transport *t = container_of(to_delayed_work(work),
+ struct tcp_transport, tx_work);
+ unsigned long delay = 0;
+ unsigned int budget = 16;
+ bool failed = false;
+ int ret;
+
+ mutex_lock(&t->tx_lock);
+ while (!READ_ONCE(t->tx_stopped) && !list_empty(&t->tx_queue) && budget--) {
+ struct tcp_tx *msg = list_first_entry(&t->tx_queue, struct tcp_tx, list);
+ struct msghdr hdr = {
+ .msg_flags = MSG_NOSIGNAL | MSG_DONTWAIT | msg->msg_flags,
+ };
+ struct kvec iov = {
+ .iov_base = msg->data + msg->offset,
+ .iov_len = msg->len - msg->offset,
+ };
+
+ /* MSG_DONTWAIT bypasses sk_sndtimeo, including for retained PDUs. */
+ if (time_after_eq(jiffies, msg->deadline)) {
+ WRITE_ONCE(t->tx_stopped, true);
+ failed = true;
+ break;
+ }
+ ret = kernel_sendmsg(t->sock, &hdr, &iov, 1, iov.iov_len);
+ if (ret > 0) {
+ msg->offset += ret;
+ t->tx_sent += ret;
+ if (msg->offset == msg->len) {
+ WRITE_ONCE(t->tx_queued, t->tx_queued - msg->len);
+ list_del(&msg->list);
+ kvfree(msg);
+ continue;
+ }
+ } else if (!ksmbd_tcp_send_retry(ret)) {
+ WRITE_ONCE(t->tx_stopped, true);
+ failed = true;
+ break;
+ }
+ /*
+ * Write-space notifications normally resume the head of the queue.
+ * Also retry after memory pressure, which may produce no notification.
+ */
+ delay = max_t(unsigned long, 1, HZ / 10);
+ break;
+ }
+ if (READ_ONCE(t->tx_stopped))
+ ksmbd_tcp_purge_tx(t);
+ else if (!list_empty(&t->tx_queue))
+ queue_delayed_work(system_unbound_wq, &t->tx_work, delay);
+ mutex_unlock(&t->tx_lock);
+ wake_up_all(&t->transport.conn->req_running_q);
+
+ if (failed)
+ ksmbd_conn_abort(t->transport.conn);
+}
+
+static void ksmbd_tcp_write_space(struct sock *sk)
+{
+ struct tcp_transport *t;
+
+ read_lock_bh(&sk->sk_callback_lock);
+ t = sk->sk_user_data;
+ if (t) {
+ t->write_space(sk);
+ if (!READ_ONCE(t->tx_stopped) && sk_stream_is_writeable(sk))
+ mod_delayed_work(system_unbound_wq, &t->tx_work, 0);
+ }
+ read_unlock_bh(&sk->sk_callback_lock);
+}
+
+static void ksmbd_tcp_stop_tx(struct tcp_transport *t)
+{
+ struct sock *sk = t->sock->sk;
+
+ WRITE_ONCE(t->tx_stopped, true);
+ write_lock_bh(&sk->sk_callback_lock);
+ if (sk->sk_user_data == t) {
+ sk->sk_write_space = t->write_space;
+ sk->sk_user_data = NULL;
+ }
+ write_unlock_bh(&sk->sk_callback_lock);
+ cancel_delayed_work_sync(&t->tx_work);
+ mutex_lock(&t->tx_lock);
+ ksmbd_tcp_purge_tx(t);
+ mutex_unlock(&t->tx_lock);
+ wake_up_all(&t->transport.conn->req_running_q);
+}
+
static int ksmbd_tcp_writev(struct ksmbd_transport *t,
const struct ksmbd_transport_write *tx)
{
- struct msghdr smb_msg = {
- .msg_flags = MSG_NOSIGNAL | tx->msg_flags,
+ struct tcp_transport *tt = TCP_TRANS(t);
+ struct sock *sk = tt->sock->sk;
+ struct msghdr hdr = {
+ .msg_flags = MSG_NOSIGNAL | MSG_DONTWAIT | tx->msg_flags,
};
+ struct tcp_tx *msg;
+ unsigned long deadline = jiffies + KSMBD_TCP_SEND_TIMEOUT;
+ long timeout;
+ int sent = 0;
+
+ for (;;) {
+ if (tx->msg_flags & MSG_DONTWAIT) {
+ if (!mutex_trylock(&tt->tx_lock))
+ return -EAGAIN;
+ } else {
+ mutex_lock(&tt->tx_lock);
+ }
+ if (READ_ONCE(tt->tx_stopped) || ksmbd_conn_exiting(t->conn) ||
+ ksmbd_conn_releasing(t->conn)) {
+ mutex_unlock(&tt->tx_lock);
+ return -EPIPE;
+ }
+ if (ksmbd_tcp_tx_has_space(tt, tx->size))
+ break;
+ mutex_unlock(&tt->tx_lock);
+ if (tx->msg_flags & MSG_DONTWAIT)
+ return -EAGAIN;
+ timeout = deadline - jiffies;
+ if (time_after_eq(jiffies, deadline) ||
+ !wait_event_timeout(t->conn->req_running_q,
+ READ_ONCE(tt->tx_stopped) ||
+ ksmbd_conn_exiting(t->conn) ||
+ ksmbd_conn_releasing(t->conn) ||
+ ksmbd_tcp_tx_has_space(tt, tx->size),
+ timeout)) {
+ /* A queued PDU may already have a prefix in the socket. */
+ ksmbd_conn_abort(t->conn);
+ return -EAGAIN;
+ }
+ }
- return kernel_sendmsg(TCP_TRANS(t)->sock, &smb_msg, tx->iov,
- tx->iov_cnt, tx->size);
+ /* Avoid an extra copy when the socket accepts the whole PDU at once. */
+ if (list_empty(&tt->tx_queue)) {
+ sent = kernel_sendmsg(tt->sock, &hdr, tx->iov, tx->iov_cnt, tx->size);
+ if (sent > 0) {
+ tt->tx_sent += sent;
+ if (sent == tx->size)
+ goto accepted;
+ } else if (ksmbd_tcp_send_retry(sent)) {
+ sent = 0;
+ } else {
+ WRITE_ONCE(tt->tx_stopped, true);
+ mutex_unlock(&tt->tx_lock);
+ ksmbd_conn_abort(t->conn);
+ return sent ?: -EPIPE;
+ }
+ }
+
+ /* The request buffers can be freed as soon as writev returns. */
+ msg = ksmbd_tcp_alloc_tx(tx, sent);
+ if (!msg) {
+ if (sent)
+ WRITE_ONCE(tt->tx_stopped, true);
+ mutex_unlock(&tt->tx_lock);
+ if (sent)
+ ksmbd_conn_abort(t->conn);
+ return -ENOMEM;
+ }
+ msg->deadline = deadline;
+ list_add_tail(&msg->list, &tt->tx_queue);
+ WRITE_ONCE(tt->tx_queued, tt->tx_queued + msg->len);
+ queue_delayed_work(system_unbound_wq, &tt->tx_work, 0);
+
+accepted:
+ tt->tx_end += tx->size;
+ if (tx->tracking) {
+ /* Include retained data, without retaining the caller's tracking object. */
+ tx->tracking->end = tt->tx_end;
+ lock_sock(sk);
+ tx->tracking->retry_after =
+ clamp_t(unsigned long, 6UL * inet_csk(sk)->icsk_rto,
+ msecs_to_jiffies(1200), 6 * HZ);
+ release_sock(sk);
+ tx->tracking->valid = true;
+ }
+ mutex_unlock(&tt->tx_lock);
+ return tx->size;
+}
+
+static bool ksmbd_tcp_is_acked(struct ksmbd_transport *t,
+ const struct ksmbd_tx_tracking *tracking)
+{
+ struct tcp_transport *tt = TCP_TRANS(t);
+ struct sock *sk = tt->sock->sk;
+ bool acked;
+
+ mutex_lock(&tt->tx_lock);
+ lock_sock(sk);
+ acked = tt->tx_sent >= tracking->end &&
+ tcp_sk(sk)->bytes_acked >= tracking->end;
+ release_sock(sk);
+ mutex_unlock(&tt->tx_lock);
+ return acked;
}
static void ksmbd_tcp_disconnect(struct ksmbd_transport *t)
@@ -452,7 +730,9 @@ static void ksmbd_tcp_disconnect(struct ksmbd_transport *t)
static void ksmbd_tcp_shutdown(struct ksmbd_transport *t)
{
+ WRITE_ONCE(TCP_TRANS(t)->tx_stopped, true);
kernel_sock_shutdown(TCP_TRANS(t)->sock, SHUT_RDWR);
+ wake_up_all(&t->conn->req_running_q);
}
static void tcp_destroy_socket(struct socket *ksmbd_socket)
@@ -721,6 +1001,7 @@ int ksmbd_tcp_set_interfaces(char *ifc_list, int ifc_list_sz)
static const struct ksmbd_transport_ops ksmbd_tcp_transport_ops = {
.read = ksmbd_tcp_read,
.writev = ksmbd_tcp_writev,
+ .is_acked = ksmbd_tcp_is_acked,
.disconnect = ksmbd_tcp_disconnect,
.shutdown = ksmbd_tcp_shutdown,
.free_transport = ksmbd_tcp_free_transport,
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 04/22] ksmbd: retry break notifications on another connection
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
2026-10-17 1:03 ` [PATCH 02/22] ksmbd: serialize oplock break ACK state updates Namjae Jeon
2026-10-17 1:03 ` [PATCH 03/22] ksmbd: track delivery of queued TCP writes Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 05/22] ksmbd: keep lease tables until their last lease is closed Namjae Jeon
` (17 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon, Pan Guihua
An oplock or lease break belongs to an open that can outlive its original
channel. A draining channel or stalled TCP writer can otherwise leave the
break pending until timeout or invalidate a durable open.
Snapshot the owner's live channels and try eligible connections with the
same notification. Track TCP delivery separately from the SMB ACK, retry
when a channel stops progressing, and share one break deadline. Run
ordinary break waits on a dedicated workqueue so blocked request
responses do not stall breaks.
Publish NONE before sending a level II break, which requires no ACK, so
an unsolicited ACK observes the completed level while TCP delivery is
pending.
Keep the first rename lease notification in the request context and
enqueue the interim writer only after the notification is accepted. Join
that work before returning so a blocked interim response does not stop
notification retries or release the request early.
Reported-by: Pan Guihua <pan.guihua@datatom.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/ksmbd_work.c | 19 +
fs/smb/server/ksmbd_work.h | 2 +
fs/smb/server/mgmt/user_session.c | 31 ++
fs/smb/server/mgmt/user_session.h | 3 +
fs/smb/server/oplock.c | 855 +++++++++++++++++-------------
fs/smb/server/oplock.h | 14 +-
fs/smb/server/vfs_cache.c | 2 +
7 files changed, 539 insertions(+), 387 deletions(-)
diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c
index 52ea1a674a67..6e5122e2d6fc 100644
--- a/fs/smb/server/ksmbd_work.c
+++ b/fs/smb/server/ksmbd_work.c
@@ -16,6 +16,7 @@
static struct kmem_cache *work_cache;
static struct workqueue_struct *ksmbd_wq;
+static struct workqueue_struct *ksmbd_break_wq;
static int ksmbd_reserve_iov(struct ksmbd_work *work, int need_iov_cnt)
{
@@ -110,6 +111,12 @@ int ksmbd_workqueue_init(void)
ksmbd_wq = alloc_workqueue("ksmbd-io", WQ_PERCPU, 0);
if (!ksmbd_wq)
return -ENOMEM;
+ ksmbd_break_wq = alloc_workqueue("ksmbd-break", WQ_UNBOUND, 0);
+ if (!ksmbd_break_wq) {
+ destroy_workqueue(ksmbd_wq);
+ ksmbd_wq = NULL;
+ return -ENOMEM;
+ }
return 0;
}
@@ -117,6 +124,8 @@ void ksmbd_workqueue_destroy(void)
{
destroy_workqueue(ksmbd_wq);
ksmbd_wq = NULL;
+ destroy_workqueue(ksmbd_break_wq);
+ ksmbd_break_wq = NULL;
}
bool ksmbd_queue_work(struct ksmbd_work *work)
@@ -124,6 +133,16 @@ bool ksmbd_queue_work(struct ksmbd_work *work)
return queue_work(ksmbd_wq, &work->work);
}
+void ksmbd_queue_io_work(struct work_struct *work)
+{
+ queue_work(ksmbd_wq, work);
+}
+
+void ksmbd_queue_break_work(struct work_struct *work)
+{
+ queue_work(ksmbd_break_wq, work);
+}
+
static inline void __ksmbd_iov_pin(struct ksmbd_work *work, void *ib,
unsigned int ib_len)
{
diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h
index 40f70a5d08b5..b51dea69cb0e 100644
--- a/fs/smb/server/ksmbd_work.h
+++ b/fs/smb/server/ksmbd_work.h
@@ -155,6 +155,8 @@ int ksmbd_work_pool_init(void);
int ksmbd_workqueue_init(void);
void ksmbd_workqueue_destroy(void);
bool ksmbd_queue_work(struct ksmbd_work *work);
+void ksmbd_queue_io_work(struct work_struct *work);
+void ksmbd_queue_break_work(struct work_struct *work);
int ksmbd_iov_pin_rsp_read(struct ksmbd_work *work, void *ib, int len,
void *aux_buf, unsigned int aux_size);
int ksmbd_iov_pin_rsp(struct ksmbd_work *work, void *ib, int len);
diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c
index d814a99ff519..554434c7673a 100644
--- a/fs/smb/server/mgmt/user_session.c
+++ b/fs/smb/server/mgmt/user_session.c
@@ -674,6 +674,37 @@ struct ksmbd_session *ksmbd_session_lookup_slowpath(unsigned long long id)
return sess;
}
+/* The caller holds a session reference and must put each returned connection. */
+unsigned int ksmbd_session_get_channels(struct ksmbd_session *sess,
+ struct ksmbd_conn **conns,
+ unsigned int max_conns)
+{
+ struct channel *chann;
+ unsigned long index;
+ unsigned int nr_conns = 0;
+
+ /*
+ * Channel connections are borrowed. Unregister can leave the channel
+ * list in a pinned session after removing it from the registry, so
+ * chann_lock alone does not keep those connections alive. Only collect
+ * registered sessions, excluding both unregister and connection cleanup
+ * until the connection references have been taken.
+ */
+ down_read(&sessions_table_lock);
+ if (hlist_unhashed(&sess->hlist))
+ goto out;
+ down_read(&sess->chann_lock);
+ xa_for_each(&sess->ksmbd_chann_list, index, chann) {
+ if (nr_conns == max_conns)
+ break;
+ conns[nr_conns++] = ksmbd_conn_get(chann->conn);
+ }
+ up_read(&sess->chann_lock);
+out:
+ up_read(&sessions_table_lock);
+ return nr_conns;
+}
+
struct ksmbd_session *ksmbd_session_lookup_all_states(struct ksmbd_conn *conn,
unsigned long long id)
{
diff --git a/fs/smb/server/mgmt/user_session.h b/fs/smb/server/mgmt/user_session.h
index 217258551d6d..aff31f1694db 100644
--- a/fs/smb/server/mgmt/user_session.h
+++ b/fs/smb/server/mgmt/user_session.h
@@ -94,6 +94,9 @@ struct ksmbd_session *ksmbd_smb2_session_create(void);
void ksmbd_session_destroy(struct ksmbd_session *sess);
struct ksmbd_session *ksmbd_session_lookup_slowpath(unsigned long long id);
+unsigned int ksmbd_session_get_channels(struct ksmbd_session *sess,
+ struct ksmbd_conn **conns,
+ unsigned int max_conns);
struct ksmbd_session *ksmbd_session_lookup(struct ksmbd_conn *conn,
unsigned long long id);
bool is_ksmbd_session_in_connection(struct ksmbd_conn *conn,
diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c
index 25ff8408f0ba..313bf01ad350 100644
--- a/fs/smb/server/oplock.c
+++ b/fs/smb/server/oplock.c
@@ -87,6 +87,7 @@ static struct oplock_info *alloc_opinfo(struct ksmbd_work *work,
return NULL;
opinfo->sess = sess;
+ opinfo->session_id = sess->id;
opinfo->conn = ksmbd_conn_get(work->conn);
opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
opinfo->op_state = OPLOCK_STATE_NONE;
@@ -302,15 +303,9 @@ static struct oplock_info *opinfo_get_list(struct ksmbd_inode *ci,
opinfo = list_first_entry_or_null(&ci->m_op_list, struct oplock_info,
op_entry);
if (opinfo) {
- if (opinfo->conn == NULL ||
- !atomic_inc_not_zero(&opinfo->refcount))
+ /* A live open may still be bound to a disconnected channel. */
+ if (!atomic_inc_not_zero(&opinfo->refcount))
opinfo = NULL;
- else {
- if (ksmbd_conn_releasing(opinfo->conn)) {
- atomic_dec(&opinfo->refcount);
- opinfo = NULL;
- }
- }
if (opinfo && snapshot && opinfo->o_fp &&
opinfo->o_fp != skip_fp &&
@@ -367,7 +362,7 @@ static void opinfo_del(struct oplock_info *opinfo)
lease_del_open(opinfo);
down_write(&ci->m_lock);
- list_del(&opinfo->op_entry);
+ list_del_init(&opinfo->op_entry);
up_write(&ci->m_lock);
}
@@ -639,19 +634,27 @@ static void grant_none_oplock(struct oplock_info *opinfo_new,
static inline int compare_guid_key(struct oplock_info *opinfo,
const char *guid1, const char *key1)
{
- const char *guid2, *key2;
+ struct lease *lease = opinfo->o_lease;
+ struct lease_table *lb;
struct ksmbd_conn *conn;
+ int ret = 0;
- conn = READ_ONCE(opinfo->conn);
- if (!conn)
+ if (memcmp(key1, lease->lease_key, SMB2_LEASE_KEY_SIZE))
return 0;
- guid2 = conn->ClientGUID;
- key2 = opinfo->o_lease->lease_key;
- if (!memcmp(guid1, guid2, SMB2_CLIENT_GUID_SIZE) &&
- !memcmp(key1, key2, SMB2_LEASE_KEY_SIZE))
- return 1;
- return 0;
+ /* The caller holds ci->m_lock, which protects opinfo->conn. */
+ conn = READ_ONCE(opinfo->conn);
+ if (conn)
+ return !memcmp(guid1, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE);
+
+ /* A detached durable lease retains its client identity in the table. */
+ read_lock(&lease_list_lock);
+ lb = lease->l_lb;
+ if (lb)
+ ret = !memcmp(guid1, lb->client_guid, SMB2_CLIENT_GUID_SIZE);
+ read_unlock(&lease_list_lock);
+
+ return ret;
}
/**
@@ -681,7 +684,7 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci,
*/
down_read(&ci->m_lock);
list_for_each_entry(opinfo, &ci->m_op_list, op_entry) {
- if (!opinfo->is_lease || !opinfo->conn)
+ if (!opinfo->is_lease)
continue;
lease = opinfo->o_lease;
@@ -734,35 +737,6 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci,
return m_opinfo;
}
-static bool wait_for_break_ack(struct oplock_info *opinfo)
-{
- int rc = 0;
-
- rc = wait_event_interruptible_timeout(opinfo->oplock_q,
- opinfo->op_state == OPLOCK_STATE_NONE ||
- opinfo->op_state == OPLOCK_CLOSING,
- OPLOCK_WAIT_TIME);
-
- /* 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;
- }
-
- return false;
-}
-
static bool oplock_break_set_ack_wait(struct oplock_info *opinfo)
{
bool ret = false;
@@ -842,330 +816,492 @@ static bool lease_break_needed(struct oplock_info *opinfo, int req_op_level,
return opinfo->level > req_op_level;
}
-/**
- * __smb2_oplock_break_noti() - send smb2 oplock break cmd from conn
- * to client
- * @wk: smb work object
- *
- * There are two ways this function can be called. 1- while file open we break
- * from exclusive/batch lock to levelII oplock and 2- while file write/truncate
- * we break from levelII oplock no oplock.
- * work->request_buf contains oplock_info.
- */
-static void __smb2_oplock_break_noti(struct work_struct *wk)
+struct smb2_break_channel {
+ struct list_head list;
+ struct ksmbd_conn *conn;
+ struct ksmbd_tx_tracking tracking;
+ bool queued;
+ bool delivered;
+ bool failed;
+};
+
+static bool smb2_break_complete(struct oplock_info *opinfo, bool expect_ack)
{
- struct smb2_oplock_break *rsp = NULL;
- struct ksmbd_work *work = container_of(wk, struct ksmbd_work, work);
- struct ksmbd_conn *conn = work->conn;
- struct oplock_break_info *br_info = work->request_buf;
- struct smb2_hdr *rsp_hdr;
- struct ksmbd_file *fp;
+ bool complete;
- fp = ksmbd_lookup_global_fd(br_info->fid);
- if (!fp)
- goto out;
+ spin_lock(&opinfo->state_lock);
+ complete = opinfo->op_state == OPLOCK_CLOSING ||
+ (expect_ack && opinfo->op_state == OPLOCK_STATE_NONE);
+ spin_unlock(&opinfo->state_lock);
+ return complete;
+}
- if (allocate_interim_rsp_buf(work)) {
- pr_err("smb2_allocate_rsp_buf failed! ");
- ksmbd_fd_put(work, fp);
- goto out;
+static int smb2_break_failed(struct oplock_info *opinfo, bool expect_ack)
+{
+ spin_lock(&opinfo->state_lock);
+ /* An ACK or close which won the race has already completed the break. */
+ if (opinfo->op_state == OPLOCK_CLOSING ||
+ (expect_ack && opinfo->op_state == OPLOCK_STATE_NONE)) {
+ spin_unlock(&opinfo->state_lock);
+ return 0;
}
+ if (opinfo->is_lease) {
+ opinfo->o_lease->state = SMB2_LEASE_NONE_LE;
+ opinfo->o_lease->new_state = SMB2_LEASE_NONE_LE;
+ lease_update_oplock_levels(opinfo->o_lease);
+ atomic_set(&opinfo->breaking_cnt, 0);
+ }
+ opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
+ opinfo->op_state = OPLOCK_STATE_NONE;
+ spin_unlock(&opinfo->state_lock);
+ wake_up_interruptible_all(&opinfo->oplock_q);
+ wake_up_interruptible_all(&opinfo->oplock_brk);
+ return ksmbd_invalidate_durable_fd(opinfo->fid);
+}
- rsp_hdr = smb_get_msg(work->response_buf);
- memset(rsp_hdr, 0, sizeof(struct smb2_hdr) + 2);
- rsp_hdr->ProtocolId = SMB2_PROTO_NUMBER;
- rsp_hdr->StructureSize = SMB2_HEADER_STRUCTURE_SIZE;
- rsp_hdr->CreditRequest = cpu_to_le16(0);
- rsp_hdr->Command = SMB2_OPLOCK_BREAK;
- rsp_hdr->Flags = (SMB2_FLAGS_SERVER_TO_REDIR);
- rsp_hdr->NextCommand = 0;
- rsp_hdr->MessageId = cpu_to_le64(-1);
- rsp_hdr->Id.SyncId.ProcessId = 0;
- rsp_hdr->Id.SyncId.TreeId = 0;
- rsp_hdr->SessionId = 0;
- memset(rsp_hdr->Signature, 0, 16);
-
- rsp = smb_get_msg(work->response_buf);
-
- rsp->StructureSize = cpu_to_le16(24);
- if (!br_info->open_trunc &&
- (br_info->level == SMB2_OPLOCK_LEVEL_BATCH ||
- br_info->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE))
- rsp->OplockLevel = SMB2_OPLOCK_LEVEL_II;
- else
- rsp->OplockLevel = SMB2_OPLOCK_LEVEL_NONE;
- rsp->Reserved = 0;
- rsp->Reserved2 = 0;
- rsp->PersistentFid = fp->persistent_id;
- rsp->VolatileFid = fp->volatile_id;
-
- ksmbd_fd_put(work, fp);
- if (ksmbd_iov_pin_rsp(work, (void *)rsp,
- sizeof(struct smb2_oplock_break)))
- goto out;
-
- ksmbd_debug(OPLOCK,
- "sending oplock break v_id %llu p_id = %llu lock level = %d\n",
- rsp->VolatileFid, rsp->PersistentFid, rsp->OplockLevel);
-
- ksmbd_conn_write(work);
+static int smb2_break_add_channel(struct list_head *head, struct ksmbd_conn *conn)
+{
+ struct smb2_break_channel *channel;
-out:
- ksmbd_free_work_struct(work);
- ksmbd_conn_r_count_dec(conn);
- ksmbd_conn_put(conn);
+ channel = kzalloc_obj(*channel, KSMBD_DEFAULT_GFP);
+ if (!channel)
+ return -ENOMEM;
+ channel->conn = ksmbd_conn_get(conn);
+ list_add_tail(&channel->list, head);
+ return 0;
}
/*
- * 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.
+ * Snapshot connections before sending. References keep their transports alive,
+ * while r_count is taken separately for each write, under request_lock.
*/
-static struct ksmbd_conn *smb2_oplock_break_conn_get(struct oplock_info *opinfo,
- struct ksmbd_inode *ci)
-{
+static int smb2_break_collect_channels(struct oplock_info *opinfo,
+ struct ksmbd_inode *ci,
+ struct smb2_break_channel *owner,
+ struct list_head *head,
+ struct smb2_oplock_break *rsp)
+{
+ struct ksmbd_session *sess = NULL;
+ struct ksmbd_conn *session_conns[KSMBD_MAX_CHANNELS];
+ struct ksmbd_file_table *ft;
+ struct ksmbd_file *fp;
struct ksmbd_conn *conn;
+ struct lease_table *lb;
+ char guid[SMB2_CLIENT_GUID_SIZE];
+ u64 session_id = 0;
+ unsigned int nr_conns, i;
+ bool have_guid = false;
+ int bkt, ret = 0;
+ /* m_lock protects membership and binding, but not the session's lifetime. */
down_read(&ci->m_lock);
- conn = READ_ONCE(opinfo->conn);
- if (conn && !ksmbd_conn_releasing(conn))
- conn = ksmbd_conn_get(conn);
- else
- conn = NULL;
+ if (!list_empty(&opinfo->op_entry)) {
+ if (!opinfo->is_lease) {
+ rsp->PersistentFid = READ_ONCE(opinfo->o_fp->persistent_id);
+ rsp->VolatileFid = READ_ONCE(opinfo->o_fp->volatile_id);
+ if (!has_file_id(rsp->PersistentFid) ||
+ !has_file_id(rsp->VolatileFid)) {
+ /* A detached oplock must take the failed-break path. */
+ ret = opinfo->conn ? -ENOENT : -ENOTCONN;
+ goto unlock;
+ }
+ session_id = opinfo->session_id;
+ }
+ conn = opinfo->conn;
+ if (conn) {
+ owner->conn = ksmbd_conn_get(conn);
+ memcpy(guid, conn->ClientGUID, sizeof(guid));
+ have_guid = true;
+ }
+ } else {
+ ret = -ENOENT;
+ }
+unlock:
up_read(&ci->m_lock);
+ if (ret)
+ return ret;
+
+ if (opinfo->is_lease && !have_guid) {
+ read_lock(&lease_list_lock);
+ lb = opinfo->o_lease->l_lb;
+ if (lb) {
+ memcpy(guid, lb->client_guid, sizeof(guid));
+ have_guid = true;
+ if (lb->conn)
+ owner->conn = ksmbd_conn_get(lb->conn);
+ }
+ read_unlock(&lease_list_lock);
+ }
+ if (owner->conn)
+ list_add_tail(&owner->list, head);
+
+ if (!opinfo->is_lease) {
+ /* Session teardown takes m_lock while holding the session table lock. */
+ sess = ksmbd_session_lookup_slowpath(session_id);
+ if (!sess)
+ return -ENOENT;
+
+ /* Revalidate the binding and IDs after taking a safe session reference. */
+ down_read(&ci->m_lock);
+ if (list_empty(&opinfo->op_entry) || opinfo->sess != sess) {
+ ret = -ENOENT;
+ } else {
+ ft = &sess->file_table;
+ read_lock(&ft->lock);
+ fp = opinfo->o_fp;
+ if (fp->f_state == FP_CLOSED ||
+ !has_file_id(fp->volatile_id) ||
+ idr_find(ft->idr, fp->volatile_id) != fp) {
+ ret = -ENOENT;
+ } else {
+ rsp->PersistentFid = READ_ONCE(fp->persistent_id);
+ rsp->VolatileFid = fp->volatile_id;
+ }
+ read_unlock(&ft->lock);
+ }
+ up_read(&ci->m_lock);
+ if (ret) {
+ ksmbd_user_session_put(sess);
+ return ret;
+ }
+ }
- return conn;
+ if (opinfo->is_lease && have_guid) {
+ down_read(&conn_list_lock);
+ hash_for_each(conn_list, bkt, conn, hlist) {
+ if (conn == owner->conn || !ksmbd_conn_good(conn) ||
+ memcmp(conn->ClientGUID, guid, sizeof(guid)))
+ continue;
+ ret = smb2_break_add_channel(head, conn);
+ if (ret)
+ break;
+ }
+ up_read(&conn_list_lock);
+ } else if (!opinfo->is_lease && sess && sess->dialect >= SMB30_PROT_ID) {
+ nr_conns = ksmbd_session_get_channels(sess, session_conns,
+ ARRAY_SIZE(session_conns));
+ for (i = 0; i < nr_conns; i++) {
+ conn = session_conns[i];
+ if (!ret && conn && conn != owner->conn && ksmbd_conn_good(conn))
+ ret = smb2_break_add_channel(head, conn);
+ /* Drop all snapshot references, also on allocation failure. */
+ ksmbd_conn_put(conn);
+ }
+ }
+ ksmbd_user_session_put(sess);
+
+ /* Allocation pressure may limit retries, but must not discard the owner. */
+ return list_empty(head) ? (ret ?: -ENOTCONN) : 0;
}
-/**
- * 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,
- struct ksmbd_inode *ci)
+static bool smb2_break_delivered(struct list_head *head, bool require_live)
{
+ struct smb2_break_channel *channel;
struct ksmbd_conn *conn;
- struct oplock_break_info *br_info;
- int ret = 0;
- struct ksmbd_work *work;
-
- conn = smb2_oplock_break_conn_get(opinfo, ci);
- if (!conn)
- return ksmbd_invalidate_durable_fd(opinfo->fid);
- work = ksmbd_alloc_work_struct();
- if (!work) {
- ksmbd_conn_put(conn);
- return -ENOMEM;
+ list_for_each_entry(channel, head, list) {
+ conn = channel->conn;
+ if (!channel->queued || (require_live && !ksmbd_conn_good(conn)))
+ continue;
+ /* A synchronous send needs no separate delivery probe. */
+ if (channel->delivered || !channel->tracking.valid ||
+ conn->transport->ops->is_acked(conn->transport,
+ &channel->tracking)) {
+ channel->delivered = true;
+ return true;
+ }
}
+ return false;
+}
- 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;
- }
+static bool smb2_break_has_unsent_channels(struct list_head *head)
+{
+ struct smb2_break_channel *channel;
- br_info->level = opinfo->level;
- br_info->fid = opinfo->fid;
- br_info->open_trunc = opinfo->open_trunc;
+ list_for_each_entry(channel, head, list) {
+ if (!channel->queued && !channel->failed)
+ return true;
+ }
+ return false;
+}
- work->request_buf = (char *)br_info;
- /* Transfer the reference acquired by smb2_oplock_break_conn_get(). */
- work->conn = conn;
- work->sess = opinfo->sess;
+static int smb2_break_wait(struct oplock_info *opinfo, struct list_head *head,
+ struct smb2_break_channel *sent_channel,
+ bool expect_ack, bool wait_ack, bool can_retry,
+ unsigned long retry_at, unsigned long deadline)
+{
+ unsigned long remaining;
+ long ret;
- ksmbd_conn_r_count_inc(conn);
- if (opinfo->op_state == OPLOCK_ACK_WAIT) {
- INIT_WORK(&work->work, __smb2_oplock_break_noti);
- ksmbd_queue_work(work);
+ for (;;) {
+ if (smb2_break_complete(opinfo, expect_ack))
+ return 0;
+ if (!wait_ack && smb2_break_delivered(head, false))
+ return 0;
+ if (time_after_eq(jiffies, deadline))
+ return -ETIMEDOUT;
+ /* TCP delivery followed by a slow SMB ACK is not a send failure. */
+ if (can_retry && !smb2_break_delivered(head, true) &&
+ ((sent_channel && !ksmbd_conn_good(sent_channel->conn)) ||
+ time_after_eq(jiffies, retry_at)))
+ return -EAGAIN;
- if (wait_for_break_ack(opinfo))
- ret = ksmbd_invalidate_durable_fd(opinfo->fid);
- } else {
- __smb2_oplock_break_noti(&work->work);
- if (opinfo->level == SMB2_OPLOCK_LEVEL_II)
- opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
+ remaining = min_t(unsigned long, deadline - jiffies,
+ max_t(unsigned long, 1, HZ / 10));
+ ret = wait_event_interruptible_timeout(opinfo->oplock_q,
+ smb2_break_complete(opinfo, expect_ack),
+ remaining);
+ if (ret < 0)
+ return ret;
}
- return ret;
}
-/**
- * __smb2_lease_break_noti() - send lease break command from server
- * to client
- * @wk: smb work object
- */
-static void __smb2_lease_break_noti(struct work_struct *wk)
+static void smb2_break_init_header(struct smb2_hdr *hdr)
{
- struct smb2_lease_break *rsp = NULL;
- struct ksmbd_work *work = container_of(wk, struct ksmbd_work, work);
- struct ksmbd_conn *conn = work->conn;
- struct lease_break_info *br_info = work->request_buf;
- struct smb2_hdr *rsp_hdr;
+ memset(hdr, 0, sizeof(*hdr));
+ hdr->ProtocolId = SMB2_PROTO_NUMBER;
+ hdr->StructureSize = SMB2_HEADER_STRUCTURE_SIZE;
+ hdr->Command = SMB2_OPLOCK_BREAK;
+ hdr->Flags = SMB2_FLAGS_SERVER_TO_REDIR;
+ hdr->MessageId = cpu_to_le64(-1);
+}
- if (allocate_interim_rsp_buf(work)) {
- ksmbd_debug(OPLOCK, "smb2_allocate_rsp_buf failed! ");
- goto out;
+/* Build one notification and reuse its payload and epoch on each connection. */
+static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci,
+ bool expect_ack, bool wait_ack, bool inc_epoch,
+ struct work_struct *interim_work)
+{
+ struct smb2_break_channel owner = {}, *channel, *tmp, *last = NULL;
+ struct ksmbd_work *work;
+ struct smb2_oplock_break *oplock_rsp;
+ struct smb2_lease_break *lease_rsp;
+ struct lease *lease = opinfo->o_lease;
+ unsigned long deadline, retry_at;
+ int ret, size;
+ LIST_HEAD(channels);
+
+ if (smb2_break_complete(opinfo, expect_ack)) {
+ ret = 0;
+ goto out_interim;
+ }
+ wait_ack = wait_ack && expect_ack;
+ work = ksmbd_alloc_work_struct();
+ if (!work) {
+ ret = smb2_break_failed(opinfo, expect_ack);
+ goto out_interim;
}
+ ret = allocate_interim_rsp_buf(work);
+ if (ret)
+ goto failed;
+ oplock_rsp = smb_get_msg(work->response_buf);
+ smb2_break_init_header(&oplock_rsp->hdr);
- rsp_hdr = smb_get_msg(work->response_buf);
- memset(rsp_hdr, 0, sizeof(struct smb2_hdr) + 2);
- rsp_hdr->ProtocolId = SMB2_PROTO_NUMBER;
- rsp_hdr->StructureSize = SMB2_HEADER_STRUCTURE_SIZE;
- rsp_hdr->CreditRequest = cpu_to_le16(0);
- rsp_hdr->Command = SMB2_OPLOCK_BREAK;
- rsp_hdr->Flags = (SMB2_FLAGS_SERVER_TO_REDIR);
- rsp_hdr->NextCommand = 0;
- rsp_hdr->MessageId = cpu_to_le64(-1);
- rsp_hdr->Id.SyncId.ProcessId = 0;
- rsp_hdr->Id.SyncId.TreeId = 0;
- rsp_hdr->SessionId = 0;
- memset(rsp_hdr->Signature, 0, 16);
-
- rsp = smb_get_msg(work->response_buf);
- rsp->StructureSize = cpu_to_le16(44);
- rsp->Epoch = br_info->epoch;
- rsp->Flags = 0;
-
- if (br_info->curr_state & (SMB2_LEASE_WRITE_CACHING_LE |
- SMB2_LEASE_HANDLE_CACHING_LE))
- rsp->Flags = SMB2_NOTIFY_BREAK_LEASE_FLAG_ACK_REQUIRED;
-
- memcpy(rsp->LeaseKey, br_info->lease_key, SMB2_LEASE_KEY_SIZE);
- rsp->CurrentLeaseState = br_info->curr_state;
- rsp->NewLeaseState = br_info->new_state;
- rsp->BreakReason = 0;
- rsp->AccessMaskHint = 0;
- rsp->ShareMaskHint = 0;
-
- if (ksmbd_iov_pin_rsp(work, (void *)rsp,
- sizeof(struct smb2_lease_break)))
+ ret = smb2_break_collect_channels(opinfo, ci, &owner, &channels, oplock_rsp);
+ if (ret == -ENOENT)
goto out;
+ if (ret)
+ goto failed;
- ksmbd_conn_write(work);
-
+ spin_lock(&opinfo->state_lock);
+ if (opinfo->op_state == OPLOCK_CLOSING ||
+ (expect_ack && opinfo->op_state == OPLOCK_STATE_NONE)) {
+ spin_unlock(&opinfo->state_lock);
+ ret = 0;
+ goto out;
+ }
+ if (opinfo->is_lease) {
+ lease_rsp = smb_get_msg(work->response_buf);
+ lease_rsp->StructureSize = cpu_to_le16(44);
+ if (lease->version == 2) {
+ if (inc_epoch)
+ lease->epoch++;
+ lease_rsp->Epoch = cpu_to_le16(lease->epoch);
+ }
+ if (lease->state & (SMB2_LEASE_WRITE_CACHING_LE |
+ SMB2_LEASE_HANDLE_CACHING_LE))
+ lease_rsp->Flags = SMB2_NOTIFY_BREAK_LEASE_FLAG_ACK_REQUIRED;
+ memcpy(lease_rsp->LeaseKey, lease->lease_key, SMB2_LEASE_KEY_SIZE);
+ lease_rsp->CurrentLeaseState = lease->state;
+ lease_rsp->NewLeaseState = lease->new_state;
+ size = sizeof(*lease_rsp);
+ } else {
+ oplock_rsp->StructureSize = cpu_to_le16(24);
+ oplock_rsp->OplockLevel = !opinfo->open_trunc &&
+ (opinfo->level == SMB2_OPLOCK_LEVEL_BATCH ||
+ opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) ?
+ SMB2_OPLOCK_LEVEL_II : SMB2_OPLOCK_LEVEL_NONE;
+ /*
+ * A level II break needs no ACK. Publish NONE before sending so
+ * an unsolicited ACK cannot observe II while delivery is pending.
+ */
+ if (!expect_ack)
+ opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
+ size = sizeof(*oplock_rsp);
+ }
+ spin_unlock(&opinfo->state_lock);
+ ret = ksmbd_iov_pin_rsp(work, oplock_rsp, size);
+ if (ret)
+ goto failed;
+
+ deadline = jiffies + OPLOCK_WAIT_TIME;
+retry:
+ list_for_each_entry(channel, &channels, list) {
+ if (channel->queued || channel->failed)
+ continue;
+ if (smb2_break_complete(opinfo, expect_ack)) {
+ ret = 0;
+ goto out;
+ }
+ if (time_after_eq(jiffies, deadline)) {
+ ret = -ETIMEDOUT;
+ break;
+ }
+ if (!ksmbd_conn_r_count_inc_if_good(channel->conn)) {
+ channel->failed = true;
+ ret = -ENOTCONN;
+ continue;
+ }
+ work->conn = channel->conn;
+ ret = ksmbd_conn_write_tracked(work, &channel->tracking);
+ ksmbd_conn_r_count_dec(channel->conn);
+ if (ret) {
+ /* Only a nonblocking send guarantees no bytes on -EAGAIN. */
+ if (ret != -EAGAIN || !channel->conn->transport->ops->is_acked)
+ channel->failed = true;
+ continue;
+ }
+ channel->queued = true;
+ if (interim_work) {
+ ksmbd_queue_io_work(interim_work);
+ interim_work = NULL;
+ }
+ last = channel;
+ retry_at = jiffies + channel->tracking.retry_after;
+ ret = smb2_break_wait(opinfo, &channels, channel, expect_ack,
+ wait_ack, smb2_break_has_unsent_channels(&channels),
+ retry_at, deadline);
+ if (ret != -EAGAIN)
+ goto done;
+ }
+ if (smb2_break_has_unsent_channels(&channels)) {
+ /* Allow busy writers to make progress before another nonblocking pass. */
+ retry_at = jiffies + max_t(unsigned long, 1, HZ / 10);
+ ret = smb2_break_wait(opinfo, &channels, NULL, expect_ack,
+ wait_ack, true, retry_at, deadline);
+ if (ret == -EAGAIN)
+ goto retry;
+ } else if (last) {
+ /* Give already queued notifications the rest of the shared wait. */
+ ret = smb2_break_wait(opinfo, &channels, last, expect_ack,
+ wait_ack, false, deadline, deadline);
+ }
+
+done:
+ if (ret)
+ goto failed;
+ if (!expect_ack && opinfo->is_lease) {
+ spin_lock(&opinfo->state_lock);
+ if (opinfo->op_state != OPLOCK_CLOSING) {
+ lease->state = lease->new_state;
+ lease_update_oplock_levels(lease);
+ }
+ spin_unlock(&opinfo->state_lock);
+ }
+ goto out;
+failed:
+ ret = smb2_break_failed(opinfo, expect_ack);
out:
ksmbd_free_work_struct(work);
- ksmbd_conn_r_count_dec(conn);
- ksmbd_conn_put(conn);
+ list_for_each_entry_safe(channel, tmp, &channels, list) {
+ list_del(&channel->list);
+ ksmbd_conn_put(channel->conn);
+ if (channel != &owner)
+ kfree(channel);
+ }
+out_interim:
+ if (interim_work)
+ ksmbd_queue_io_work(interim_work);
+ return ret;
}
-/*
- * Select and pin the connection used for a lease break before doing any
- * allocations which may sleep. opinfo->conn is cleared under ci->m_lock,
- * while lease->l_lb and the lease table lifetime are protected by
- * lease_list_lock.
- */
-static struct ksmbd_conn *smb2_lease_break_conn_get(struct oplock_info *opinfo)
+struct smb2_interim_work {
+ struct work_struct work;
+ struct ksmbd_work *in_work;
+};
+
+static void smb2_break_interim_work(struct work_struct *work)
{
- struct lease *lease = opinfo->o_lease;
- struct lease_table *lb;
- struct ksmbd_conn *conn;
+ struct smb2_interim_work *iw = container_of(work, struct smb2_interim_work, work);
- /* Keep the connection which owns the open, when it is still active. */
- down_read(&lease->ci->m_lock);
- conn = READ_ONCE(opinfo->conn);
- if (conn && !ksmbd_conn_releasing(conn))
- conn = ksmbd_conn_get(conn);
- else
- conn = NULL;
- up_read(&lease->ci->m_lock);
+ if (!setup_async_work(iw->in_work, NULL, NULL)) {
+ smb2_send_interim_resp(iw->in_work, STATUS_PENDING);
+ release_async_work(iw->in_work);
+ }
+}
- if (conn || lease->version != 2)
- return conn;
+struct smb2_break_work {
+ struct work_struct work;
+ struct oplock_info *opinfo;
+ struct ksmbd_inode *ci;
+ bool expect_ack;
+ bool wait_ack;
+ bool inc_epoch;
+ int ret;
+};
- /* Otherwise route v2 lease breaks through the shared lease channel. */
- read_lock(&lease_list_lock);
- lb = lease->l_lb;
- if (lb && lb->conn && !ksmbd_conn_releasing(lb->conn))
- conn = ksmbd_conn_get(lb->conn);
- read_unlock(&lease_list_lock);
+static void smb2_break_notify_work(struct work_struct *work)
+{
+ struct smb2_break_work *bw = container_of(work, struct smb2_break_work, work);
- return conn;
+ bw->ret = smb2_break_notify(bw->opinfo, bw->ci, bw->expect_ack,
+ bw->wait_ack, bw->inc_epoch, NULL);
}
-/**
- * smb2_lease_break_noti() - break lease when a new client request
- * write lease
- * @opinfo: contains lease state information
- * @sync: send the lease break notification synchronously
- * @inc_epoch: increment the lease epoch before sending the break
- *
- * Return: 0 on success, otherwise error
- */
-static int smb2_lease_break_noti(struct oplock_info *opinfo, bool sync,
- bool inc_epoch)
+static int smb2_break_notify_with_interim(struct oplock_info *opinfo,
+ struct ksmbd_inode *ci,
+ bool expect_ack, bool wait_ack,
+ bool inc_epoch, struct ksmbd_work *in_work,
+ bool notify_first)
{
- struct ksmbd_conn *conn;
- struct ksmbd_work *work;
- struct lease_break_info *br_info;
- struct lease *lease = opinfo->o_lease;
+ struct smb2_break_work bw = {
+ .opinfo = opinfo,
+ .ci = ci,
+ .expect_ack = expect_ack,
+ .wait_ack = wait_ack,
+ .inc_epoch = inc_epoch,
+ };
- conn = smb2_lease_break_conn_get(opinfo);
- if (!conn)
- return ksmbd_invalidate_durable_fd(opinfo->fid);
+ if (!in_work)
+ return smb2_break_notify(opinfo, ci, expect_ack, wait_ack, inc_epoch, NULL);
- work = ksmbd_alloc_work_struct();
- if (!work) {
- ksmbd_conn_put(conn);
- return -ENOMEM;
- }
+ if (notify_first) {
+ struct smb2_interim_work iw = { .in_work = in_work };
+ int ret;
- br_info = kmalloc_obj(struct lease_break_info, KSMBD_DEFAULT_GFP);
- if (!br_info) {
- ksmbd_free_work_struct(work);
- ksmbd_conn_put(conn);
- return -ENOMEM;
- }
-
- br_info->curr_state = lease->state;
- br_info->new_state = lease->new_state;
- if (lease->version == 2) {
- if (inc_epoch)
- lease->epoch++;
- br_info->epoch = cpu_to_le16(lease->epoch);
- } else {
- br_info->epoch = 0;
+ /*
+ * Rename sends the first notification in the request context.
+ * Queue the interim writer after that send so a blocked response
+ * cannot stop notification retries or the break ACK wait.
+ */
+ INIT_WORK_ONSTACK(&iw.work, smb2_break_interim_work);
+ ret = smb2_break_notify(opinfo, ci, expect_ack, wait_ack,
+ inc_epoch, &iw.work);
+ flush_work(&iw.work);
+ destroy_work_on_stack(&iw.work);
+ return ret;
}
- memcpy(br_info->lease_key, lease->lease_key, SMB2_LEASE_KEY_SIZE);
- work->request_buf = (char *)br_info;
- /* Transfer the reference acquired by smb2_lease_break_conn_get(). */
- work->conn = conn;
- work->sess = opinfo->sess;
-
- ksmbd_conn_r_count_inc(conn);
- if (opinfo->op_state == OPLOCK_ACK_WAIT) {
- if (sync) {
- __smb2_lease_break_noti(&work->work);
- } else {
- INIT_WORK(&work->work, __smb2_lease_break_noti);
- ksmbd_queue_work(work);
- }
- } else {
- __smb2_lease_break_noti(&work->work);
- if (opinfo->o_lease->new_state == SMB2_LEASE_NONE_LE) {
- opinfo->o_lease->state = SMB2_LEASE_NONE_LE;
- lease_update_oplock_levels(opinfo->o_lease);
- }
+ /* A blocked interim writer must not stop notification sends or retries. */
+ INIT_WORK_ONSTACK(&bw.work, smb2_break_notify_work);
+ ksmbd_queue_break_work(&bw.work);
+ if (!setup_async_work(in_work, NULL, NULL)) {
+ smb2_send_interim_resp(in_work, STATUS_PENDING);
+ release_async_work(in_work);
}
- return 0;
+ /* The caller keeps opinfo and ci alive until the notifier has finished. */
+ flush_work(&bw.work);
+ destroy_work_on_stack(&bw.work);
+ return bw.ret;
}
static void wait_lease_breaking(struct oplock_info *opinfo)
@@ -1187,10 +1323,11 @@ static void wait_lease_breaking(struct oplock_info *opinfo)
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)
+ bool share_break, bool notify_first)
{
int err = 0;
bool sent_interim = false;
+ bool expect_ack = false;
/* Need to break exclusive/batch oplock, write lease or overwrite_if */
ksmbd_debug(OPLOCK,
@@ -1248,8 +1385,9 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
}
}
- if (lease->state & (SMB2_LEASE_WRITE_CACHING_LE |
- SMB2_LEASE_HANDLE_CACHING_LE)) {
+ expect_ack = lease->state & (SMB2_LEASE_WRITE_CACHING_LE |
+ SMB2_LEASE_HANDLE_CACHING_LE);
+ if (expect_ack) {
if (!oplock_break_set_ack_wait(brk_opinfo)) {
atomic_dec_if_positive(&brk_opinfo->breaking_cnt);
clear_and_wake_up_bit(0, &brk_opinfo->pending_break);
@@ -1265,16 +1403,12 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
inc_epoch = false;
lease->reuse_epoch = false;
}
- err = smb2_lease_break_noti(brk_opinfo, sync_lease_break, inc_epoch);
+ err = smb2_break_notify_with_interim(brk_opinfo, ci, expect_ack,
+ wait_ack, inc_epoch,
+ sent_interim ? NULL : in_work,
+ notify_first);
+ sent_interim = true;
inc_epoch = false;
- if (in_work && !sent_interim) {
- setup_async_work(in_work, NULL, NULL);
- smb2_send_interim_resp(in_work, STATUS_PENDING);
- release_async_work(in_work);
- sent_interim = true;
- }
- if (wait_ack && !err && wait_for_break_ack(brk_opinfo))
- err = ksmbd_invalidate_durable_fd(brk_opinfo->fid);
ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level);
if (brk_opinfo->op_state == OPLOCK_CLOSING)
@@ -1310,26 +1444,16 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
if (brk_opinfo->level == SMB2_OPLOCK_LEVEL_BATCH ||
brk_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) {
+ expect_ack = true;
if (!oplock_break_set_ack_wait(brk_opinfo)) {
clear_and_wake_up_bit(0, &brk_opinfo->pending_break);
return -ENOENT;
}
}
-
- /*
- * Keep a conflicting CREATE asynchronous while waiting for an
- * oplock-break acknowledgement. Besides avoiding a blocked client
- * request, this lets a replay arrive while the original CREATE is
- * still pending and be rejected with FILE_NOT_AVAILABLE.
- */
- if (in_work) {
- setup_async_work(in_work, NULL, NULL);
- smb2_send_interim_resp(in_work, STATUS_PENDING);
- release_async_work(in_work);
- }
}
- err = smb2_oplock_break_noti(brk_opinfo, ci);
+ err = smb2_break_notify_with_interim(brk_opinfo, ci, expect_ack,
+ true, false, in_work, false);
ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level);
if (brk_opinfo->op_state == OPLOCK_CLOSING)
@@ -1491,7 +1615,7 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp,
down_read(&p_ci->m_lock);
list_for_each_entry(opinfo, &p_ci->m_op_list, op_entry) {
- if (opinfo->conn == NULL || !opinfo->is_lease)
+ if (!opinfo->is_lease)
continue;
if (opinfo->o_lease->state != SMB2_OPLOCK_LEVEL_NONE &&
@@ -1502,11 +1626,6 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp,
if (!atomic_inc_not_zero(&opinfo->refcount))
continue;
- if (ksmbd_conn_releasing(opinfo->conn)) {
- opinfo_put(opinfo);
- continue;
- }
-
if (oplock_break_add(&brk_list, opinfo))
opinfo_put(opinfo);
}
@@ -1539,18 +1658,13 @@ void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp)
down_read(&p_ci->m_lock);
list_for_each_entry(opinfo, &p_ci->m_op_list, op_entry) {
- if (opinfo->conn == NULL || !opinfo->is_lease)
+ if (!opinfo->is_lease)
continue;
if (opinfo->o_lease->state != SMB2_OPLOCK_LEVEL_NONE) {
if (!atomic_inc_not_zero(&opinfo->refcount))
continue;
- if (ksmbd_conn_releasing(opinfo->conn)) {
- opinfo_put(opinfo);
- continue;
- }
-
if (oplock_break_add(&brk_list, opinfo))
opinfo_put(opinfo);
}
@@ -1699,7 +1813,11 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid,
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))
+ /* Keep a detached durable lease if its break retains handle caching. */
+ if ((prev_durable_detached &&
+ (!prev_op_has_lease ||
+ !(prev_opinfo->o_lease->state & SMB2_LEASE_HANDLE_CACHING_LE))) ||
+ (prev_durable_open && err == -ENOENT))
ksmbd_invalidate_durable_fd(prev_fid);
opinfo_put(prev_opinfo);
if (err == -EAGAIN) {
@@ -1803,8 +1921,7 @@ static bool smb_break_all_write_oplock(struct ksmbd_work *work,
}
brk_opinfo->open_trunc = is_trunc;
- oplock_break(brk_opinfo, fp->f_ci, 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);
@@ -1819,12 +1936,12 @@ static bool smb_break_all_write_oplock(struct ksmbd_work *work,
* @is_trunc: truncate on open
* @send_interim: send interim response to the client
* @send_oplock_break: send oplock break notification to the client
- * @sync_lease_break: send the lease break notification synchronously
+ * @notify_first: send the lease break notification before the interim response
*/
static void __smb_break_all_levII_oplock(struct ksmbd_work *work,
struct ksmbd_file *fp, int is_trunc,
bool send_interim, bool send_oplock_break,
- bool sync_lease_break)
+ bool notify_first)
{
struct oplock_info *op, *brk_op;
struct oplock_break_entry *ent, *tmp;
@@ -1842,17 +1959,9 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work,
down_read(&ci->m_lock);
list_for_each_entry(brk_op, &ci->m_op_list, op_entry) {
- if (brk_op->conn == NULL)
- continue;
-
if (!atomic_inc_not_zero(&brk_op->refcount))
continue;
- if (ksmbd_conn_releasing(brk_op->conn)) {
- opinfo_put(brk_op);
- continue;
- }
-
if (!brk_op->is_lease &&
brk_op->level != SMB2_OPLOCK_LEVEL_II) {
ksmbd_debug(OPLOCK, "unexpected oplock(0x%x)\n",
@@ -1867,10 +1976,8 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work,
goto next;
if (op && op->is_lease && brk_op->is_lease &&
- !memcmp(conn->ClientGUID, brk_op->conn->ClientGUID,
- SMB2_CLIENT_GUID_SIZE) &&
- !memcmp(op->o_lease->lease_key, brk_op->o_lease->lease_key,
- SMB2_LEASE_KEY_SIZE))
+ compare_guid_key(brk_op, conn->ClientGUID,
+ op->o_lease->lease_key))
goto next;
brk_op->open_trunc = is_trunc;
@@ -1900,7 +2007,7 @@ static void __smb_break_all_levII_oplock(struct ksmbd_work *work,
brk_op->is_lease && !is_trunc ?
SMB2_OPLOCK_LEVEL_II : SMB2_OPLOCK_LEVEL_NONE,
send_interim && !sent_interim ? work : NULL,
- false, sync_lease_break);
+ false, notify_first);
}
sent_interim = true;
list_del(&ent->list);
diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h
index b08d21758e07..e114e2148c30 100644
--- a/fs/smb/server/oplock.h
+++ b/fs/smb/server/oplock.h
@@ -62,6 +62,7 @@ struct lease {
struct oplock_info {
struct ksmbd_conn *conn;
struct ksmbd_session *sess;
+ u64 session_id;
struct ksmbd_work *work;
struct ksmbd_file *o_fp;
int level;
@@ -82,19 +83,6 @@ struct oplock_info {
struct rcu_head rcu;
};
-struct lease_break_info {
- __le32 curr_state;
- __le32 new_state;
- __le16 epoch;
- char lease_key[SMB2_LEASE_KEY_SIZE];
-};
-
-struct oplock_break_info {
- int level;
- int open_trunc;
- int fid;
-};
-
int smb_grant_oplock(struct ksmbd_work *work, int req_op_level,
u64 pid, struct ksmbd_file *fp, __u16 tid,
struct lease_ctx_info *lctx, int share_ret, bool replay);
diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c
index 496055ce4a4d..11c123673f9a 100644
--- a/fs/smb/server/vfs_cache.c
+++ b/fs/smb/server/vfs_cache.c
@@ -1767,6 +1767,7 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon,
ksmbd_conn_put(op->conn);
op->conn = NULL;
op->sess = NULL;
+ op->session_id = 0;
}
up_write(&ci->m_lock);
@@ -1953,6 +1954,7 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp)
continue;
op->conn = ksmbd_conn_get(fp->conn);
op->sess = work->sess;
+ op->session_id = work->sess->id;
}
up_write(&ci->m_lock);
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 05/22] ksmbd: keep lease tables until their last lease is closed
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (2 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 04/22] ksmbd: retry break notifications on another connection Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 06/22] ksmbd: index byte-range locks independently of their connection Namjae Jeon
` (16 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon, Pan Guihua
Disconnecting one channel destroys the lease table for its ClientGuid,
even when another channel still uses the same opens. A lease break ACK
on the surviving channel can no longer find the lease. Detached durable
opens also lose their lease lookup entries before reconnect or expiry.
Tie a client table to its lease entries instead of the connection which
created it. Remove the table when its last lease is removed, and release
any remaining entries at server shutdown. Do not destroy client tables
from connection teardown or keep a connection reference in each table.
For a detached lease, collect break candidates using the table's
ClientGuid directly. The existing notification retry path takes its own
references on matching live connections.
Fixes: 0626e6641f6b ("cifsd: add server handler for central processing and tranport layers")
Reported-by: Pan Guihua <pan.guihua@datatom.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/oplock.c | 46 +++++++++++++++++-------------------------
fs/smb/server/oplock.h | 3 +--
fs/smb/server/server.c | 3 +--
3 files changed, 20 insertions(+), 32 deletions(-)
diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c
index 313bf01ad350..96b36b8329a6 100644
--- a/fs/smb/server/oplock.c
+++ b/fs/smb/server/oplock.c
@@ -125,6 +125,12 @@ static void lease_add_table(struct lease *lease, struct lease_table *lb)
spin_unlock(&lb->lb_lock);
}
+static void free_lease_table(struct lease_table *lb)
+{
+ kfree(lb);
+}
+
+/* The caller holds lease_list_lock for write. */
static void lease_del_table(struct lease *lease)
{
struct lease_table *lb = lease->l_lb;
@@ -143,6 +149,10 @@ static void lease_del_table(struct lease *lease)
spin_unlock(&lb->lb_lock);
lease_put(lease);
+ if (list_empty(&lb->lease_list)) {
+ list_del(&lb->l_entry);
+ free_lease_table(lb);
+ }
}
static struct lease_table *alloc_lease_table(struct oplock_info *opinfo)
@@ -155,21 +165,11 @@ static struct lease_table *alloc_lease_table(struct oplock_info *opinfo)
memcpy(lb->client_guid, opinfo->conn->ClientGUID,
SMB2_CLIENT_GUID_SIZE);
- lb->conn = ksmbd_conn_get(opinfo->conn);
INIT_LIST_HEAD(&lb->lease_list);
spin_lock_init(&lb->lb_lock);
return lb;
}
-static void free_lease_table(struct lease_table *lb)
-{
- if (!lb)
- return;
-
- ksmbd_conn_put(lb->conn);
- kfree(lb);
-}
-
static struct lease *alloc_lease(struct lease_ctx_info *lctx,
struct ksmbd_inode *ci)
{
@@ -927,8 +927,6 @@ static int smb2_break_collect_channels(struct oplock_info *opinfo,
if (lb) {
memcpy(guid, lb->client_guid, sizeof(guid));
have_guid = true;
- if (lb->conn)
- owner->conn = ksmbd_conn_get(lb->conn);
}
read_unlock(&lease_list_lock);
}
@@ -1495,25 +1493,17 @@ static void oplock_break_drain_none(struct list_head *head,
}
}
-void destroy_lease_table(struct ksmbd_conn *conn)
+void destroy_lease_table(void)
{
- struct lease_table *lb, *lbtmp;
- struct lease *lease, *ltmp;
+ struct lease_table *lb;
+ struct lease *lease;
write_lock(&lease_list_lock);
- if (list_empty(&lease_table_list)) {
- write_unlock(&lease_list_lock);
- return;
- }
-
- list_for_each_entry_safe(lb, lbtmp, &lease_table_list, l_entry) {
- if (conn && memcmp(lb->client_guid, conn->ClientGUID,
- SMB2_CLIENT_GUID_SIZE))
- continue;
- list_for_each_entry_safe(lease, ltmp, &lb->lease_list, l_entry)
- lease_del_table(lease);
- list_del(&lb->l_entry);
- free_lease_table(lb);
+ while (!list_empty(&lease_table_list)) {
+ lb = list_first_entry(&lease_table_list, struct lease_table,
+ l_entry);
+ lease = list_first_entry(&lb->lease_list, struct lease, l_entry);
+ lease_del_table(lease);
}
write_unlock(&lease_list_lock);
}
diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h
index e114e2148c30..5ef05166e502 100644
--- a/fs/smb/server/oplock.h
+++ b/fs/smb/server/oplock.h
@@ -34,7 +34,6 @@ struct lease_ctx_info {
struct lease_table {
char client_guid[SMB2_CLIENT_GUID_SIZE];
- struct ksmbd_conn *conn;
struct list_head lease_list;
struct list_head l_entry;
spinlock_t lb_lock;
@@ -122,7 +121,7 @@ struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn,
char *lease_key);
int find_same_lease_key(struct ksmbd_conn *conn, struct ksmbd_inode *ci,
struct lease_ctx_info *lctx);
-void destroy_lease_table(struct ksmbd_conn *conn);
+void destroy_lease_table(void);
void smb_send_parent_lease_break_noti(struct ksmbd_file *fp,
struct lease_ctx_info *lctx);
void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp);
diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c
index 1cb995a36b21..effc539548dc 100644
--- a/fs/smb/server/server.c
+++ b/fs/smb/server/server.c
@@ -436,7 +436,6 @@ static int ksmbd_server_process_request(struct ksmbd_conn *conn)
static int ksmbd_server_terminate_conn(struct ksmbd_conn *conn)
{
ksmbd_conn_sessions_cleanup(conn);
- destroy_lease_table(conn);
return 0;
}
@@ -664,7 +663,7 @@ static int ksmbd_server_shutdown(void)
ksmbd_proc_cleanup();
ksmbd_crypto_destroy();
ksmbd_free_global_file_table();
- destroy_lease_table(NULL);
+ destroy_lease_table();
ksmbd_work_pool_destroy();
ksmbd_exit_file_cache();
server_conf_free();
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 06/22] ksmbd: index byte-range locks independently of their connection
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (3 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 05/22] ksmbd: keep lease tables until their last lease is closed Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 07/22] ksmbd: rebind completed CREATE replays without draining the old channel Namjae Jeon
` (15 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon, Pan Guihua
SMB locks belong to an open, but the lock conflict and UNLOCK scan only
visits connections in conn_list. When a channel is removed, its locks
remain attached to live opens and become invisible to another channel.
An exact UNLOCK returns STATUS_RANGE_NOT_LOCKED, and zero-byte lock
conflicts are no longer detected. A request finishing on the old channel
after it is removed can publish another invisible lock.
Index granted SMB locks by VFS inode, with the open's lock_list retaining
ownership. Protect lookup, publication and removal with one spinlock.
Use the inode rather than the dentry-based ksmbd_inode so hard-link opens
still participate in the same conflict checks.
Keep these entries through durable detach and reconnect. Publish a batch
only after the existing LOCK handler commits it, and remove every entry
before final close drops the file reference. Drop the per-lock connection
references and connection-local lists; channel changes no longer require
moving lock entries or waiting for unrelated requests to finish.
Prefer an exclusive lock when several locks on an open match an UNLOCK.
This preserves stacked zero-byte lock behavior despite the inode hash
inserting newer locks before older entries.
Fixes: d63528eb0d43 ("ksmbd: free ksmbd_lock when file is closed")
Reported-by: Pan Guihua <pan.guihua@datatom.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/connection.c | 2 -
fs/smb/server/connection.h | 2 -
fs/smb/server/smb2pdu.c | 104 ++-----------------------
fs/smb/server/vfs_cache.c | 150 ++++++++++++++++++++++++++-----------
fs/smb/server/vfs_cache.h | 7 +-
5 files changed, 120 insertions(+), 145 deletions(-)
diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c
index 19d32681bb92..0a4be9e423a3 100644
--- a/fs/smb/server/connection.c
+++ b/fs/smb/server/connection.c
@@ -327,8 +327,6 @@ struct ksmbd_conn *ksmbd_conn_alloc(void)
ida_init(&conn->async_ida);
xa_init(&conn->sessions);
- spin_lock_init(&conn->llist_lock);
- INIT_LIST_HEAD(&conn->lock_list);
init_rwsem(&conn->session_lock);
diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h
index 85e2d2073cd2..53eb91f618b4 100644
--- a/fs/smb/server/connection.h
+++ b/fs/smb/server/connection.h
@@ -112,8 +112,6 @@ struct ksmbd_conn {
char ClientGUID[SMB2_CLIENT_GUID_SIZE];
struct ntlmssp_auth ntlmssp;
- spinlock_t llist_lock;
- struct list_head lock_list;
struct preauth_integrity_info *preauth_info;
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index b45453a29fbb..853a892e2ad5 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -10271,7 +10271,7 @@ static struct ksmbd_lock *smb2_lock_init(struct file_lock *flock,
lock->end = flock->fl_end;
lock->flags = flags;
lock->zero_len = zero_len;
- INIT_LIST_HEAD(&lock->clist);
+ INIT_HLIST_NODE(&lock->hlist);
INIT_LIST_HEAD(&lock->flist);
INIT_LIST_HEAD(&lock->llist);
list_add_tail(&lock->llist, lock_list);
@@ -10300,12 +10300,6 @@ static void smb2_free_blocked_lock(struct file_lock *flock)
locks_free_lock(flock);
}
-static inline bool lock_defer_pending(struct file_lock *fl)
-{
- /* check pending lock waiters */
- return waitqueue_active(&fl->c.flc_wait);
-}
-
/**
* smb2_lock() - handler for smb2 file lock command
* @work: smb work containing lock command buffer
@@ -10325,12 +10319,10 @@ int smb2_lock(struct ksmbd_work *work)
int cmd = 0;
int err = -EIO, i, rc = 0;
u64 lock_start, lock_length;
- struct ksmbd_lock *smb_lock = NULL, *cmp_lock, *tmp, *tmp2;
- struct ksmbd_conn *conn;
- int nolock = 0;
+ struct ksmbd_lock *smb_lock = NULL, *cmp_lock, *tmp;
LIST_HEAD(lock_list);
LIST_HEAD(rollback_list);
- int prior_lock = 0, bkt;
+ int prior_lock = 0;
unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID;
bool lock_replayed;
@@ -10463,79 +10455,10 @@ int smb2_lock(struct ksmbd_work *work)
!(smb_lock->flags & SMB2_LOCKFLAG_FAIL_IMMEDIATELY))
goto no_check_cl;
- nolock = 1;
- /* check locks in connection list */
- down_read(&conn_list_lock);
- hash_for_each(conn_list, bkt, conn, hlist) {
- spin_lock(&conn->llist_lock);
- list_for_each_entry_safe(cmp_lock, tmp2, &conn->lock_list, clist) {
- if (file_inode(cmp_lock->fl->c.flc_file) !=
- file_inode(smb_lock->fl->c.flc_file))
- continue;
-
- if (lock_is_unlock(smb_lock->fl)) {
- if (cmp_lock->fl->c.flc_file == smb_lock->fl->c.flc_file &&
- cmp_lock->start == smb_lock->start &&
- cmp_lock->end == smb_lock->end &&
- !lock_defer_pending(cmp_lock->fl)) {
- nolock = 0;
- list_del_init(&cmp_lock->flist);
- list_del_init(&cmp_lock->clist);
- cmp_lock->conn = NULL;
- spin_unlock(&conn->llist_lock);
- up_read(&conn_list_lock);
-
- ksmbd_conn_put(conn);
- smb2_free_lock(cmp_lock->fl);
- kfree(cmp_lock);
- goto out_check_cl;
- }
- continue;
- }
-
- if (cmp_lock->fl->c.flc_file == smb_lock->fl->c.flc_file) {
- if (smb_lock->flags & SMB2_LOCKFLAG_SHARED)
- continue;
- } else {
- if (cmp_lock->flags & SMB2_LOCKFLAG_SHARED)
- continue;
- }
-
- /* check zero byte lock range */
- if (cmp_lock->zero_len && !smb_lock->zero_len &&
- cmp_lock->start > smb_lock->start &&
- cmp_lock->start <= smb_lock->end) {
- spin_unlock(&conn->llist_lock);
- up_read(&conn_list_lock);
- pr_err("previous lock conflict with zero byte lock range\n");
- goto out;
- }
-
- if (smb_lock->zero_len && !cmp_lock->zero_len &&
- smb_lock->start > cmp_lock->start &&
- smb_lock->start <= cmp_lock->end) {
- spin_unlock(&conn->llist_lock);
- up_read(&conn_list_lock);
- pr_err("current lock conflict with zero byte lock range\n");
- goto out;
- }
-
- if (cmp_lock->start <= smb_lock->end &&
- smb_lock->start <= cmp_lock->end &&
- !cmp_lock->zero_len && !smb_lock->zero_len) {
- spin_unlock(&conn->llist_lock);
- up_read(&conn_list_lock);
- pr_err("Not allow lock operation on exclusive lock range\n");
- goto out;
- }
- }
- spin_unlock(&conn->llist_lock);
- }
- up_read(&conn_list_lock);
-out_check_cl:
- if (lock_is_unlock(smb_lock->fl) && nolock) {
- pr_err("Try to unlock nolocked range\n");
- rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED;
+ err = ksmbd_file_lock_check(smb_lock);
+ if (err) {
+ if (err == -ENOENT)
+ rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED;
goto out;
}
@@ -10649,18 +10572,7 @@ int smb2_lock(struct ksmbd_work *work)
goto out;
/* publish only once the whole batch has committed */
- if (!list_empty(&rollback_list)) {
- spin_lock(&work->conn->llist_lock);
- list_for_each_entry_safe(smb_lock, tmp, &rollback_list, llist) {
- list_del_init(&smb_lock->llist);
- smb_lock->conn = ksmbd_conn_get(work->conn);
- list_add_tail(&smb_lock->clist,
- &work->conn->lock_list);
- list_add_tail(&smb_lock->flist,
- &fp->lock_list);
- }
- spin_unlock(&work->conn->llist_lock);
- }
+ ksmbd_file_lock_add(fp, &rollback_list);
if (!lock_replayed)
smb2_update_lock_sequence(work, fp, req);
diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c
index 11c123673f9a..459783910c04 100644
--- a/fs/smb/server/vfs_cache.c
+++ b/fs/smb/server/vfs_cache.c
@@ -11,6 +11,7 @@
#include <linux/kthread.h>
#include <linux/freezer.h>
#include <linux/dcache.h>
+#include <linux/hashtable.h>
#include "glob.h"
#include "vfs_cache.h"
@@ -38,6 +39,10 @@ static struct ksmbd_file_table global_ft;
static atomic_long_t fd_limit;
static struct kmem_cache *filp_cache;
+/* Both the inode hash and each open's lock_list use this lock. */
+static DEFINE_SPINLOCK(file_locks_lock);
+static DEFINE_HASHTABLE(file_locks, 8);
+
static int ksmbd_mark_fp_closed(struct ksmbd_file *fp);
#define OPLOCK_NONE 0
@@ -613,10 +618,109 @@ static void __ksmbd_remove_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp
write_unlock(&ft->lock);
}
+/**
+ * ksmbd_file_lock_check() - check conflicts or remove an exact unlock match
+ * @lock: unregistered lock from the current request
+ *
+ * Return: 0 on success, -ENOENT for an unmatched unlock, or -EAGAIN on conflict.
+ */
+int ksmbd_file_lock_check(struct ksmbd_lock *lock)
+{
+ struct inode *inode = file_inode(lock->fl->c.flc_file);
+ struct ksmbd_lock *cmp, *lock_to_remove = NULL;
+ int ret = lock_is_unlock(lock->fl) ? -ENOENT : 0;
+
+ spin_lock(&file_locks_lock);
+ hash_for_each_possible(file_locks, cmp, hlist, (unsigned long)inode) {
+ if (file_inode(cmp->fl->c.flc_file) != inode)
+ continue;
+
+ if (lock_is_unlock(lock->fl)) {
+ if (cmp->fl->c.flc_file != lock->fl->c.flc_file ||
+ cmp->start != lock->start || cmp->end != lock->end)
+ continue;
+ if (waitqueue_active(&cmp->fl->c.flc_wait)) /* VFS waiters */
+ continue;
+
+ /* Prefer an exclusive lock over matching shared locks. */
+ lock_to_remove = cmp;
+ if (cmp->flags & SMB2_LOCKFLAG_EXCLUSIVE)
+ break;
+ continue;
+ }
+
+ if (cmp->fl->c.flc_file == lock->fl->c.flc_file) {
+ if (lock->flags & SMB2_LOCKFLAG_SHARED)
+ continue;
+ } else if (cmp->flags & SMB2_LOCKFLAG_SHARED) {
+ continue;
+ }
+
+ if ((cmp->zero_len && !lock->zero_len &&
+ cmp->start > lock->start && cmp->start <= lock->end) ||
+ (lock->zero_len && !cmp->zero_len &&
+ lock->start > cmp->start && lock->start <= cmp->end) ||
+ (!cmp->zero_len && !lock->zero_len &&
+ cmp->start <= lock->end && lock->start <= cmp->end)) {
+ ret = -EAGAIN;
+ break;
+ }
+ }
+ if (lock_to_remove) {
+ hash_del(&lock_to_remove->hlist);
+ list_del_init(&lock_to_remove->flist);
+ ret = 0;
+ }
+ spin_unlock(&file_locks_lock);
+ if (lock_to_remove) {
+ ksmbd_vfs_posix_lock_unblock(lock_to_remove->fl);
+ locks_free_lock(lock_to_remove->fl);
+ kfree(lock_to_remove);
+ }
+ return ret;
+}
+
+/**
+ * ksmbd_file_lock_add() - publish a committed batch independently of its channel
+ * @fp: open holding the locks
+ * @locks: granted locks awaiting publication
+ */
+void ksmbd_file_lock_add(struct ksmbd_file *fp, struct list_head *locks)
+{
+ struct ksmbd_lock *lock, *tmp;
+
+ spin_lock(&file_locks_lock);
+ list_for_each_entry_safe(lock, tmp, locks, llist) {
+ list_del_init(&lock->llist);
+ hash_add(file_locks, &lock->hlist,
+ (unsigned long)file_inode(fp->filp));
+ list_add_tail(&lock->flist, &fp->lock_list);
+ }
+ spin_unlock(&file_locks_lock);
+}
+
+static void ksmbd_file_lock_clear(struct ksmbd_file *fp)
+{
+ struct ksmbd_lock *lock, *tmp;
+ LIST_HEAD(dispose);
+
+ spin_lock(&file_locks_lock);
+ list_for_each_entry(lock, &fp->lock_list, flist)
+ hash_del(&lock->hlist);
+ list_splice_init(&fp->lock_list, &dispose);
+ spin_unlock(&file_locks_lock);
+
+ list_for_each_entry_safe(lock, tmp, &dispose, flist) {
+ list_del_init(&lock->flist);
+ ksmbd_vfs_posix_lock_unblock(lock->fl);
+ locks_free_lock(lock->fl);
+ kfree(lock);
+ }
+}
+
static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp)
{
struct file *filp;
- struct ksmbd_lock *smb_lock, *tmp_lock;
fd_limit_close();
ksmbd_remove_durable_fd(fp);
@@ -626,31 +730,12 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp)
close_id_del_oplock(fp);
filp = fp->filp;
+ /* Remove lookup entries before dropping the file's inode reference. */
+ ksmbd_file_lock_clear(fp);
__ksmbd_inode_close(fp);
if (!IS_ERR_OR_NULL(filp))
fput(filp);
- /*
- * The zero fp reference count serializes access to fp->lock_list, but
- * the VFS may still have blocked requests chained below these locks.
- */
- list_for_each_entry_safe(smb_lock, tmp_lock, &fp->lock_list, flist) {
- struct ksmbd_conn *conn = smb_lock->conn;
-
- if (conn) {
- spin_lock(&conn->llist_lock);
- list_del_init(&smb_lock->clist);
- smb_lock->conn = NULL;
- spin_unlock(&conn->llist_lock);
- ksmbd_conn_put(conn);
- }
-
- list_del_init(&smb_lock->flist);
- ksmbd_vfs_posix_lock_unblock(smb_lock->fl);
- locks_free_lock(smb_lock->fl);
- kfree(smb_lock);
- }
-
/*
* Drop fp's strong reference on conn (taken in ksmbd_open_fd() /
* ksmbd_reopen_durable_fd()). Durable fps that reached the
@@ -1738,7 +1823,6 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon,
struct ksmbd_inode *ci;
struct oplock_info *op;
struct ksmbd_conn *conn;
- struct ksmbd_lock *smb_lock, *tmp_lock;
if (!is_reconnectable(fp))
return false;
@@ -1771,18 +1855,6 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon,
}
up_write(&ci->m_lock);
- list_for_each_entry_safe(smb_lock, tmp_lock, &fp->lock_list, flist) {
- struct ksmbd_conn *lock_conn = smb_lock->conn;
-
- if (!lock_conn)
- continue;
- spin_lock(&lock_conn->llist_lock);
- list_del_init(&smb_lock->clist);
- smb_lock->conn = NULL;
- spin_unlock(&lock_conn->llist_lock);
- ksmbd_conn_put(lock_conn);
- }
-
spin_lock(&fp->f_lock);
fp->conn = NULL;
fp->tcon = NULL;
@@ -1889,7 +1961,6 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp)
struct oplock_info *op;
struct ksmbd_conn *conn = work->conn;
struct ksmbd_share_config *old_share;
- struct ksmbd_lock *smb_lock;
unsigned int old_f_state;
write_lock(&global_ft.lock);
@@ -1939,13 +2010,6 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp)
return -EBADF;
}
- list_for_each_entry(smb_lock, &fp->lock_list, flist) {
- smb_lock->conn = ksmbd_conn_get(conn);
- spin_lock(&conn->llist_lock);
- list_add_tail(&smb_lock->clist, &conn->lock_list);
- spin_unlock(&conn->llist_lock);
- }
-
ci = fp->f_ci;
down_write(&ci->m_lock);
list_for_each_entry_rcu(op, &ci->m_op_list, op_entry,
diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h
index 706633bb0419..6df48ae04548 100644
--- a/fs/smb/server/vfs_cache.h
+++ b/fs/smb/server/vfs_cache.h
@@ -34,11 +34,11 @@
struct ksmbd_conn;
struct ksmbd_session;
+struct ksmbd_file;
struct ksmbd_lock {
struct file_lock *fl;
- struct ksmbd_conn *conn;
- struct list_head clist;
+ struct hlist_node hlist;
struct list_head flist;
struct list_head llist;
unsigned int flags;
@@ -48,6 +48,9 @@ struct ksmbd_lock {
unsigned long long end;
};
+int ksmbd_file_lock_check(struct ksmbd_lock *lock);
+void ksmbd_file_lock_add(struct ksmbd_file *fp, struct list_head *locks);
+
struct stream {
char *name;
ssize_t size;
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 07/22] ksmbd: rebind completed CREATE replays without draining the old channel
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (4 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 06/22] ksmbd: index byte-range locks independently of their connection Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 08/22] ksmbd: track SMB2 locks above OFFSET_MAX in server lock table Namjae Jeon
` (14 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon, Pan Guihua
A completed DurableHandleReqV2 CREATE can be replayed through another
channel while its original channel is exiting. The existing replay path
returns the open but leaves its connection and oplock references pointing
at the old channel. Its open count also stays on that channel, and a
later CLOSE through the surviving channel decrements the wrong counter.
After checking the owner, lease key and persistent flag, verify that an
active open is still published in the requesting session's file table.
Do this even when no oplock was granted, and before processing a consumed
replay. Do not read a borrowed opinfo session pointer for this check.
Keep a healthy original binding. If the original channel is exiting or
releasing, move the open and its own oplock references to the requesting
channel. Serialize this with CLOSE using the session file-table lock,
and with detach, reconnect and notification snapshots using m_lock.
The earlier lock index and lease-table changes make their lifetimes
independent of this connection change. No old-channel r_count wait is
needed, including when an unrelated request remains active there.
Account final close against fp->conn in one place, balance durable detach
and reconnect, and transfer the count with a replay binding change. A
final global lookup put may run after CLOSE has removed the volatile id;
unlink the open from the inode list on that final close as well.
A live completed CREATE replay may follow a break that lowered the
durable open's batch oplock. Apply the batch eligibility check only to
detached reconnects, while retaining the owner, lease key and persistent
flag checks for active replays.
Fixes: bb8bf7eb13b5 ("ksmbd: add SMB3 request replay support")
Reported-by: Pan Guihua <pan.guihua@datatom.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/smb2pdu.c | 59 ++++----------
fs/smb/server/vfs_cache.c | 164 ++++++++++++++++++++++----------------
fs/smb/server/vfs_cache.h | 1 +
3 files changed, 112 insertions(+), 112 deletions(-)
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 853a892e2ad5..d2f02a0957a7 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -3747,7 +3747,7 @@ static int smb2_check_durable_replay(struct ksmbd_work *work,
struct oplock_info *opinfo;
int ret = 0;
- if (!fp->is_durable && !fp->is_persistent)
+ if (!fp->conn && !fp->is_durable && !fp->is_persistent)
return -EACCES;
if (ksmbd_vfs_compare_durable_owner(fp, work->sess->user) == false)
@@ -3760,11 +3760,6 @@ static int smb2_check_durable_replay(struct ksmbd_work *work,
if (!opinfo)
return 0;
- if (opinfo->sess && opinfo->sess->id != work->sess->id) {
- ret = -ENOEXEC;
- goto out;
- }
-
if (opinfo->is_lease) {
if (!lc ||
memcmp(opinfo->o_lease->lease_key, lc->lease_key,
@@ -3778,7 +3773,9 @@ static int smb2_check_durable_replay(struct ksmbd_work *work,
goto out;
}
- if (fp->is_durable && opinfo->level != SMB2_OPLOCK_LEVEL_BATCH)
+ /* A live CREATE replay may follow a completed oplock break. */
+ if (!fp->conn && fp->is_durable &&
+ opinfo->level != SMB2_OPLOCK_LEVEL_BATCH)
ret = -EACCES;
}
out:
@@ -3982,35 +3979,18 @@ static int parse_durable_handle_context(struct ksmbd_work *work,
goto out;
}
- if (!dh_info->fp->is_durable &&
- !dh_info->fp->is_persistent) {
- /*
- * A DurableHandleReqV2 CREATE can complete
- * without granting durability (for example, if
- * it requested no oplock). Its CreateGuid still
- * identifies a completed CREATE for replay.
- */
- if (dh_info->fp->conn &&
- ksmbd_vfs_compare_durable_owner(
- dh_info->fp, work->sess->user)) {
- if (smb2_durable_replay_consumed(
- dh_info->fp)) {
- ksmbd_put_durable_fd(dh_info->fp);
- dh_info->fp = NULL;
- dh_info->type = dh_idx;
- dh_info->replay_consumed = true;
- break;
- }
- if (smb2_durable_replay_differs(
- dh_info->fp, req))
- smb2_mark_durable_replay_consumed(
- dh_info->fp);
- dh_info->replay = true;
- dh_info->type = dh_idx;
- goto out;
- }
+ err = smb2_check_durable_replay(work,
+ dh_info->fp,
+ lc,
+ dh_info->persistent);
+ if (err) {
+ ksmbd_put_durable_fd(dh_info->fp);
+ goto out;
+ }
+
+ err = ksmbd_rebind_replay_fd(work, dh_info->fp);
+ if (err) {
ksmbd_put_durable_fd(dh_info->fp);
- err = -EACCES;
goto out;
}
@@ -4023,15 +4003,6 @@ static int parse_durable_handle_context(struct ksmbd_work *work,
break;
}
- err = smb2_check_durable_replay(work,
- dh_info->fp,
- lc,
- dh_info->persistent);
- if (err) {
- ksmbd_put_durable_fd(dh_info->fp);
- goto out;
- }
-
if (dh_info->fp->conn) {
if (smb2_durable_replay_differs(dh_info->fp,
req))
diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c
index 459783910c04..c5217433299e 100644
--- a/fs/smb/server/vfs_cache.c
+++ b/fs/smb/server/vfs_cache.c
@@ -610,7 +610,7 @@ static void __ksmbd_remove_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp
list_del_init(&fp->node);
up_write(&fp->f_ci->m_lock);
- if (!has_file_id(fp->volatile_id))
+ if (!ft || !has_file_id(fp->volatile_id))
return;
write_lock(&ft->lock);
@@ -724,8 +724,7 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp)
fd_limit_close();
ksmbd_remove_durable_fd(fp);
- if (ft)
- __ksmbd_remove_fd(ft, fp);
+ __ksmbd_remove_fd(ft, fp);
close_id_del_oplock(fp);
filp = fp->filp;
@@ -743,6 +742,7 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp)
* in which case there is nothing to drop here.
*/
if (fp->conn) {
+ atomic_dec(&fp->conn->stats.open_files_count);
ksmbd_conn_put(fp->conn);
fp->conn = NULL;
}
@@ -860,22 +860,7 @@ static struct ksmbd_file *__ksmbd_lookup_fd(struct ksmbd_file_table *ft,
static void __put_fd_final(struct ksmbd_work *work, struct ksmbd_file *fp)
{
- /*
- * Detached durable fp -- session_fd_check() cleared fp->conn at
- * preserve, so this fp is no longer tracked by any conn's
- * stats.open_files_count. This happens when
- * ksmbd_scavenger_dispose_dh() hands the final close off to an
- * m_fp_list walker (e.g. ksmbd_lookup_fd_inode()) whose work->conn
- * is unrelated to the conn that originally opened the handle; close
- * via the NULL-ft path so we do not underflow that unrelated
- * counter.
- */
- if (!fp->conn) {
- __ksmbd_close_fd(NULL, fp);
- return;
- }
- __ksmbd_close_fd(&work->sess->file_table, fp);
- atomic_dec(&work->conn->stats.open_files_count);
+ __ksmbd_close_fd(fp->conn ? &work->sess->file_table : NULL, fp);
}
static void set_close_state_blocked_works(struct ksmbd_file *fp)
@@ -1127,11 +1112,8 @@ int ksmbd_close_fd_app_instance_id(struct ksmbd_work *work,
up_write(&fp->f_ci->m_lock);
*closed = true;
- if (atomic_sub_and_test(n_to_drop, &fp->refcount)) {
- if (fp->conn)
- atomic_dec(&fp->conn->stats.open_files_count);
+ if (atomic_sub_and_test(n_to_drop, &fp->refcount))
__ksmbd_close_fd(NULL, fp);
- }
return 0;
out:
@@ -1421,7 +1403,7 @@ static int ksmbd_mark_fp_closed(struct ksmbd_file *fp)
return 1;
}
-static int
+static void
__close_file_table_ids(struct ksmbd_session *sess,
struct ksmbd_tree_connect *tcon,
bool (*skip)(struct ksmbd_tree_connect *tcon,
@@ -1432,7 +1414,6 @@ __close_file_table_ids(struct ksmbd_session *sess,
struct ksmbd_file_table *ft = &sess->file_table;
struct ksmbd_file *fp;
unsigned int id = 0;
- int num = 0;
while (1) {
int n_to_drop;
@@ -1516,10 +1497,8 @@ __close_file_table_ids(struct ksmbd_session *sess,
}
/*
- * fp->volatile_id is already cleared to prevent stale idr
- * removal from a deferred final close. Remove fp from
- * m_fp_list here because __ksmbd_remove_fd() will skip the
- * list unlink when volatile_id is KSMBD_NO_FID.
+ * Stop inode walkers from finding the open after close is committed.
+ * Final close also unlinks fp if a global lookup owns the last put.
*/
down_write(&fp->f_ci->m_lock);
list_del_init(&fp->node);
@@ -1538,22 +1517,13 @@ __close_file_table_ids(struct ksmbd_session *sess,
* the original reference until ksmbd_update_fstate()
* observes the cleared volatile id.
*
- * If we end up as the final putter, finalize fp and
- * account the open_files_count decrement via the caller's
- * atomic_sub(num, ...). Otherwise the remaining user's
- * ksmbd_fd_put() reaches __put_fd_final(), which does its
- * own atomic_dec(&open_files_count), so we must not count
- * this fp here -- doing so would double-decrement the
- * connection-wide counter.
+ * The final close accounts the open against fp->conn,
+ * including when an in-flight user drops the last reference.
*/
- if (atomic_sub_and_test(n_to_drop, &fp->refcount)) {
+ if (atomic_sub_and_test(n_to_drop, &fp->refcount))
__ksmbd_close_fd(NULL, fp);
- num++;
- }
id++;
}
-
- return num;
}
static inline bool is_reconnectable(struct ksmbd_file *fp)
@@ -1841,29 +1811,29 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon,
* / ksmbd_reopen_durable_fd()), so conn stays valid for the whole
* body of this function regardless of any op->conn puts below.
*/
- conn = fp->conn;
ci = fp->f_ci;
down_write(&ci->m_lock);
+ conn = fp->conn;
list_for_each_entry_rcu(op, &ci->m_op_list, op_entry,
lockdep_is_held(&ci->m_lock)) {
- if (op->conn != conn)
+ if (op->o_fp != fp)
continue;
ksmbd_conn_put(op->conn);
op->conn = NULL;
op->sess = NULL;
op->session_id = 0;
}
- up_write(&ci->m_lock);
-
- spin_lock(&fp->f_lock);
+ write_lock(&global_ft.lock);
fp->conn = NULL;
fp->tcon = NULL;
fp->volatile_id = KSMBD_NO_FID;
- spin_unlock(&fp->f_lock);
if (fp->durable_timeout)
fp->durable_scavenger_timeout =
jiffies_to_msecs(jiffies) + fp->durable_timeout;
+ atomic_dec(&conn->stats.open_files_count);
+ write_unlock(&global_ft.lock);
+ up_write(&ci->m_lock);
/* Drop fp's own reference on conn. */
ksmbd_conn_put(conn);
@@ -1872,22 +1842,12 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon,
void ksmbd_close_tree_conn_fds(struct ksmbd_work *work)
{
- int num = __close_file_table_ids(work->sess,
- work->tcon,
- tree_conn_fd_check,
- false);
-
- atomic_sub(num, &work->conn->stats.open_files_count);
+ __close_file_table_ids(work->sess, work->tcon, tree_conn_fd_check, false);
}
void ksmbd_close_session_fds(struct ksmbd_work *work)
{
- int num = __close_file_table_ids(work->sess,
- work->tcon,
- session_fd_check,
- true);
-
- atomic_sub(num, &work->conn->stats.open_files_count);
+ __close_file_table_ids(work->sess, work->tcon, session_fd_check, true);
}
int ksmbd_init_global_file_table(void)
@@ -1957,23 +1917,26 @@ int ksmbd_validate_name_reconnect(struct ksmbd_share_config *share,
int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp)
{
- struct ksmbd_inode *ci;
+ struct ksmbd_inode *ci = fp->f_ci;
struct oplock_info *op;
struct ksmbd_conn *conn = work->conn;
struct ksmbd_share_config *old_share;
unsigned int old_f_state;
+ int ret = -EBADF;
+ down_write(&ci->m_lock);
write_lock(&global_ft.lock);
- if ((!fp->is_durable && !fp->is_persistent) || fp->conn || fp->tcon) {
+ if ((!fp->is_durable && !fp->is_persistent) || fp->conn || fp->tcon ||
+ fp->f_state != FP_INITED) {
write_unlock(&global_ft.lock);
pr_err("Invalid durable fd [%p:%p]\n", fp->conn, fp->tcon);
- return -EBADF;
+ goto out;
}
if (has_file_id(fp->volatile_id)) {
write_unlock(&global_ft.lock);
pr_err("Still in use durable fd: %llu\n", fp->volatile_id);
- return -EBADF;
+ goto out;
}
/*
@@ -1993,6 +1956,7 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp)
fp->conn = ksmbd_conn_get(conn);
fp->tcon = work->tcon;
spin_unlock(&fp->f_lock);
+ atomic_inc(&conn->stats.open_files_count);
write_unlock(&global_ft.lock);
__open_id(&work->sess->file_table, fp, OPEN_ID_TYPE_VOLATILE_ID);
@@ -2004,14 +1968,13 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp)
fp->share_conf = old_share;
fp->f_state = old_f_state;
spin_unlock(&fp->f_lock);
+ atomic_dec(&conn->stats.open_files_count);
write_unlock(&global_ft.lock);
ksmbd_conn_put(conn);
ksmbd_share_config_put(work->tcon->share_conf);
- return -EBADF;
+ goto out;
}
- ci = fp->f_ci;
- down_write(&ci->m_lock);
list_for_each_entry_rcu(op, &ci->m_op_list, op_entry,
lockdep_is_held(&ci->m_lock)) {
if (op->conn || op->o_fp != fp)
@@ -2020,7 +1983,6 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp)
op->sess = work->sess;
op->session_id = work->sess->id;
}
- up_write(&ci->m_lock);
spin_lock(&fp->f_lock);
fp->owner.uid = fp->owner.gid = 0;
@@ -2029,7 +1991,73 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp)
spin_unlock(&fp->f_lock);
ksmbd_share_config_put(old_share);
- return 0;
+ ret = 0;
+out:
+ up_write(&ci->m_lock);
+ return ret;
+}
+
+/**
+ * ksmbd_rebind_replay_fd() - bind a completed replay to a surviving channel
+ * @work: replay request, with a referenced session
+ * @fp: referenced open whose owner and lease key have been checked
+ *
+ * Return: 0 on success, -ENOEXEC for another session, or -EBADF for a closed open.
+ */
+int ksmbd_rebind_replay_fd(struct ksmbd_work *work, struct ksmbd_file *fp)
+{
+ struct ksmbd_file_table *ft = &work->sess->file_table;
+ struct ksmbd_inode *ci = fp->f_ci;
+ struct ksmbd_conn *old_conn = NULL;
+ struct oplock_info *op;
+ int ret = 0;
+
+ /* CLOSE unpublishes under ft->lock. Detach takes m_lock afterward. */
+ down_write(&ci->m_lock);
+ read_lock(&ft->lock);
+ if (fp->f_state != FP_INITED) {
+ ret = -EBADF;
+ goto out;
+ }
+ /* Detached durable opens follow the existing reconnect path. */
+ if (!fp->conn)
+ goto out;
+ if (!has_file_id(fp->volatile_id)) {
+ ret = -EBADF;
+ goto out;
+ }
+ if (idr_find(ft->idr, fp->volatile_id) != fp) {
+ ret = -ENOEXEC;
+ goto out;
+ }
+ if (fp->conn == work->conn ||
+ (!ksmbd_conn_exiting(fp->conn) && !ksmbd_conn_releasing(fp->conn)))
+ goto out;
+
+ write_lock(&global_ft.lock);
+ old_conn = fp->conn;
+ fp->conn = ksmbd_conn_get(work->conn);
+ atomic_inc(&fp->conn->stats.open_files_count);
+ atomic_dec(&old_conn->stats.open_files_count);
+ write_unlock(&global_ft.lock);
+
+ list_for_each_entry_rcu(op, &ci->m_op_list, op_entry,
+ lockdep_is_held(&ci->m_lock)) {
+ struct ksmbd_conn *conn;
+
+ if (op->o_fp != fp)
+ continue;
+ conn = op->conn;
+ op->conn = ksmbd_conn_get(fp->conn);
+ op->sess = work->sess;
+ op->session_id = work->sess->id;
+ ksmbd_conn_put(conn);
+ }
+out:
+ read_unlock(&ft->lock);
+ up_write(&ci->m_lock);
+ ksmbd_conn_put(old_conn);
+ return ret;
}
int ksmbd_init_file_table(struct ksmbd_file_table *ft)
diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h
index 6df48ae04548..694453c75ec4 100644
--- a/fs/smb/server/vfs_cache.h
+++ b/fs/smb/server/vfs_cache.h
@@ -260,6 +260,7 @@ void ksmbd_fd_set_delete_on_close(struct ksmbd_file *fp,
void ksmbd_fd_set_delete_pending(struct ksmbd_file *fp);
void ksmbd_fd_clear_delete_pending(struct ksmbd_file *fp);
int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp);
+int ksmbd_rebind_replay_fd(struct ksmbd_work *work, struct ksmbd_file *fp);
int ksmbd_validate_name_reconnect(struct ksmbd_share_config *share,
struct ksmbd_file *fp, char *name);
int ksmbd_init_file_cache(void);
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 08/22] ksmbd: track SMB2 locks above OFFSET_MAX in server lock table
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (5 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 07/22] ksmbd: rebind completed CREATE replays without draining the old channel Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 09/22] ksmbd: expire unacknowledged oplock breaks Namjae Jeon
` (13 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
SMB2 lock ranges use unsigned 64-bit offsets, but file_lock stores signed
loff_t values. Track valid ranges above OFFSET_MAX in ksmbd's inode-based
lock index and arbitrate conflicts there. Keep these offsets out of
file_lock, and handle blocking requests, unlocks, rollback, close cleanup
and cancellation in the SMB layer.
Reserve large ranges under the index spinlock until the full request
commits. Keep lookup and ownership independent of the connection so
channel removal and hard-link opens use the same conflict checks.
Finish large-range unlocks in the SMB layer after unlocking any VFS
portion. Wake large-range lock waiters when normal locks are removed
by unlock or close.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/smb2pdu.c | 235 +++++++++++++++++++++++++++++++-------
fs/smb/server/vfs_cache.c | 119 ++++++++++++++++++-
fs/smb/server/vfs_cache.h | 9 ++
3 files changed, 321 insertions(+), 42 deletions(-)
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index d2f02a0957a7..cdc214363d19 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -10226,8 +10226,10 @@ static int smb2_set_flock_flags(struct file_lock *flock, int flags)
return cmd;
}
+/* Track SMB locks that cannot be represented by a signed loff_t. */
static struct ksmbd_lock *smb2_lock_init(struct file_lock *flock,
unsigned int cmd, int flags, bool zero_len,
+ u64 start, u64 end, bool virtual_lock,
struct list_head *lock_list)
{
struct ksmbd_lock *lock;
@@ -10238,10 +10240,11 @@ static struct ksmbd_lock *smb2_lock_init(struct file_lock *flock,
lock->cmd = cmd;
lock->fl = flock;
- lock->start = flock->fl_start;
- lock->end = flock->fl_end;
+ lock->start = start;
+ lock->end = end;
lock->flags = flags;
lock->zero_len = zero_len;
+ lock->virtual_lock = virtual_lock;
INIT_HLIST_NODE(&lock->hlist);
INIT_LIST_HEAD(&lock->flist);
INIT_LIST_HEAD(&lock->llist);
@@ -10250,18 +10253,112 @@ static struct ksmbd_lock *smb2_lock_init(struct file_lock *flock,
return lock;
}
-static void smb2_remove_blocked_lock(void **argv)
+static void smb2_virtual_lock_cancel(void **argv)
{
- struct file_lock *flock = (struct file_lock *)argv[0];
+ (void)argv;
+ wake_up_all(&ksmbd_lock_wait);
+}
- ksmbd_vfs_posix_lock_unblock(flock);
- locks_wake_up(flock);
+static int smb2_virtual_lock_wait(struct ksmbd_work *work,
+ struct ksmbd_file *fp,
+ struct ksmbd_lock *smb_lock,
+ struct smb2_lock_rsp *rsp)
+{
+ void **argv = NULL;
+ bool async = false;
+ int ret;
+
+ for (;;) {
+ ret = ksmbd_file_lock_reserve(fp, smb_lock);
+ if (ret != -EAGAIN ||
+ (smb_lock->flags & SMB2_LOCKFLAG_FAIL_IMMEDIATELY))
+ break;
+
+ if (!async) {
+ argv = kmalloc_obj(*argv, KSMBD_DEFAULT_GFP);
+ if (!argv)
+ return -ENOMEM;
+ argv[0] = NULL;
+ ret = setup_async_work(work, smb2_virtual_lock_cancel, argv);
+ if (ret) {
+ kfree(argv);
+ return ret;
+ }
+ async = true;
+
+ read_lock(&work->sess->file_table.lock);
+ if (fp->f_state != FP_INITED) {
+ read_unlock(&work->sess->file_table.lock);
+ release_async_work(work);
+ return -ENOENT;
+ }
+ spin_lock(&fp->f_lock);
+ list_add(&work->fp_entry, &fp->blocked_works);
+ spin_unlock(&fp->f_lock);
+ read_unlock(&work->sess->file_table.lock);
+ smb2_send_interim_resp(work, STATUS_PENDING);
+ }
+
+ wait_event(ksmbd_lock_wait,
+ READ_ONCE(work->state) != KSMBD_WORK_ACTIVE ||
+ !ksmbd_file_lock_conflict(smb_lock));
+
+ if (READ_ONCE(work->state) != KSMBD_WORK_ACTIVE) {
+ spin_lock(&fp->f_lock);
+ list_del_init(&work->fp_entry);
+ spin_unlock(&fp->f_lock);
+ if (work->state == KSMBD_WORK_CANCELLED) {
+ rsp->hdr.Status = STATUS_CANCELLED;
+ smb2_send_interim_resp(work, STATUS_CANCELLED);
+ work->send_no_response = 1;
+ ret = -ECANCELED;
+ } else {
+ rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED;
+ ret = -EIO;
+ }
+ release_async_work(work);
+ return ret;
+ }
+ }
+
+ if (async) {
+ bool cancelled;
+
+ spin_lock(&fp->f_lock);
+ list_del_init(&work->fp_entry);
+ cancelled = READ_ONCE(work->state) != KSMBD_WORK_ACTIVE;
+ spin_unlock(&fp->f_lock);
+ release_async_work(work);
+ if (cancelled) {
+ ksmbd_file_lock_remove(smb_lock);
+ if (work->state == KSMBD_WORK_CANCELLED) {
+ rsp->hdr.Status = STATUS_CANCELLED;
+ smb2_send_interim_resp(work, STATUS_CANCELLED);
+ work->send_no_response = 1;
+ return -ECANCELED;
+ }
+ rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED;
+ return -EIO;
+ }
+ }
+
+ return ret;
+}
+
+static void smb2_free_ksmbd_lock(struct ksmbd_lock *smb_lock)
+{
+ if (smb_lock->vfs_locked)
+ ksmbd_vfs_posix_lock_unblock(smb_lock->fl);
+ locks_free_lock(smb_lock->fl);
+ kfree(smb_lock);
}
-static void smb2_free_lock(struct file_lock *flock)
+static void smb2_remove_blocked_lock(void **argv)
{
+ struct file_lock *flock = (struct file_lock *)argv[0];
+
ksmbd_vfs_posix_lock_unblock(flock);
- locks_free_lock(flock);
+ locks_wake_up(flock);
}
static void smb2_free_blocked_lock(struct file_lock *flock)
@@ -10289,13 +10386,14 @@ int smb2_lock(struct ksmbd_work *work)
int flags = 0;
int cmd = 0;
int err = -EIO, i, rc = 0;
- u64 lock_start, lock_length;
+ u64 lock_start, lock_length, lock_end;
struct ksmbd_lock *smb_lock = NULL, *cmp_lock, *tmp;
LIST_HEAD(lock_list);
LIST_HEAD(rollback_list);
int prior_lock = 0;
unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID;
bool lock_replayed;
+ bool virtual_lock;
WORK_BUFFERS(work, req, rsp);
@@ -10358,23 +10456,29 @@ int smb2_lock(struct ksmbd_work *work)
lock_start = le64_to_cpu(lock_ele[i].Offset);
lock_length = le64_to_cpu(lock_ele[i].Length);
- if (lock_start > OFFSET_MAX ||
- (lock_length &&
- lock_length - 1 > OFFSET_MAX - lock_start)) {
+ if (lock_length && lock_length - 1 > U64_MAX - lock_start) {
pr_err("Invalid lock range requested\n");
rsp->hdr.Status = STATUS_INVALID_LOCK_RANGE;
locks_free_lock(flock);
goto out;
}
- flock->fl_start = lock_start;
- flock->fl_end = lock_length ?
- flock->fl_start + lock_length - 1 : flock->fl_start;
+ lock_end = lock_length ? lock_start + lock_length - 1 : lock_start;
+ virtual_lock = lock_end > OFFSET_MAX;
+
+ /* Lock the representable portion in VFS as well. */
+ if (lock_start > OFFSET_MAX) {
+ flock->fl_start = 0;
+ flock->fl_end = 0;
+ } else {
+ flock->fl_start = lock_start;
+ flock->fl_end = min_t(u64, lock_end, OFFSET_MAX);
+ }
/* Check conflict locks in one request */
list_for_each_entry(cmp_lock, &lock_list, llist) {
- if (cmp_lock->fl->fl_start <= flock->fl_start &&
- cmp_lock->fl->fl_end >= flock->fl_end) {
+ if (cmp_lock->start <= lock_start &&
+ cmp_lock->end >= lock_end) {
if (cmp_lock->fl->c.flc_type != F_UNLCK &&
flock->c.flc_type != F_UNLCK) {
pr_err("conflict two locks in one request\n");
@@ -10386,6 +10490,7 @@ int smb2_lock(struct ksmbd_work *work)
}
smb_lock = smb2_lock_init(flock, cmd, flags, !lock_length,
+ lock_start, lock_end, virtual_lock,
&lock_list);
if (!smb_lock) {
err = -EINVAL;
@@ -10422,6 +10527,22 @@ int smb2_lock(struct ksmbd_work *work)
prior_lock = smb_lock->flags;
+ /* The SMB lock table stores ranges that file_lock cannot express. */
+ if (smb_lock->virtual_lock &&
+ !(smb_lock->flags & SMB2_LOCKFLAG_UNLOCK))
+ goto no_check_cl;
+ if (smb_lock->virtual_lock &&
+ (smb_lock->flags & SMB2_LOCKFLAG_UNLOCK)) {
+ err = ksmbd_file_lock_unlock(smb_lock);
+ if (err) {
+ rsp->hdr.Status = STATUS_RANGE_NOT_LOCKED;
+ goto out;
+ }
+ list_del(&smb_lock->llist);
+ smb2_free_ksmbd_lock(smb_lock);
+ continue;
+ }
+
if (!(smb_lock->flags & SMB2_LOCKFLAG_UNLOCK) &&
!(smb_lock->flags & SMB2_LOCKFLAG_FAIL_IMMEDIATELY))
goto no_check_cl;
@@ -10437,7 +10558,21 @@ int smb2_lock(struct ksmbd_work *work)
flock = smb_lock->fl;
list_del(&smb_lock->llist);
+ if (smb_lock->virtual_lock &&
+ !(smb_lock->flags & SMB2_LOCKFLAG_UNLOCK)) {
+ rc = smb2_virtual_lock_wait(work, fp, smb_lock, rsp);
+ if (rc) {
+ err = rc;
+ smb2_free_ksmbd_lock(smb_lock);
+ goto out;
+ }
+ list_add(&smb_lock->llist, &rollback_list);
+ if (smb_lock->start > OFFSET_MAX)
+ continue;
+ }
+
if (smb_lock->zero_len) {
+ rc = 0;
err = 0;
goto skip;
}
@@ -10464,6 +10599,11 @@ int smb2_lock(struct ksmbd_work *work)
argv = kmalloc(sizeof(void *), KSMBD_DEFAULT_GFP);
if (!argv) {
err = -ENOMEM;
+ if (smb_lock->virtual_lock) {
+ ksmbd_vfs_posix_lock_unblock(flock);
+ locks_wake_up(flock);
+ goto out;
+ }
smb2_free_blocked_lock(flock);
kfree(smb_lock);
goto out;
@@ -10476,11 +10616,17 @@ int smb2_lock(struct ksmbd_work *work)
if (rc) {
kfree(argv);
err = -ENOMEM;
+ if (smb_lock->virtual_lock) {
+ ksmbd_vfs_posix_lock_unblock(flock);
+ locks_wake_up(flock);
+ goto out;
+ }
smb2_free_blocked_lock(flock);
kfree(smb_lock);
goto out;
}
- list_add(&smb_lock->llist, &rollback_list);
+ if (!smb_lock->virtual_lock)
+ list_add(&smb_lock->llist, &rollback_list);
spin_lock(&fp->f_lock);
list_add(&work->fp_entry, &fp->blocked_works);
spin_unlock(&fp->f_lock);
@@ -10493,16 +10639,19 @@ int smb2_lock(struct ksmbd_work *work)
list_del(&work->fp_entry);
spin_unlock(&fp->f_lock);
- list_del(&smb_lock->llist);
+ if (!smb_lock->virtual_lock)
+ list_del(&smb_lock->llist);
if (work->state == KSMBD_WORK_CANCELLED) {
rsp->hdr.Status = STATUS_CANCELLED;
- kfree(smb_lock);
smb2_send_interim_resp(work,
STATUS_CANCELLED);
release_async_work(work);
- locks_free_lock(flock);
work->send_no_response = 1;
+ if (!smb_lock->virtual_lock) {
+ kfree(smb_lock);
+ locks_free_lock(flock);
+ }
goto out;
}
@@ -10511,20 +10660,25 @@ int smb2_lock(struct ksmbd_work *work)
if (work->state == KSMBD_WORK_ACTIVE)
goto retry;
- locks_free_lock(flock);
-
rsp->hdr.Status =
STATUS_RANGE_NOT_LOCKED;
- kfree(smb_lock);
+ if (!smb_lock->virtual_lock) {
+ locks_free_lock(flock);
+ kfree(smb_lock);
+ }
/* rollback_list may still hold earlier grants */
goto out;
} else if (!rc) {
- list_add(&smb_lock->llist, &rollback_list);
+ smb_lock->vfs_locked = !smb_lock->zero_len;
+ if (!smb_lock->virtual_lock)
+ list_add(&smb_lock->llist, &rollback_list);
ksmbd_debug(SMB, "successful in taking lock\n");
} else {
- locks_free_lock(flock);
- kfree(smb_lock);
err = rc;
+ if (!smb_lock->virtual_lock) {
+ locks_free_lock(flock);
+ kfree(smb_lock);
+ }
goto out;
}
}
@@ -10561,24 +10715,27 @@ int smb2_lock(struct ksmbd_work *work)
list_for_each_entry_safe(smb_lock, tmp, &rollback_list, llist) {
struct file_lock *rlock = NULL;
- rlock = smb_flock_init(filp);
- if (rlock) {
- rlock->c.flc_type = F_UNLCK;
- rlock->fl_start = smb_lock->start;
- rlock->fl_end = smb_lock->end;
+ if (smb_lock->vfs_locked) {
+ rlock = smb_flock_init(filp);
+ if (rlock) {
+ rlock->c.flc_type = F_UNLCK;
+ rlock->fl_start = smb_lock->fl->fl_start;
+ rlock->fl_end = smb_lock->fl->fl_end;
- rc = vfs_lock_file(filp, F_SETLK, rlock, NULL);
- if (rc)
- pr_err("rollback unlock fail : %d\n", rc);
- } else {
- pr_err("rollback unlock alloc failed\n");
+ rc = vfs_lock_file(filp, F_SETLK, rlock, NULL);
+ if (rc)
+ pr_err("rollback unlock fail : %d\n", rc);
+ } else {
+ pr_err("rollback unlock alloc failed\n");
+ }
}
+ if (smb_lock->virtual_lock)
+ ksmbd_file_lock_remove(smb_lock);
list_del(&smb_lock->llist);
- smb2_free_lock(smb_lock->fl);
if (rlock)
locks_free_lock(rlock);
- kfree(smb_lock);
+ smb2_free_ksmbd_lock(smb_lock);
}
out2:
ksmbd_debug(SMB, "failed in taking lock(flags : %x), err : %d\n", flags, err);
diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c
index c5217433299e..e5dbc8e41bfb 100644
--- a/fs/smb/server/vfs_cache.c
+++ b/fs/smb/server/vfs_cache.c
@@ -38,6 +38,7 @@ static DEFINE_RWLOCK(inode_hash_lock);
static struct ksmbd_file_table global_ft;
static atomic_long_t fd_limit;
static struct kmem_cache *filp_cache;
+DECLARE_WAIT_QUEUE_HEAD(ksmbd_lock_wait);
/* Both the inode hash and each open's lock_list use this lock. */
static DEFINE_SPINLOCK(file_locks_lock);
@@ -618,6 +619,109 @@ static void __ksmbd_remove_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp
write_unlock(&ft->lock);
}
+/* file_locks_lock protects both committed locks and pending reservations. */
+static bool __ksmbd_file_lock_conflict(struct ksmbd_lock *lock)
+{
+ struct inode *inode = file_inode(lock->fl->c.flc_file);
+ struct ksmbd_lock *cmp;
+
+ hash_for_each_possible(file_locks, cmp, hlist, (unsigned long)inode) {
+ if (cmp == lock || file_inode(cmp->fl->c.flc_file) != inode)
+ continue;
+ if ((cmp->flags & SMB2_LOCKFLAG_SHARED) &&
+ (lock->flags & SMB2_LOCKFLAG_SHARED))
+ continue;
+ if ((cmp->zero_len && !lock->zero_len &&
+ cmp->start > lock->start && cmp->start <= lock->end) ||
+ (lock->zero_len && !cmp->zero_len &&
+ lock->start > cmp->start && lock->start <= cmp->end) ||
+ (!cmp->zero_len && !lock->zero_len &&
+ cmp->start <= lock->end && lock->start <= cmp->end))
+ return true;
+ }
+ return false;
+}
+
+bool ksmbd_file_lock_conflict(struct ksmbd_lock *lock)
+{
+ bool conflict;
+
+ spin_lock(&file_locks_lock);
+ conflict = __ksmbd_file_lock_conflict(lock);
+ spin_unlock(&file_locks_lock);
+ return conflict;
+}
+
+int ksmbd_file_lock_reserve(struct ksmbd_file *fp, struct ksmbd_lock *lock)
+{
+ int ret = 0;
+
+ spin_lock(&file_locks_lock);
+ if (__ksmbd_file_lock_conflict(lock)) {
+ ret = -EAGAIN;
+ } else {
+ /* Reserve the range until the whole LOCK request commits. */
+ lock->pending = true;
+ hash_add(file_locks, &lock->hlist,
+ (unsigned long)file_inode(fp->filp));
+ list_add_tail(&lock->flist, &fp->lock_list);
+ }
+ spin_unlock(&file_locks_lock);
+ return ret;
+}
+
+void ksmbd_file_lock_remove(struct ksmbd_lock *lock)
+{
+ spin_lock(&file_locks_lock);
+ if (!hlist_unhashed(&lock->hlist)) {
+ hash_del(&lock->hlist);
+ list_del_init(&lock->flist);
+ }
+ spin_unlock(&file_locks_lock);
+ wake_up_all(&ksmbd_lock_wait);
+}
+
+/* Unlock a large range, including any portion represented by a VFS lock. */
+int ksmbd_file_lock_unlock(struct ksmbd_lock *lock)
+{
+ struct inode *inode = file_inode(lock->fl->c.flc_file);
+ struct ksmbd_lock *cmp, *match = NULL;
+ int ret = 0;
+
+ spin_lock(&file_locks_lock);
+ hash_for_each_possible(file_locks, cmp, hlist, (unsigned long)inode) {
+ if (cmp->fl->c.flc_file != lock->fl->c.flc_file ||
+ cmp->start != lock->start || cmp->end != lock->end ||
+ cmp->pending)
+ continue;
+ if (waitqueue_active(&cmp->fl->c.flc_wait)) /* VFS waiters */
+ continue;
+ match = cmp;
+ if (cmp->flags & SMB2_LOCKFLAG_EXCLUSIVE)
+ break;
+ }
+ if (match)
+ match->pending = true;
+ spin_unlock(&file_locks_lock);
+ if (!match)
+ return -ENOENT;
+
+ if (match->vfs_locked)
+ ret = vfs_lock_file(lock->fl->c.flc_file, F_SETLK, lock->fl, NULL);
+ if (ret) {
+ spin_lock(&file_locks_lock);
+ match->pending = false;
+ spin_unlock(&file_locks_lock);
+ return ret;
+ }
+ ksmbd_file_lock_remove(match);
+ if (match->vfs_locked)
+ ksmbd_vfs_posix_lock_unblock(match->fl);
+ locks_free_lock(match->fl);
+ kfree(match);
+ return 0;
+}
+
/**
* ksmbd_file_lock_check() - check conflicts or remove an exact unlock match
* @lock: unregistered lock from the current request
@@ -637,7 +741,8 @@ int ksmbd_file_lock_check(struct ksmbd_lock *lock)
if (lock_is_unlock(lock->fl)) {
if (cmp->fl->c.flc_file != lock->fl->c.flc_file ||
- cmp->start != lock->start || cmp->end != lock->end)
+ cmp->start != lock->start || cmp->end != lock->end ||
+ cmp->pending)
continue;
if (waitqueue_active(&cmp->fl->c.flc_wait)) /* VFS waiters */
continue;
@@ -673,9 +778,11 @@ int ksmbd_file_lock_check(struct ksmbd_lock *lock)
}
spin_unlock(&file_locks_lock);
if (lock_to_remove) {
- ksmbd_vfs_posix_lock_unblock(lock_to_remove->fl);
+ if (lock_to_remove->vfs_locked)
+ ksmbd_vfs_posix_lock_unblock(lock_to_remove->fl);
locks_free_lock(lock_to_remove->fl);
kfree(lock_to_remove);
+ wake_up_all(&ksmbd_lock_wait);
}
return ret;
}
@@ -692,6 +799,10 @@ void ksmbd_file_lock_add(struct ksmbd_file *fp, struct list_head *locks)
spin_lock(&file_locks_lock);
list_for_each_entry_safe(lock, tmp, locks, llist) {
list_del_init(&lock->llist);
+ if (!hlist_unhashed(&lock->hlist)) {
+ lock->pending = false;
+ continue;
+ }
hash_add(file_locks, &lock->hlist,
(unsigned long)file_inode(fp->filp));
list_add_tail(&lock->flist, &fp->lock_list);
@@ -712,10 +823,12 @@ static void ksmbd_file_lock_clear(struct ksmbd_file *fp)
list_for_each_entry_safe(lock, tmp, &dispose, flist) {
list_del_init(&lock->flist);
- ksmbd_vfs_posix_lock_unblock(lock->fl);
+ if (lock->vfs_locked)
+ ksmbd_vfs_posix_lock_unblock(lock->fl);
locks_free_lock(lock->fl);
kfree(lock);
}
+ wake_up_all(&ksmbd_lock_wait);
}
static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp)
diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h
index 694453c75ec4..6831aca22cff 100644
--- a/fs/smb/server/vfs_cache.h
+++ b/fs/smb/server/vfs_cache.h
@@ -12,6 +12,7 @@
#include <linux/rwsem.h>
#include <linux/spinlock.h>
#include <linux/idr.h>
+#include <linux/wait.h>
#include <linux/workqueue.h>
#include "vfs.h"
@@ -44,12 +45,20 @@ struct ksmbd_lock {
unsigned int flags;
int cmd;
int zero_len;
+ bool virtual_lock;
+ bool vfs_locked;
+ bool pending;
unsigned long long start;
unsigned long long end;
};
int ksmbd_file_lock_check(struct ksmbd_lock *lock);
void ksmbd_file_lock_add(struct ksmbd_file *fp, struct list_head *locks);
+bool ksmbd_file_lock_conflict(struct ksmbd_lock *lock);
+int ksmbd_file_lock_reserve(struct ksmbd_file *fp, struct ksmbd_lock *lock);
+void ksmbd_file_lock_remove(struct ksmbd_lock *lock);
+int ksmbd_file_lock_unlock(struct ksmbd_lock *lock);
+extern wait_queue_head_t ksmbd_lock_wait;
struct stream {
char *name;
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 09/22] ksmbd: expire unacknowledged oplock breaks
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (6 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 08/22] ksmbd: track SMB2 locks above OFFSET_MAX in server lock table Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 10/22] ksmbd: expire unacknowledged lease breaks Namjae Jeon
` (12 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
Arm the oplock break acknowledgment timer once the common notification
path accepts a classic break for transmission. Preserve its shared retry
deadline when another channel is tried. Store a deadline per open and scan
the global open table when the earliest deadline expires. Drop expired
breaks to NONE and restart the timer while other breaks remain.
Serialize timer expiry with client acknowledgments and close using the
oplock state lock. Wake blocked requests when a break expires or sending
the notification fails, and invalidate durable reconnect for the open.
Use wake_up_all() for acknowledgment, timeout, close and send failure.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/oplock.c | 30 ++++++++++++++--
fs/smb/server/oplock.h | 3 ++
fs/smb/server/smb2pdu.c | 6 ++--
fs/smb/server/vfs_cache.c | 74 +++++++++++++++++++++++++++++++++++++++
fs/smb/server/vfs_cache.h | 1 +
5 files changed, 110 insertions(+), 4 deletions(-)
diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c
index 96b36b8329a6..352a9e2293b5 100644
--- a/fs/smb/server/oplock.c
+++ b/fs/smb/server/oplock.c
@@ -551,9 +551,10 @@ void close_id_del_oplock(struct ksmbd_file *fp)
* takes the same lock before it acquires pending_break or sets ACK_WAIT.
*/
opinfo->op_state = OPLOCK_CLOSING;
+ opinfo->oplock_timeout_set = false;
clear_bit_unlock(0, &opinfo->pending_break);
spin_unlock(&opinfo->state_lock);
- wake_up_interruptible_all(&opinfo->oplock_q);
+ wake_up_all(&opinfo->oplock_q);
if (opinfo->is_lease)
wake_up_interruptible_all(&opinfo->oplock_brk);
/* memory barrier is needed for wake_up_bit() */
@@ -744,6 +745,8 @@ static bool oplock_break_set_ack_wait(struct oplock_info *opinfo)
spin_lock(&opinfo->state_lock);
if (opinfo->op_state != OPLOCK_CLOSING) {
opinfo->op_state = OPLOCK_ACK_WAIT;
+ opinfo->oplock_timeout_set = false;
+ opinfo->oplock_timed_out = false;
ret = true;
}
spin_unlock(&opinfo->state_lock);
@@ -816,6 +819,23 @@ static bool lease_break_needed(struct oplock_info *opinfo, int req_op_level,
return opinfo->level > req_op_level;
}
+static void smb2_break_start_timer(struct oplock_info *opinfo,
+ unsigned long timeout)
+{
+ bool start = false;
+
+ spin_lock(&opinfo->state_lock);
+ if (!opinfo->is_lease && opinfo->op_state == OPLOCK_ACK_WAIT &&
+ !opinfo->oplock_timeout_set) {
+ opinfo->oplock_timeout = timeout;
+ opinfo->oplock_timeout_set = true;
+ start = true;
+ }
+ spin_unlock(&opinfo->state_lock);
+ if (start)
+ ksmbd_schedule_oplock_break_timer(timeout);
+}
+
struct smb2_break_channel {
struct list_head list;
struct ksmbd_conn *conn;
@@ -853,8 +873,10 @@ static int smb2_break_failed(struct oplock_info *opinfo, bool expect_ack)
}
opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
opinfo->op_state = OPLOCK_STATE_NONE;
+ opinfo->oplock_timeout_set = false;
+ opinfo->oplock_timed_out = true;
spin_unlock(&opinfo->state_lock);
- wake_up_interruptible_all(&opinfo->oplock_q);
+ wake_up_all(&opinfo->oplock_q);
wake_up_interruptible_all(&opinfo->oplock_brk);
return ksmbd_invalidate_durable_fd(opinfo->fid);
}
@@ -1169,6 +1191,8 @@ static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci,
continue;
}
channel->queued = true;
+ if (expect_ack)
+ smb2_break_start_timer(opinfo, deadline);
if (interim_work) {
ksmbd_queue_io_work(interim_work);
interim_work = NULL;
@@ -1209,6 +1233,8 @@ static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci,
failed:
ret = smb2_break_failed(opinfo, expect_ack);
out:
+ if (expect_ack && READ_ONCE(opinfo->oplock_timed_out))
+ ret = ksmbd_invalidate_durable_fd(opinfo->fid);
ksmbd_free_work_struct(work);
list_for_each_entry_safe(channel, tmp, &channels, list) {
list_del(&channel->list);
diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h
index 5ef05166e502..03a5b93da2ec 100644
--- a/fs/smb/server/oplock.h
+++ b/fs/smb/server/oplock.h
@@ -67,6 +67,9 @@ struct oplock_info {
int level;
int op_state;
spinlock_t state_lock;
+ unsigned long oplock_timeout;
+ bool oplock_timeout_set;
+ bool oplock_timed_out;
unsigned long pending_break;
u64 fid;
atomic_t breaking_cnt;
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index cdc214363d19..77f3b9a5cdfc 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -12030,8 +12030,9 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work)
opinfo->level = rsp_oplevel;
opinfo->op_state = OPLOCK_STATE_NONE;
+ opinfo->oplock_timeout_set = false;
spin_unlock(&opinfo->state_lock);
- wake_up_interruptible_all(&opinfo->oplock_q);
+ wake_up_all(&opinfo->oplock_q);
rsp->StructureSize = cpu_to_le16(24);
rsp->OplockLevel = rsp_oplevel;
@@ -12048,8 +12049,9 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work)
err_out:
if (opinfo->op_state != OPLOCK_CLOSING)
opinfo->op_state = OPLOCK_STATE_NONE;
+ opinfo->oplock_timeout_set = false;
spin_unlock(&opinfo->state_lock);
- wake_up_interruptible_all(&opinfo->oplock_q);
+ wake_up_all(&opinfo->oplock_q);
rsp->hdr.Status = status;
smb2_set_err_rsp(work);
out_no_state_change:
diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c
index e5dbc8e41bfb..d63fed289c79 100644
--- a/fs/smb/server/vfs_cache.c
+++ b/fs/smb/server/vfs_cache.c
@@ -12,6 +12,7 @@
#include <linux/freezer.h>
#include <linux/dcache.h>
#include <linux/hashtable.h>
+#include <linux/jiffies.h>
#include "glob.h"
#include "vfs_cache.h"
@@ -36,6 +37,75 @@ static struct hlist_head *inode_hashtable __read_mostly;
static DEFINE_RWLOCK(inode_hash_lock);
static struct ksmbd_file_table global_ft;
+static DEFINE_MUTEX(oplock_break_timer_lock);
+static struct delayed_work oplock_break_timer_work;
+static unsigned long oplock_break_timer_next;
+static bool oplock_break_timer_pending;
+
+static void ksmbd_oplock_break_timer(struct work_struct *work)
+{
+ struct ksmbd_file *fp;
+ struct oplock_info *opinfo;
+ unsigned long next = 0, now;
+ unsigned int id;
+ bool pending = false;
+ bool expired;
+
+ mutex_lock(&oplock_break_timer_lock);
+ now = jiffies;
+ read_lock(&global_ft.lock);
+ idr_for_each_entry(global_ft.idr, fp, id) {
+ opinfo = opinfo_get(fp);
+ if (!opinfo)
+ continue;
+
+ expired = false;
+ spin_lock(&opinfo->state_lock);
+ if (!opinfo->is_lease &&
+ opinfo->op_state == OPLOCK_ACK_WAIT &&
+ opinfo->oplock_timeout_set) {
+ if (time_after_eq(now, opinfo->oplock_timeout)) {
+ /* ksmbd owns the oplock state for the local file. */
+ opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
+ opinfo->op_state = OPLOCK_STATE_NONE;
+ opinfo->oplock_timeout_set = false;
+ opinfo->oplock_timed_out = true;
+ expired = true;
+ } else if (!pending ||
+ time_before(opinfo->oplock_timeout, next)) {
+ next = opinfo->oplock_timeout;
+ pending = true;
+ }
+ }
+ spin_unlock(&opinfo->state_lock);
+ if (expired)
+ wake_up_all(&opinfo->oplock_q);
+ opinfo_put(opinfo);
+ }
+ read_unlock(&global_ft.lock);
+
+ oplock_break_timer_pending = pending;
+ if (pending) {
+ oplock_break_timer_next = next;
+ mod_delayed_work(system_wq, &oplock_break_timer_work,
+ time_after_eq(jiffies, next) ? 0 : next - jiffies);
+ }
+ mutex_unlock(&oplock_break_timer_lock);
+}
+
+void ksmbd_schedule_oplock_break_timer(unsigned long timeout)
+{
+ mutex_lock(&oplock_break_timer_lock);
+ if (!oplock_break_timer_pending ||
+ time_before(timeout, oplock_break_timer_next)) {
+ oplock_break_timer_pending = true;
+ oplock_break_timer_next = timeout;
+ mod_delayed_work(system_wq, &oplock_break_timer_work,
+ time_after_eq(jiffies, timeout) ? 0 : timeout - jiffies);
+ }
+ mutex_unlock(&oplock_break_timer_lock);
+}
+
static atomic_long_t fd_limit;
static struct kmem_cache *filp_cache;
DECLARE_WAIT_QUEUE_HEAD(ksmbd_lock_wait);
@@ -1965,6 +2035,8 @@ void ksmbd_close_session_fds(struct ksmbd_work *work)
int ksmbd_init_global_file_table(void)
{
+ INIT_DELAYED_WORK(&oplock_break_timer_work, ksmbd_oplock_break_timer);
+ oplock_break_timer_pending = false;
if (create_proc_files())
pr_warn("Unable to create files procfs entry\n");
return ksmbd_init_file_table(&global_ft);
@@ -1975,6 +2047,8 @@ void ksmbd_free_global_file_table(void)
struct ksmbd_file *fp = NULL;
unsigned int id;
+ cancel_delayed_work_sync(&oplock_break_timer_work);
+
idr_for_each_entry(global_ft.idr, fp, id) {
ksmbd_remove_durable_fd(fp);
__ksmbd_close_fd(NULL, fp);
diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h
index 6831aca22cff..9de41b6e28ba 100644
--- a/fs/smb/server/vfs_cache.h
+++ b/fs/smb/server/vfs_cache.h
@@ -242,6 +242,7 @@ void ksmbd_wake_session_blocked_works(struct ksmbd_session *sess);
int ksmbd_close_inode_fds(struct ksmbd_work *work, struct inode *inode);
int ksmbd_init_global_file_table(void);
void ksmbd_free_global_file_table(void);
+void ksmbd_schedule_oplock_break_timer(unsigned long timeout);
void ksmbd_set_fd_limit(unsigned long limit);
int ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp,
unsigned int state);
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 10/22] ksmbd: expire unacknowledged lease breaks
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (7 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 09/22] ksmbd: expire unacknowledged oplock breaks Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 11/22] ksmbd: support SMB2 quota query and set information Namjae Jeon
` (11 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
Arm a lease break acknowledgment timer once the common notification path
accepts an ACK-required notification for transmission. Keep the deadline
on the lease across channel retries and scan every lease in the global
lease tables when the timer expires. Set timed-out leases and all their
opens to NONE, then rearm for the next deadline.
Serialize client acknowledgments, notification state and timer expiry
under the lease lock, taking each open's state lock after it. Update every
open before publishing break completion so a late acknowledgment cannot
restore an expired lease. Wake blocked opens on timeout, send failure and
global lease-table teardown, and cancel the timer at teardown.
Keep the existing connection-independent lease-table lifetime and use the
notifier's opinfo reference to retain the lease. Stop after selecting one
open during lease-key lookup.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/oplock.c | 184 +++++++++++++++++++++++++++++++++++-----
fs/smb/server/oplock.h | 3 +
fs/smb/server/smb2pdu.c | 30 +++++--
3 files changed, 191 insertions(+), 26 deletions(-)
diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c
index 352a9e2293b5..5873bd349e08 100644
--- a/fs/smb/server/oplock.c
+++ b/fs/smb/server/oplock.c
@@ -20,6 +20,11 @@
static LIST_HEAD(lease_table_list);
static DEFINE_RWLOCK(lease_list_lock);
+static DEFINE_MUTEX(lease_break_timer_lock);
+static unsigned long lease_break_timer_next;
+static bool lease_break_timer_pending;
+static void lease_break_timer(struct work_struct *work);
+static DECLARE_DELAYED_WORK(lease_break_timer_work, lease_break_timer);
#define SMB2_LEASE_STATE_MASK_LE (SMB2_LEASE_READ_CACHING_LE | \
SMB2_LEASE_HANDLE_CACHING_LE | \
@@ -116,6 +121,82 @@ static void lease_put(struct lease *lease)
kfree(lease);
}
+/* The caller holds lease->lock. */
+static void lease_break_to_none_locked(struct lease *lease)
+{
+ struct oplock_info *opinfo;
+ bool wake;
+
+ lease->state = SMB2_LEASE_NONE_LE;
+ lease->new_state = SMB2_LEASE_NONE_LE;
+ lease->breaking = false;
+ lease->break_timeout_set = false;
+ list_for_each_entry(opinfo, &lease->open_list, lease_entry) {
+ spin_lock(&opinfo->state_lock);
+ opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
+ wake = opinfo->op_state == OPLOCK_ACK_WAIT;
+ if (wake) {
+ opinfo->op_state = OPLOCK_STATE_NONE;
+ opinfo->oplock_timed_out = true;
+ atomic_set(&opinfo->breaking_cnt, 0);
+ }
+ spin_unlock(&opinfo->state_lock);
+ if (wake) {
+ wake_up_all(&opinfo->oplock_q);
+ wake_up_interruptible_all(&opinfo->oplock_brk);
+ }
+ }
+}
+
+static void lease_break_timer(struct work_struct *work)
+{
+ struct lease_table *lb;
+ struct lease *lease;
+ unsigned long next = 0, now;
+ bool pending = false;
+
+ mutex_lock(&lease_break_timer_lock);
+ now = jiffies;
+ read_lock(&lease_list_lock);
+ list_for_each_entry(lb, &lease_table_list, l_entry) {
+ list_for_each_entry(lease, &lb->lease_list, l_entry) {
+ spin_lock(&lease->lock);
+ if (lease->breaking && lease->break_timeout_set) {
+ if (time_after_eq(now, lease->break_timeout)) {
+ lease_break_to_none_locked(lease);
+ } else if (!pending ||
+ time_before(lease->break_timeout, next)) {
+ next = lease->break_timeout;
+ pending = true;
+ }
+ }
+ spin_unlock(&lease->lock);
+ }
+ }
+ read_unlock(&lease_list_lock);
+
+ lease_break_timer_pending = pending;
+ if (pending) {
+ lease_break_timer_next = next;
+ mod_delayed_work(system_wq, &lease_break_timer_work,
+ time_after_eq(jiffies, next) ? 0 : next - jiffies);
+ }
+ mutex_unlock(&lease_break_timer_lock);
+}
+
+static void schedule_lease_break_timer(unsigned long timeout)
+{
+ mutex_lock(&lease_break_timer_lock);
+ if (!lease_break_timer_pending ||
+ time_before(timeout, lease_break_timer_next)) {
+ lease_break_timer_pending = true;
+ lease_break_timer_next = timeout;
+ mod_delayed_work(system_wq, &lease_break_timer_work,
+ time_after_eq(jiffies, timeout) ? 0 : timeout - jiffies);
+ }
+ mutex_unlock(&lease_break_timer_lock);
+}
+
static void lease_add_table(struct lease *lease, struct lease_table *lb)
{
lease_get(lease);
@@ -190,6 +271,8 @@ static struct lease *alloc_lease(struct lease_ctx_info *lctx,
lease->epoch = lctx->version == 2 ? le16_to_cpu(lctx->epoch) + 1 : 0;
lease->ci = ci;
lease->reuse_epoch = false;
+ lease->breaking = false;
+ lease->break_timeout_set = false;
lease->l_lb = NULL;
INIT_LIST_HEAD(&lease->l_entry);
INIT_LIST_HEAD(&lease->open_list);
@@ -822,11 +905,38 @@ static bool lease_break_needed(struct oplock_info *opinfo, int req_op_level,
static void smb2_break_start_timer(struct oplock_info *opinfo,
unsigned long timeout)
{
- bool start = false;
+ struct lease *lease = opinfo->o_lease;
+ struct oplock_info *open;
+ bool start = false, waiting;
+
+ if (opinfo->is_lease) {
+ read_lock(&lease_list_lock);
+ spin_lock(&lease->lock);
+ if (!lease->l_lb) {
+ lease_break_to_none_locked(lease);
+ } else if (!lease->breaking) {
+ list_for_each_entry(open, &lease->open_list, lease_entry) {
+ spin_lock(&open->state_lock);
+ waiting = open->op_state == OPLOCK_ACK_WAIT;
+ spin_unlock(&open->state_lock);
+ if (!waiting)
+ continue;
+ lease->break_timeout = timeout;
+ lease->break_timeout_set = true;
+ lease->breaking = true;
+ start = true;
+ break;
+ }
+ }
+ spin_unlock(&lease->lock);
+ read_unlock(&lease_list_lock);
+ if (start)
+ schedule_lease_break_timer(timeout);
+ return;
+ }
spin_lock(&opinfo->state_lock);
- if (!opinfo->is_lease && opinfo->op_state == OPLOCK_ACK_WAIT &&
- !opinfo->oplock_timeout_set) {
+ if (opinfo->op_state == OPLOCK_ACK_WAIT && !opinfo->oplock_timeout_set) {
opinfo->oplock_timeout = timeout;
opinfo->oplock_timeout_set = true;
start = true;
@@ -858,25 +968,33 @@ static bool smb2_break_complete(struct oplock_info *opinfo, bool expect_ack)
static int smb2_break_failed(struct oplock_info *opinfo, bool expect_ack)
{
+ struct lease *lease = opinfo->o_lease;
+ bool timed_out;
+
+ if (opinfo->is_lease)
+ spin_lock(&lease->lock);
spin_lock(&opinfo->state_lock);
/* An ACK or close which won the race has already completed the break. */
if (opinfo->op_state == OPLOCK_CLOSING ||
(expect_ack && opinfo->op_state == OPLOCK_STATE_NONE)) {
+ timed_out = opinfo->oplock_timed_out;
spin_unlock(&opinfo->state_lock);
- return 0;
+ if (opinfo->is_lease)
+ spin_unlock(&lease->lock);
+ return timed_out ? ksmbd_invalidate_durable_fd(opinfo->fid) : 0;
}
if (opinfo->is_lease) {
- opinfo->o_lease->state = SMB2_LEASE_NONE_LE;
- opinfo->o_lease->new_state = SMB2_LEASE_NONE_LE;
- lease_update_oplock_levels(opinfo->o_lease);
- atomic_set(&opinfo->breaking_cnt, 0);
+ spin_unlock(&opinfo->state_lock);
+ lease_break_to_none_locked(lease);
+ spin_unlock(&lease->lock);
+ } else {
+ opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
+ opinfo->op_state = OPLOCK_STATE_NONE;
+ opinfo->oplock_timeout_set = false;
+ opinfo->oplock_timed_out = true;
+ spin_unlock(&opinfo->state_lock);
+ wake_up_all(&opinfo->oplock_q);
}
- opinfo->level = SMB2_OPLOCK_LEVEL_NONE;
- opinfo->op_state = OPLOCK_STATE_NONE;
- opinfo->oplock_timeout_set = false;
- opinfo->oplock_timed_out = true;
- spin_unlock(&opinfo->state_lock);
- wake_up_all(&opinfo->oplock_q);
wake_up_interruptible_all(&opinfo->oplock_brk);
return ksmbd_invalidate_durable_fd(opinfo->fid);
}
@@ -1096,6 +1214,7 @@ static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci,
struct smb2_oplock_break *oplock_rsp;
struct smb2_lease_break *lease_rsp;
struct lease *lease = opinfo->o_lease;
+ struct oplock_info *open;
unsigned long deadline, retry_at;
int ret, size;
LIST_HEAD(channels);
@@ -1122,10 +1241,14 @@ static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci,
if (ret)
goto failed;
+ if (opinfo->is_lease)
+ spin_lock(&lease->lock);
spin_lock(&opinfo->state_lock);
if (opinfo->op_state == OPLOCK_CLOSING ||
(expect_ack && opinfo->op_state == OPLOCK_STATE_NONE)) {
spin_unlock(&opinfo->state_lock);
+ if (opinfo->is_lease)
+ spin_unlock(&lease->lock);
ret = 0;
goto out;
}
@@ -1159,6 +1282,8 @@ static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci,
size = sizeof(*oplock_rsp);
}
spin_unlock(&opinfo->state_lock);
+ if (opinfo->is_lease)
+ spin_unlock(&lease->lock);
ret = ksmbd_iov_pin_rsp(work, oplock_rsp, size);
if (ret)
goto failed;
@@ -1222,12 +1347,16 @@ static int smb2_break_notify(struct oplock_info *opinfo, struct ksmbd_inode *ci,
if (ret)
goto failed;
if (!expect_ack && opinfo->is_lease) {
- spin_lock(&opinfo->state_lock);
- if (opinfo->op_state != OPLOCK_CLOSING) {
+ spin_lock(&lease->lock);
+ if (READ_ONCE(opinfo->op_state) != OPLOCK_CLOSING) {
lease->state = lease->new_state;
- lease_update_oplock_levels(lease);
+ list_for_each_entry(open, &lease->open_list, lease_entry) {
+ spin_lock(&open->state_lock);
+ open->level = smb2_map_lease_to_oplock(lease->state);
+ spin_unlock(&open->state_lock);
+ }
}
- spin_unlock(&opinfo->state_lock);
+ spin_unlock(&lease->lock);
}
goto out;
failed:
@@ -1379,6 +1508,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
open_trunc = brk_opinfo->open_trunc;
again:
+ spin_lock(&lease->lock);
atomic_inc(&brk_opinfo->breaking_cnt);
if (open_trunc) {
/*
@@ -1413,6 +1543,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
SMB2_LEASE_HANDLE_CACHING_LE);
if (expect_ack) {
if (!oplock_break_set_ack_wait(brk_opinfo)) {
+ spin_unlock(&lease->lock);
atomic_dec_if_positive(&brk_opinfo->breaking_cnt);
clear_and_wake_up_bit(0, &brk_opinfo->pending_break);
return -ENOENT;
@@ -1427,6 +1558,7 @@ static int oplock_break(struct oplock_info *brk_opinfo, struct ksmbd_inode *ci,
inc_epoch = false;
lease->reuse_epoch = false;
}
+ spin_unlock(&lease->lock);
err = smb2_break_notify_with_interim(brk_opinfo, ci, expect_ack,
wait_ack, inc_epoch,
sent_interim ? NULL : in_work,
@@ -1524,11 +1656,20 @@ void destroy_lease_table(void)
struct lease_table *lb;
struct lease *lease;
+ cancel_delayed_work_sync(&lease_break_timer_work);
+ mutex_lock(&lease_break_timer_lock);
+ lease_break_timer_pending = false;
+ mutex_unlock(&lease_break_timer_lock);
+
write_lock(&lease_list_lock);
while (!list_empty(&lease_table_list)) {
lb = list_first_entry(&lease_table_list, struct lease_table,
l_entry);
lease = list_first_entry(&lb->lease_list, struct lease, l_entry);
+ spin_lock(&lease->lock);
+ if (lease->breaking)
+ lease_break_to_none_locked(lease);
+ spin_unlock(&lease->lock);
lease_del_table(lease);
}
write_unlock(&lease_list_lock);
@@ -2551,11 +2692,13 @@ struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn,
list_for_each_entry(lease, <->lease_list, l_entry) {
if (memcmp(lease->lease_key, lease_key, SMB2_LEASE_KEY_SIZE))
continue;
+ spin_lock(&lease->lock);
if (!(lease->state & (SMB2_LEASE_HANDLE_CACHING_LE |
- SMB2_LEASE_WRITE_CACHING_LE)))
+ SMB2_LEASE_WRITE_CACHING_LE))) {
+ spin_unlock(&lease->lock);
break;
+ }
- spin_lock(&lease->lock);
list_for_each_entry(opinfo, &lease->open_list, lease_entry) {
if (!opinfo->op_state ||
opinfo->op_state == OPLOCK_CLOSING)
@@ -2563,6 +2706,7 @@ struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn,
if (!atomic_inc_not_zero(&opinfo->refcount))
continue;
ret_op = opinfo;
+ break;
}
spin_unlock(&lease->lock);
if (ret_op) {
diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h
index 03a5b93da2ec..d81013c514e3 100644
--- a/fs/smb/server/oplock.h
+++ b/fs/smb/server/oplock.h
@@ -50,6 +50,9 @@ struct lease {
unsigned short epoch;
bool is_dir;
bool reuse_epoch;
+ bool breaking;
+ bool break_timeout_set;
+ unsigned long break_timeout;
struct ksmbd_inode *ci;
struct lease_table *l_lb;
struct list_head l_entry;
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 77f3b9a5cdfc..9d6426450827 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -12086,10 +12086,12 @@ static void smb21_lease_break_ack(struct ksmbd_work *work)
struct ksmbd_conn *conn = work->conn;
struct smb2_lease_ack *req;
struct smb2_lease_ack *rsp;
- struct oplock_info *opinfo;
+ struct oplock_info *opinfo, *open;
int ret;
__le32 lease_state;
struct lease *lease;
+ u8 level;
+ bool wake;
WORK_BUFFERS(work, req, rsp);
@@ -12105,6 +12107,7 @@ static void smb21_lease_break_ack(struct ksmbd_work *work)
lease = opinfo->o_lease;
/* Publish NONE only after the lease state and all open levels are updated. */
+ spin_lock(&lease->lock);
spin_lock(&opinfo->state_lock);
if (opinfo->op_state != OPLOCK_ACK_WAIT) {
if (opinfo->op_state == OPLOCK_STATE_NONE &&
@@ -12132,17 +12135,31 @@ static void smb21_lease_break_ack(struct ksmbd_work *work)
lease_state = req->LeaseState;
lease->state = lease_state;
lease->new_state = SMB2_LEASE_NONE_LE;
- lease_update_oplock_levels(lease);
- opinfo->op_state = OPLOCK_STATE_NONE;
- atomic_dec_if_positive(&opinfo->breaking_cnt);
+ lease->breaking = false;
+ lease->break_timeout_set = false;
+ level = smb2_map_lease_to_oplock(lease_state);
spin_unlock(&opinfo->state_lock);
- wake_up_interruptible_all(&opinfo->oplock_q);
- wake_up_interruptible_all(&opinfo->oplock_brk);
+ list_for_each_entry(open, &lease->open_list, lease_entry) {
+ spin_lock(&open->state_lock);
+ open->level = level;
+ wake = open->op_state == OPLOCK_ACK_WAIT;
+ if (wake) {
+ open->op_state = OPLOCK_STATE_NONE;
+ atomic_set(&open->breaking_cnt, 0);
+ }
+ spin_unlock(&open->state_lock);
+ if (wake) {
+ wake_up_all(&open->oplock_q);
+ wake_up_interruptible_all(&open->oplock_brk);
+ }
+ }
+ spin_unlock(&lease->lock);
goto response;
replay_rsp:
lease_state = lease->state;
spin_unlock(&opinfo->state_lock);
+ spin_unlock(&lease->lock);
response:
rsp->StructureSize = cpu_to_le16(36);
rsp->Reserved = 0;
@@ -12158,6 +12175,7 @@ static void smb21_lease_break_ack(struct ksmbd_work *work)
err_unlock:
spin_unlock(&opinfo->state_lock);
+ spin_unlock(&lease->lock);
err_out:
smb2_set_err_rsp(work);
opinfo_put(opinfo);
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 11/22] ksmbd: support SMB2 quota query and set information
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (8 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 10/22] ksmbd: expire unacknowledged lease breaks Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 12/22] ksmbd: validate POSIX negotiate contexts Namjae Jeon
` (10 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
Handle SMB2_0_INFO_QUOTA in QUERY_INFO and SET_INFO using the user
quota operations on the volume containing the open. Validate the
request buffers and linked SID entries before accessing quota data.
Enumerate quotas with a per-open cursor, apply restart and single-entry
requests, and return aligned FILE_QUOTA_INFORMATION records. Return
zeroed records for SIDs without a quota entry and report buffer and
end-of-list statuses required by SMB2.
Allow the root session user to update quota limits. Reject deletion
and explicit zero-byte limits because Linux user quotas cannot
represent those values without changing their meaning.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/smb2pdu.c | 423 ++++++++++++++++++++++++++++++++++++++
fs/smb/server/smb2pdu.h | 26 +++
fs/smb/server/vfs_cache.c | 1 +
fs/smb/server/vfs_cache.h | 2 +
4 files changed, 452 insertions(+)
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 9d6426450827..856648d93f6e 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -19,6 +19,7 @@
#include <linux/math.h>
#include <linux/timekeeping.h>
#include <linux/unaligned.h>
+#include <linux/quota.h>
#include "glob.h"
#include "../common/smbfsctl.h"
@@ -8064,6 +8065,342 @@ static int smb2_get_info_sec(struct ksmbd_work *work,
return rc;
}
+static unsigned int smb2_current_req_len(struct ksmbd_work *work,
+ struct smb2_hdr *hdr);
+
+static int smb2_quota_sid_len(const u8 *buf, u32 len)
+{
+ const struct smb_sid *sid = (const struct smb_sid *)buf;
+ u32 size;
+
+ if (len < CIFS_SID_BASE_SIZE || sid->revision != 1 ||
+ sid->num_subauth > SID_MAX_SUB_AUTHORITIES)
+ return -EINVAL;
+ size = CIFS_SID_BASE_SIZE + sid->num_subauth * sizeof(__le32);
+ return size <= len ? size : -EINVAL;
+}
+
+/* Linux user quotas can name local users through the server or Unix SID. */
+static int smb2_quota_sid_to_uid(const u8 *buf, u32 len, u32 *uid)
+{
+ const struct smb_sid *sid = (const struct smb_sid *)buf;
+ struct smb_sid expected;
+ u32 id;
+
+ if (smb2_quota_sid_len(buf, len) != len || !sid->num_subauth)
+ return -EINVAL;
+ id = le32_to_cpu(sid->sub_auth[sid->num_subauth - 1]);
+ if (server_conf.domain_sid.num_subauth < SID_MAX_SUB_AUTHORITIES) {
+ id_to_sid(id, SIDOWNER, &expected);
+ if (expected.num_subauth == sid->num_subauth &&
+ !memcmp(&expected, sid, len)) {
+ *uid = id;
+ return 0;
+ }
+ }
+ id_to_sid(id, SIDUNIX_USER, &expected);
+ if (expected.num_subauth == sid->num_subauth &&
+ !memcmp(&expected, sid, len)) {
+ *uid = id;
+ return 0;
+ }
+ return -ENOENT;
+}
+
+static int smb2_quota_check_list(const u8 *buf, u32 len, bool set)
+{
+ u32 pos = 0, base = set ? sizeof(struct smb2_file_quota_info) :
+ sizeof(struct smb2_get_quota_info);
+ u32 align = set ? 8 : 4;
+
+ if (!len)
+ return -EINVAL;
+ while (pos < len) {
+ u32 next;
+ u32 sid_len;
+
+ if (len - pos < base)
+ return -EINVAL;
+ next = get_unaligned_le32(buf + pos);
+ sid_len = get_unaligned_le32(buf + pos + sizeof(__le32));
+ if (sid_len > len - pos - base ||
+ smb2_quota_sid_len(buf + pos + base, sid_len) != sid_len)
+ return -EINVAL;
+ if (!next)
+ return len - pos - base - sid_len < align ? 0 : -EINVAL;
+ if (next < ALIGN(base + sid_len, align) || next > len - pos ||
+ !IS_ALIGNED(next, align))
+ return -EINVAL;
+ pos += next;
+ }
+ return -EINVAL;
+}
+
+static void smb2_quota_set_status(struct smb2_hdr *hdr, int rc)
+{
+ switch (rc) {
+ case -EOPNOTSUPP:
+ case -ENOSYS:
+ case -ESRCH:
+ hdr->Status = STATUS_NOT_SUPPORTED;
+ break;
+ case -EACCES:
+ case -EPERM:
+ hdr->Status = STATUS_ACCESS_DENIED;
+ break;
+ case -ENOSPC:
+ hdr->Status = STATUS_BUFFER_TOO_SMALL;
+ break;
+ case -EINVAL:
+ hdr->Status = STATUS_INVALID_PARAMETER;
+ break;
+ case -ENOMEM:
+ hdr->Status = STATUS_INSUFFICIENT_RESOURCES;
+ break;
+ case -EDQUOT:
+ hdr->Status = STATUS_QUOTA_EXCEEDED;
+ break;
+ case -EROFS:
+ hdr->Status = STATUS_MEDIA_WRITE_PROTECTED;
+ break;
+ default:
+ hdr->Status = STATUS_UNEXPECTED_IO_ERROR;
+ }
+}
+
+static int smb2_quota_append(struct smb2_query_info_rsp *rsp, u32 max_len,
+ u32 *used, struct smb2_file_quota_info **last,
+ const u8 *sid, u32 sid_len,
+ const struct qc_dqblk *dq)
+{
+ struct smb2_file_quota_info *entry;
+ u32 pos = *used;
+
+ if (*last)
+ pos = ALIGN(pos, 8);
+ if (pos > max_len || sid_len > max_len - pos ||
+ sizeof(*entry) > max_len - pos - sid_len)
+ return -ENOSPC;
+ if (*last) {
+ (*last)->NextEntryOffset = cpu_to_le32(pos -
+ ((u8 *)*last - rsp->Buffer));
+ memset(rsp->Buffer + *used, 0, pos - *used);
+ }
+ entry = (struct smb2_file_quota_info *)(rsp->Buffer + pos);
+ memset(entry, 0, sizeof(*entry));
+ entry->SidLength = cpu_to_le32(sid_len);
+ if (dq) {
+ entry->QuotaUsed = cpu_to_le64(dq->d_space);
+ entry->QuotaThreshold = cpu_to_le64(dq->d_spc_softlimit ?
+ dq->d_spc_softlimit : U64_MAX);
+ entry->QuotaLimit = cpu_to_le64(dq->d_spc_hardlimit ?
+ dq->d_spc_hardlimit : U64_MAX);
+ }
+ memcpy(entry->Sid, sid, sid_len);
+ *used = pos + sizeof(*entry) + sid_len;
+ *last = entry;
+ return 0;
+}
+
+static int smb2_get_info_quota(struct ksmbd_work *work,
+ struct smb2_query_info_req *req,
+ struct smb2_query_info_rsp *rsp)
+{
+ struct ksmbd_file *fp;
+ struct smb2_query_quota_info *query;
+ struct smb2_file_quota_info *last = NULL;
+ struct super_block *sb;
+ struct qc_dqblk dq;
+ struct smb_sid sid;
+ struct kqid qid;
+ const u8 *list;
+ u32 in_len = le32_to_cpu(req->InputBufferLength);
+ u32 list_len, start_len, start_off, used = 0, pos = 0, next_id = 0;
+ u32 uid, sid_len, entry_len;
+ u32 sid_type;
+ int max_len, rc = 0;
+ bool explicit_list, scan;
+
+ if (in_len < sizeof(*query) ||
+ le16_to_cpu(req->InputBufferOffset) < offsetof(typeof(*req), Buffer) ||
+ le16_to_cpu(req->InputBufferOffset) >
+ smb2_current_req_len(work, &req->hdr) ||
+ in_len > smb2_current_req_len(work, &req->hdr) -
+ le16_to_cpu(req->InputBufferOffset)) {
+ rc = -EINVAL;
+ goto out_status;
+ }
+ query = (void *)req + le16_to_cpu(req->InputBufferOffset);
+ list_len = le32_to_cpu(query->SidListLength);
+ start_len = le32_to_cpu(query->StartSidLength);
+ start_off = le32_to_cpu(query->StartSidOffset);
+ if (list_len) {
+ if (list_len > in_len - sizeof(*query) ||
+ smb2_quota_check_list(query->SidBuffer, list_len, false)) {
+ rc = -EINVAL;
+ goto out_status;
+ }
+ } else if (start_len || start_off) {
+ if (!start_len || start_off > in_len - sizeof(*query) ||
+ start_len > in_len - sizeof(*query) - start_off ||
+ smb2_quota_sid_len(query->SidBuffer + start_off,
+ start_len) != start_len) {
+ rc = -EINVAL;
+ goto out_status;
+ }
+ }
+
+ max_len = smb2_calc_max_out_buf_len(work,
+ offsetof(struct smb2_query_info_rsp, Buffer),
+ le32_to_cpu(req->OutputBufferLength));
+ if (max_len < 0) {
+ rc = -EINVAL;
+ goto out_status;
+ }
+ if (!max_len) {
+ rc = -ENOSPC;
+ goto out_status;
+ }
+
+ if (req->hdr.Flags & SMB2_FLAGS_RELATED_OPERATIONS &&
+ !has_file_id(req->VolatileFileId))
+ fp = ksmbd_lookup_fd_slow(work, work->compound_fid,
+ work->compound_pfid);
+ else
+ fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId,
+ req->PersistentFileId);
+ if (!fp) {
+ rc = -ENOENT;
+ rsp->hdr.Status = STATUS_FILE_CLOSED;
+ goto out_status;
+ }
+ sb = file_inode(fp->filp)->i_sb;
+ if (!sb->s_qcop || !sb->s_qcop->get_nextdqblk) {
+ rc = -EOPNOTSUPP;
+ goto out_fd;
+ }
+ explicit_list = list_len != 0;
+ scan = !explicit_list;
+ list = query->SidBuffer;
+ if (scan) {
+ sid_type = server_conf.domain_sid.num_subauth <
+ SID_MAX_SUB_AUTHORITIES ? SIDOWNER : SIDUNIX_USER;
+ id_to_sid(0, sid_type, &sid);
+ sid_len = CIFS_SID_BASE_SIZE + sid.num_subauth * sizeof(__le32);
+ if (max_len < sizeof(struct smb2_file_quota_info) + sid_len) {
+ rc = -ENOSPC;
+ goto out_fd;
+ }
+ if (start_len) {
+ rc = smb2_quota_sid_to_uid(query->SidBuffer + start_off,
+ start_len, &uid);
+ if (rc == -ENOENT) {
+ rc = -EINVAL;
+ goto out_fd;
+ }
+ if (rc)
+ goto out_fd;
+ if (uid == U32_MAX)
+ goto no_more;
+ next_id = uid + 1;
+ } else {
+ mutex_lock(&fp->quota_lock);
+ if (query->RestartScan)
+ fp->current_quota_id = 0;
+ next_id = fp->current_quota_id;
+ }
+ }
+
+ while (true) {
+ if (explicit_list) {
+ const struct smb2_get_quota_info *item =
+ (const void *)(list + pos);
+ bool found = false;
+
+ sid_len = le32_to_cpu(item->SidLength);
+ rc = smb2_quota_sid_to_uid(item->Sid, sid_len, &uid);
+ if (rc == -EINVAL)
+ goto out_scan;
+ entry_len = sizeof(struct smb2_file_quota_info) + sid_len;
+ if (ALIGN(used, 8) + entry_len > max_len) {
+ rc = used ? 0 : -ENOSPC;
+ break;
+ }
+ if (!rc) {
+ qid = make_kqid(&init_user_ns, USRQUOTA, uid);
+ rc = sb->s_qcop->get_nextdqblk(sb, &qid, &dq);
+ if (rc == -ENOENT)
+ rc = 0;
+ else if (rc)
+ goto out_scan;
+ else
+ found = from_kqid(&init_user_ns, qid) == uid;
+ } else if (rc == -ENOENT) {
+ rc = 0;
+ }
+ rc = smb2_quota_append(rsp, max_len, &used, &last,
+ item->Sid, sid_len,
+ found ? &dq : NULL);
+ if (rc)
+ goto out_scan;
+ if (!item->NextEntryOffset || query->ReturnSingle)
+ break;
+ pos += le32_to_cpu(item->NextEntryOffset);
+ } else {
+ if (next_id == U32_MAX)
+ break;
+ if (ALIGN(used, 8) + sizeof(struct smb2_file_quota_info) +
+ sid_len > max_len)
+ break;
+ qid = make_kqid(&init_user_ns, USRQUOTA, next_id);
+ rc = sb->s_qcop->get_nextdqblk(sb, &qid, &dq);
+ if (rc == -ENOENT) {
+ rc = 0;
+ break;
+ }
+ if (rc)
+ goto out_scan;
+ uid = from_kqid(&init_user_ns, qid);
+ if (uid < next_id) {
+ rc = -EIO;
+ goto out_scan;
+ }
+ id_to_sid(uid, sid_type, &sid);
+ rc = smb2_quota_append(rsp, max_len, &used, &last,
+ (const u8 *)&sid, sid_len, &dq);
+ if (rc)
+ goto out_scan;
+ next_id = uid == U32_MAX ? U32_MAX : uid + 1;
+ if (uid == U32_MAX || query->ReturnSingle)
+ break;
+ }
+ }
+out_scan:
+ if (scan && !start_len) {
+ if (!rc && used)
+ fp->current_quota_id = next_id;
+ mutex_unlock(&fp->quota_lock);
+ }
+ if (rc)
+ goto out_fd;
+no_more:
+ if (!used) {
+ rsp->hdr.Status = STATUS_NO_MORE_ENTRIES;
+ rsp->OutputBufferOffset = 0;
+ rsp->OutputBufferLength = 0;
+ } else {
+ rsp->OutputBufferLength = cpu_to_le32(used);
+ }
+ rc = ksmbd_iov_pin_rsp(work, rsp,
+ offsetof(struct smb2_query_info_rsp, Buffer) + used);
+out_fd:
+ ksmbd_fd_put(work, fp);
+out_status:
+ if (rc && !rsp->hdr.Status)
+ smb2_quota_set_status(&rsp->hdr, rc);
+ return rc;
+}
+
/**
* smb2_query_info() - handler for smb2 query info command
* @work: smb work containing query info request buffer
@@ -8104,6 +8441,9 @@ int smb2_query_info(struct ksmbd_work *work)
ksmbd_debug(SMB, "GOT SMB2_O_INFO_SECURITY\n");
rc = smb2_get_info_sec(work, req, rsp);
break;
+ case SMB2_O_INFO_QUOTA:
+ rc = smb2_get_info_quota(work, req, rsp);
+ break;
default:
ksmbd_debug(SMB, "InfoType %d not supported yet\n",
req->InfoType);
@@ -8896,6 +9236,80 @@ static int smb2_set_info_sec(struct ksmbd_file *fp, int addition_info,
buf_len, false, true);
}
+static int smb2_set_info_quota(struct ksmbd_work *work, struct ksmbd_file *fp,
+ struct smb2_set_info_req *req)
+{
+ struct super_block *sb = file_inode(fp->filp)->i_sb;
+ const u8 *buf;
+ u32 len = le32_to_cpu(req->BufferLength);
+ u32 pos = 0, uid, next;
+ int rc;
+
+ if (!sb->s_qcop || !sb->s_qcop->set_dqblk)
+ return -EOPNOTSUPP;
+ /* The SMB session must have the volume's quota administrator right. */
+ if (user_uid(work->sess->user) != 0)
+ return -EACCES;
+ if (le16_to_cpu(req->BufferOffset) < offsetof(typeof(*req), Buffer) ||
+ le16_to_cpu(req->BufferOffset) >
+ smb2_current_req_len(work, &req->hdr) ||
+ len > smb2_current_req_len(work, &req->hdr) -
+ le16_to_cpu(req->BufferOffset))
+ return -EINVAL;
+ buf = (const u8 *)req + le16_to_cpu(req->BufferOffset);
+ rc = smb2_quota_check_list(buf, len, true);
+ if (rc)
+ return rc;
+
+ /* Validate the whole list before applying any of its entries. */
+ while (true) {
+ const struct smb2_file_quota_info *item =
+ (const void *)(buf + pos);
+ s64 soft = (s64)le64_to_cpu(item->QuotaThreshold);
+ s64 hard = (s64)le64_to_cpu(item->QuotaLimit);
+
+ rc = smb2_quota_sid_to_uid(item->Sid,
+ le32_to_cpu(item->SidLength), &uid);
+ if (rc)
+ return rc == -ENOENT ? -EOPNOTSUPP : rc;
+ /* Linux quotas have no delete operation or explicit zero limit. */
+ if (soft < -1 || hard < -1 || !soft || !hard)
+ return -EOPNOTSUPP;
+ next = le32_to_cpu(item->NextEntryOffset);
+ if (!next)
+ break;
+ pos += next;
+ }
+
+ pos = 0;
+ while (true) {
+ const struct smb2_file_quota_info *item =
+ (const void *)(buf + pos);
+ struct qc_dqblk dq = {
+ .d_fieldmask = QC_SPC_SOFT | QC_SPC_HARD,
+ };
+ struct kqid qid;
+ s64 soft = (s64)le64_to_cpu(item->QuotaThreshold);
+ s64 hard = (s64)le64_to_cpu(item->QuotaLimit);
+
+ rc = smb2_quota_sid_to_uid(item->Sid,
+ le32_to_cpu(item->SidLength), &uid);
+ if (rc)
+ return rc == -ENOENT ? -EOPNOTSUPP : rc;
+ qid = make_kqid(&init_user_ns, USRQUOTA, uid);
+ dq.d_spc_softlimit = soft == -1 ? 0 : soft;
+ dq.d_spc_hardlimit = hard == -1 ? 0 : hard;
+ rc = sb->s_qcop->set_dqblk(sb, qid, &dq);
+ if (rc)
+ return rc;
+ next = le32_to_cpu(item->NextEntryOffset);
+ if (!next)
+ break;
+ pos += next;
+ }
+ return 0;
+}
+
/**
* smb2_set_info() - handler for smb2 set info command handler
* @work: smb work containing set info request buffer
@@ -8969,6 +9383,9 @@ int smb2_set_info(struct ksmbd_work *work)
(char *)req + le16_to_cpu(req->BufferOffset),
le32_to_cpu(req->BufferLength));
break;
+ case SMB2_O_INFO_QUOTA:
+ rc = smb2_set_info_quota(work, fp, req);
+ break;
default:
rc = -EOPNOTSUPP;
}
@@ -9012,6 +9429,12 @@ int smb2_set_info(struct ksmbd_work *work)
rsp->hdr.Status = STATUS_INVALID_HANDLE;
else if (rc == -EEXIST)
rsp->hdr.Status = STATUS_OBJECT_NAME_COLLISION;
+ else if (req->InfoType == SMB2_O_INFO_QUOTA && !rsp->hdr.Status) {
+ if (rc == -ENOSPC)
+ rsp->hdr.Status = STATUS_DISK_FULL;
+ else
+ smb2_quota_set_status(&rsp->hdr, rc);
+ }
else if (rsp->hdr.Status == 0 || rc == -EOPNOTSUPP)
rsp->hdr.Status = STATUS_INVALID_INFO_CLASS;
smb2_set_err_rsp(work);
diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h
index c90a1d4e9f11..e07700d0a405 100644
--- a/fs/smb/server/smb2pdu.h
+++ b/fs/smb/server/smb2pdu.h
@@ -225,6 +225,32 @@ struct file_sparse {
#define FILE_NETWORK_OPEN_INFORMATION_SIZE 56
#define FILE_ATTRIBUTE_TAG_INFORMATION_SIZE 8
+struct smb2_query_quota_info {
+ u8 ReturnSingle;
+ u8 RestartScan;
+ __le16 Reserved;
+ __le32 SidListLength;
+ __le32 StartSidLength;
+ __le32 StartSidOffset;
+ u8 SidBuffer[];
+} __packed;
+
+struct smb2_get_quota_info {
+ __le32 NextEntryOffset;
+ __le32 SidLength;
+ u8 Sid[];
+} __packed;
+
+struct smb2_file_quota_info {
+ __le32 NextEntryOffset;
+ __le32 SidLength;
+ __le64 ChangeTime;
+ __le64 QuotaUsed;
+ __le64 QuotaThreshold;
+ __le64 QuotaLimit;
+ u8 Sid[];
+} __packed;
+
/* FS Info response size */
#define FS_DEVICE_INFORMATION_SIZE 8
#define FS_ATTRIBUTE_INFORMATION_SIZE 16
diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c
index d63fed289c79..85f583189437 100644
--- a/fs/smb/server/vfs_cache.c
+++ b/fs/smb/server/vfs_cache.c
@@ -1491,6 +1491,7 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp)
INIT_LIST_HEAD(&fp->lock_list);
spin_lock_init(&fp->f_lock);
mutex_init(&fp->readdir_lock);
+ mutex_init(&fp->quota_lock);
atomic_set(&fp->refcount, 1);
fp->filp = filp;
diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h
index 9de41b6e28ba..1054002ec4c5 100644
--- a/fs/smb/server/vfs_cache.h
+++ b/fs/smb/server/vfs_cache.h
@@ -157,6 +157,8 @@ struct ksmbd_file {
/* if ls is happening on directory, below is valid*/
struct ksmbd_readdir_data readdir_data;
struct mutex readdir_lock;
+ struct mutex quota_lock;
+ u32 current_quota_id;
int dot_dotdot[2];
unsigned int f_state;
bool reserve_lease_break;
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 12/22] ksmbd: validate POSIX negotiate contexts
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (9 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 11/22] ksmbd: support SMB2 quota query and set information Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 13/22] ksmbd: validate POSIX create contexts Namjae Jeon
` (9 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
The POSIX negotiate context currently enables extensions without
checking its payload, and duplicate contexts are accepted.
Reject duplicate contexts and invalid payload lengths. Enable POSIX
extensions only for the published tag, and use the same tag constant
when building the negotiate response.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/smb2pdu.c | 28 ++++++++++------------------
1 file changed, 10 insertions(+), 18 deletions(-)
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 856648d93f6e..17bd7413e137 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -1431,23 +1431,7 @@ static void build_posix_ctxt(struct smb2_posix_neg_context *pneg_ctxt)
{
pneg_ctxt->ContextType = SMB2_POSIX_EXTENSIONS_AVAILABLE;
pneg_ctxt->DataLength = cpu_to_le16(POSIX_CTXT_DATA_LEN);
- /* SMB2_CREATE_TAG_POSIX is "0x93AD25509CB411E7B42383DE968BCD7C" */
- pneg_ctxt->Name[0] = 0x93;
- pneg_ctxt->Name[1] = 0xAD;
- pneg_ctxt->Name[2] = 0x25;
- pneg_ctxt->Name[3] = 0x50;
- pneg_ctxt->Name[4] = 0x9C;
- pneg_ctxt->Name[5] = 0xB4;
- pneg_ctxt->Name[6] = 0x11;
- pneg_ctxt->Name[7] = 0xE7;
- pneg_ctxt->Name[8] = 0xB4;
- pneg_ctxt->Name[9] = 0x23;
- pneg_ctxt->Name[10] = 0x83;
- pneg_ctxt->Name[11] = 0xDE;
- pneg_ctxt->Name[12] = 0x96;
- pneg_ctxt->Name[13] = 0x8B;
- pneg_ctxt->Name[14] = 0xCD;
- pneg_ctxt->Name[15] = 0x7C;
+ memcpy(pneg_ctxt->Name, SMB2_CREATE_TAG_POSIX, POSIX_CTXT_DATA_LEN);
}
static unsigned int assemble_neg_contexts(struct ksmbd_conn *conn,
@@ -1753,6 +1737,7 @@ static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn,
unsigned int neg_ctxt_cnt = le16_to_cpu(req->NegotiateContextCount);
__le32 status = STATUS_INVALID_PARAMETER;
int compress_ctxt_cnt = 0, rdma_transform_ctxt_cnt = 0;
+ bool posix_ctxt_seen = false;
ksmbd_debug(SMB, "decoding %d negotiate contexts\n", neg_ctxt_cnt);
if (len_of_smb <= offset) {
@@ -1825,7 +1810,14 @@ static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn,
} else if (pctx->ContextType == SMB2_POSIX_EXTENSIONS_AVAILABLE) {
ksmbd_debug(SMB,
"deassemble SMB2_POSIX_EXTENSIONS_AVAILABLE context\n");
- conn->posix_ext_supported = true;
+ if (posix_ctxt_seen || clen != POSIX_CTXT_DATA_LEN) {
+ status = STATUS_INVALID_PARAMETER;
+ break;
+ }
+ posix_ctxt_seen = true;
+ if (!memcmp((char *)pctx + sizeof(*pctx),
+ SMB2_CREATE_TAG_POSIX, POSIX_CTXT_DATA_LEN))
+ conn->posix_ext_supported = true;
} else if (pctx->ContextType == SMB2_SIGNING_CAPABILITIES) {
ksmbd_debug(SMB,
"deassemble SMB2_SIGNING_CAPABILITIES context\n");
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 13/22] ksmbd: validate POSIX create contexts
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (10 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 12/22] ksmbd: validate POSIX negotiate contexts Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 14/22] ksmbd: enforce POSIX append-only writes Namjae Jeon
` (8 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
POSIX create contexts are only examined when the tree has POSIX
extensions enabled, and IPC requests bypass this validation.
Examine POSIX contexts before selecting the IPC create path. Reject
duplicate contexts and require an SMB3.1.1 connection, a POSIX-enabled
tree and a non-IPC share for POSIX creates.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/oplock.c | 14 +++++++++++---
fs/smb/server/smb2pdu.c | 24 ++++++++++++++++++------
2 files changed, 29 insertions(+), 9 deletions(-)
diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c
index 5873bd349e08..a911ff70b29e 100644
--- a/fs/smb/server/oplock.c
+++ b/fs/smb/server/oplock.c
@@ -2371,6 +2371,9 @@ struct create_context *smb2_find_context_vals(void *open_req, const char *tag, i
struct create_context *cc;
unsigned int next = 0;
char *name;
+ struct create_context *found = NULL;
+ bool posix_tag = tag_len == POSIX_CTXT_DATA_LEN &&
+ !memcmp(tag, SMB2_CREATE_TAG_POSIX, POSIX_CTXT_DATA_LEN);
struct smb2_create_req *req = (struct smb2_create_req *)open_req;
unsigned int remain_len, name_off, name_len, value_off, value_len,
cc_len;
@@ -2408,13 +2411,18 @@ struct create_context *smb2_find_context_vals(void *open_req, const char *tag, i
return ERR_PTR(-EINVAL);
name = (char *)cc + name_off;
- if (name_len == tag_len && !memcmp(name, tag, name_len))
- return cc;
+ if (name_len == tag_len && !memcmp(name, tag, name_len)) {
+ if (!posix_tag)
+ return cc;
+ if (found)
+ return ERR_PTR(-EINVAL);
+ found = cc;
+ }
remain_len -= next;
} while (next != 0);
- return NULL;
+ return found;
}
/**
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 17bd7413e137..a1e553ddde46 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -4264,12 +4264,7 @@ int smb2_open(struct ksmbd_work *work)
return -EINVAL;
}
- if (test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE)) {
- ksmbd_debug(SMB, "IPC pipe create request\n");
- return create_smb2_pipe(work);
- }
-
- if (req->CreateContextsOffset && tcon->posix_extensions) {
+ if (req->CreateContextsOffset) {
context = smb2_find_context_vals(req, SMB2_CREATE_TAG_POSIX, 16);
if (IS_ERR(context)) {
rc = PTR_ERR(context);
@@ -4277,6 +4272,19 @@ int smb2_open(struct ksmbd_work *work)
} else if (context) {
struct create_posix *posix = (struct create_posix *)context;
+ if (conn->dialect != SMB311_PROT_ID) {
+ rsp->hdr.Status = STATUS_INVALID_INFO_CLASS;
+ rc = -ENOTTY;
+ goto err_out2;
+ }
+ if (!tcon->posix_extensions) {
+ rc = -EOPNOTSUPP;
+ goto err_out2;
+ }
+ if (test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE)) {
+ rc = -EOPNOTSUPP;
+ goto err_out2;
+ }
if (le16_to_cpu(context->DataOffset) +
le32_to_cpu(context->DataLength) <
sizeof(struct create_posix) - 4) {
@@ -4289,6 +4297,10 @@ int smb2_open(struct ksmbd_work *work)
posix_ctxt = true;
}
}
+ if (test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE)) {
+ ksmbd_debug(SMB, "IPC pipe create request\n");
+ return create_smb2_pipe(work);
+ }
if (req->NameLength) {
name = smb2_get_name((char *)req + le16_to_cpu(req->NameOffset),
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 14/22] ksmbd: enforce POSIX append-only writes
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (11 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 13/22] ksmbd: validate POSIX create contexts Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 15/22] ksmbd: apply byte-range lock checks per open Namjae Jeon
` (7 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
POSIX append-only opens do not set O_APPEND, and the SMB append sentinel
is rejected as a negative write offset.
Track append-only POSIX opens, set O_APPEND and require the U64_MAX
offset sentinel for their writes. Accept FILE_WRITE_DATA or
FILE_APPEND_DATA for WRITE requests, preserving ordinary SMB write
permissions. Use the POSIX append flag and offset check to prevent
POSIX append-only opens from overwriting existing data.
For synchronous writes, derive the fsync range from the completed write
position so append writes flush the range actually written.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/smb2pdu.c | 14 ++++++++++++--
fs/smb/server/vfs.c | 3 +--
fs/smb/server/vfs_cache.h | 1 +
3 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index a1e553ddde46..396f10fb2a91 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -4736,6 +4736,9 @@ int smb2_open(struct ksmbd_work *work)
&may_flags,
req->CreateOptions,
file_present ? d_inode(path.dentry)->i_mode : 0);
+ if (posix_ctxt && (req->DesiredAccess & FILE_APPEND_DATA_LE) &&
+ !(req->DesiredAccess & FILE_WRITE_DATA_LE))
+ open_flags |= O_APPEND;
if (!test_tree_conn_flag(tcon, KSMBD_TREE_CONN_FLAG_WRITABLE)) {
if (open_flags & (O_CREAT | O_TRUNC)) {
@@ -4975,6 +4978,7 @@ int smb2_open(struct ksmbd_work *work)
FILE_WRITE_ATTRIBUTES_LE | FILE_SYNCHRONIZE_LE));
fp->is_posix_ctxt = posix_ctxt;
+ fp->posix_append = posix_ctxt && !!(open_flags & O_APPEND);
/* fp should be searchable through ksmbd_inode.m_fp_list
* after daccess, saccess, attrib_only, and stream are
@@ -10223,6 +10227,7 @@ int smb2_write(struct ksmbd_work *work)
ssize_t nbytes;
char *data_buf;
bool writethrough = false, is_rdma_channel = false;
+ bool append_write;
bool async_interim = false;
bool chseq_err = false;
int err = 0;
@@ -10254,7 +10259,8 @@ int smb2_write(struct ksmbd_work *work)
return smb2_write_pipe(work);
}
- offset = le64_to_cpu(req->Offset);
+ append_write = le64_to_cpu(req->Offset) == U64_MAX;
+ offset = append_write ? 0 : le64_to_cpu(req->Offset);
if (offset < 0) {
err = -EINVAL;
goto out;
@@ -10357,11 +10363,15 @@ int smb2_write(struct ksmbd_work *work)
goto out;
}
- if (!(fp->daccess & (FILE_WRITE_DATA_LE | FILE_READ_ATTRIBUTES_LE))) {
+ if (!(fp->daccess & (FILE_WRITE_DATA_LE | FILE_APPEND_DATA_LE))) {
pr_err("Not permitted to write : 0x%x\n", fp->daccess);
err = -EACCES;
goto out;
}
+ if (append_write != fp->posix_append) {
+ err = -EINVAL;
+ goto out;
+ }
if (work->next_smb2_rcv_hdr_off && !req->hdr.NextCommand) {
err = setup_async_work(work, NULL, NULL);
diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c
index 181d27f43f31..f917f6aeb84d 100644
--- a/fs/smb/server/vfs.c
+++ b/fs/smb/server/vfs.c
@@ -526,7 +526,6 @@ int ksmbd_vfs_write(struct ksmbd_work *work, struct ksmbd_file *fp,
ssize_t *written)
{
struct file *filp;
- loff_t offset = *pos;
int err = 0;
if (work->conn->connection_type) {
@@ -573,7 +572,7 @@ int ksmbd_vfs_write(struct ksmbd_work *work, struct ksmbd_file *fp,
*written = err;
err = 0;
if (sync) {
- err = vfs_fsync_range(filp, offset, offset + *written, 0);
+ err = vfs_fsync_range(filp, *pos - *written, *pos, 0);
if (err < 0)
pr_err("fsync failed for filename = %pD, err = %d\n",
fp->filp, err);
diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h
index 1054002ec4c5..064a053b92b1 100644
--- a/fs/smb/server/vfs_cache.h
+++ b/fs/smb/server/vfs_cache.h
@@ -173,6 +173,7 @@ struct ksmbd_file {
bool durable_replay_consumed;
bool is_posix_ctxt;
+ bool posix_append;
struct durable_owner owner;
__le16 channel_sequence;
unsigned int outstanding_requests;
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 15/22] ksmbd: apply byte-range lock checks per open
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (12 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 14/22] ksmbd: enforce POSIX append-only writes Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 16/22] ksmbd: use requested mode in POSIX creates Namjae Jeon
` (6 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
Using the tree's POSIX extension flag to skip byte-range lock checks
also skips them for ordinary opens on a POSIX-enabled tree.
Base the read and write lock checks on each file's POSIX create context
instead.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/vfs.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c
index f917f6aeb84d..de08e63a7794 100644
--- a/fs/smb/server/vfs.c
+++ b/fs/smb/server/vfs.c
@@ -362,7 +362,7 @@ int ksmbd_vfs_read(struct ksmbd_work *work, struct ksmbd_file *fp, size_t count,
if (ksmbd_stream_fd(fp))
return ksmbd_vfs_stream_read(fp, rbuf, pos, count);
- if (!work->tcon->posix_extensions) {
+ if (!fp->is_posix_ctxt) {
int ret;
ret = check_lock_range(filp, *pos, *pos + count - 1, READ);
@@ -547,7 +547,7 @@ int ksmbd_vfs_write(struct ksmbd_work *work, struct ksmbd_file *fp,
goto out;
}
- if (!work->tcon->posix_extensions) {
+ if (!fp->is_posix_ctxt) {
err = check_lock_range(filp, *pos, *pos + count - 1, WRITE);
if (err) {
pr_err("unable to write due to lock\n");
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 16/22] ksmbd: use requested mode in POSIX creates
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (13 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 15/22] ksmbd: apply byte-range lock checks per open Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 17/22] ksmbd: apply POSIX mode ACEs in security updates Namjae Jeon
` (5 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
The share mode helpers treat a requested POSIX mode of zero as an
unspecified mode, replacing it with the share's default permissions.
Use the requested mode, including zero, before applying the share masks
and forced modes. Pass an explicit default-mode sentinel for non-POSIX
creates to keep their existing mode selection.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/mgmt/share_config.h | 4 ++--
fs/smb/server/smb2pdu.c | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/fs/smb/server/mgmt/share_config.h b/fs/smb/server/mgmt/share_config.h
index 91691b525907..390c966726f4 100644
--- a/fs/smb/server/mgmt/share_config.h
+++ b/fs/smb/server/mgmt/share_config.h
@@ -45,7 +45,7 @@ static inline umode_t
share_config_create_mode(struct ksmbd_share_config *share,
umode_t posix_mode)
{
- umode_t mode = (posix_mode ?: (umode_t)-1) & share->create_mask;
+ umode_t mode = posix_mode & share->create_mask;
return mode | share->force_create_mode;
}
@@ -54,7 +54,7 @@ static inline umode_t
share_config_directory_mode(struct ksmbd_share_config *share,
umode_t posix_mode)
{
- umode_t mode = (posix_mode ?: (umode_t)-1) & share->directory_mask;
+ umode_t mode = posix_mode & share->directory_mask;
return mode | share->force_directory_mode;
}
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 396f10fb2a91..953a2eeaa15a 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -4752,7 +4752,7 @@ int smb2_open(struct ksmbd_work *work)
/*create file if not present */
if (!file_present) {
rc = smb2_creat(work, &path, name, open_flags,
- posix_mode,
+ posix_ctxt ? posix_mode : (umode_t)-1,
req->CreateOptions & FILE_DIRECTORY_FILE_LE);
if (rc) {
if (rc == -ENOENT) {
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 17/22] ksmbd: apply POSIX mode ACEs in security updates
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (14 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 16/22] ksmbd: use requested mode in POSIX creates Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 18/22] ksmbd: correct POSIX file metadata in responses Namjae Jeon
` (4 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
Explicit modes carried by NFS mode ACEs can be overwritten by the DACL
permission fallback, and security updates discard special mode bits.
Track modes supplied by ACCESS_ALLOWED NFS mode ACEs and bypass the
permission fallback for them. Apply both permission and special bits
when setting an explicit POSIX mode.
For SET_INFO, require a POSIX open for explicit mode changes and apply
the mode only when DACL security information is requested.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/smb2pdu.c | 7 +++++--
fs/smb/server/smbacl.c | 26 +++++++++++++++++++++++---
fs/smb/server/smbacl.h | 4 +++-
3 files changed, 31 insertions(+), 6 deletions(-)
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 953a2eeaa15a..92489bb86791 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -3667,7 +3667,8 @@ static int smb2_create_sd_buffer(struct ksmbd_work *work,
sizeof(struct create_sd_buf_req))
return -EINVAL;
return set_info_sec(work->conn, work->tcon, path, &sd_buf->ntsd,
- le32_to_cpu(sd_buf->ccontext.DataLength), true, false);
+ le32_to_cpu(sd_buf->ccontext.DataLength), true, false,
+ true, true);
}
static int ksmbd_acls_fattr(struct smb_fattr *fattr,
@@ -9241,7 +9242,9 @@ static int smb2_set_info_sec(struct ksmbd_file *fp, int addition_info,
return -EACCES;
return set_info_sec(fp->conn, fp->tcon, &fp->filp->f_path, pntsd,
- buf_len, false, true);
+ buf_len, false, true,
+ fp->conn->dialect == SMB311_PROT_ID && fp->is_posix_ctxt,
+ addition_info & DACL_SECINFO);
}
static int smb2_set_info_quota(struct ksmbd_work *work, struct ksmbd_file *fp,
diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c
index e75247915c87..a01e0a8b76cb 100644
--- a/fs/smb/server/smbacl.c
+++ b/fs/smb/server/smbacl.c
@@ -477,10 +477,12 @@ static int parse_dacl(struct mnt_idmap *idmap,
ppace[i]->access_req =
smb_map_generic_desired_access(ppace[i]->access_req);
- if (ppace[i]->sid.num_subauth >= 3 &&
+ if (ppace[i]->type == ACCESS_ALLOWED &&
+ ppace[i]->sid.num_subauth >= 3 &&
!(compare_sids(&ppace[i]->sid, &sid_unix_NFS_mode))) {
fattr->cf_mode =
le32_to_cpu(ppace[i]->sid.sub_auth[2]);
+ fattr->cf_posix_mode = true;
break;
} else if (!compare_sids(&ppace[i]->sid, pownersid)) {
acl_mode = access_flags_to_mode(fattr,
@@ -546,6 +548,10 @@ static int parse_dacl(struct mnt_idmap *idmap,
((acl_mode & 0700) >> 6) | 0004;
}
}
+ if (fattr->cf_posix_mode) {
+ ret = 0;
+ goto out;
+ }
if (owner_found) {
/* The owner must be set to at least read-only. */
@@ -1673,7 +1679,8 @@ EXPORT_SYMBOL_IF_KUNIT(smb_check_perm_dacl);
int set_info_sec(struct ksmbd_conn *conn, struct ksmbd_tree_connect *tcon,
const struct path *path, struct smb_ntsd *pntsd, int ntsd_len,
- bool type_check, bool get_write)
+ bool type_check, bool get_write, bool posix_open,
+ bool dacl_requested)
{
int rc;
struct smb_fattr fattr = {{0}};
@@ -1688,6 +1695,14 @@ int set_info_sec(struct ksmbd_conn *conn, struct ksmbd_tree_connect *tcon,
rc = parse_sec_desc(idmap, pntsd, ntsd_len, &fattr);
if (rc)
goto out;
+ if (fattr.cf_posix_mode && !dacl_requested) {
+ fattr.cf_posix_mode = false;
+ fattr.cf_mode = inode->i_mode;
+ }
+ if (fattr.cf_posix_mode && !posix_open) {
+ rc = -EOPNOTSUPP;
+ goto out;
+ }
newattrs.ia_valid = ATTR_CTIME;
if (!uid_eq(fattr.cf_uid, INVALID_UID)) {
@@ -1699,7 +1714,12 @@ int set_info_sec(struct ksmbd_conn *conn, struct ksmbd_tree_connect *tcon,
newattrs.ia_gid = fattr.cf_gid;
}
newattrs.ia_valid |= ATTR_MODE;
- newattrs.ia_mode = (inode->i_mode & ~0777) | (fattr.cf_mode & 0777);
+ if (fattr.cf_posix_mode)
+ newattrs.ia_mode = (inode->i_mode & ~07777) |
+ (fattr.cf_mode & 07777);
+ else
+ newattrs.ia_mode = (inode->i_mode & ~0777) |
+ (fattr.cf_mode & 0777);
ksmbd_vfs_remove_acl_xattrs(idmap, path);
/* Update posix acls */
diff --git a/fs/smb/server/smbacl.h b/fs/smb/server/smbacl.h
index 01810c16cc04..c9705548efb2 100644
--- a/fs/smb/server/smbacl.h
+++ b/fs/smb/server/smbacl.h
@@ -43,6 +43,7 @@ struct smb_fattr {
kuid_t cf_uid;
kgid_t cf_gid;
umode_t cf_mode;
+ bool cf_posix_mode;
__le32 daccess;
struct posix_acl *cf_acls;
struct posix_acl *cf_dacls;
@@ -99,7 +100,8 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path,
bool strict);
int set_info_sec(struct ksmbd_conn *conn, struct ksmbd_tree_connect *tcon,
const struct path *path, struct smb_ntsd *pntsd, int ntsd_len,
- bool type_check, bool get_write);
+ bool type_check, bool get_write, bool posix_open,
+ bool dacl_requested);
void id_to_sid(unsigned int cid, uint sidtype, struct smb_sid *ssid);
void ksmbd_init_domain(u32 *sub_auth);
size_t smb_acl_sec_desc_scratch_len(struct smb_fattr *fattr,
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 18/22] ksmbd: correct POSIX file metadata in responses
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (15 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 17/22] ksmbd: apply POSIX mode ACEs in security updates Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 19/22] ksmbd: include filenames in POSIX query info Namjae Jeon
` (3 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
POSIX responses omit special permission bits, and the create response
reports a file identifier as its reparse tag without encoding the file
type. Directory and file queries also report rdev as the filesystem
device identifier.
Report the POSIX mode and file type in create responses, include
special permission bits in all three response paths, and use the
special-file reparse tag consistently. Set the corresponding DOS
attribute in directory and file queries, report the filesystem device
identifier and initialize reserved fields.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/oplock.c | 23 +++++++++++++++++++++--
fs/smb/server/smb2pdu.c | 19 ++++++++++++++-----
fs/smb/server/smb2pdu.h | 2 ++
3 files changed, 37 insertions(+), 7 deletions(-)
diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c
index a911ff70b29e..10c81550b8a7 100644
--- a/fs/smb/server/oplock.c
+++ b/fs/smb/server/oplock.c
@@ -2569,8 +2569,27 @@ void create_posix_rsp_buf(char *cc, struct ksmbd_file *fp)
buf->Name[15] = 0x7C;
buf->nlink = cpu_to_le32(inode->i_nlink);
- buf->reparse_tag = cpu_to_le32(fp->volatile_id);
- buf->mode = cpu_to_le32(inode->i_mode & 0777);
+ buf->reparse_tag = smb2_get_reparse_tag_special_file(inode->i_mode);
+ buf->mode = cpu_to_le32(inode->i_mode & 07777);
+ switch (inode->i_mode & S_IFMT) {
+ case S_IFDIR:
+ buf->mode |= cpu_to_le32(POSIX_TYPE_DIR << POSIX_FILETYPE_SHIFT);
+ break;
+ case S_IFLNK:
+ buf->mode |= cpu_to_le32(POSIX_TYPE_SYMLINK << POSIX_FILETYPE_SHIFT);
+ break;
+ case S_IFCHR:
+ buf->mode |= cpu_to_le32(POSIX_TYPE_CHARDEV << POSIX_FILETYPE_SHIFT);
+ break;
+ case S_IFBLK:
+ buf->mode |= cpu_to_le32(POSIX_TYPE_BLKDEV << POSIX_FILETYPE_SHIFT);
+ break;
+ case S_IFIFO:
+ buf->mode |= cpu_to_le32(POSIX_TYPE_FIFO << POSIX_FILETYPE_SHIFT);
+ break;
+ case S_IFSOCK:
+ buf->mode |= cpu_to_le32(POSIX_TYPE_SOCKET << POSIX_FILETYPE_SHIFT);
+ }
/*
* SidBuffer(44) contain two sids(Domain sid(28), UNIX group sid(16)).
* Domain sid(28) = revision(1) + num_subauth(1) + authority(6) +
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 92489bb86791..a4908b6e566e 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -1294,7 +1294,7 @@ void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status)
ksmbd_free_work_struct(in_work);
}
-static __le32 smb2_get_reparse_tag_special_file(umode_t mode)
+__le32 smb2_get_reparse_tag_special_file(umode_t mode)
{
if (S_ISDIR(mode) || S_ISREG(mode))
return 0;
@@ -6020,6 +6020,7 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level,
posix_info = (struct smb2_posix_info *)kstat;
posix_info->Ignored = 0;
+ posix_info->Zero = 0;
posix_info->CreationTime = cpu_to_le64(ksmbd_kstat->create_time);
time = ksmbd_UnixTimeToNT(ksmbd_kstat->kstat->ctime);
posix_info->ChangeTime = cpu_to_le64(time);
@@ -6029,9 +6030,11 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level,
posix_info->LastWriteTime = cpu_to_le64(time);
posix_info->EndOfFile = cpu_to_le64(ksmbd_kstat->kstat->size);
posix_info->AllocationSize = cpu_to_le64(ksmbd_kstat->kstat->blocks << 9);
- posix_info->DeviceId = cpu_to_le32(ksmbd_kstat->kstat->rdev);
+ posix_info->DeviceId = cpu_to_le32(ksmbd_kstat->kstat->dev);
posix_info->HardLinks = cpu_to_le32(ksmbd_kstat->kstat->nlink);
- posix_info->Mode = cpu_to_le32(ksmbd_kstat->kstat->mode & 0777);
+ posix_info->ReparseTag =
+ smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode);
+ posix_info->Mode = cpu_to_le32(ksmbd_kstat->kstat->mode & 07777);
switch (ksmbd_kstat->kstat->mode & S_IFMT) {
case S_IFDIR:
posix_info->Mode |= cpu_to_le32(POSIX_TYPE_DIR << POSIX_FILETYPE_SHIFT);
@@ -6056,6 +6059,8 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level,
posix_info->DosAttributes =
S_ISDIR(ksmbd_kstat->kstat->mode) ?
FILE_ATTRIBUTE_DIRECTORY_LE : FILE_ATTRIBUTE_ARCHIVE_LE;
+ if (posix_info->ReparseTag)
+ posix_info->DosAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE;
if (d_info->hide_dot_file && d_info->name[0] == '.')
posix_info->DosAttributes |= FILE_ATTRIBUTE_HIDDEN_LE;
/*
@@ -7500,7 +7505,7 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp,
file_info->AllocationSize = cpu_to_le64((u64)seof);
}
file_info->HardLinks = cpu_to_le32(stat.nlink);
- file_info->Mode = cpu_to_le32(stat.mode & 0777);
+ file_info->Mode = cpu_to_le32(stat.mode & 07777);
switch (stat.mode & S_IFMT) {
case S_IFDIR:
file_info->Mode |= cpu_to_le32(POSIX_TYPE_DIR << POSIX_FILETYPE_SHIFT);
@@ -7521,7 +7526,11 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp,
file_info->Mode |= cpu_to_le32(POSIX_TYPE_SOCKET << POSIX_FILETYPE_SHIFT);
}
- file_info->DeviceId = cpu_to_le32(stat.rdev);
+ file_info->DeviceId = cpu_to_le32(stat.dev);
+ file_info->Zero = 0;
+ file_info->ReparseTag = smb2_get_reparse_tag_special_file(stat.mode);
+ if (file_info->ReparseTag)
+ file_info->DosAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE;
/*
* Sids(32) contain two sids(Domain sid(16), UNIX group sid(16)).
diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h
index e07700d0a405..e706aa52352c 100644
--- a/fs/smb/server/smb2pdu.h
+++ b/fs/smb/server/smb2pdu.h
@@ -168,6 +168,8 @@ struct create_posix_rsp {
u8 SidBuffer[44];
} __packed;
+__le32 smb2_get_reparse_tag_special_file(umode_t mode);
+
#define SMB2_0_IOCTL_IS_FSCTL 0x00000001
struct sockaddr_storage_rsp {
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 19/22] ksmbd: include filenames in POSIX query info
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (16 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 18/22] ksmbd: correct POSIX file metadata in responses Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 20/22] ksmbd: require POSIX opens for file and directory queries Namjae Jeon
` (2 subsequent siblings)
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
The POSIX file query response ends after the owner and group SIDs,
omitting the filename length and filename.
Append the path relative to the share in UTF-16, without a leading
separator, and include its byte length in the response. Check both the
fixed fields and the converted filename against the available output
buffer before writing the response.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/smb2pdu.c | 56 ++++++++++++++++++++++++++++++++++++-----
1 file changed, 50 insertions(+), 6 deletions(-)
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index a4908b6e566e..6fd8ec6be222 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -7463,8 +7463,10 @@ static int get_file_attribute_tag_info(struct smb2_query_info_rsp *rsp,
return 0;
}
-static int find_file_posix_info(struct smb2_query_info_rsp *rsp,
- struct ksmbd_file *fp, void *rsp_org)
+static int find_file_posix_info(struct ksmbd_work *work,
+ struct smb2_query_info_req *req,
+ struct smb2_query_info_rsp *rsp,
+ struct ksmbd_file *fp)
{
struct smb311_posix_qinfo *file_info;
struct inode *inode = file_inode(fp->filp);
@@ -7472,9 +7474,13 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp,
vfsuid_t vfsuid = i_uid_into_vfsuid(idmap, inode);
vfsgid_t vfsgid = i_gid_into_vfsgid(idmap, inode);
struct kstat stat;
+ char *filename;
+ __le16 *utf16_name;
+ __le32 *filename_len;
+ size_t name_bytes;
u64 time;
- int out_buf_len = sizeof(struct smb311_posix_qinfo) + 32;
- int ret;
+ int fixed_len = sizeof(struct smb311_posix_qinfo) + 32 + sizeof(__le32);
+ int max_len, conv_len, ret;
if (!(fp->daccess & FILE_READ_ATTRIBUTES_LE)) {
pr_err("no right to read the attributes : 0x%x\n",
@@ -7485,6 +7491,38 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp,
ret = get_file_allocation_stat(fp, &stat);
if (ret)
return ret;
+ max_len = smb2_calc_max_out_buf_len(work,
+ offsetof(struct smb2_query_info_rsp, Buffer),
+ le32_to_cpu(req->OutputBufferLength));
+ if (max_len < fixed_len) {
+ rsp->hdr.Status = STATUS_INFO_LENGTH_MISMATCH;
+ return -EINVAL;
+ }
+ filename = convert_to_nt_pathname(work->tcon->share_conf,
+ &fp->filp->f_path);
+ if (IS_ERR(filename))
+ return PTR_ERR(filename);
+ if (filename[0] == '\\')
+ memmove(filename, filename + 1, strlen(filename));
+ name_bytes = strlen(filename);
+ utf16_name = kmalloc_array(name_bytes + 1, sizeof(*utf16_name),
+ KSMBD_DEFAULT_GFP);
+ if (!utf16_name) {
+ kfree(filename);
+ return -ENOMEM;
+ }
+ conv_len = smbConvertToUTF16(utf16_name, filename, name_bytes,
+ work->conn->local_nls, 0);
+ kfree(filename);
+ if (conv_len < 0) {
+ kfree(utf16_name);
+ return conv_len;
+ }
+ if (conv_len > (max_len - fixed_len) / sizeof(*utf16_name)) {
+ kfree(utf16_name);
+ rsp->hdr.Status = STATUS_INFO_LENGTH_MISMATCH;
+ return -EINVAL;
+ }
file_info = (struct smb311_posix_qinfo *)rsp->Buffer;
file_info->CreationTime = cpu_to_le64(fp->create_time);
@@ -7541,8 +7579,14 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp,
SIDUNIX_USER, (struct smb_sid *)&file_info->Sids[0]);
id_to_sid(from_kgid_munged(&init_user_ns, vfsgid_into_kgid(vfsgid)),
SIDUNIX_GROUP, (struct smb_sid *)&file_info->Sids[16]);
+ filename_len = (__le32 *)&file_info->Sids[32];
+ memcpy(&file_info->Sids[36], utf16_name,
+ conv_len * sizeof(*utf16_name));
+ kfree(utf16_name);
+ *filename_len = cpu_to_le32(conv_len * sizeof(__le16));
- rsp->OutputBufferLength = cpu_to_le32(out_buf_len);
+ rsp->OutputBufferLength = cpu_to_le32(fixed_len +
+ conv_len * sizeof(__le16));
return 0;
}
@@ -7652,7 +7696,7 @@ static int smb2_get_info_file(struct ksmbd_work *work,
pr_err("client doesn't negotiate with SMB3.1.1 POSIX Extensions\n");
rc = -EOPNOTSUPP;
} else {
- rc = find_file_posix_info(rsp, fp, work->response_buf);
+ rc = find_file_posix_info(work, req, rsp, fp);
}
break;
default:
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 20/22] ksmbd: require POSIX opens for file and directory queries
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (17 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 19/22] ksmbd: include filenames in POSIX query info Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 21/22] ksmbd: query POSIX filesystem info through the open file Namjae Jeon
2026-10-17 1:03 ` [PATCH 22/22] ksmbd: correct POSIX filesystem block size and response length Namjae Jeon
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
POSIX file information queries only check the tree's extension flag,
while directory queries do not verify that the directory was opened with
a POSIX create context.
Require SMB3.1.1, negotiated POSIX tree extensions and a POSIX open for
both file and directory POSIX information requests. Return
STATUS_INVALID_INFO_CLASS when these conditions are not met.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/smb2pdu.c | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 6fd8ec6be222..b3683e85cb31 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -6470,6 +6470,12 @@ int smb2_query_dir(struct ksmbd_work *work)
rc = -EBADF;
goto err_out2;
}
+ if (req->FileInformationClass == SMB_FIND_FILE_POSIX_INFO &&
+ (conn->dialect != SMB311_PROT_ID ||
+ !work->tcon->posix_extensions || !dir_fp->is_posix_ctxt)) {
+ rc = -EFAULT;
+ goto err_out2;
+ }
if (!(dir_fp->daccess & FILE_LIST_DIRECTORY_LE) ||
inode_permission(file_mnt_idmap(dir_fp->filp),
@@ -7692,8 +7698,9 @@ static int smb2_get_info_file(struct ksmbd_work *work,
rc = get_file_attribute_tag_info(rsp, fp, work->response_buf);
break;
case SMB_FIND_FILE_POSIX_INFO:
- if (!work->tcon->posix_extensions) {
- pr_err("client doesn't negotiate with SMB3.1.1 POSIX Extensions\n");
+ if (work->conn->dialect != SMB311_PROT_ID ||
+ !work->tcon->posix_extensions || !fp->is_posix_ctxt) {
+ rsp->hdr.Status = STATUS_INVALID_INFO_CLASS;
rc = -EOPNOTSUPP;
} else {
rc = find_file_posix_info(work, req, rsp, fp);
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 21/22] ksmbd: query POSIX filesystem info through the open file
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (18 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 20/22] ksmbd: require POSIX opens for file and directory queries Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
2026-10-17 1:03 ` [PATCH 22/22] ksmbd: correct POSIX filesystem block size and response length Namjae Jeon
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
POSIX filesystem queries use the share root for statfs and do not
validate the requested file handle. This can report the wrong filesystem
for files below a mount point within the share.
Require SMB3.1.1 POSIX extensions and a valid POSIX open. Resolve the
requested handle, including compound file IDs, and use its referenced
path for the filesystem query.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/smb2pdu.c | 73 ++++++++++++++++++++++++++---------------
1 file changed, 47 insertions(+), 26 deletions(-)
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index b3683e85cb31..6dfeb6078600 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -7747,20 +7747,47 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work,
{
struct ksmbd_conn *conn = work->conn;
struct ksmbd_share_config *share = work->tcon->share_conf;
+ struct ksmbd_file *fp;
int fsinfoclass = 0;
struct kstatfs stfs;
struct path path;
int rc = 0, len;
unsigned int fixed_len = 0;
+ u64 id, pid;
- if (!share->path)
- return -EIO;
+ if (req->FileInfoClass == FS_POSIX_INFORMATION) {
+ if (conn->dialect != SMB311_PROT_ID ||
+ !work->tcon->posix_extensions) {
+ rsp->hdr.Status = STATUS_INVALID_INFO_CLASS;
+ return -EOPNOTSUPP;
+ }
+ id = req->VolatileFileId;
+ pid = req->PersistentFileId;
+ if (work->next_smb2_rcv_hdr_off && !has_file_id(id)) {
+ id = work->compound_fid;
+ pid = work->compound_pfid;
+ }
+ fp = ksmbd_lookup_fd_slow(work, id, pid);
+ if (!fp)
+ return -ENOENT;
+ if (!fp->is_posix_ctxt) {
+ ksmbd_fd_put(work, fp);
+ rsp->hdr.Status = STATUS_INVALID_INFO_CLASS;
+ return -EOPNOTSUPP;
+ }
+ path = fp->filp->f_path;
+ path_get(&path);
+ ksmbd_fd_put(work, fp);
+ } else {
+ if (!share->path)
+ return -EIO;
- scoped_with_init_fs()
- rc = kern_path(share->path, LOOKUP_NO_SYMLINKS, &path);
- if (rc) {
- pr_err("cannot create vfs path\n");
- return -EIO;
+ scoped_with_init_fs()
+ rc = kern_path(share->path, LOOKUP_NO_SYMLINKS, &path);
+ if (rc) {
+ pr_err("cannot create vfs path\n");
+ return -EIO;
+ }
}
rc = vfs_statfs(&path, &stfs);
@@ -7973,25 +8000,19 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work,
{
FILE_SYSTEM_POSIX_INFO *info;
- if (!work->tcon->posix_extensions) {
- pr_err("client doesn't negotiate with SMB3.1.1 POSIX Extensions\n");
- path_put(&path);
- return -EOPNOTSUPP;
- } else {
- info = (FILE_SYSTEM_POSIX_INFO *)(rsp->Buffer);
- info->OptimalTransferSize = cpu_to_le32(stfs.f_bsize);
- info->BlockSize = cpu_to_le32(stfs.f_bsize);
- info->TotalBlocks = cpu_to_le64(stfs.f_blocks);
- info->BlocksAvail = cpu_to_le64(stfs.f_bfree);
- info->UserBlocksAvail = cpu_to_le64(stfs.f_bavail);
- info->TotalFileNodes = cpu_to_le64(stfs.f_files);
- info->FreeFileNodes = cpu_to_le64(stfs.f_ffree);
- info->FileSysIdentifier =
- cpu_to_le64((u64)(u32)stfs.f_fsid.val[1] << 32 |
- (u32)stfs.f_fsid.val[0]);
- rsp->OutputBufferLength = cpu_to_le32(56);
- fixed_len = 56;
- }
+ info = (FILE_SYSTEM_POSIX_INFO *)(rsp->Buffer);
+ info->OptimalTransferSize = cpu_to_le32(stfs.f_bsize);
+ info->BlockSize = cpu_to_le32(stfs.f_bsize);
+ info->TotalBlocks = cpu_to_le64(stfs.f_blocks);
+ info->BlocksAvail = cpu_to_le64(stfs.f_bfree);
+ info->UserBlocksAvail = cpu_to_le64(stfs.f_bavail);
+ info->TotalFileNodes = cpu_to_le64(stfs.f_files);
+ info->FreeFileNodes = cpu_to_le64(stfs.f_ffree);
+ info->FileSysIdentifier =
+ cpu_to_le64((u64)(u32)stfs.f_fsid.val[1] << 32 |
+ (u32)stfs.f_fsid.val[0]);
+ rsp->OutputBufferLength = cpu_to_le32(56);
+ fixed_len = 56;
break;
}
default:
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH 22/22] ksmbd: correct POSIX filesystem block size and response length
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
` (19 preceding siblings ...)
2026-10-17 1:03 ` [PATCH 21/22] ksmbd: query POSIX filesystem info through the open file Namjae Jeon
@ 2026-10-17 1:03 ` Namjae Jeon
20 siblings, 0 replies; 22+ messages in thread
From: Namjae Jeon @ 2026-10-17 1:03 UTC (permalink / raw)
To: linux-cifs
Cc: senozhatsky, tom, atteh.mailbox, chenxiaosong, chenxiaosong,
Namjae Jeon
POSIX filesystem information uses f_bsize for BlockSize even when the
filesystem reports a distinct fragment size for its block counts.
Use f_frsize when available, falling back to f_bsize. Keep f_bsize as
the optimal transfer size and derive the response length from the
information structure instead of a hard-coded value.
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
---
fs/smb/server/smb2pdu.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 6dfeb6078600..19ff569c001b 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -8002,7 +8002,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work,
info = (FILE_SYSTEM_POSIX_INFO *)(rsp->Buffer);
info->OptimalTransferSize = cpu_to_le32(stfs.f_bsize);
- info->BlockSize = cpu_to_le32(stfs.f_bsize);
+ info->BlockSize = cpu_to_le32(stfs.f_frsize ?: stfs.f_bsize);
info->TotalBlocks = cpu_to_le64(stfs.f_blocks);
info->BlocksAvail = cpu_to_le64(stfs.f_bfree);
info->UserBlocksAvail = cpu_to_le64(stfs.f_bavail);
@@ -8011,8 +8011,8 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work,
info->FileSysIdentifier =
cpu_to_le64((u64)(u32)stfs.f_fsid.val[1] << 32 |
(u32)stfs.f_fsid.val[0]);
- rsp->OutputBufferLength = cpu_to_le32(56);
- fixed_len = 56;
+ rsp->OutputBufferLength = cpu_to_le32(sizeof(*info));
+ fixed_len = sizeof(*info);
break;
}
default:
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
end of thread, other threads:[~2026-10-17 1:04 UTC | newest]
Thread overview: 22+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-17 1:03 [PATCH 01/22] ksmbd: release connection lock while draining old session Namjae Jeon
2026-10-17 1:03 ` [PATCH 02/22] ksmbd: serialize oplock break ACK state updates Namjae Jeon
2026-10-17 1:03 ` [PATCH 03/22] ksmbd: track delivery of queued TCP writes Namjae Jeon
2026-10-17 1:03 ` [PATCH 04/22] ksmbd: retry break notifications on another connection Namjae Jeon
2026-10-17 1:03 ` [PATCH 05/22] ksmbd: keep lease tables until their last lease is closed Namjae Jeon
2026-10-17 1:03 ` [PATCH 06/22] ksmbd: index byte-range locks independently of their connection Namjae Jeon
2026-10-17 1:03 ` [PATCH 07/22] ksmbd: rebind completed CREATE replays without draining the old channel Namjae Jeon
2026-10-17 1:03 ` [PATCH 08/22] ksmbd: track SMB2 locks above OFFSET_MAX in server lock table Namjae Jeon
2026-10-17 1:03 ` [PATCH 09/22] ksmbd: expire unacknowledged oplock breaks Namjae Jeon
2026-10-17 1:03 ` [PATCH 10/22] ksmbd: expire unacknowledged lease breaks Namjae Jeon
2026-10-17 1:03 ` [PATCH 11/22] ksmbd: support SMB2 quota query and set information Namjae Jeon
2026-10-17 1:03 ` [PATCH 12/22] ksmbd: validate POSIX negotiate contexts Namjae Jeon
2026-10-17 1:03 ` [PATCH 13/22] ksmbd: validate POSIX create contexts Namjae Jeon
2026-10-17 1:03 ` [PATCH 14/22] ksmbd: enforce POSIX append-only writes Namjae Jeon
2026-10-17 1:03 ` [PATCH 15/22] ksmbd: apply byte-range lock checks per open Namjae Jeon
2026-10-17 1:03 ` [PATCH 16/22] ksmbd: use requested mode in POSIX creates Namjae Jeon
2026-10-17 1:03 ` [PATCH 17/22] ksmbd: apply POSIX mode ACEs in security updates Namjae Jeon
2026-10-17 1:03 ` [PATCH 18/22] ksmbd: correct POSIX file metadata in responses Namjae Jeon
2026-10-17 1:03 ` [PATCH 19/22] ksmbd: include filenames in POSIX query info Namjae Jeon
2026-10-17 1:03 ` [PATCH 20/22] ksmbd: require POSIX opens for file and directory queries Namjae Jeon
2026-10-17 1:03 ` [PATCH 21/22] ksmbd: query POSIX filesystem info through the open file Namjae Jeon
2026-10-17 1:03 ` [PATCH 22/22] ksmbd: correct POSIX filesystem block size and response length Namjae Jeon
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox