summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-10-25 15:47:41 -1000
committerLinus Torvalds <torvalds@linux-foundation.org>2026-10-25 15:47:41 -1000
commit4e1b4fdf6113b4eb9564bdf3b76ee3c3132da7d4 (patch)
tree0f88f96f5977b697047fd793ed8f86a982a10252
parent9e089e670e9ad1caf4a5b2b63e01b2dc076efbe3 (diff)
parent2c36c4a86104d763ee798d4a89e2d3384b4cd626 (diff)
Merge tag 'ksmbd-for-7.4-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/linkinjeon/smb
Pull smb server updates from Namjae Jeon: - Add extended SMB2 directory information support. Implement FileIdExtdDirectoryInformation, FileId64ExtdDirectoryInformation, FileId64ExtdBothDirectoryInformation, FileIdAllExtdDirectoryInformation, and FileIdAllExtdBothDirectoryInformation, including reparse tags, file attributes, and initialized response padding. - Track unsigned 64-bit lock ranges in the server lock index, with conflict detection, blocking waits, cancellation, rollback, and cleanup for ranges beyond the VFS signed-offset limit. - Add configurable per-connection session limits. Reserve slots for new sessions and multichannel bindings, release them during failure and cleanup, and expose the configured limit through procfs. - Validate POSIX negotiate and CREATE contexts, including payload lengths, extension tags, duplicate contexts, dialect requirements, and share eligibility. Require POSIX opens for POSIX file, directory, and filesystem information queries. - Apply requested POSIX create modes, including mode zero, and NFS mode ACEs with special permission bits. Enforce append-only writes and base byte-range lock checks on each open's POSIX state. - Complete POSIX metadata responses with filenames, file types, permission bits, and filesystem device identifiers. Query filesystem information through the requested open and report the filesystem fragment size and response length. - Validate SMB3 encryption transform flags and ensure decrypted compound requests use the session authenticated by the transform, including requests containing compressed payloads. - Serialize negotiation with receive processing, size CREATE responses for their contexts, reserve UTF-16 terminator space in stream information, and validate WRITE data against the active compound request to prevent buffer overruns and stale-data writes. - Validate security descriptor offsets before pointer arithmetic, handle DACL offset overflow on 32-bit systems, and propagate inode ACL lookup failures safely. - Deny access through empty DACLs while retaining the owner's implicit security-management rights. Add KUnit regression coverage for DACL walk boundaries and maximal-access calculations. - Validate RPC pipe ownership for IPC$ reads and writes, and fix FSCTL_PIPE_TRANSCEIVE handle lookup and response file IDs. - Fix durable reconnect path validation for symlinked share paths, shares exporting "/", and handles opened at the share root. Check path boundaries before comparisons to prevent out-of-bounds reads and cross-share confusion. - Rebind completed durable CREATE replays to surviving channels without draining unrelated requests on the old channel. Transfer open accounting with the binding and avoid consuming additional open-file budget during durable reconnects. - Scope AppInstanceId takeovers to the matching share, path, stream, and client identity. Check access and AppInstanceVersion before closing an existing open. - Retry oplock and lease break notifications through other live channels, track queued TCP delivery, serialize acknowledgment updates, and expire unacknowledged breaks while waking blocked requests. - Keep lease tables and byte-range lock indexes independent of their original connection so surviving channels and detached durable opens retain access to lease and lock state. - Reclaim ended sessions after logoff and expiration, hold connection references for session channels, and drain procfs readers before releasing session table references. Avoid deadlocks during previous-session teardown and connection scavenging. - Prevent file publication after tree disconnect and retain tree references until final file release. Take parent dentry references during directory enumeration and serialize directory emptiness checks with readdir. - Prevent share-root removal through empty removal paths and return appropriate errors for directory zero-data requests and temporary directory attributes. - Fix named-stream writes, EOF changes, end-of-file reads, and content copying into rename targets. Reuse existing xattr names, report sizes by exact xattr name, and avoid emptying streams after lookup or permission failures. - Round allocation requests to the advertised allocation unit and avoid allocation changes that leave on-disk EOF inconsistent with the in-memory size. - Inherit FileIds across related compound requests, including CHANGE_NOTIFY, and use the existing async request machinery for notify waiting and cancellation. - Restore share configuration and Kerberos authentication compatibility with released ksmbd-tools. Advertise extended session security for raw NTLMSSP when requested by the client. - Improve Windows chained-compression compatibility, synthesized AFP_AfpInfo headers, generated short names, and UTF-16 alignment in partial normalized-name responses. - Fix interface-query locking, response chaining, link-speed reporting, and independent IPv4/IPv6 address enumeration. Display native IPv6 client addresses accurately in procfs. - Improve SMB Direct socket cleanup by reclaiming failed pending connections, skipping failed sockets during accept, avoiding indefinite disconnect waits, and handling unexpected receive opcodes without triggering kernel warnings. - Select CRYPTO_AES for SMB3 encryption, replace deprecated system workqueues, and update feature status documentation and POSIX specification references. * tag 'ksmbd-for-7.4-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/linkinjeon/smb: (95 commits) ksmbd: select CRYPTO_AES ksmbd: prevent share root removal through empty paths ksmbd: serialize directory emptiness checks with readdir ksmbd: fix parent dentry lifetime in directory enumeration smb/server: prevent file publication after tree disconnect ksmbd: restore Kerberos logins with released ksmbd-tools smb/server: replace deprecated system workqueues ksmbd: reject FILE_ATTRIBUTE_TEMPORARY on directories ksmbd: deny access for an empty DACL instead of granting the request ksmbd: reject foreign RPC pipe handles on IPC$ read and write ksmbd: fix fd_limit accounting across durable reconnects ksmbd: drain session procfs readers before dropping table references ksmbd: reclaim ended sessions on logoff and expiration ksmbd: limit sessions on each connection ksmbd: hold a connection reference for each session channel ksmbd: defer connection scavenging while session locks are busy ksmbd: accept share config responses from ksmbd-tools before 3.5.3 ksmbd: round SetAllocation to the advertised allocation unit ksmbd: avoid restoring only in-memory EOF after allocation change ksmbd: inherit FileIds in related compound requests ...
-rw-r--r--Documentation/filesystems/smb/ksmbd.rst14
-rw-r--r--fs/smb/common/compress/compress.c16
-rw-r--r--fs/smb/common/fscc.h5
-rw-r--r--fs/smb/common/smbglob.h1
-rw-r--r--fs/smb/server/Kconfig3
-rw-r--r--fs/smb/server/Makefile1
-rw-r--r--fs/smb/server/auth.c6
-rw-r--r--fs/smb/server/compress.c1
-rw-r--r--fs/smb/server/connection.c59
-rw-r--r--fs/smb/server/connection.h31
-rw-r--r--fs/smb/server/ksmbd_netlink.h8
-rw-r--r--fs/smb/server/ksmbd_work.c22
-rw-r--r--fs/smb/server/ksmbd_work.h18
-rw-r--r--fs/smb/server/mgmt/share_config.c43
-rw-r--r--fs/smb/server/mgmt/share_config.h6
-rw-r--r--fs/smb/server/mgmt/tree_connect.c7
-rw-r--r--fs/smb/server/mgmt/tree_connect.h2
-rw-r--r--fs/smb/server/mgmt/user_session.c221
-rw-r--r--fs/smb/server/mgmt/user_session.h12
-rw-r--r--fs/smb/server/oplock.c1126
-rw-r--r--fs/smb/server/oplock.h23
-rw-r--r--fs/smb/server/proc.c2
-rw-r--r--fs/smb/server/server.c31
-rw-r--r--fs/smb/server/server.h3
-rw-r--r--fs/smb/server/smb2ops.c33
-rw-r--r--fs/smb/server/smb2pdu.c2210
-rw-r--r--fs/smb/server/smb2pdu.h26
-rw-r--r--fs/smb/server/smb_common.c10
-rw-r--r--fs/smb/server/smb_common.h98
-rw-r--r--fs/smb/server/smbacl.c77
-rw-r--r--fs/smb/server/smbacl.h4
-rw-r--r--fs/smb/server/tests/Kconfig15
-rw-r--r--fs/smb/server/tests/Makefile4
-rw-r--r--fs/smb/server/tests/smbacl_kunit.c301
-rw-r--r--fs/smb/server/transport_ipc.c23
-rw-r--r--fs/smb/server/transport_tcp.c307
-rw-r--r--fs/smb/server/vfs.c126
-rw-r--r--fs/smb/server/vfs.h8
-rw-r--r--fs/smb/server/vfs_cache.c732
-rw-r--r--fs/smb/server/vfs_cache.h35
-rw-r--r--fs/smb/smbdirect/accept.c227
-rw-r--r--fs/smb/smbdirect/connect.c2
-rw-r--r--fs/smb/smbdirect/connection.c14
-rw-r--r--fs/smb/smbdirect/internal.h2
-rw-r--r--fs/smb/smbdirect/listen.c137
-rw-r--r--fs/smb/smbdirect/socket.c113
-rw-r--r--fs/smb/smbdirect/socket.h42
47 files changed, 4495 insertions, 1712 deletions
diff --git a/Documentation/filesystems/smb/ksmbd.rst b/Documentation/filesystems/smb/ksmbd.rst
index 672c5d3892ff..728cfca13205 100644
--- a/Documentation/filesystems/smb/ksmbd.rst
+++ b/Documentation/filesystems/smb/ksmbd.rst
@@ -82,10 +82,12 @@ Signing Update Supported.
Pre-authentication integrity Supported.
SMB3 encryption(CCM, GCM) Supported. (CCM/GCM128 and CCM/GCM256 supported)
SMB direct(RDMA) Supported.
-SMB3 Multi-channel Partially Supported. Planned to implement
- replay/retry mechanisms for future.
+SMB3 Multi-channel Supported.
Receive Side Scaling mode Supported.
SMB3.1.1 POSIX extension Supported.
+MSDFS Planned for future.
+Continuous Availability (CA) Under development.
+AD/DC Under development.
ACLs Partially Supported. only DACLs available, SACLs
(auditing) is planned for the future. For
ownership (SIDs) ksmbd generates random subauth
@@ -98,8 +100,9 @@ ACLs Partially Supported. only DACLs available, SACLs
member.
Kerberos Supported.
Durable handle v1,v2 Supported.
-Persistent handle Planned for future.
-SMB2 notify Planned for future.
+Persistent handle Under development.
+Resilient handle Planned for future.
+SMB2 notify Under development.
Sparse file support Supported.
DCE/RPC support Partially Supported. a few calls(NetShareEnumAll,
NetServerGetInfo, SAMR, LSARPC) that are needed
@@ -113,7 +116,8 @@ ksmbd/nfsd interoperability Planned for future. The features that ksmbd
support are Leases, Notify, ACLs and Share modes.
SMB3.1.1 Compression Supported.
SMB3.1.1 over QUIC Planned for future.
-Signing/Encryption over RDMA Planned for future.
+Signing over RDMA Under development.
+Encryption over RDMA Supported.
SMB3.1.1 GMAC signing support Planned for future.
============================== =================================================
diff --git a/fs/smb/common/compress/compress.c b/fs/smb/common/compress/compress.c
index a4123c8f1c0a..4e5164b6dc33 100644
--- a/fs/smb/common/compress/compress.c
+++ b/fs/smb/common/compress/compress.c
@@ -130,12 +130,18 @@ static int smb_decompress_chained(__le16 alg, bool allow_chained,
len = le32_to_cpu(payload->Length);
/*
- * CHAINED marks only the first payload. Requiring NONE on every
- * later payload rejects ambiguous or independently chained data.
+ * Conforming chains must set CHAINED on the first payload.
+ * Windows 11 clients leave uninitialized residual bits in
+ * Flags on subsequent payload headers. Only reject trailing
+ * payloads that attempt to initiate an invalid nested chain.
*/
- if ((first && flags != cpu_to_le16(SMB2_COMPRESSION_FLAG_CHAINED)) ||
- (!first && flags != cpu_to_le16(SMB2_COMPRESSION_FLAG_NONE)))
- return -EINVAL;
+ if (first) {
+ if (flags != cpu_to_le16(SMB2_COMPRESSION_FLAG_CHAINED))
+ return -EINVAL;
+ } else {
+ if (flags == cpu_to_le16(SMB2_COMPRESSION_FLAG_CHAINED))
+ return -EINVAL;
+ }
src += SMB2_COMPRESSION_PAYLOAD_BASE_LEN;
remaining -= SMB2_COMPRESSION_PAYLOAD_BASE_LEN;
diff --git a/fs/smb/common/fscc.h b/fs/smb/common/fscc.h
index e46d3379b779..00f09e1a3afe 100644
--- a/fs/smb/common/fscc.h
+++ b/fs/smb/common/fscc.h
@@ -399,7 +399,7 @@ static_assert(offsetof(struct smb2_file_rename_info, FileName) == sizeof(struct
#define FS_OBJECT_ID_INFORMATION 8 /* Query, Set */
#define FS_DRIVER_PATH_INFORMATION 9 /* Query */
#define FS_SECTOR_SIZE_INFORMATION 11 /* SMB3 or later. Query */
-/* See POSIX Extensions to MS-FSCC 2.3.1.1 */
+/* See POSIX-FSCC 2.3 */
#define FS_POSIX_INFORMATION 100 /* SMB3.1.1 POSIX. Query */
/* See MS-FSCC 2.5.1 */
@@ -575,8 +575,7 @@ struct file_notify_information {
} __packed;
/*
- * See POSIX Extensions to MS-FSCC 2.3.2.1
- * Link: https://gitlab.com/samba-team/smb3-posix-spec/-/blob/master/fscc_posix_extensions.md
+ * See POSIX-FSCC 2.3.1
*/
typedef struct {
/* For undefined recommended transfer size return -1 in that field */
diff --git a/fs/smb/common/smbglob.h b/fs/smb/common/smbglob.h
index d9c7e6e7af29..fdf840888062 100644
--- a/fs/smb/common/smbglob.h
+++ b/fs/smb/common/smbglob.h
@@ -40,6 +40,7 @@ struct smb_version_values {
size_t create_disk_id_size;
size_t create_posix_size;
size_t create_aapl_size;
+ size_t create_rsp_size;
};
static inline unsigned int get_rfc1002_len(void *buf)
diff --git a/fs/smb/server/Kconfig b/fs/smb/server/Kconfig
index 221ec9717a83..f8b03605db59 100644
--- a/fs/smb/server/Kconfig
+++ b/fs/smb/server/Kconfig
@@ -16,6 +16,7 @@ config SMB_SERVER
select CRYPTO_AEAD2
select CRYPTO_CCM
select CRYPTO_GCM
+ select CRYPTO_AES
select ASN1
select OID_REGISTRY
select CRC32
@@ -71,3 +72,5 @@ config SMB_SERVER_KERBEROS5
bool "Support for Kerberos 5"
depends on SMB_SERVER
default y
+
+source "fs/smb/server/tests/Kconfig"
diff --git a/fs/smb/server/Makefile b/fs/smb/server/Makefile
index a3e9306055e8..9bc87695a53c 100644
--- a/fs/smb/server/Makefile
+++ b/fs/smb/server/Makefile
@@ -19,3 +19,4 @@ $(obj)/ksmbd_spnego_negtokentarg.asn1.o: $(obj)/ksmbd_spnego_negtokentarg.asn1.c
ksmbd-$(CONFIG_SMB_SERVER_SMBDIRECT) += transport_rdma.o
ksmbd-$(CONFIG_PROC_FS) += proc.o
+obj-$(CONFIG_SMB_SERVER_KUNIT_TESTS) += tests/
diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c
index 1b24eb3f3998..0c51ca0cb14d 100644
--- a/fs/smb/server/auth.c
+++ b/fs/smb/server/auth.c
@@ -336,8 +336,7 @@ ksmbd_build_ntlmssp_challenge_blob(struct challenge_message *chgblob,
if (cflags & NTLMSSP_REQUEST_TARGET)
flags |= NTLMSSP_REQUEST_TARGET;
- if (conn->use_spnego &&
- (cflags & NTLMSSP_NEGOTIATE_EXTENDED_SEC))
+ if (cflags & NTLMSSP_NEGOTIATE_EXTENDED_SEC)
flags |= NTLMSSP_NEGOTIATE_EXTENDED_SEC;
if (cflags & NTLMSSP_NEGOTIATE_KEY_XCH)
@@ -464,7 +463,8 @@ int ksmbd_krb5_authenticate(struct ksmbd_session *sess, char *in_blob,
memcpy(out_blob, resp->payload + resp->session_key_len,
resp->spnego_blob_len);
*out_len = resp->spnego_blob_len;
- sess->kerberos_expiry = resp->session_expiry;
+ sess->kerberos_expiry = (u64)resp->login_response.session_expiry_hi << 32 |
+ resp->login_response.session_expiry_lo;
retval = 0;
out:
kvfree_sensitive(resp, sizeof(*resp) + resp->session_key_len +
diff --git a/fs/smb/server/compress.c b/fs/smb/server/compress.c
index 5162fb84c755..3fcb3e44302e 100644
--- a/fs/smb/server/compress.c
+++ b/fs/smb/server/compress.c
@@ -65,6 +65,7 @@ static int __ksmbd_decompress_request(struct ksmbd_conn *conn,
return -ENOMEM;
*(__be32 *)out = cpu_to_be32(out_size);
+ out[out_size + 4] = 0;
rc = smb_compression_decompress(conn->compress_algorithm,
conn->compress_chained,
conn->compress_pattern,
diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c
index d211861ff86f..0fcf90b877ae 100644
--- a/fs/smb/server/connection.c
+++ b/fs/smb/server/connection.c
@@ -114,7 +114,7 @@ static int proc_show_clients(struct seq_file *m, void *v)
sessions++;
rcu_read_unlock();
#if IS_ENABLED(CONFIG_IPV6)
- if (!conn->inet_addr)
+ if (conn->is_ipv6)
seq_printf(m, "client:\t%pI6c\n", &conn->inet6_addr);
else
#endif
@@ -306,6 +306,7 @@ struct ksmbd_conn *ksmbd_conn_alloc(void)
atomic_set(&conn->req_running, 0);
atomic_set(&conn->r_count, 0);
atomic_set(&conn->refcnt, 1);
+ atomic64_set(&conn->session_count, 0);
conn->total_credits = 1;
conn->outstanding_credits = 0;
@@ -327,8 +328,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);
@@ -457,17 +456,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 +529,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 +555,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 = {
@@ -641,6 +657,7 @@ static void ksmbd_session_expiration_worker(struct work_struct *work)
goto reschedule;
ksmbd_expire_sessions();
+ ksmbd_reclaim_ended_sessions();
/*
* An old connection without a Valid or Expired session must be
@@ -752,6 +769,7 @@ recheck:
if (!conn->request_buf)
break;
+ conn->request_buf[pdu_size + 4] = 0;
memcpy(conn->request_buf, hdr_buf, sizeof(hdr_buf));
/*
@@ -804,7 +822,12 @@ recheck:
}
}
+ /* 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));
@@ -831,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 371f17b4f02a..e202bf7f3b98 100644
--- a/fs/smb/server/connection.h
+++ b/fs/smb/server/connection.h
@@ -68,6 +68,9 @@ struct ksmbd_conn {
u8 inet6_addr[16];
#endif
};
+#if IS_ENABLED(CONFIG_IPV6)
+ bool is_ipv6;
+#endif
unsigned int inet_hash;
char *request_buf;
struct ksmbd_transport *transport;
@@ -77,6 +80,8 @@ struct ksmbd_conn {
struct rw_semaphore session_lock;
/* smb session 1 per user */
struct xarray sessions;
+ /* New sessions and SMB3 channels each reserve one connection slot. */
+ atomic64_t session_count;
unsigned long creation_time;
unsigned long last_active;
/* How many request are running currently */
@@ -100,6 +105,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;
@@ -107,8 +114,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;
@@ -159,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;
@@ -166,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 {
@@ -173,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,
@@ -214,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,
@@ -232,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);
/*
@@ -247,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
@@ -281,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)
@@ -316,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/ksmbd_netlink.h b/fs/smb/server/ksmbd_netlink.h
index 2673522c76bc..ad050988659e 100644
--- a/fs/smb/server/ksmbd_netlink.h
+++ b/fs/smb/server/ksmbd_netlink.h
@@ -114,7 +114,8 @@ struct ksmbd_startup_request {
__s8 bind_interfaces_only;
__u32 max_ip_connections; /* Number of maximum connection per ip address */
__s8 aapl_model[32]; /* AAPL model string for Finder icon, e.g. "Xserve" */
- __s8 reserved[467]; /* Reserved room */
+ __u32 max_sessions_per_connection; /* Maximum sessions per connection */
+ __s8 reserved[463]; /* Reserved room */
__u32 ifc_list_sz; /* interfaces list size */
__s8 ____payload[];
} __packed;
@@ -148,7 +149,9 @@ struct ksmbd_login_response {
__u16 status;
__u16 hash_sz; /* hash size */
__s8 hash[KSMBD_REQ_MAX_HASH_SZ]; /* password hash */
- __u32 reserved[16]; /* Reserved room */
+ __u32 session_expiry_lo; /* Kerberos ticket expiry, SPNEGO only */
+ __u32 session_expiry_hi;
+ __u32 reserved[14]; /* Reserved room */
};
/*
@@ -286,7 +289,6 @@ struct ksmbd_spnego_authen_response {
* stored in SecurityBuffer of SMB2 SESSION
* SETUP response
*/
- __u64 session_expiry; /* Kerberos ticket expiry time */
__u8 payload[]; /* session key + AP_REP */
};
diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c
index d307aefe0aec..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)
{
@@ -57,7 +58,6 @@ struct ksmbd_work *ksmbd_alloc_work_struct(void)
INIT_LIST_HEAD(&work->request_entry);
INIT_LIST_HEAD(&work->async_request_entry);
INIT_LIST_HEAD(&work->fp_entry);
- INIT_LIST_HEAD(&work->notify_entry);
INIT_LIST_HEAD(&work->aux_read_list);
work->iov_alloc_cnt = ARRAY_SIZE(work->iov_inline);
work->iov = work->iov_inline;
@@ -87,8 +87,6 @@ void ksmbd_free_work_struct(struct ksmbd_work *work)
if (work->async_id)
ksmbd_release_id(&work->conn->async_ida, work->async_id);
- if (work->owns_conn_ref)
- ksmbd_conn_put(work->conn);
ksmbd_fd_put(work, work->request_open);
kmem_cache_free(work_cache, work);
}
@@ -113,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;
}
@@ -120,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)
@@ -127,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 0844aa929f55..1ab9ddd20506 100644
--- a/fs/smb/server/ksmbd_work.h
+++ b/fs/smb/server/ksmbd_work.h
@@ -54,10 +54,7 @@ struct ksmbd_work {
/* Current cmd hdr in compound rsp buf*/
int curr_smb2_rsp_hdr_off;
- /*
- * Current Local FID assigned compound response if SMB2 CREATE
- * command is present in compound request
- */
+ /* FileId generated or used by the previous operation in a compound. */
u64 compound_fid;
u64 compound_pfid;
u64 compound_sid;
@@ -91,11 +88,15 @@ struct ksmbd_work {
bool compress_response:1;
/* Is this SYNC or ASYNC ksmbd_work */
bool asynchronous:1;
- /* Work owns a reference to @conn. */
- bool owns_conn_ref:1;
bool need_invalidate_rkey:1;
bool request_open_chseq_tracked:1;
+ /* The previous command carries a FileId or is a CREATE. */
+ bool compound_fid_present:1;
+ /* A related command failed because there was no FileId to inherit. */
+ bool compound_fid_missing: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 */
@@ -115,9 +116,8 @@ struct ksmbd_work {
struct list_head request_entry;
/* List head at conn->async_requests */
struct list_head async_request_entry;
+ /* List head at ksmbd_file->blocked_works */
struct list_head fp_entry;
- /* List head at ksmbd_file->notify_pendings */
- struct list_head notify_entry;
};
/**
@@ -156,6 +156,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/share_config.c b/fs/smb/server/mgmt/share_config.c
index cc9f18ede80d..f85134c38cdd 100644
--- a/fs/smb/server/mgmt/share_config.c
+++ b/fs/smb/server/mgmt/share_config.c
@@ -8,6 +8,7 @@
#include <linux/slab.h>
#include <linux/rwsem.h>
#include <linux/parser.h>
+#include <linux/dcache.h>
#include <linux/namei.h>
#include <linux/fs_struct.h>
#include <linux/sched.h>
@@ -108,6 +109,7 @@ static void kill_share(struct ksmbd_share_config *share)
path_put(&share->vfs_path);
kfree(share->name);
kfree(share->path);
+ kfree(share->real_path);
kfree(share);
}
@@ -182,6 +184,43 @@ static int parse_veto_list(struct ksmbd_share_config *share,
return 0;
}
+static int share_config_resolve_path(struct ksmbd_share_config *share)
+{
+ char *buf, *path;
+ int ret;
+
+ ret = kern_path(share->path, 0, &share->vfs_path);
+ if (ret)
+ return ret;
+
+ buf = kmalloc(PATH_MAX, KSMBD_DEFAULT_GFP);
+ if (!buf) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ path = d_path(&share->vfs_path, buf, PATH_MAX);
+ if (IS_ERR(path)) {
+ ret = PTR_ERR(path);
+ goto out_buf;
+ }
+
+ path = kstrdup(path, KSMBD_DEFAULT_GFP);
+ if (!path) {
+ ret = -ENOMEM;
+ goto out_buf;
+ }
+
+ share->real_path = path;
+ share->real_path_sz = strlen(path);
+out_buf:
+ kfree(buf);
+out:
+ if (ret)
+ path_put(&share->vfs_path);
+ return ret;
+}
+
static struct ksmbd_share_config *share_config_request(struct ksmbd_work *work,
const char *name)
{
@@ -272,12 +311,12 @@ static struct ksmbd_share_config *share_config_request(struct ksmbd_work *work,
}
scoped_with_init_fs()
- ret = kern_path(share->path, 0, &share->vfs_path);
+ ret = share_config_resolve_path(share);
ksmbd_revert_fsids(work);
if (ret) {
ksmbd_debug(SMB, "failed to access '%s'\n",
share->path);
- /* Avoid put_path() */
+ /* No path reference is retained on failure. */
kfree(share->path);
share->path = NULL;
}
diff --git a/fs/smb/server/mgmt/share_config.h b/fs/smb/server/mgmt/share_config.h
index d157545fe7d1..390c966726f4 100644
--- a/fs/smb/server/mgmt/share_config.h
+++ b/fs/smb/server/mgmt/share_config.h
@@ -16,8 +16,10 @@ struct ksmbd_work;
struct ksmbd_share_config {
char *name;
char *path;
+ char *real_path;
unsigned int path_sz;
+ unsigned int real_path_sz;
unsigned int flags;
struct list_head veto_list;
@@ -43,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;
}
@@ -52,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/mgmt/tree_connect.c b/fs/smb/server/mgmt/tree_connect.c
index dd1db3554cae..837175ac5ce8 100644
--- a/fs/smb/server/mgmt/tree_connect.c
+++ b/fs/smb/server/mgmt/tree_connect.c
@@ -103,6 +103,13 @@ out_error:
return status;
}
+struct ksmbd_tree_connect *
+ksmbd_tree_connect_get(struct ksmbd_tree_connect *tcon)
+{
+ atomic_inc(&tcon->refcount);
+ return tcon;
+}
+
void ksmbd_tree_connect_put(struct ksmbd_tree_connect *tcon)
{
if (atomic_dec_and_test(&tcon->refcount)) {
diff --git a/fs/smb/server/mgmt/tree_connect.h b/fs/smb/server/mgmt/tree_connect.h
index f0023d86716f..82891ff9b7b9 100644
--- a/fs/smb/server/mgmt/tree_connect.h
+++ b/fs/smb/server/mgmt/tree_connect.h
@@ -51,6 +51,8 @@ struct ksmbd_session;
struct ksmbd_tree_conn_status
ksmbd_tree_conn_connect(struct ksmbd_work *work, const char *share_name);
+struct ksmbd_tree_connect *
+ksmbd_tree_connect_get(struct ksmbd_tree_connect *tcon);
void ksmbd_tree_connect_put(struct ksmbd_tree_connect *tcon);
int ksmbd_tree_conn_disconnect(struct ksmbd_session *sess,
diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c
index 44dc3f800cd4..3ff3671cd019 100644
--- a/fs/smb/server/mgmt/user_session.c
+++ b/fs/smb/server/mgmt/user_session.c
@@ -3,6 +3,7 @@
* Copyright (C) 2018 Samsung Electronics Co., Ltd.
*/
+#include <linux/atomic.h>
#include <linux/list.h>
#include <linux/slab.h>
#include <linux/rwsem.h>
@@ -18,6 +19,7 @@
#include "../vfs_cache.h"
#include "../misc.h"
#include "../stats.h"
+#include "../server.h"
static DEFINE_IDA(session_ida);
@@ -26,6 +28,21 @@ static DEFINE_IDA(session_ida);
static DEFINE_HASHTABLE(sessions_table, SESSION_HASH_BITS);
static DECLARE_RWSEM(sessions_table_lock);
+int ksmbd_session_reserve_slot(struct ksmbd_conn *conn)
+{
+ if (atomic64_inc_return(&conn->session_count) >
+ READ_ONCE(server_conf.max_sessions_per_connection)) {
+ atomic64_dec(&conn->session_count);
+ return -ENOSPC;
+ }
+ return 0;
+}
+
+void ksmbd_session_release_slot(struct ksmbd_conn *conn)
+{
+ atomic64_dec(&conn->session_count);
+}
+
struct ksmbd_session_rpc {
int id;
unsigned int method;
@@ -95,8 +112,10 @@ static unsigned int session_open_file_count(struct ksmbd_session *session)
unsigned int id;
read_lock(&session->file_table.lock);
- idr_for_each_entry(session->file_table.idr, fp, id)
- count++;
+ if (session->file_table.idr) {
+ idr_for_each_entry(session->file_table.idr, fp, id)
+ count++;
+ }
read_unlock(&session->file_table.lock);
return count;
}
@@ -130,7 +149,7 @@ static int show_proc_session(struct seq_file *m, void *v)
const char *name;
#if IS_ENABLED(CONFIG_IPV6)
- if (chan->conn->inet_addr)
+ if (!chan->conn->is_ipv6)
seq_printf(m, "client:\t%pI4\n",
&chan->conn->inet_addr);
else
@@ -212,8 +231,10 @@ static int create_proc_session(struct ksmbd_session *sess)
static void delete_proc_session(struct ksmbd_session *sess)
{
- if (sess->proc_entry)
+ if (sess->proc_entry) {
proc_remove(sess->proc_entry);
+ sess->proc_entry = NULL;
+ }
}
static int show_proc_sessions(struct seq_file *m, void *v)
@@ -231,7 +252,7 @@ static int show_proc_sessions(struct seq_file *m, void *v)
ksmbd_user_session_get(session);
#if IS_ENABLED(CONFIG_IPV6)
- if (!chan->conn->inet_addr)
+ if (chan->conn->is_ipv6)
seq_printf(m, "client:\t%pI6c\n", &chan->conn->inet6_addr);
else
#endif
@@ -271,6 +292,8 @@ static void free_channel_list(struct ksmbd_session *sess)
down_write(&sess->chann_lock);
xa_for_each(&sess->ksmbd_chann_list, index, chann) {
xa_erase(&sess->ksmbd_chann_list, index);
+ ksmbd_session_release_slot(chann->conn);
+ ksmbd_conn_put(chann->conn);
kfree_sensitive(chann);
}
@@ -397,9 +420,12 @@ int ksmbd_session_rpc_method(struct ksmbd_session *sess, int id)
void ksmbd_session_destroy(struct ksmbd_session *sess)
{
+ struct ksmbd_conn *setup_conn;
+
if (!sess)
return;
+ setup_conn = sess->setup_conn;
delete_proc_session(sess);
ksmbd_tree_conn_session_logoff(sess);
ksmbd_destroy_file_table(sess);
@@ -409,9 +435,14 @@ void ksmbd_session_destroy(struct ksmbd_session *sess)
ksmbd_session_rpc_clear_list(sess);
free_channel_list(sess);
kfree_sensitive(sess->Preauth_HashValue);
- ksmbd_release_id(&session_ida, sess->id);
+ if (sess->id)
+ ksmbd_release_id(&session_ida, sess->id);
ida_destroy(&sess->tree_conn_ida);
kfree_sensitive(sess);
+ if (setup_conn) {
+ ksmbd_session_release_slot(setup_conn);
+ ksmbd_conn_put(setup_conn);
+ }
}
static void ksmbd_session_remove_from_table(struct ksmbd_session *sess)
@@ -420,6 +451,17 @@ static void ksmbd_session_remove_from_table(struct ksmbd_session *sess)
ksmbd_counter_dec(KSMBD_COUNTER_SESSIONS);
}
+static void ksmbd_dispose_sessions(struct list_head *dispose)
+{
+ struct ksmbd_session *sess, *tmp;
+
+ list_for_each_entry_safe(sess, tmp, dispose, dispose_entry) {
+ list_del_init(&sess->dispose_entry);
+ delete_proc_session(sess);
+ ksmbd_user_session_put(sess);
+ }
+}
+
struct ksmbd_session *__session_lookup(unsigned long long id)
{
struct ksmbd_session *sess;
@@ -433,8 +475,9 @@ struct ksmbd_session *__session_lookup(unsigned long long id)
return NULL;
}
-static bool ksmbd_too_many_session_setups(struct ksmbd_conn *conn)
+bool ksmbd_too_many_session_setups(struct ksmbd_conn *conn)
{
+ LIST_HEAD(dispose);
unsigned long id;
struct ksmbd_session *sess;
unsigned int pending = 0;
@@ -445,18 +488,20 @@ static bool ksmbd_too_many_session_setups(struct ksmbd_conn *conn)
if (READ_ONCE(sess->state) != SMB2_SESSION_IN_PROGRESS)
continue;
- if (atomic_read(&sess->refcnt) <= 1 &&
+ /* Keep response signing keys until every request has dropped its reference. */
+ if (atomic_read(&sess->refcnt) == 1 &&
time_after(jiffies, sess->last_active +
KSMBD_UNAUTHENTICATED_CONN_TIMEOUT)) {
xa_erase(&conn->sessions, sess->id);
ksmbd_session_remove_from_table(sess);
- ksmbd_session_destroy(sess);
+ list_add_tail(&sess->dispose_entry, &dispose);
continue;
}
pending++;
}
up_write(&conn->session_lock);
up_write(&sessions_table_lock);
+ ksmbd_dispose_sessions(&dispose);
return pending >= KSMBD_MAX_PENDING_SESSIONS;
}
@@ -467,24 +512,22 @@ int ksmbd_session_register(struct ksmbd_conn *conn,
sess->dialect = conn->dialect;
memcpy(sess->ClientGUID, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE);
- /* Bound abandoned SessionId-zero authentication exchanges. */
- if (ksmbd_too_many_session_setups(conn))
- ret = -ENOSPC;
- else
- ret = xa_err(xa_store(&conn->sessions, sess->id, sess,
- KSMBD_DEFAULT_GFP));
+ ret = xa_err(xa_store(&conn->sessions, sess->id, sess,
+ KSMBD_DEFAULT_GFP));
if (ret) {
down_write(&sessions_table_lock);
ksmbd_session_remove_from_table(sess);
up_write(&sessions_table_lock);
+ delete_proc_session(sess);
ksmbd_user_session_put(sess);
}
return ret;
}
-void ksmbd_session_unregister(struct ksmbd_conn *conn,
- struct ksmbd_session *sess)
+/* The caller holds sessions_table_lock for writing. */
+static bool __ksmbd_session_unregister(struct ksmbd_conn *conn,
+ struct ksmbd_session *sess)
{
struct ksmbd_conn *session_conns[KSMBD_MAX_CHANNELS];
struct channel *chann;
@@ -492,10 +535,13 @@ void ksmbd_session_unregister(struct ksmbd_conn *conn,
unsigned int nr_conns = 0, i;
bool removed = false;
- down_write(&sessions_table_lock);
+ lockdep_assert_held_write(&sessions_table_lock);
if (!hlist_unhashed(&sess->hlist)) {
- /* Keep each channel connection stable under sessions_table_lock. */
+ /* Existing references stay valid under sessions_table_lock. */
down_read(&sess->chann_lock);
+ /* SMB2.0/2.1 sessions have no channel holding their connection. */
+ if (sess->setup_conn)
+ session_conns[nr_conns++] = sess->setup_conn;
xa_for_each(&sess->ksmbd_chann_list, index, chann) {
if (nr_conns == ARRAY_SIZE(session_conns))
break;
@@ -507,10 +553,12 @@ void ksmbd_session_unregister(struct ksmbd_conn *conn,
removed = true;
}
- down_write(&conn->session_lock);
- if (xa_load(&conn->sessions, sess->id) == sess)
- xa_erase(&conn->sessions, sess->id);
- up_write(&conn->session_lock);
+ if (conn) {
+ down_write(&conn->session_lock);
+ if (xa_load(&conn->sessions, sess->id) == sess)
+ xa_erase(&conn->sessions, sess->id);
+ up_write(&conn->session_lock);
+ }
for (i = 0; i < nr_conns; i++) {
if (session_conns[i] == conn)
continue;
@@ -519,10 +567,47 @@ void ksmbd_session_unregister(struct ksmbd_conn *conn,
xa_erase(&session_conns[i]->sessions, sess->id);
up_write(&session_conns[i]->session_lock);
}
+ return removed;
+}
+
+void ksmbd_session_unregister(struct ksmbd_conn *conn,
+ struct ksmbd_session *sess)
+{
+ bool removed;
+
+ down_write(&sessions_table_lock);
+ removed = __ksmbd_session_unregister(conn, sess);
up_write(&sessions_table_lock);
- if (removed)
+ if (removed) {
+ /*
+ * Keep the table reference until procfs readers have finished,
+ * so none can remove its own proc entry on the last put.
+ */
+ delete_proc_session(sess);
ksmbd_user_session_put(sess);
+ }
+}
+
+void ksmbd_reclaim_ended_sessions(void)
+{
+ LIST_HEAD(dispose);
+ struct ksmbd_session *sess;
+ struct hlist_node *tmp;
+ int bkt;
+
+ down_write(&sessions_table_lock);
+ hash_for_each_safe(sessions_table, bkt, tmp, sess, hlist) {
+ if (READ_ONCE(sess->state) != SMB2_SESSION_EXPIRED ||
+ READ_ONCE(sess->kerberos_expiry) ||
+ atomic_read(&sess->refcnt) != 1)
+ continue;
+
+ __ksmbd_session_unregister(NULL, sess);
+ list_add_tail(&sess->dispose_entry, &dispose);
+ }
+ up_write(&sessions_table_lock);
+ ksmbd_dispose_sessions(&dispose);
}
bool ksmbd_conn_has_valid_or_expired_session(struct ksmbd_conn *conn)
@@ -532,7 +617,12 @@ bool ksmbd_conn_has_valid_or_expired_session(struct ksmbd_conn *conn)
int state, bkt;
bool found = false;
- down_read(&conn->session_lock);
+ /*
+ * The expiration worker holds conn_list_lock, which session teardown
+ * takes after the session locks. Defer scavenging if either is busy.
+ */
+ if (!down_read_trylock(&conn->session_lock))
+ return true;
xa_for_each(&conn->sessions, id, sess) {
state = READ_ONCE(sess->state);
if (state == SMB2_SESSION_VALID ||
@@ -546,7 +636,8 @@ bool ksmbd_conn_has_valid_or_expired_session(struct ksmbd_conn *conn)
return true;
/* A session bound through SMB3 multichannel is not in conn->sessions. */
- down_read(&sessions_table_lock);
+ if (!down_read_trylock(&sessions_table_lock))
+ return true;
hash_for_each(sessions_table, bkt, sess, hlist) {
state = READ_ONCE(sess->state);
if (state != SMB2_SESSION_VALID &&
@@ -592,12 +683,15 @@ static int ksmbd_chann_del(struct ksmbd_conn *conn, struct ksmbd_session *sess)
if (!chann)
return -ENOENT;
+ ksmbd_session_release_slot(chann->conn);
+ ksmbd_conn_put(chann->conn);
kfree_sensitive(chann);
return 0;
}
void ksmbd_conn_sessions_cleanup(struct ksmbd_conn *conn)
{
+ LIST_HEAD(dispose);
struct ksmbd_session *sess;
unsigned long id;
struct hlist_node *tmp;
@@ -611,8 +705,7 @@ void ksmbd_conn_sessions_cleanup(struct ksmbd_conn *conn)
down_write(&conn->session_lock);
xa_erase(&conn->sessions, sess->id);
up_write(&conn->session_lock);
- if (atomic_dec_and_test(&sess->refcnt))
- ksmbd_session_destroy(sess);
+ list_add_tail(&sess->dispose_entry, &dispose);
}
}
@@ -622,12 +715,12 @@ void ksmbd_conn_sessions_cleanup(struct ksmbd_conn *conn)
if (xa_empty(&sess->ksmbd_chann_list)) {
xa_erase(&conn->sessions, sess->id);
ksmbd_session_remove_from_table(sess);
- if (atomic_dec_and_test(&sess->refcnt))
- ksmbd_session_destroy(sess);
+ list_add_tail(&sess->dispose_entry, &dispose);
}
}
up_write(&conn->session_lock);
up_write(&sessions_table_lock);
+ ksmbd_dispose_sessions(&dispose);
}
bool is_ksmbd_session_in_connection(struct ksmbd_conn *conn,
@@ -674,6 +767,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)
{
@@ -759,6 +883,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 +906,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 +937,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,
@@ -830,7 +969,7 @@ static int __init_smb2_session(struct ksmbd_session *sess)
return 0;
}
-static struct ksmbd_session *__session_create(int protocol)
+static struct ksmbd_session *__session_create(struct ksmbd_conn *conn, int protocol)
{
struct ksmbd_session *sess;
int ret;
@@ -838,27 +977,33 @@ static struct ksmbd_session *__session_create(int protocol)
if (protocol != CIFDS_SESSION_FLAG_SMB2)
return NULL;
+ if (ksmbd_session_reserve_slot(conn))
+ return NULL;
+
sess = kzalloc_obj(struct ksmbd_session, KSMBD_DEFAULT_GFP);
- if (!sess)
+ if (!sess) {
+ ksmbd_session_release_slot(conn);
return NULL;
+ }
+ sess->setup_conn = ksmbd_conn_get(conn);
ida_init(&sess->tree_conn_ida);
-
- if (ksmbd_init_file_table(&sess->file_table))
- goto error;
-
sess->last_active = jiffies;
sess->state = SMB2_SESSION_IN_PROGRESS;
set_session_flag(sess, protocol);
xa_init(&sess->tree_conns);
xa_init(&sess->ksmbd_chann_list);
xa_init(&sess->rpc_handle_list);
+ INIT_LIST_HEAD(&sess->dispose_entry);
sess->sequence_number = 1;
atomic_set(&sess->refcnt, 2);
init_rwsem(&sess->tree_conns_lock);
init_rwsem(&sess->rpc_lock);
init_rwsem(&sess->chann_lock);
+ if (ksmbd_init_file_table(&sess->file_table))
+ goto error;
+
ret = __init_smb2_session(sess);
if (ret)
goto error;
@@ -877,9 +1022,9 @@ error:
return NULL;
}
-struct ksmbd_session *ksmbd_smb2_session_create(void)
+struct ksmbd_session *ksmbd_smb2_session_create(struct ksmbd_conn *conn)
{
- return __session_create(CIFDS_SESSION_FLAG_SMB2);
+ return __session_create(conn, CIFDS_SESSION_FLAG_SMB2);
}
int ksmbd_acquire_tree_conn_id(struct ksmbd_session *sess)
diff --git a/fs/smb/server/mgmt/user_session.h b/fs/smb/server/mgmt/user_session.h
index 217258551d6d..8e4ddb04f0e6 100644
--- a/fs/smb/server/mgmt/user_session.h
+++ b/fs/smb/server/mgmt/user_session.h
@@ -37,6 +37,8 @@ struct ksmbd_session {
char ClientGUID[SMB2_CLIENT_GUID_SIZE];
struct ksmbd_user *user;
+ /* Holds the connection slot until an SMB3 channel takes it over. */
+ struct ksmbd_conn *setup_conn;
unsigned int sequence_number;
unsigned int flags;
@@ -51,6 +53,7 @@ struct ksmbd_session {
u64 kerberos_expiry;
struct hlist_node hlist;
+ struct list_head dispose_entry;
struct rw_semaphore chann_lock;
struct xarray ksmbd_chann_list;
struct xarray tree_conns;
@@ -89,15 +92,21 @@ static inline void clear_session_flag(struct ksmbd_session *sess, int bit)
sess->flags &= ~bit;
}
-struct ksmbd_session *ksmbd_smb2_session_create(void);
+struct ksmbd_session *ksmbd_smb2_session_create(struct ksmbd_conn *conn);
+int ksmbd_session_reserve_slot(struct ksmbd_conn *conn);
+void ksmbd_session_release_slot(struct ksmbd_conn *conn);
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,
unsigned long long id);
+bool ksmbd_too_many_session_setups(struct ksmbd_conn *conn);
int ksmbd_session_register(struct ksmbd_conn *conn,
struct ksmbd_session *sess);
void ksmbd_session_unregister(struct ksmbd_conn *conn,
@@ -105,6 +114,7 @@ void ksmbd_session_unregister(struct ksmbd_conn *conn,
void ksmbd_conn_sessions_cleanup(struct ksmbd_conn *conn);
bool ksmbd_conn_has_valid_or_expired_session(struct ksmbd_conn *conn);
void ksmbd_expire_sessions(void);
+void ksmbd_reclaim_ended_sessions(void);
struct ksmbd_session *__session_lookup(unsigned long long id);
struct ksmbd_session *ksmbd_session_lookup_all(struct ksmbd_conn *conn,
unsigned long long id);
diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c
index d0f18ebf471b..d479508c06c5 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 | \
@@ -87,6 +92,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;
@@ -115,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_percpu_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_percpu_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);
@@ -124,6 +206,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;
@@ -142,6 +230,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)
@@ -154,21 +246,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)
{
@@ -189,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);
@@ -302,15 +386,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 +445,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);
}
@@ -556,9 +634,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() */
@@ -639,19 +718,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 +768,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,43 +821,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 void wake_up_oplock_break(struct oplock_info *opinfo)
-{
- clear_bit_unlock(0, &opinfo->pending_break);
- /* memory barrier is needed for wake_up_bit() */
- smp_mb__after_atomic();
- wake_up_bit(&opinfo->pending_break, 0);
-}
-
static bool oplock_break_set_ack_wait(struct oplock_info *opinfo)
{
bool ret = false;
@@ -778,6 +828,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);
@@ -826,13 +878,13 @@ static int oplock_break_pending(struct oplock_info *opinfo, int req_op_level)
if (opinfo->level <= req_op_level) {
if (opinfo->is_lease == false) {
- wake_up_oplock_break(opinfo);
+ clear_and_wake_up_bit(0, &opinfo->pending_break);
return 1;
}
if (opinfo->o_lease->state !=
(SMB2_LEASE_HANDLE_CACHING_LE |
SMB2_LEASE_READ_CACHING_LE)) {
- wake_up_oplock_break(opinfo);
+ clear_and_wake_up_bit(0, &opinfo->pending_break);
return 1;
}
}
@@ -850,330 +902,559 @@ 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)
+static void smb2_break_start_timer(struct oplock_info *opinfo,
+ unsigned long timeout)
{
- 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;
+ struct lease *lease = opinfo->o_lease;
+ struct oplock_info *open;
+ bool start = false, waiting;
- fp = ksmbd_lookup_global_fd(br_info->fid);
- if (!fp)
- goto out;
+ 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;
+ }
- if (allocate_interim_rsp_buf(work)) {
- pr_err("smb2_allocate_rsp_buf failed! ");
- ksmbd_fd_put(work, fp);
- goto out;
+ spin_lock(&opinfo->state_lock);
+ if (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);
+}
- 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;
+struct smb2_break_channel {
+ struct list_head list;
+ struct ksmbd_conn *conn;
+ struct ksmbd_tx_tracking tracking;
+ bool queued;
+ bool delivered;
+ bool failed;
+};
- ksmbd_debug(OPLOCK,
- "sending oplock break v_id %llu p_id = %llu lock level = %d\n",
- rsp->VolatileFid, rsp->PersistentFid, rsp->OplockLevel);
+static bool smb2_break_complete(struct oplock_info *opinfo, bool expect_ack)
+{
+ bool complete;
- ksmbd_conn_write(work);
+ 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;
+}
-out:
- ksmbd_free_work_struct(work);
- ksmbd_conn_r_count_dec(conn);
- ksmbd_conn_put(conn);
+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);
+ if (opinfo->is_lease)
+ spin_unlock(&lease->lock);
+ return timed_out ? ksmbd_invalidate_durable_fd(opinfo->fid) : 0;
+ }
+ if (opinfo->is_lease) {
+ 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);
+ }
+ wake_up_interruptible_all(&opinfo->oplock_brk);
+ return ksmbd_invalidate_durable_fd(opinfo->fid);
+}
+
+static int smb2_break_add_channel(struct list_head *head, struct ksmbd_conn *conn)
+{
+ struct smb2_break_channel *channel;
+
+ 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;
+ }
+ read_unlock(&lease_list_lock);
+ }
+ if (owner->conn)
+ list_add_tail(&owner->list, head);
- return conn;
+ 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;
+ }
+ }
+
+ 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;
+ struct oplock_info *open;
+ 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);
-
+ 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;
+ }
+ 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);
+ if (opinfo->is_lease)
+ spin_unlock(&lease->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 (expect_ack)
+ smb2_break_start_timer(opinfo, deadline);
+ 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(&lease->lock);
+ if (READ_ONCE(opinfo->op_state) != OPLOCK_CLOSING) {
+ lease->state = lease->new_state;
+ 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(&lease->lock);
+ }
+ goto out;
+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);
- 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)
@@ -1195,10 +1476,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,
@@ -1226,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) {
/*
@@ -1256,11 +1539,13 @@ again:
}
}
- 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)) {
+ spin_unlock(&lease->lock);
atomic_dec_if_positive(&brk_opinfo->breaking_cnt);
- wake_up_oplock_break(brk_opinfo);
+ clear_and_wake_up_bit(0, &brk_opinfo->pending_break);
return -ENOENT;
}
} else
@@ -1273,16 +1558,13 @@ again:
inc_epoch = false;
lease->reuse_epoch = false;
}
- err = smb2_lease_break_noti(brk_opinfo, sync_lease_break, inc_epoch);
+ spin_unlock(&lease->lock);
+ 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)
@@ -1309,7 +1591,7 @@ again:
lease->state != SMB2_LEASE_NONE_LE)))
goto again;
- wake_up_oplock_break(brk_opinfo);
+ clear_and_wake_up_bit(0, &brk_opinfo->pending_break);
return err;
} else {
err = oplock_break_pending(brk_opinfo, req_op_level);
@@ -1318,31 +1600,21 @@ again:
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)) {
- wake_up_oplock_break(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)
err = -EAGAIN;
- wake_up_oplock_break(brk_opinfo);
+ clear_and_wake_up_bit(0, &brk_opinfo->pending_break);
return err;
}
@@ -1379,25 +1651,26 @@ 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;
- }
+ 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);
- 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);
+ 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);
}
@@ -1499,7 +1772,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 &&
@@ -1510,11 +1783,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);
}
@@ -1547,18 +1815,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);
}
@@ -1707,7 +1970,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) {
@@ -1811,8 +2078,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);
@@ -1827,12 +2093,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;
@@ -1850,17 +2116,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",
@@ -1875,10 +2133,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;
@@ -1908,7 +2164,7 @@ next:
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);
@@ -2115,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;
@@ -2152,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;
}
/**
@@ -2305,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) +
@@ -2436,11 +2719,13 @@ found:
list_for_each_entry(lease, &lt->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)
@@ -2448,6 +2733,7 @@ found:
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 b08d21758e07..d81013c514e3 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;
@@ -51,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;
@@ -62,11 +64,15 @@ 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;
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;
@@ -82,19 +88,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);
@@ -134,7 +127,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/proc.c b/fs/smb/server/proc.c
index 19f0f2cfbf54..e5a9158c09bd 100644
--- a/fs/smb/server/proc.c
+++ b/fs/smb/server/proc.c
@@ -161,6 +161,8 @@ static int proc_show_ksmbd_stats(struct seq_file *m, void *v)
seq_printf(m, "max_connections:\t%u\n", server_conf.max_connections);
seq_printf(m, "max_connections_per_ip:\t%u\n",
server_conf.max_ip_connections);
+ seq_printf(m, "max_sessions_per_connection:\t%u\n",
+ server_conf.max_sessions_per_connection);
seq_printf(m, "max_inflight_requests:\t%u\n",
server_conf.max_inflight_req);
seq_printf(m, "deadtime_seconds:\t%lu\n", server_conf.deadtime / HZ);
diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c
index 0827c8c51006..064f99427ca6 100644
--- a/fs/smb/server/server.c
+++ b/fs/smb/server/server.c
@@ -157,7 +157,10 @@ andx_again:
}
}
- ret = cmds->proc(work);
+ if (smb2_check_compound_fid(work))
+ ret = -EBADF;
+ else
+ ret = cmds->proc(work);
if (conn->ops->inc_reqs) {
struct smb2_hdr *rsp = ksmbd_resp_buf_curr(work);
@@ -187,6 +190,16 @@ static void __handle_ksmbd_work(struct ksmbd_work *work,
if (conn->ops->is_transform_hdr &&
conn->ops->is_transform_hdr(work->request_buf)) {
+ u64 tr_sess_id;
+
+ if (get_rfc1002_len(work->request_buf) <
+ sizeof(struct smb2_transform_hdr)) {
+ ksmbd_conn_abort(conn);
+ return;
+ }
+ tr_sess_id = le64_to_cpu(((struct smb2_transform_hdr *)
+ smb_get_msg(work->request_buf))->SessionId);
+
rc = conn->ops->decrypt_req(work);
if (rc < 0) {
ksmbd_conn_abort(conn);
@@ -215,6 +228,12 @@ static void __handle_ksmbd_work(struct ksmbd_work *work,
ksmbd_conn_abort(conn);
return;
}
+
+ rc = ksmbd_check_transform_session(work, tr_sess_id);
+ if (rc < 0) {
+ ksmbd_conn_abort(conn);
+ return;
+ }
}
if (conn->ops->allocate_rsp_buf(work))
@@ -382,6 +401,7 @@ static void handle_ksmbd_work(struct work_struct *wk)
static int queue_ksmbd_work(struct ksmbd_conn *conn)
{
struct ksmbd_work *work;
+ bool wait_for_neg = ksmbd_conn_new(conn) || ksmbd_conn_need_negotiate(conn);
int err;
err = ksmbd_init_smb_server(conn);
@@ -404,6 +424,10 @@ static int queue_ksmbd_work(struct ksmbd_conn *conn)
conn->last_active = jiffies;
INIT_WORK(&work->work, handle_ksmbd_work);
ksmbd_queue_work(work);
+ /* Negotiation can change conn->ops, finish it before reading another PDU. */
+ if (wait_for_neg)
+ wait_event(conn->req_running_q,
+ atomic_read(&conn->req_running) == 0);
return 0;
}
@@ -415,7 +439,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;
}
@@ -451,6 +474,8 @@ static int server_conf_init(void)
KSMBD_AUTH_MSKRB5;
#endif
server_conf.max_inflight_req = SMB2_MAX_CREDITS;
+ server_conf.max_sessions_per_connection =
+ KSMBD_DEFAULT_MAX_SESSIONS_PER_CONNECTION;
return 0;
}
@@ -643,7 +668,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();
diff --git a/fs/smb/server/server.h b/fs/smb/server/server.h
index 4d4d268b59d5..7b7df3fafe5b 100644
--- a/fs/smb/server/server.h
+++ b/fs/smb/server/server.h
@@ -8,6 +8,8 @@
#include "smbacl.h"
+#define KSMBD_DEFAULT_MAX_SESSIONS_PER_CONNECTION 64
+
/*
* Server state type
*/
@@ -44,6 +46,7 @@ struct ksmbd_server_config {
unsigned int max_connections;
unsigned int max_inflight_req;
unsigned int max_ip_connections;
+ unsigned int max_sessions_per_connection;
char *conf[SERVER_CONF_WORK_GROUP + 1];
struct task_struct *dh_task;
diff --git a/fs/smb/server/smb2ops.c b/fs/smb/server/smb2ops.c
index 4578291fb172..747276b171bd 100644
--- a/fs/smb/server/smb2ops.c
+++ b/fs/smb/server/smb2ops.c
@@ -13,6 +13,35 @@
#include "server.h"
#include "stats.h"
+/*
+ * work->response_sz includes the RFC1002 length field while smb_get_msg()
+ * skips over it. Durable v1 and v2 response contexts are mutually exclusive,
+ * and POSIX CREATE contexts are only negotiated for SMB3.1.1.
+ */
+#define SMB2_CREATE_RSP_SIZE(lease_size, durable_size) \
+ (sizeof(__be32) + offsetof(struct smb2_create_rsp, Buffer) + \
+ (lease_size) + (durable_size) + \
+ sizeof(struct create_mxac_rsp) + \
+ sizeof(struct create_disk_id_rsp) + \
+ AAPL_RSP_MAX_SIZE)
+
+#define SMB21_CREATE_RSP_SIZE \
+ SMB2_CREATE_RSP_SIZE(sizeof(struct create_lease), \
+ sizeof(struct create_durable_rsp))
+
+#define SMB3_CREATE_DURABLE_RSP_SIZE \
+ ((sizeof(struct create_durable_rsp) > \
+ sizeof(struct create_durable_rsp_v2)) ? \
+ sizeof(struct create_durable_rsp) : \
+ sizeof(struct create_durable_rsp_v2))
+
+#define SMB3_CREATE_RSP_SIZE \
+ SMB2_CREATE_RSP_SIZE(sizeof(struct create_lease_v2), \
+ SMB3_CREATE_DURABLE_RSP_SIZE)
+
+#define SMB311_CREATE_RSP_SIZE \
+ (SMB3_CREATE_RSP_SIZE + sizeof(struct create_posix_rsp))
+
static struct smb_version_values smb21_server_values = {
.version_string = SMB21_VERSION_STRING,
.protocol_id = SMB21_PROT_ID,
@@ -38,6 +67,7 @@ static struct smb_version_values smb21_server_values = {
.create_disk_id_size = sizeof(struct create_disk_id_rsp),
.create_posix_size = sizeof(struct create_posix_rsp),
.create_aapl_size = AAPL_RSP_MAX_SIZE,
+ .create_rsp_size = SMB21_CREATE_RSP_SIZE,
};
static struct smb_version_values smb30_server_values = {
@@ -66,6 +96,7 @@ static struct smb_version_values smb30_server_values = {
.create_disk_id_size = sizeof(struct create_disk_id_rsp),
.create_posix_size = sizeof(struct create_posix_rsp),
.create_aapl_size = AAPL_RSP_MAX_SIZE,
+ .create_rsp_size = SMB3_CREATE_RSP_SIZE,
};
static struct smb_version_values smb302_server_values = {
@@ -94,6 +125,7 @@ static struct smb_version_values smb302_server_values = {
.create_disk_id_size = sizeof(struct create_disk_id_rsp),
.create_posix_size = sizeof(struct create_posix_rsp),
.create_aapl_size = AAPL_RSP_MAX_SIZE,
+ .create_rsp_size = SMB3_CREATE_RSP_SIZE,
};
static struct smb_version_values smb311_server_values = {
@@ -122,6 +154,7 @@ static struct smb_version_values smb311_server_values = {
.create_disk_id_size = sizeof(struct create_disk_id_rsp),
.create_posix_size = sizeof(struct create_posix_rsp),
.create_aapl_size = AAPL_RSP_MAX_SIZE,
+ .create_rsp_size = SMB311_CREATE_RSP_SIZE,
};
static struct smb_version_ops smb2_0_server_ops = {
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index 18602a4759c0..f5fe3cf8ba32 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -16,6 +16,7 @@
#include <linux/mount.h>
#include <linux/filelock.h>
#include <linux/fileattr.h>
+#include <linux/math.h>
#include <linux/timekeeping.h>
#include <linux/unaligned.h>
@@ -58,10 +59,6 @@ static void __wbuf(struct ksmbd_work *work, void **req, void **rsp)
}
}
-static struct ksmbd_work *smb2_notify_cancel_claim(void **argv);
-static void smb2_notify_cancel_fn(void **argv);
-static void smb2_complete_notify_cancel(struct ksmbd_work *in_work);
-
#define WORK_BUFFERS(w, rq, rs) __wbuf((w), (void **)&(rq), (void **)&(rs))
#define SMB2_CREATE_FILE_ATTRIBUTE_MASK \
@@ -116,13 +113,28 @@ static int register_session_channel(struct ksmbd_session *sess,
goto out;
}
- chann->conn = conn;
+ if (sess->setup_conn != conn) {
+ rc = ksmbd_session_reserve_slot(conn);
+ if (rc) {
+ kfree_sensitive(chann);
+ goto out;
+ }
+ }
+
+ chann->conn = ksmbd_conn_get(conn);
memcpy(chann->sess_key, sess_key, sizeof(chann->sess_key));
old = xa_store(&sess->ksmbd_chann_list, (long)conn, chann,
KSMBD_DEFAULT_GFP);
if (xa_is_err(old)) {
+ if (sess->setup_conn != conn)
+ ksmbd_session_release_slot(conn);
+ ksmbd_conn_put(chann->conn);
kfree_sensitive(chann);
rc = xa_err(old);
+ } else if (sess->setup_conn == conn) {
+ /* Transfer the initial reservation and reference to this channel. */
+ sess->setup_conn = NULL;
+ ksmbd_conn_put(conn);
}
out:
up_write(&sess->chann_lock);
@@ -443,6 +455,95 @@ int smb2_set_rsp_credits(struct ksmbd_work *work)
return 0;
}
+static bool smb2_get_file_ids(struct smb2_hdr *hdr, u64 *volatile_id,
+ u64 *persistent_id)
+{
+ unsigned int offset;
+
+ switch (hdr->Command) {
+ case SMB2_CLOSE:
+ offset = offsetof(struct smb2_close_req, PersistentFileId);
+ break;
+ case SMB2_FLUSH:
+ offset = offsetof(struct smb2_flush_req, PersistentFileId);
+ break;
+ case SMB2_READ:
+ offset = offsetof(struct smb2_read_req, PersistentFileId);
+ break;
+ case SMB2_WRITE:
+ offset = offsetof(struct smb2_write_req, PersistentFileId);
+ break;
+ case SMB2_LOCK:
+ offset = offsetof(struct smb2_lock_req, PersistentFileId);
+ break;
+ case SMB2_IOCTL: {
+ struct smb2_ioctl_req *req = (struct smb2_ioctl_req *)hdr;
+
+ switch (le32_to_cpu(req->CtlCode)) {
+ case FSCTL_DFS_GET_REFERRALS:
+ case FSCTL_DFS_GET_REFERRALS_EX:
+ case FSCTL_QUERY_NETWORK_INTERFACE_INFO:
+ case FSCTL_VALIDATE_NEGOTIATE_INFO:
+ case FSCTL_PIPE_WAIT:
+ return false;
+ }
+ offset = offsetof(struct smb2_ioctl_req, PersistentFileId);
+ break;
+ }
+ case SMB2_QUERY_DIRECTORY:
+ offset = offsetof(struct smb2_query_directory_req, PersistentFileId);
+ break;
+ case SMB2_CHANGE_NOTIFY:
+ offset = offsetof(struct smb2_change_notify_req, PersistentFileId);
+ break;
+ case SMB2_QUERY_INFO:
+ offset = offsetof(struct smb2_query_info_req, PersistentFileId);
+ break;
+ case SMB2_SET_INFO:
+ offset = offsetof(struct smb2_set_info_req, PersistentFileId);
+ break;
+ case SMB2_OPLOCK_BREAK:
+ if (((struct smb2_oplock_break *)hdr)->StructureSize !=
+ cpu_to_le16(OP_BREAK_STRUCT_SIZE_20))
+ return false;
+ offset = offsetof(struct smb2_oplock_break, PersistentFid);
+ break;
+ default:
+ return false;
+ }
+
+ /* FileId contains consecutive persistent and volatile opaque values. */
+ memcpy(persistent_id, (char *)hdr + offset, sizeof(*persistent_id));
+ memcpy(volatile_id, (char *)hdr + offset + sizeof(*persistent_id),
+ sizeof(*volatile_id));
+ return true;
+}
+
+/* Called after message validation and signature verification. */
+bool smb2_check_compound_fid(struct ksmbd_work *work)
+{
+ struct smb2_hdr *req = ksmbd_req_buf_next(work);
+ struct smb2_hdr *rsp = ksmbd_resp_buf_next(work);
+ u64 volatile_id, persistent_id;
+
+ if (!work->next_smb2_rcv_hdr_off ||
+ !(req->Flags & SMB2_FLAGS_RELATED_OPERATIONS))
+ return false;
+
+ if (!work->compound_fid_missing &&
+ !work->compound_fid_present &&
+ smb2_get_file_ids(req, &volatile_id, &persistent_id))
+ work->compound_fid_missing = true;
+
+ if (!work->compound_fid_missing)
+ return false;
+
+ /* MS-SMB2 3.3.5.2.7.2 requires all subsequent operations to fail too. */
+ rsp->Status = STATUS_INVALID_HANDLE;
+ smb2_set_err_rsp(work);
+ return true;
+}
+
/**
* init_chained_smb2_rsp() - initialize smb2 chained response
* @work: smb work containing smb response buffer
@@ -455,6 +556,21 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work)
struct smb2_hdr *rcv_hdr;
int next_hdr_offset = 0;
int len, new_len;
+ u64 volatile_id = KSMBD_NO_FID, persistent_id = KSMBD_NO_FID;
+ bool req_has_fid;
+
+ req_has_fid = smb2_get_file_ids(req, &volatile_id, &persistent_id);
+ /* A failed CREATE is handled through compound_status instead. */
+ work->compound_fid_present = req_has_fid || req->Command == SMB2_CREATE;
+ if (!work->compound_fid_present) {
+ work->compound_fid = KSMBD_NO_FID;
+ work->compound_pfid = KSMBD_NO_FID;
+ }
+
+ /* Related requests keep the inherited SessionId. */
+ if (!work->next_smb2_rcv_hdr_off ||
+ !(req->Flags & SMB2_FLAGS_RELATED_OPERATIONS))
+ work->compound_sid = le64_to_cpu(rsp->SessionId);
/* Len of this response = updated RFC len - offset of previous cmd
* in the compound rsp
@@ -466,48 +582,24 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work)
if (req->Command == SMB2_CREATE && rsp->Status == STATUS_SUCCESS) {
work->compound_fid = ((struct smb2_create_rsp *)rsp)->VolatileFileId;
work->compound_pfid = ((struct smb2_create_rsp *)rsp)->PersistentFileId;
- work->compound_sid = le64_to_cpu(rsp->SessionId);
work->compound_status = STATUS_SUCCESS;
- } else if ((req->Command == SMB2_FLUSH ||
- req->Command == SMB2_READ ||
- req->Command == SMB2_WRITE) &&
- rsp->Status == STATUS_SUCCESS) {
- u64 volatile_id = KSMBD_NO_FID;
- u64 persistent_id = KSMBD_NO_FID;
-
- if (req->Command == SMB2_FLUSH) {
- struct smb2_flush_req *flush_req =
- (struct smb2_flush_req *)req;
-
- volatile_id = flush_req->VolatileFileId;
- persistent_id = flush_req->PersistentFileId;
- } else if (req->Command == SMB2_READ) {
- struct smb2_read_req *read_req =
- (struct smb2_read_req *)req;
-
- volatile_id = read_req->VolatileFileId;
- persistent_id = read_req->PersistentFileId;
+ } else if (rsp->Status == STATUS_SUCCESS) {
+ if (req_has_fid && req->Command != SMB2_CLOSE) {
+ /* Preserve the inherited FID when the request uses all ones. */
+ if (has_file_id(volatile_id)) {
+ work->compound_fid = volatile_id;
+ work->compound_pfid = persistent_id;
+ }
} else {
- struct smb2_write_req *write_req =
- (struct smb2_write_req *)req;
-
- volatile_id = write_req->VolatileFileId;
- persistent_id = write_req->PersistentFileId;
- }
-
- if (has_file_id(volatile_id)) {
- work->compound_fid = volatile_id;
- work->compound_pfid = persistent_id;
- work->compound_sid = le64_to_cpu(rsp->SessionId);
- work->compound_status = STATUS_SUCCESS;
+ work->compound_fid = KSMBD_NO_FID;
+ work->compound_pfid = KSMBD_NO_FID;
}
+ work->compound_status = STATUS_SUCCESS;
} else if (req->Command == SMB2_CREATE) {
work->compound_fid = KSMBD_NO_FID;
work->compound_pfid = KSMBD_NO_FID;
- work->compound_sid = le64_to_cpu(rsp->SessionId);
work->compound_status = rsp->Status;
} else if (rsp->Status != STATUS_SUCCESS) {
- work->compound_sid = le64_to_cpu(rsp->SessionId);
/*
* Only carry the failed status forward when the failing command
* was itself part of the related chain. An unrelated command
@@ -546,6 +638,8 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work)
work->compound_fid = KSMBD_NO_FID;
work->compound_pfid = KSMBD_NO_FID;
work->compound_status = STATUS_SUCCESS;
+ work->compound_fid_present = false;
+ work->compound_fid_missing = false;
}
memset((char *)rsp_hdr, 0, sizeof(struct smb2_hdr) + 2);
rsp_hdr->ProtocolId = SMB2_PROTO_NUMBER;
@@ -858,14 +952,18 @@ static void smb2_update_lock_sequence(struct ksmbd_work *work,
int smb2_allocate_rsp_buf(struct ksmbd_work *work)
{
struct smb2_hdr *hdr = smb_get_msg(work->request_buf);
+ struct smb_version_values *vals = work->conn->vals;
size_t small_sz = MAX_CIFS_SMALL_BUFFER_SIZE;
- size_t large_sz = small_sz + work->conn->vals->max_trans_size;
+ size_t large_sz = small_sz + vals->max_trans_size;
size_t sz = small_sz;
int cmd = le16_to_cpu(hdr->Command);
if (cmd == SMB2_IOCTL_HE || cmd == SMB2_QUERY_DIRECTORY_HE)
sz = large_sz;
+ if (cmd == SMB2_CREATE_HE)
+ sz = max_t(size_t, sz, vals->create_rsp_size);
+
if (cmd == SMB2_QUERY_INFO_HE) {
struct smb2_query_info_req *req;
@@ -1246,6 +1344,7 @@ void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status)
{
struct smb2_hdr *rsp_hdr;
struct ksmbd_work *in_work = ksmbd_alloc_work_struct();
+ u16 command;
if (!in_work)
return;
@@ -1269,12 +1368,29 @@ void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status)
smb2_set_err_rsp(in_work);
rsp_hdr->Status = status;
+ /*
+ * Async interim responses are unsigned, but final responses must
+ * follow the normal signing rules. The synthetic work has no
+ * request buffer, so use the original work for request signing
+ * checks and the response header for SMB3 command selection.
+ */
+ command = work->conn->ops->get_cmd_val(work);
+ if (status != STATUS_PENDING && !work->encrypted && work->sess &&
+ work->conn->ops->set_sign_rsp &&
+ (work->sess->sign ||
+ (work->conn->ops->is_sign_req &&
+ work->conn->ops->is_sign_req(work, command)))) {
+ in_work->sess = work->sess;
+ work->conn->ops->set_sign_rsp(in_work);
+ in_work->sess = NULL;
+ }
+
if (smb2_send_interim_work(in_work, work, true))
ksmbd_debug(SMB, "failed to send interim response\n");
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;
@@ -1411,23 +1527,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,
@@ -1733,6 +1833,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) {
@@ -1805,7 +1906,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");
@@ -2187,14 +2295,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;
@@ -2259,9 +2367,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
@@ -2323,7 +2429,8 @@ out:
#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;
@@ -2332,7 +2439,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;
@@ -2354,10 +2460,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);
@@ -2426,7 +2529,8 @@ out:
#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;
}
@@ -2440,6 +2544,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");
@@ -2458,7 +2564,12 @@ int smb2_sess_setup(struct ksmbd_work *work)
ksmbd_conn_lock(conn);
if (!req->hdr.SessionId) {
- sess = ksmbd_smb2_session_create();
+ /* Bound abandoned SessionId-zero authentication exchanges. */
+ if (ksmbd_too_many_session_setups(conn)) {
+ rc = -ENOSPC;
+ goto out_err;
+ }
+ sess = ksmbd_smb2_session_create(conn);
if (!sess) {
rc = -ENOMEM;
goto out_err;
@@ -2629,14 +2740,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);
@@ -2645,14 +2753,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;
@@ -2785,6 +2891,21 @@ out_err:
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;
}
@@ -3054,9 +3175,10 @@ int smb2_tree_disconnect(struct ksmbd_work *work)
goto err_out;
}
+ err = ksmbd_tree_conn_disconnect(sess, tcon);
+
ksmbd_close_tree_conn_fds(work);
- err = ksmbd_tree_conn_disconnect(sess, tcon);
if (err) {
rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED;
goto err_out;
@@ -3146,6 +3268,8 @@ int smb2_session_logoff(struct ksmbd_work *work)
up_write(&conn->session_lock);
ksmbd_all_conn_set_status(sess, KSMBD_SESS_NEED_SETUP);
+ /* Drop the table reference while work->sess pins the response keys. */
+ ksmbd_session_unregister(conn, sess);
if (err)
return err;
@@ -3322,7 +3446,7 @@ static int smb2_set_ea(struct smb2_ea_info *eabuf, unsigned int buf_len,
path->dentry,
attr_name,
XATTR_USER_PREFIX_LEN +
- eabuf->EaNameLength);
+ eabuf->EaNameLength, NULL);
/* delete the EA only when it exits */
if (rc > 0) {
@@ -3394,13 +3518,15 @@ static noinline int smb2_set_stream_name_xattr(const struct path *path,
fp->stream.name = xattr_stream_name;
fp->stream.size = xattr_stream_size;
- /* Check if there is stream prefix in xattr space */
+ /* Keep the existing xattr's case for subsequent writes and removal. */
rc = ksmbd_vfs_casexattr_len(idmap,
path->dentry,
xattr_stream_name,
- xattr_stream_size);
+ xattr_stream_size, xattr_stream_name);
if (rc >= 0)
return 0;
+ if (rc != -ENOENT && rc != -ENODATA)
+ return rc;
if (fp->cdoption == FILE_OPEN_LE) {
if (!strcmp(stream_name, "AFP_AfpInfo") &&
@@ -3419,8 +3545,9 @@ static noinline int smb2_set_stream_name_xattr(const struct path *path,
* AAPL there too.
*/
static const u8 afpinfo_empty[60] = {
- 0x00, 0x05, 0x16, 0x07, /* magic 0x00051607 BE */
- 0x00, 0x02, 0x00, 0x00, /* version 0x00020000 BE */
+ 'A', 'F', 'P', 0x00, /* signature */
+ 0x00, 0x00, 0x01, 0x00, /* version */
+ [15] = 0x80, /* backup time */
};
rc = ksmbd_vfs_setxattr(idmap, path, xattr_stream_name,
(void *)afpinfo_empty,
@@ -3449,7 +3576,7 @@ static loff_t ksmbd_stream_eof(struct ksmbd_file *fp)
ssize_t slen = ksmbd_vfs_casexattr_len(file_mnt_idmap(fp->filp),
fp->filp->f_path.dentry,
fp->stream.name,
- fp->stream.size);
+ fp->stream.size, NULL);
return slen < 0 ? 0 : (loff_t)slen;
}
@@ -3644,13 +3771,15 @@ 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 void ksmbd_acls_fattr(struct smb_fattr *fattr,
- const struct mnt_idmap *idmap,
- struct inode *inode)
+static int ksmbd_acls_fattr(struct smb_fattr *fattr,
+ const struct mnt_idmap *idmap,
+ struct inode *inode)
{
+ struct posix_acl *acl;
vfsuid_t vfsuid = i_uid_into_vfsuid(idmap, inode);
vfsgid_t vfsgid = i_gid_into_vfsgid(idmap, inode);
@@ -3661,10 +3790,28 @@ static void ksmbd_acls_fattr(struct smb_fattr *fattr,
fattr->cf_dacls = NULL;
if (IS_ENABLED(CONFIG_FS_POSIX_ACL)) {
- fattr->cf_acls = get_inode_acl(inode, ACL_TYPE_ACCESS);
- if (S_ISDIR(inode->i_mode))
- fattr->cf_dacls = get_inode_acl(inode, ACL_TYPE_DEFAULT);
+ acl = get_inode_acl(inode, ACL_TYPE_ACCESS);
+ if (IS_ERR(acl)) {
+ if (acl != ERR_PTR(-EOPNOTSUPP))
+ return PTR_ERR(acl);
+ acl = NULL;
+ }
+ fattr->cf_acls = acl;
+
+ if (S_ISDIR(inode->i_mode)) {
+ acl = get_inode_acl(inode, ACL_TYPE_DEFAULT);
+ if (IS_ERR(acl)) {
+ if (acl != ERR_PTR(-EOPNOTSUPP)) {
+ posix_acl_release(fattr->cf_acls);
+ return PTR_ERR(acl);
+ }
+ acl = NULL;
+ }
+ fattr->cf_dacls = acl;
+ }
}
+
+ return 0;
}
enum {
@@ -3698,7 +3845,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)
@@ -3711,11 +3858,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,
@@ -3729,7 +3871,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:
@@ -3933,35 +4077,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;
}
@@ -3974,15 +4101,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))
@@ -4080,18 +4198,76 @@ static int parse_app_instance_version(struct smb2_create_req *req,
return 0;
}
-static int smb2_handle_app_instance_id(struct smb2_create_rsp *rsp,
- struct durable_info *dh_info)
+static int smb2_app_instance_stream_matches(struct ksmbd_file *fp,
+ char *stream_name, int s_type,
+ bool *matches)
{
+ char *xattr_stream_name;
+ size_t xattr_stream_size;
+ int rc;
+
+ if (!stream_name) {
+ *matches = !ksmbd_stream_fd(fp);
+ return 0;
+ }
+
+ rc = ksmbd_vfs_xattr_stream_name(stream_name, &xattr_stream_name,
+ &xattr_stream_size, s_type);
+ if (rc)
+ return rc;
+
+ *matches = fp->stream.name && fp->stream.size >= 0 &&
+ (size_t)fp->stream.size == xattr_stream_size &&
+ !strncasecmp(fp->stream.name, xattr_stream_name,
+ xattr_stream_size);
+ kfree(xattr_stream_name);
+ return 0;
+}
+
+static int smb2_handle_app_instance_id(struct ksmbd_work *work,
+ struct smb2_create_rsp *rsp,
+ struct durable_info *dh_info,
+ const struct path *path,
+ char *stream_name, int s_type,
+ bool *closed)
+{
+ struct ksmbd_conn *conn = work->conn;
struct ksmbd_file *old_fp;
- bool reject = false;
+ const struct path *old_path;
+ char *app_instance_id;
+ __le32 maximal_access = FILE_MAXIMAL_ACCESS_LE;
+ bool reject = false, stream_matches;
+ int cursor = 0, rc;
+ *closed = false;
if (!dh_info->app_instance_id)
return 0;
+ app_instance_id = dh_info->AppInstanceId;
- old_fp = ksmbd_lookup_fd_app_instance_id(dh_info->AppInstanceId);
- if (!old_fp)
- return 0;
+ while (true) {
+ old_fp = ksmbd_lookup_fd_app_instance_id_next(app_instance_id,
+ &cursor);
+ if (!old_fp)
+ return 0;
+
+ if (!ksmbd_app_instance_id_matches(work, old_fp) ||
+ !path_equal(&old_fp->filp->f_path, path)) {
+ ksmbd_put_durable_fd(old_fp);
+ continue;
+ }
+
+ rc = smb2_app_instance_stream_matches(old_fp, stream_name,
+ s_type, &stream_matches);
+ if (rc) {
+ ksmbd_put_durable_fd(old_fp);
+ return rc;
+ }
+ if (!stream_matches) {
+ ksmbd_put_durable_fd(old_fp);
+ continue;
+ }
+ break;
+ }
if (dh_info->app_instance_version_valid) {
if (old_fp->app_instance_version_valid &&
@@ -4106,13 +4282,34 @@ static int smb2_handle_app_instance_id(struct smb2_create_rsp *rsp,
reject = true;
}
- ksmbd_put_durable_fd(old_fp);
if (reject) {
+ ksmbd_put_durable_fd(old_fp);
rsp->hdr.Status = STATUS_FILE_FORCED_CLOSED;
return -EIO;
}
- return ksmbd_close_fd_app_instance_id(dh_info->AppInstanceId);
+ /* Authorize the referenced open even if its name changes meanwhile. */
+ old_path = &old_fp->filp->f_path;
+ rc = smb_check_perm_dacl(conn, old_path, &maximal_access, 0,
+ work->sess->user->uid, false);
+ if (rc) {
+ /* Access denial prevents takeover, not the CREATE itself. */
+ ksmbd_put_durable_fd(old_fp);
+ return rc == -EACCES ? 0 : rc;
+ }
+ if (maximal_access == FILE_MAXIMAL_ACCESS_LE)
+ ksmbd_vfs_query_maximal_access(mnt_idmap(old_path->mnt),
+ old_path->dentry, &maximal_access);
+ /* Also require read access with the effective VFS credentials. */
+ if ((le32_to_cpu(maximal_access) & GENERIC_READ_FLAGS) !=
+ GENERIC_READ_FLAGS ||
+ inode_permission(mnt_idmap(old_path->mnt),
+ d_inode(old_path->dentry), MAY_OPEN | MAY_READ)) {
+ ksmbd_put_durable_fd(old_fp);
+ return 0;
+ }
+
+ return ksmbd_close_fd_app_instance_id(work, old_fp, closed);
}
/**
@@ -4152,6 +4349,7 @@ int smb2_open(struct ksmbd_work *work)
char *name = NULL;
char *stream_name = NULL;
bool file_present = false, created = false, already_permitted = false;
+ bool app_instance_id_checked = false, app_instance_closed;
int share_ret, need_truncate = 0;
u64 time, alloc_size = 0;
umode_t posix_mode = 0;
@@ -4171,12 +4369,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);
@@ -4184,6 +4377,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) {
@@ -4196,6 +4402,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),
@@ -4293,8 +4503,12 @@ int smb2_open(struct ksmbd_work *work)
goto err_out2;
rc = ksmbd_reopen_durable_fd(work, dh_info.fp);
- if (rc)
+ if (rc) {
+ if (rc == -ENOTCONN)
+ rsp->hdr.Status =
+ STATUS_NETWORK_NAME_DELETED;
goto err_out2;
+ }
fp = dh_info.fp;
@@ -4326,13 +4540,6 @@ int smb2_open(struct ksmbd_work *work)
}
}
- if (dh_info.app_instance_id && !dh_info.reconnected &&
- !dh_info.replay) {
- rc = smb2_handle_app_instance_id(rsp, &dh_info);
- if (rc)
- goto err_out2;
- }
-
if (le32_to_cpu(req->ImpersonationLevel) > le32_to_cpu(IL_DELEGATE)) {
pr_err("Invalid impersonationlevel : 0x%x\n",
le32_to_cpu(req->ImpersonationLevel));
@@ -4445,9 +4652,29 @@ int smb2_open(struct ksmbd_work *work)
goto err_out2;
}
+resolve_path:
+ file_present = false;
+ maximal_access = 0;
rc = ksmbd_vfs_kern_path(work, name, LOOKUP_NO_SYMLINKS,
&path, 1);
+ /* Validate takeover before disconnected delete-on-close cleanup. */
+ if (!rc && dh_info.app_instance_id && !dh_info.reconnected &&
+ !dh_info.replay && !app_instance_id_checked) {
+ app_instance_id_checked = true;
+ rc = smb2_handle_app_instance_id(work, rsp, &dh_info, &path,
+ stream_name, s_type,
+ &app_instance_closed);
+ if (rc) {
+ path_put(&path);
+ goto err_out1;
+ }
+ if (app_instance_closed) {
+ path_put(&path);
+ goto resolve_path;
+ }
+ }
+
/*
* A durable handle opened with delete-on-close is preserved across a
* disconnect so it can be reclaimed by a durable reconnect. When a new
@@ -4618,6 +4845,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)) {
@@ -4631,7 +4861,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) {
@@ -4715,6 +4945,8 @@ int smb2_open(struct ksmbd_work *work)
if (IS_ERR(fp)) {
fput(filp);
rc = PTR_ERR(fp);
+ if (rc == -ENOTCONN)
+ rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED;
fp = NULL;
goto err_out;
}
@@ -4791,7 +5023,10 @@ int smb2_open(struct ksmbd_work *work)
int pntsd_size;
size_t scratch_len;
- ksmbd_acls_fattr(&fattr, idmap, inode);
+ rc = ksmbd_acls_fattr(&fattr, idmap, inode);
+ if (rc)
+ goto err_out;
+
scratch_len = smb_acl_sec_desc_scratch_len(&fattr,
NULL, 0,
OWNER_SECINFO | GROUP_SECINFO |
@@ -4854,6 +5089,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
@@ -5315,11 +5551,20 @@ err_out1:
err_out2:
if (!rc) {
- if (!dh_info.replay)
+ if (!dh_info.replay) {
+ smb2_complete_request_open(work);
rc = ksmbd_update_fstate(&work->sess->file_table, fp,
FP_INITED);
- if (!rc)
+ if (rc == -ENOENT)
+ rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED;
+ if (!rc) {
+ work->request_open = fp;
+ work->request_open_chseq =
+ smb3_hdr_channel_sequence(&req->hdr);
+ }
+ } else {
rc = smb2_set_request_open(work, fp, &req->hdr, false, false);
+ }
if (!rc)
rc = ksmbd_iov_pin_rsp(work, (void *)rsp, iov_len);
}
@@ -5334,8 +5579,7 @@ err_out2:
rsp->hdr.Status = STATUS_PRIVILEGE_NOT_HELD;
else
rsp->hdr.Status = STATUS_ACCESS_DENIED;
- }
- else if (rc == -ENOENT)
+ } else if (rc == -ENOENT && !rsp->hdr.Status)
rsp->hdr.Status = STATUS_OBJECT_NAME_INVALID;
else if (rc == -EPERM)
rsp->hdr.Status = STATUS_SHARING_VIOLATION;
@@ -5358,8 +5602,14 @@ err_out2:
if (!rsp->hdr.Status)
rsp->hdr.Status = STATUS_UNEXPECTED_IO_ERROR;
- if (fp && !dh_info.replay)
- ksmbd_fd_put(work, fp);
+ if (fp && !dh_info.replay) {
+ if (work->request_open == fp) {
+ ksmbd_close_fd(work, fp->volatile_id);
+ smb2_complete_request_open(work);
+ } else {
+ ksmbd_fd_put(work, fp);
+ }
+ }
smb2_set_err_rsp(work);
ksmbd_debug(SMB, "Error response: %x\n", rsp->hdr.Status);
}
@@ -5402,6 +5652,16 @@ static int readdir_info_level_struct_sz(int info_level)
return sizeof(FILE_ID_FULL_DIR_INFO);
case FILEID_BOTH_DIRECTORY_INFORMATION:
return sizeof(struct file_id_both_directory_info);
+ case FILE_ID_EXTD_DIRECTORY_INFORMATION:
+ return sizeof(struct file_id_extd_directory_info);
+ case FileId64ExtdDirectoryInformation:
+ return sizeof(struct file_id_64_extd_directory_info);
+ case FileId64ExtdBothDirectoryInformation:
+ return sizeof(struct file_id_64_extd_both_directory_info);
+ case FileIdAllExtdDirectoryInformation:
+ return sizeof(struct file_id_all_extd_directory_info);
+ case FileIdAllExtdBothDirectoryInformation:
+ return sizeof(struct file_id_all_extd_both_directory_info);
case SMB_FIND_FILE_POSIX_INFO:
return sizeof(struct smb2_posix_info);
default:
@@ -5472,6 +5732,56 @@ static int dentry_name(struct ksmbd_dir_info *d_info, int info_level)
d_info->name_len = le32_to_cpu(fibdinfo->FileNameLength);
return 0;
}
+ case FILE_ID_EXTD_DIRECTORY_INFORMATION:
+ {
+ struct file_id_extd_directory_info *info;
+
+ info = (struct file_id_extd_directory_info *)d_info->rptr;
+ d_info->rptr += le32_to_cpu(info->NextEntryOffset);
+ d_info->name = info->FileName;
+ d_info->name_len = le32_to_cpu(info->FileNameLength);
+ return 0;
+ }
+ case FileId64ExtdDirectoryInformation:
+ {
+ struct file_id_64_extd_directory_info *info;
+
+ info = (struct file_id_64_extd_directory_info *)d_info->rptr;
+ d_info->rptr += le32_to_cpu(info->NextEntryOffset);
+ d_info->name = info->FileName;
+ d_info->name_len = le32_to_cpu(info->FileNameLength);
+ return 0;
+ }
+ case FileId64ExtdBothDirectoryInformation:
+ {
+ struct file_id_64_extd_both_directory_info *info;
+
+ info = (struct file_id_64_extd_both_directory_info *)d_info->rptr;
+ d_info->rptr += le32_to_cpu(info->NextEntryOffset);
+ d_info->name = info->FileName;
+ d_info->name_len = le32_to_cpu(info->FileNameLength);
+ return 0;
+ }
+ case FileIdAllExtdDirectoryInformation:
+ {
+ struct file_id_all_extd_directory_info *info;
+
+ info = (struct file_id_all_extd_directory_info *)d_info->rptr;
+ d_info->rptr += le32_to_cpu(info->NextEntryOffset);
+ d_info->name = info->FileName;
+ d_info->name_len = le32_to_cpu(info->FileNameLength);
+ return 0;
+ }
+ case FileIdAllExtdBothDirectoryInformation:
+ {
+ struct file_id_all_extd_both_directory_info *info;
+
+ info = (struct file_id_all_extd_both_directory_info *)d_info->rptr;
+ d_info->rptr += le32_to_cpu(info->NextEntryOffset);
+ d_info->name = info->FileName;
+ d_info->name_len = le32_to_cpu(info->FileNameLength);
+ return 0;
+ }
case SMB_FIND_FILE_POSIX_INFO:
{
struct smb2_posix_info *posix_info;
@@ -5709,6 +6019,124 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level,
fibdinfo->NextEntryOffset = cpu_to_le32(next_entry_offset);
break;
}
+ case FILE_ID_EXTD_DIRECTORY_INFORMATION:
+ {
+ struct file_id_extd_directory_info *info;
+
+ info = (struct file_id_extd_directory_info *)kstat;
+ info->FileNameLength = cpu_to_le32(conv_len);
+ info->EaSize = 0;
+ info->ReparsePointTag =
+ smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode);
+ if (info->ReparsePointTag)
+ info->ExtFileAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE;
+ /* The VFS does not provide a 128-bit file ID. */
+ memset(info->FileId, 0, sizeof(info->FileId));
+ if (d_info->hide_dot_file && d_info->name[0] == '.')
+ info->ExtFileAttributes |= FILE_ATTRIBUTE_HIDDEN_LE;
+ memcpy(info->FileName, conv_name, conv_len);
+ memset(info->FileName + conv_len, 0, d_info->last_entry_off_align);
+ info->NextEntryOffset = cpu_to_le32(next_entry_offset);
+ break;
+ }
+ case FileId64ExtdDirectoryInformation:
+ {
+ struct file_id_64_extd_directory_info *info;
+
+ info = (struct file_id_64_extd_directory_info *)kstat;
+ info->FileNameLength = cpu_to_le32(conv_len);
+ info->EaSize = 0;
+ info->ReparsePointTag =
+ smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode);
+ if (info->ReparsePointTag)
+ info->ExtFileAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE;
+ if (conn->is_aapl)
+ info->FileId = 0;
+ else
+ info->FileId = cpu_to_le64(ksmbd_kstat->kstat->ino);
+ if (d_info->hide_dot_file && d_info->name[0] == '.')
+ info->ExtFileAttributes |= FILE_ATTRIBUTE_HIDDEN_LE;
+ memcpy(info->FileName, conv_name, conv_len);
+ memset(info->FileName + conv_len, 0, d_info->last_entry_off_align);
+ info->NextEntryOffset = cpu_to_le32(next_entry_offset);
+ break;
+ }
+ case FileId64ExtdBothDirectoryInformation:
+ {
+ struct file_id_64_extd_both_directory_info *info;
+
+ info = (struct file_id_64_extd_both_directory_info *)kstat;
+ info->FileNameLength = cpu_to_le32(conv_len);
+ info->EaSize = 0;
+ info->ReparsePointTag =
+ smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode);
+ if (info->ReparsePointTag)
+ info->ExtFileAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE;
+ if (conn->is_aapl)
+ info->FileId = 0;
+ else
+ info->FileId = cpu_to_le64(ksmbd_kstat->kstat->ino);
+ info->ShortNameLength = 0;
+ info->Reserved1 = 0;
+ memset(info->ShortName, 0, sizeof(info->ShortName));
+ if (d_info->hide_dot_file && d_info->name[0] == '.')
+ info->ExtFileAttributes |= FILE_ATTRIBUTE_HIDDEN_LE;
+ memcpy(info->FileName, conv_name, conv_len);
+ memset(info->FileName + conv_len, 0, d_info->last_entry_off_align);
+ info->NextEntryOffset = cpu_to_le32(next_entry_offset);
+ break;
+ }
+ case FileIdAllExtdDirectoryInformation:
+ {
+ struct file_id_all_extd_directory_info *info;
+
+ info = (struct file_id_all_extd_directory_info *)kstat;
+ info->FileNameLength = cpu_to_le32(conv_len);
+ info->EaSize = 0;
+ info->ReparsePointTag =
+ smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode);
+ if (info->ReparsePointTag)
+ info->ExtFileAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE;
+ if (conn->is_aapl)
+ info->FileId = 0;
+ else
+ info->FileId = cpu_to_le64(ksmbd_kstat->kstat->ino);
+ /* The VFS does not provide a 128-bit file ID. */
+ memset(info->FileId128, 0, sizeof(info->FileId128));
+ if (d_info->hide_dot_file && d_info->name[0] == '.')
+ info->ExtFileAttributes |= FILE_ATTRIBUTE_HIDDEN_LE;
+ memcpy(info->FileName, conv_name, conv_len);
+ memset(info->FileName + conv_len, 0, d_info->last_entry_off_align);
+ info->NextEntryOffset = cpu_to_le32(next_entry_offset);
+ break;
+ }
+ case FileIdAllExtdBothDirectoryInformation:
+ {
+ struct file_id_all_extd_both_directory_info *info;
+
+ info = (struct file_id_all_extd_both_directory_info *)kstat;
+ info->FileNameLength = cpu_to_le32(conv_len);
+ info->EaSize = 0;
+ info->ReparsePointTag =
+ smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode);
+ if (info->ReparsePointTag)
+ info->ExtFileAttributes |= FILE_ATTRIBUTE_REPARSE_POINT_LE;
+ if (conn->is_aapl)
+ info->FileId = 0;
+ else
+ info->FileId = cpu_to_le64(ksmbd_kstat->kstat->ino);
+ /* The VFS does not provide a 128-bit file ID. */
+ memset(info->FileId128, 0, sizeof(info->FileId128));
+ info->ShortNameLength = 0;
+ info->Reserved1 = 0;
+ memset(info->ShortName, 0, sizeof(info->ShortName));
+ if (d_info->hide_dot_file && d_info->name[0] == '.')
+ info->ExtFileAttributes |= FILE_ATTRIBUTE_HIDDEN_LE;
+ memcpy(info->FileName, conv_name, conv_len);
+ memset(info->FileName + conv_len, 0, d_info->last_entry_off_align);
+ info->NextEntryOffset = cpu_to_le32(next_entry_offset);
+ break;
+ }
case SMB_FIND_FILE_POSIX_INFO:
{
struct smb2_posix_info *posix_info;
@@ -5716,6 +6144,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);
@@ -5725,9 +6154,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);
@@ -5752,6 +6183,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;
/*
@@ -5955,6 +6388,61 @@ static int reserve_populate_dentry(struct ksmbd_dir_info *d_info,
fibdinfo->NextEntryOffset = cpu_to_le32(next_entry_offset);
break;
}
+ case FILE_ID_EXTD_DIRECTORY_INFORMATION:
+ {
+ struct file_id_extd_directory_info *info;
+
+ info = (struct file_id_extd_directory_info *)d_info->wptr;
+ memcpy(info->FileName, d_info->name, d_info->name_len);
+ info->FileName[d_info->name_len] = 0x00;
+ info->FileNameLength = cpu_to_le32(d_info->name_len);
+ info->NextEntryOffset = cpu_to_le32(next_entry_offset);
+ break;
+ }
+ case FileId64ExtdDirectoryInformation:
+ {
+ struct file_id_64_extd_directory_info *info;
+
+ info = (struct file_id_64_extd_directory_info *)d_info->wptr;
+ memcpy(info->FileName, d_info->name, d_info->name_len);
+ info->FileName[d_info->name_len] = 0x00;
+ info->FileNameLength = cpu_to_le32(d_info->name_len);
+ info->NextEntryOffset = cpu_to_le32(next_entry_offset);
+ break;
+ }
+ case FileId64ExtdBothDirectoryInformation:
+ {
+ struct file_id_64_extd_both_directory_info *info;
+
+ info = (struct file_id_64_extd_both_directory_info *)d_info->wptr;
+ memcpy(info->FileName, d_info->name, d_info->name_len);
+ info->FileName[d_info->name_len] = 0x00;
+ info->FileNameLength = cpu_to_le32(d_info->name_len);
+ info->NextEntryOffset = cpu_to_le32(next_entry_offset);
+ break;
+ }
+ case FileIdAllExtdDirectoryInformation:
+ {
+ struct file_id_all_extd_directory_info *info;
+
+ info = (struct file_id_all_extd_directory_info *)d_info->wptr;
+ memcpy(info->FileName, d_info->name, d_info->name_len);
+ info->FileName[d_info->name_len] = 0x00;
+ info->FileNameLength = cpu_to_le32(d_info->name_len);
+ info->NextEntryOffset = cpu_to_le32(next_entry_offset);
+ break;
+ }
+ case FileIdAllExtdBothDirectoryInformation:
+ {
+ struct file_id_all_extd_both_directory_info *info;
+
+ info = (struct file_id_all_extd_both_directory_info *)d_info->wptr;
+ memcpy(info->FileName, d_info->name, d_info->name_len);
+ info->FileName[d_info->name_len] = 0x00;
+ info->FileNameLength = cpu_to_le32(d_info->name_len);
+ info->NextEntryOffset = cpu_to_le32(next_entry_offset);
+ break;
+ }
case SMB_FIND_FILE_POSIX_INFO:
{
struct smb2_posix_info *posix_info;
@@ -6016,6 +6504,11 @@ static int verify_info_level(int info_level)
case FILEID_FULL_DIRECTORY_INFORMATION:
case FILEID_BOTH_DIRECTORY_INFORMATION:
case SMB_FIND_FILE_POSIX_INFO:
+ case FILE_ID_EXTD_DIRECTORY_INFORMATION:
+ case FileId64ExtdDirectoryInformation:
+ case FileId64ExtdBothDirectoryInformation:
+ case FileIdAllExtdDirectoryInformation:
+ case FileIdAllExtdBothDirectoryInformation:
break;
default:
return -EOPNOTSUPP;
@@ -6101,6 +6594,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),
@@ -6741,12 +7240,12 @@ static void get_file_alternate_info(struct ksmbd_work *work,
void *rsp_org)
{
struct ksmbd_conn *conn = work->conn;
- struct smb2_file_alt_name_info *file_info;
+ struct smb2_file_name_info *file_info;
struct dentry *dentry = fp->filp->f_path.dentry;
int conv_len;
spin_lock(&dentry->d_lock);
- file_info = (struct smb2_file_alt_name_info *)rsp->Buffer;
+ file_info = (struct smb2_file_name_info *)rsp->Buffer;
conv_len = ksmbd_extract_shortname(conn,
dentry->d_name.name,
file_info->FileName);
@@ -6793,7 +7292,7 @@ static int get_file_normalized_name_info(struct ksmbd_work *work,
struct smb2_query_info_rsp *rsp,
struct ksmbd_file *fp)
{
- struct smb2_file_alt_name_info *file_info;
+ struct smb2_file_name_info *file_info;
char *filename, *normalized, *stream_name;
int buf_free_len, conv_len, filename_len;
@@ -6827,7 +7326,7 @@ static int get_file_normalized_name_info(struct ksmbd_work *work,
return -EINVAL;
}
- file_info = (struct smb2_file_alt_name_info *)rsp->Buffer;
+ file_info = (struct smb2_file_name_info *)rsp->Buffer;
conv_len = smbConvertToUTF16((__le16 *)file_info->FileName,
normalized, filename_len,
work->conn->local_nls, 0);
@@ -6900,7 +7399,8 @@ static int get_file_stream_info(struct ksmbd_work *work,
":%s", &stream_name[XATTR_NAME_STREAM_LEN]);
next = sizeof(struct smb2_file_stream_info) + streamlen * 2;
- if (next > buf_free_len) {
+ /* smbConvertToUTF16() also writes a terminating UTF-16 unit. */
+ if (next + sizeof(__le16) > buf_free_len) {
kfree(stream_buf);
break;
}
@@ -6912,17 +7412,8 @@ static int get_file_stream_info(struct ksmbd_work *work,
streamlen *= 2;
kfree(stream_buf);
file_info->StreamNameLength = cpu_to_le32(streamlen);
- /*
- * stream_name_len is the byte length of the xattr's *name*,
- * not its value -- same class of bug ksmbd_stream_eof()
- * (smb2pdu.c) already fixes for EndOfFile/AllocationSize on
- * a stream handle; this enumeration path needs the same
- * real xattr value length, not the name length reused as a
- * size.
- */
- slen = ksmbd_vfs_casexattr_len(file_mnt_idmap(fp->filp),
- path->dentry, stream_name,
- strlen(stream_name) + 1);
+ slen = ksmbd_vfs_xattr_len(file_mnt_idmap(fp->filp),
+ path->dentry, stream_name);
ssize = slen < 0 ? 0 : (loff_t)slen;
file_info->StreamSize = cpu_to_le64(ssize);
file_info->StreamAllocationSize = cpu_to_le64(ssize);
@@ -6934,7 +7425,7 @@ static int get_file_stream_info(struct ksmbd_work *work,
out:
if (!S_ISDIR(stat.mode) &&
- buf_free_len >= sizeof(struct smb2_file_stream_info) + 7 * 2) {
+ buf_free_len >= sizeof(struct smb2_file_stream_info) + (7 + 1) * 2) {
file_info = (struct smb2_file_stream_info *)
&rsp->Buffer[nbytes];
streamlen = smbConvertToUTF16((__le16 *)file_info->StreamName,
@@ -7103,8 +7594,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);
@@ -7112,9 +7605,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",
@@ -7125,6 +7622,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);
@@ -7145,7 +7674,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);
@@ -7166,7 +7695,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)).
@@ -7177,8 +7710,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;
}
@@ -7190,7 +7729,7 @@ static int smb2_get_info_file(struct ksmbd_work *work,
struct ksmbd_file *fp;
int fileinfoclass = 0;
int rc = 0;
- unsigned int fixed_len;
+ unsigned int fixed_len, req_output_len;
unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID;
if (test_share_config_flag(work->tcon->share_conf,
@@ -7284,11 +7823,12 @@ 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(rsp, fp, work->response_buf);
+ rc = find_file_posix_info(work, req, rsp, fp);
}
break;
default:
@@ -7297,24 +7837,24 @@ static int smb2_get_info_file(struct ksmbd_work *work,
rc = -EOPNOTSUPP;
}
if (!rc) {
+ req_output_len = le32_to_cpu(req->OutputBufferLength);
fixed_len = le32_to_cpu(rsp->OutputBufferLength);
switch (fileinfoclass) {
case FILE_ALL_INFORMATION:
fixed_len = FILE_ALL_INFORMATION_SIZE;
break;
case FILE_ALTERNATE_NAME_INFORMATION:
- fixed_len = FILE_ALTERNATE_NAME_INFORMATION_SIZE;
+ fixed_len = FILE_NAME_INFORMATION_SIZE;
break;
case FILE_NORMALIZED_NAME_INFORMATION:
- fixed_len = FILE_NORMALIZED_NAME_INFORMATION_SIZE;
+ fixed_len = FILE_NAME_INFORMATION_SIZE;
+ req_output_len = round_down(req_output_len, 2);
break;
case FILE_STREAM_INFORMATION:
fixed_len = FILE_STREAM_INFORMATION_SIZE;
break;
}
- rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength),
- fixed_len,
- rsp);
+ rc = buffer_check_err(req_output_len, fixed_len, rsp);
}
ksmbd_fd_put(work, fp);
@@ -7332,20 +7872,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);
@@ -7558,25 +8125,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_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);
+ 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(sizeof(*info));
+ fixed_len = sizeof(*info);
break;
}
default:
@@ -7652,7 +8213,11 @@ static int smb2_get_info_sec(struct ksmbd_work *work,
idmap = file_mnt_idmap(fp->filp);
inode = file_inode(fp->filp);
- ksmbd_acls_fattr(&fattr, idmap, inode);
+ rc = ksmbd_acls_fattr(&fattr, idmap, inode);
+ if (rc) {
+ ksmbd_fd_put(work, fp);
+ return rc;
+ }
if (test_share_config_flag(work->tcon->share_conf,
KSMBD_SHARE_FLAG_ACL_XATTR))
@@ -7982,9 +8547,11 @@ static int smb2_rename(struct ksmbd_work *work,
return PTR_ERR(new_name);
if (fp->is_posix_ctxt == false && strchr(new_name, ':')) {
- int s_type;
+ int s_type = 0;
char *xattr_stream_name, *stream_name = NULL;
+ char *stream_buf = NULL;
size_t xattr_stream_size;
+ ssize_t stream_buf_len = 0;
int len;
rc = parse_stream_name(new_name, &stream_name, &s_type);
@@ -7998,6 +8565,10 @@ static int smb2_rename(struct ksmbd_work *work,
goto out;
}
+ /* An empty stream name is the base file's default stream. */
+ if (!stream_name || !stream_name[0])
+ goto out;
+
rc = ksmbd_vfs_xattr_stream_name(stream_name,
&xattr_stream_name,
&xattr_stream_size,
@@ -8005,15 +8576,34 @@ static int smb2_rename(struct ksmbd_work *work,
if (rc)
goto out;
+ /* A handle opened without a stream has no source to copy. */
+ if (ksmbd_stream_fd(fp)) {
+ if (!strcasecmp(xattr_stream_name, fp->stream.name)) {
+ kfree(xattr_stream_name);
+ goto out;
+ }
+
+ stream_buf_len = ksmbd_vfs_getcasexattr(file_mnt_idmap(fp->filp),
+ fp->filp->f_path.dentry,
+ fp->stream.name,
+ fp->stream.size,
+ &stream_buf);
+ if (stream_buf_len < 0) {
+ rc = stream_buf_len;
+ kfree(xattr_stream_name);
+ goto out;
+ }
+ }
+
rc = ksmbd_vfs_setxattr(file_mnt_idmap(fp->filp),
&fp->filp->f_path,
xattr_stream_name,
- NULL, 0, 0, true);
- if (rc < 0) {
+ stream_buf, stream_buf_len, 0, true);
+ kfree(stream_buf);
+ if (rc < 0)
pr_err("failed to store stream name in xattr: %d\n",
rc);
- rc = -EINVAL;
- }
+
kfree(xattr_stream_name);
goto out;
}
@@ -8126,6 +8716,11 @@ static int set_file_basic_info(struct ksmbd_file *fp,
inode = file_inode(filp);
idmap = file_mnt_idmap(filp);
+ /* MS-FSA FileBasicInformation: a directory can't be marked TEMPORARY */
+ if (S_ISDIR(inode->i_mode) &&
+ (file_info->Attributes & FILE_ATTRIBUTE_TEMPORARY_LE))
+ return -EINVAL;
+
if (file_info->CreationTime)
fp->create_time = le64_to_cpu(file_info->CreationTime);
@@ -8201,10 +8796,11 @@ static int set_file_allocation_info(struct ksmbd_work *work,
* properly with any smb.conf option
*/
- loff_t alloc_blks;
- u64 alloc_size;
+ u64 alloc_size, target, alloc_blks;
+ u32 alloc_unit;
struct inode *inode;
struct kstat stat;
+ struct kstatfs stfs;
int rc;
if (!(fp->daccess & FILE_WRITE_DATA_LE))
@@ -8217,51 +8813,48 @@ static int set_file_allocation_info(struct ksmbd_work *work,
AT_STATX_SYNC_AS_STAT);
if (rc)
return rc;
+ rc = vfs_statfs(&fp->filp->f_path, &stfs);
+ if (rc)
+ return rc;
/*
- * AllocationSize is fully client-controlled (the caller only
- * validates the fixed 8-byte buffer length). Reject values that
- * would overflow the "round up to 512-byte blocks" conversion
- * below instead of silently wrapping it to a tiny block count,
- * which would truncate the file to a size the client never
- * asked for.
+ * Use the allocation unit advertised in FS_SIZE_INFORMATION.
+ * Reject requests that overflow when rounded to a whole unit.
*/
alloc_size = le64_to_cpu(file_alloc_info->AllocationSize);
- if (alloc_size > MAX_LFS_FILESIZE - 511)
+ if (stfs.f_bsize <= 0 || (u64)stfs.f_bsize > U32_MAX)
+ return -EINVAL;
+ alloc_unit = stfs.f_bsize;
+ if (alloc_size > MAX_LFS_FILESIZE - (alloc_unit - 1))
return -EINVAL;
- alloc_blks = (alloc_size + 511) >> 9;
+ target = DIV_ROUND_UP_ULL(alloc_size, alloc_unit) * alloc_unit;
+ alloc_blks = DIV_ROUND_UP_ULL(target, 512);
inode = file_inode(fp->filp);
+ if (target > inode->i_sb->s_maxbytes)
+ return -EINVAL;
if (alloc_blks > stat.blocks) {
smb_break_all_levII_oplock(work, fp, 1);
- rc = vfs_fallocate(fp->filp, FALLOC_FL_KEEP_SIZE, 0,
- alloc_blks * 512);
+ rc = vfs_fallocate(fp->filp, FALLOC_FL_KEEP_SIZE, 0, target);
if (rc && rc != -EOPNOTSUPP) {
pr_err("vfs_fallocate is failed : %d\n", rc);
return rc;
}
- } else if (alloc_blks < stat.blocks) {
- loff_t size;
-
+ } else if (alloc_blks < stat.blocks &&
+ target <= i_size_read(inode)) {
/*
- * Allocation size could be smaller than original one
- * which means allocated blocks in file should be
- * deallocated. use truncate to cut out it, but inode
- * size is also updated with truncate offset.
- * inode size is retained by backup inode size.
+ * Truncate at or below EOF to discard excess allocation.
+ * Truncating above EOF would extend the file on disk.
*/
- size = i_size_read(inode);
- rc = ksmbd_vfs_truncate(work, fp, alloc_blks * 512);
+ rc = ksmbd_vfs_truncate(work, fp, target);
if (rc) {
pr_err("truncate failed!, err %d\n", rc);
return rc;
}
- if (size < alloc_blks * 512)
- i_size_write(inode, size);
}
- fp->allocation_size = le64_to_cpu(file_alloc_info->AllocationSize);
+ fp->allocation_size = target;
fp->allocation_size_set = true;
return 0;
}
@@ -8278,6 +8871,8 @@ static int set_end_of_file_info(struct ksmbd_work *work, struct ksmbd_file *fp,
newsize = le64_to_cpu(file_eof_info->EndOfFile);
inode = file_inode(fp->filp);
+ if (ksmbd_stream_fd(fp))
+ return ksmbd_vfs_stream_truncate(fp, newsize);
/*
* If FILE_END_OF_FILE_INFORMATION of set_info_file is called
@@ -8286,8 +8881,7 @@ static int set_end_of_file_info(struct ksmbd_work *work, struct ksmbd_file *fp,
* truncate of some filesystem like FAT32 fill zero data in
* truncated range.
*/
- if (inode->i_sb->s_magic != MSDOS_SUPER_MAGIC &&
- ksmbd_stream_fd(fp) == false) {
+ if (inode->i_sb->s_magic != MSDOS_SUPER_MAGIC) {
ksmbd_debug(SMB, "truncated to newsize %lld\n", newsize);
rc = ksmbd_vfs_truncate(work, fp, newsize);
if (rc) {
@@ -8324,6 +8918,7 @@ static int set_file_disposition_info(struct ksmbd_work *work,
struct smb2_file_disposition_info *file_info)
{
struct inode *inode;
+ int err;
if (!(fp->daccess & FILE_DELETE_LE)) {
pr_err("no right to delete : 0x%x\n", fp->daccess);
@@ -8338,9 +8933,13 @@ static int set_file_disposition_info(struct ksmbd_work *work,
if (ksmbd_has_stream_without_delete_share(fp))
return -ESHARE;
- if (S_ISDIR(inode->i_mode) && !ksmbd_stream_fd(fp) &&
- ksmbd_vfs_empty_dir(fp) == -ENOTEMPTY)
- return -EBUSY;
+ if (S_ISDIR(inode->i_mode) && !ksmbd_stream_fd(fp)) {
+ mutex_lock(&fp->readdir_lock);
+ err = ksmbd_vfs_empty_dir(fp);
+ mutex_unlock(&fp->readdir_lock);
+ if (err == -ENOTEMPTY)
+ return -EBUSY;
+ }
smb_break_all_levII_oplock_for_delete(work, fp);
ksmbd_fd_set_delete_pending(fp);
} else {
@@ -8518,7 +9117,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);
}
/**
@@ -8663,6 +9264,16 @@ static noinline int smb2_read_pipe(struct ksmbd_work *work)
id = req->VolatileFileId;
+ lockdep_assert_not_held(&work->sess->rpc_lock);
+ down_read(&work->sess->rpc_lock);
+ if (!ksmbd_session_rpc_method(work->sess, id)) {
+ up_read(&work->sess->rpc_lock);
+ rsp->hdr.Status = STATUS_FILE_CLOSED;
+ smb2_set_err_rsp(work);
+ return -ENOENT;
+ }
+ up_read(&work->sess->rpc_lock);
+
rpc_resp = ksmbd_rpc_read(work->sess, id);
if (rpc_resp) {
void *aux_payload_buf;
@@ -9276,23 +9887,35 @@ static noinline int smb2_write_pipe(struct ksmbd_work *work)
int err = 0, ret = 0;
char *data_buf;
size_t length;
+ unsigned int data_offset, req_len;
WORK_BUFFERS(work, req, rsp);
length = le32_to_cpu(req->Length);
id = req->VolatileFileId;
+ data_offset = le16_to_cpu(req->DataOffset);
+ req_len = smb2_current_req_len(work, &req->hdr);
- if ((u64)le16_to_cpu(req->DataOffset) + length >
- get_rfc1002_len(work->request_buf)) {
- pr_err("invalid write data offset %u, smb_len %u\n",
- le16_to_cpu(req->DataOffset),
- get_rfc1002_len(work->request_buf));
+ if (data_offset < offsetof(struct smb2_write_req, Buffer) ||
+ data_offset > req_len || length > req_len - data_offset) {
+ pr_err("invalid write data offset %u, length %zu, req_len %u\n",
+ data_offset, length, req_len);
err = -EINVAL;
goto out;
}
+ lockdep_assert_not_held(&work->sess->rpc_lock);
+ down_read(&work->sess->rpc_lock);
+ if (!ksmbd_session_rpc_method(work->sess, id)) {
+ up_read(&work->sess->rpc_lock);
+ rsp->hdr.Status = STATUS_FILE_CLOSED;
+ smb2_set_err_rsp(work);
+ return -ENOENT;
+ }
+ up_read(&work->sess->rpc_lock);
+
data_buf = (char *)(((char *)&req->hdr.ProtocolId) +
- le16_to_cpu(req->DataOffset));
+ data_offset);
rpc_resp = ksmbd_rpc_write(work->sess, id, data_buf, length);
if (rpc_resp) {
@@ -9419,6 +10042,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;
@@ -9450,7 +10074,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;
@@ -9553,11 +10178,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);
@@ -9579,14 +10208,21 @@ int smb2_write(struct ksmbd_work *work)
writethrough = true;
if (is_rdma_channel == false) {
- if (le16_to_cpu(req->DataOffset) <
- offsetof(struct smb2_write_req, Buffer)) {
+ unsigned int data_offset = le16_to_cpu(req->DataOffset);
+ unsigned int req_len = smb2_current_req_len(work, &req->hdr);
+
+ if (data_offset < offsetof(struct smb2_write_req, Buffer) ||
+ data_offset > req_len ||
+ length > req_len - data_offset) {
+ ksmbd_debug(SMB,
+ "invalid write data offset %u, length %zu, req_len %u\n",
+ data_offset, length, req_len);
err = -EINVAL;
goto out;
}
data_buf = (char *)(((char *)&req->hdr.ProtocolId) +
- le16_to_cpu(req->DataOffset));
+ data_offset);
ksmbd_debug(SMB, "filename %pD, offset %lld, len %zu\n",
fp->filp, offset, length);
@@ -9708,7 +10344,6 @@ int smb2_cancel(struct ksmbd_work *work)
struct smb2_hdr *hdr = smb_get_msg(work->request_buf);
struct smb2_hdr *chdr;
struct ksmbd_work *iter;
- struct ksmbd_work *cancelled_notify = NULL;
struct list_head *command_list;
if (work->next_smb2_rcv_hdr_off)
@@ -9746,23 +10381,11 @@ int smb2_cancel(struct ksmbd_work *work)
"smb2 with AsyncId %llu cancelled command = 0x%x\n",
le64_to_cpu(hdr->Id.AsyncId),
le16_to_cpu(chdr->Command));
- if (iter->cancel_fn == smb2_notify_cancel_fn)
- cancelled_notify =
- smb2_notify_cancel_claim(iter->cancel_argv);
- else if (iter->cancel_fn)
+ if (iter->cancel_fn)
iter->cancel_fn(iter->cancel_argv);
break;
}
spin_unlock(&conn->request_lock);
-
- /*
- * Complete a cancelled notify before this CANCEL handler returns.
- * Deferring it to the system workqueue lets a following request and
- * its response overtake STATUS_CANCELLED, leaving clients waiting
- * for the original notify even though the cancellation was accepted.
- */
- if (cancelled_notify)
- smb2_complete_notify_cancel(cancelled_notify);
} else {
command_list = &conn->requests;
@@ -9855,8 +10478,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;
@@ -9867,11 +10492,12 @@ 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;
- INIT_LIST_HEAD(&lock->clist);
+ lock->virtual_lock = virtual_lock;
+ INIT_HLIST_NODE(&lock->hlist);
INIT_LIST_HEAD(&lock->flist);
INIT_LIST_HEAD(&lock->llist);
list_add_tail(&lock->llist, lock_list);
@@ -9879,31 +10505,119 @@ 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_lock(struct file_lock *flock)
+static void smb2_free_ksmbd_lock(struct ksmbd_lock *smb_lock)
{
- ksmbd_vfs_posix_lock_unblock(flock);
- locks_free_lock(flock);
+ 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_blocked_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_wake_up(flock);
- locks_free_lock(flock);
}
-static inline bool lock_defer_pending(struct file_lock *fl)
+static void smb2_free_blocked_lock(struct file_lock *flock)
{
- /* check pending lock waiters */
- return waitqueue_active(&fl->c.flc_wait);
+ ksmbd_vfs_posix_lock_unblock(flock);
+ locks_wake_up(flock);
+ locks_free_lock(flock);
}
/**
@@ -9924,15 +10638,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;
- struct ksmbd_lock *smb_lock = NULL, *cmp_lock, *tmp, *tmp2;
- struct ksmbd_conn *conn;
- int nolock = 0;
+ 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, bkt;
+ 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);
@@ -9995,23 +10708,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");
@@ -10023,6 +10742,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;
@@ -10059,83 +10779,30 @@ 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;
- 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;
}
@@ -10143,7 +10810,21 @@ no_check_cl:
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;
}
@@ -10170,6 +10851,11 @@ skip:
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;
@@ -10182,11 +10868,17 @@ skip:
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);
@@ -10199,16 +10891,19 @@ skip:
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;
}
@@ -10217,20 +10912,25 @@ skip:
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;
}
}
@@ -10249,18 +10949,7 @@ lock_success:
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);
@@ -10278,24 +10967,27 @@ out:
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);
@@ -10466,6 +11158,27 @@ static __be32 idev_ipv4_address(struct in_device *idev)
return addr;
}
+static struct network_interface_info_ioctl_rsp *
+ksmbd_iface_entry_init(struct smb2_ioctl_rsp *rsp, int nbytes,
+ struct net_device *netdev, unsigned long long speed)
+{
+ struct network_interface_info_ioctl_rsp *nii_rsp;
+
+ nii_rsp = (struct network_interface_info_ioctl_rsp *)&rsp->Buffer[nbytes];
+ nii_rsp->IfIndex = cpu_to_le32(netdev->ifindex);
+ nii_rsp->Capability = 0;
+ if (netdev->real_num_tx_queues > 1)
+ nii_rsp->Capability |= RSS_CAPABLE;
+ if (ksmbd_rdma_capable_netdev(netdev))
+ nii_rsp->Capability |= RDMA_CAPABLE;
+ nii_rsp->Next = cpu_to_le32(152);
+ nii_rsp->Reserved = 0;
+ nii_rsp->LinkSpeed = cpu_to_le64(speed);
+ memset(nii_rsp->SockAddr_Storage, 0, 128);
+
+ return nii_rsp;
+}
+
static int fsctl_query_iface_info_ioctl(struct ksmbd_conn *conn,
struct smb2_ioctl_rsp *rsp,
unsigned int out_buf_len)
@@ -10476,10 +11189,16 @@ static int fsctl_query_iface_info_ioctl(struct ksmbd_conn *conn,
struct sockaddr_storage_rsp *sockaddr_storage;
unsigned int flags;
unsigned long long speed;
+ struct ethtool_link_ksettings cmd;
rtnl_lock();
for_each_netdev(&init_net, netdev) {
- bool ipv4_set = false;
+ struct inet6_ifaddr *ifa;
+ struct inet6_dev *idev6;
+ struct in_device *idev;
+ struct in6_addr ip6 = { };
+ bool have_ip6 = false;
+ __be32 ip4 = 0;
if (netdev->type == ARPHRD_LOOPBACK)
continue;
@@ -10490,87 +11209,80 @@ static int fsctl_query_iface_info_ioctl(struct ksmbd_conn *conn,
flags = netif_get_flags(netdev);
if (!(flags & IFF_RUNNING))
continue;
-ipv6_retry:
- if (out_buf_len <
- nbytes + sizeof(struct network_interface_info_ioctl_rsp)) {
- rtnl_unlock();
- return -ENOSPC;
- }
- nii_rsp = (struct network_interface_info_ioctl_rsp *)
- &rsp->Buffer[nbytes];
- nii_rsp->IfIndex = cpu_to_le32(netdev->ifindex);
-
- nii_rsp->Capability = 0;
- if (netdev->real_num_tx_queues > 1)
- nii_rsp->Capability |= RSS_CAPABLE;
- if (ksmbd_rdma_capable_netdev(netdev))
- nii_rsp->Capability |= RDMA_CAPABLE;
-
- nii_rsp->Next = cpu_to_le32(152);
- nii_rsp->Reserved = 0;
-
- if (netdev->ethtool_ops->get_link_ksettings) {
- struct ethtool_link_ksettings cmd;
-
- netdev->ethtool_ops->get_link_ksettings(netdev, &cmd);
+ if (!__ethtool_get_link_ksettings(netdev, &cmd) &&
+ cmd.base.speed && cmd.base.speed != SPEED_UNKNOWN) {
speed = cmd.base.speed;
} else {
ksmbd_debug(SMB, "%s %s\n", netdev->name,
"speed is unknown, defaulting to 1Gb/sec");
speed = SPEED_1000;
}
-
speed *= 1000000;
- nii_rsp->LinkSpeed = cpu_to_le64(speed);
- sockaddr_storage = (struct sockaddr_storage_rsp *)
- nii_rsp->SockAddr_Storage;
- memset(sockaddr_storage, 0, 128);
+ /*
+ * Query IPv4 and IPv6 independently; emit an entry only when a
+ * usable address exists, so an interface missing one family is
+ * still reported for the other and 0.0.0.0 / :: placeholders are
+ * never advertised.
+ */
+ idev = __in_dev_get_rtnl(netdev);
+ if (idev)
+ ip4 = idev_ipv4_address(idev);
+
+ idev6 = __in6_dev_get(netdev);
+ if (idev6) {
+ rcu_read_lock();
+ list_for_each_entry_rcu(ifa, &idev6->addr_list, if_list) {
+ if (ifa->flags & (IFA_F_TENTATIVE | IFA_F_DEPRECATED))
+ continue;
+ memcpy(&ip6, ifa->addr.s6_addr, sizeof(ip6));
+ have_ip6 = true;
+ break;
+ }
+ rcu_read_unlock();
+ }
- if (!ipv4_set) {
- struct in_device *idev;
+ if (ip4) {
+ if (out_buf_len <
+ nbytes + sizeof(struct network_interface_info_ioctl_rsp)) {
+ rtnl_unlock();
+ return -ENOSPC;
+ }
+ nii_rsp = ksmbd_iface_entry_init(rsp, nbytes, netdev, speed);
+ sockaddr_storage = (struct sockaddr_storage_rsp *)nii_rsp->SockAddr_Storage;
sockaddr_storage->Family = INTERNETWORK;
sockaddr_storage->addr4.Port = 0;
-
- idev = __in_dev_get_rtnl(netdev);
- if (!idev)
- continue;
- sockaddr_storage->addr4.IPv4Address =
- idev_ipv4_address(idev);
+ sockaddr_storage->addr4.IPv4Address = ip4;
nbytes += sizeof(struct network_interface_info_ioctl_rsp);
- ipv4_set = true;
- goto ipv6_retry;
- } else {
- struct inet6_dev *idev6;
- struct inet6_ifaddr *ifa;
- __u8 *ipv6_addr = sockaddr_storage->addr6.IPv6Address;
+ }
+ if (have_ip6) {
+ if (out_buf_len <
+ nbytes + sizeof(struct network_interface_info_ioctl_rsp)) {
+ rtnl_unlock();
+ return -ENOSPC;
+ }
+
+ nii_rsp = ksmbd_iface_entry_init(rsp, nbytes, netdev, speed);
+ sockaddr_storage = (struct sockaddr_storage_rsp *)nii_rsp->SockAddr_Storage;
sockaddr_storage->Family = INTERNETWORKV6;
sockaddr_storage->addr6.Port = 0;
sockaddr_storage->addr6.FlowInfo = 0;
-
- idev6 = __in6_dev_get(netdev);
- if (!idev6)
- continue;
-
- list_for_each_entry(ifa, &idev6->addr_list, if_list) {
- if (ifa->flags & (IFA_F_TENTATIVE |
- IFA_F_DEPRECATED))
- continue;
- memcpy(ipv6_addr, ifa->addr.s6_addr, 16);
- break;
- }
+ memcpy(sockaddr_storage->addr6.IPv6Address, ip6.s6_addr, 16);
sockaddr_storage->addr6.ScopeId = 0;
nbytes += sizeof(struct network_interface_info_ioctl_rsp);
}
}
rtnl_unlock();
- /* zero if this is last one */
- if (nii_rsp)
+ /* Clear Next of the last committed entry to terminate the list. */
+ if (nbytes > 0) {
+ nii_rsp = (struct network_interface_info_ioctl_rsp *)
+ &rsp->Buffer[nbytes - sizeof(*nii_rsp)];
nii_rsp->Next = 0;
+ }
rsp->PersistentFileId = SMB2_NO_FID;
rsp->VolatileFileId = SMB2_NO_FID;
@@ -10857,9 +11569,28 @@ int smb2_ioctl(struct ksmbd_work *work)
case FSCTL_QUERY_NETWORK_INTERFACE_INFO:
case FSCTL_VALIDATE_NEGOTIATE_INFO:
case FSCTL_PIPE_WAIT:
- case FSCTL_PIPE_TRANSCEIVE:
no_fileid_ioctl = true;
break;
+ case FSCTL_PIPE_TRANSCEIVE:
+ if (!test_share_config_flag(work->tcon->share_conf,
+ KSMBD_SHARE_FLAG_PIPE)) {
+ ret = -EOPNOTSUPP;
+ goto out;
+ }
+
+ /* RPC pipe handles are not in the regular file table. */
+ if (has_file_id(id) && !pid) {
+ down_read(&work->sess->rpc_lock);
+ no_fileid_ioctl =
+ ksmbd_session_rpc_method(work->sess, id) != 0;
+ up_read(&work->sess->rpc_lock);
+ }
+ if (!no_fileid_ioctl) {
+ ret = -EBADF;
+ rsp->hdr.Status = STATUS_FILE_CLOSED;
+ goto out2;
+ }
+ break;
default:
break;
}
@@ -11018,6 +11749,8 @@ int smb2_ioctl(struct ksmbd_work *work)
break;
}
case FSCTL_PIPE_TRANSCEIVE:
+ rsp->PersistentFileId = pid;
+ rsp->VolatileFileId = id;
out_buf_len = min_t(u32, KSMBD_IPC_MAX_PAYLOAD, out_buf_len);
nbytes = fsctl_pipe_transceive(work, id, out_buf_len, req, rsp);
break;
@@ -11444,7 +12177,7 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work)
__le32 status = STATUS_SUCCESS;
int ret;
u64 volatile_id, persistent_id;
- char req_oplevel = 0, rsp_oplevel = 0;
+ u8 req_oplevel = 0, rsp_oplevel = 0;
WORK_BUFFERS(work, req, rsp);
@@ -11478,6 +12211,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);
@@ -11489,6 +12224,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)
@@ -11545,6 +12281,10 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work)
rsp_oplevel = req_oplevel;
opinfo->level = rsp_oplevel;
+ opinfo->op_state = OPLOCK_STATE_NONE;
+ opinfo->oplock_timeout_set = false;
+ spin_unlock(&opinfo->state_lock);
+ wake_up_all(&opinfo->oplock_q);
rsp->StructureSize = cpu_to_le16(24);
rsp->OplockLevel = rsp_oplevel;
@@ -11556,18 +12296,16 @@ 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;
+ 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:
opinfo_put(opinfo);
ksmbd_fd_put(work, fp);
@@ -11600,10 +12338,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;
- int ret = 0;
+ struct oplock_info *opinfo, *open;
+ int ret;
__le32 lease_state;
struct lease *lease;
+ u8 level;
+ bool wake;
WORK_BUFFERS(work, req, rsp);
@@ -11612,26 +12352,28 @@ 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(&lease->lock);
+ 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)) {
@@ -11639,40 +12381,43 @@ 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;
+ 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);
- atomic_dec_if_positive(&opinfo->breaking_cnt);
- wake_up_interruptible_all(&opinfo->oplock_brk);
- opinfo_put(opinfo);
- return;
+ 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;
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)
@@ -11680,10 +12425,12 @@ replay_rsp:
opinfo_put(opinfo);
return;
+err_unlock:
+ spin_unlock(&opinfo->state_lock);
+ spin_unlock(&lease->lock);
err_out:
smb2_set_err_rsp(work);
opinfo_put(opinfo);
- return;
}
/**
@@ -11719,137 +12466,26 @@ int smb2_oplock_break(struct ksmbd_work *work)
return 0;
}
-/*
- * Cancel handler for a deferred CHANGE_NOTIFY. Races against
- * __ksmbd_close_fd()'s notify_pendings drain (vfs_cache.c), which can run
- * concurrently on a different connection closing the same handle -- only
- * one of the two may claim and free in_work, so both sides check
- * list_empty() under fp->f_lock before touching it (list_del_init()
- * leaves a node empty, so whichever side removes it first is the owner;
- * the loser must not touch in_work again, since the winner may already be
- * freeing it).
- *
- * smb2_cancel() holds conn->request_lock (a spinlock) for the entire
- * time it walks conn->async_requests and calls this function -- so this
- * runs with preemption disabled and must not sleep or re-acquire that
- * same lock. release_async_work() does both (it takes conn->request_lock
- * itself, and frees things that can involve sleeping paths), so calling
- * it from here would self-deadlock the very thread processing the
- * client's CANCEL command. ksmbd_conn_write() can also sleep (it takes
- * conn's write mutex). So: do only the non-sleeping, no-relock cleanup
- * inline here. smb2_cancel() sends and frees the claimed notify after it
- * drops request_lock, preserving response order for a client CANCEL. The
- * connection teardown caller has no such post-unlock path, so its wrapper
- * defers the send and free to a workqueue.
- */
-struct notify_cancel_ctx {
- struct work_struct work;
- struct ksmbd_work *in_work;
+struct ksmbd_notify_req {
+ wait_queue_head_t wait;
};
-static void smb2_send_notify_cancelled(struct ksmbd_work *work)
-{
- struct smb2_hdr *hdr = smb_get_msg(work->response_buf);
- struct ksmbd_conn *conn = work->conn;
- struct ksmbd_session *sess;
-
- sess = ksmbd_session_lookup(conn, le64_to_cpu(hdr->SessionId));
- if (sess) {
- work->sess = sess;
- if (work->encrypted && sess->enc && conn->ops->encrypt_resp) {
- conn->ops->encrypt_resp(work);
- } else if (conn->ops->is_sign_req && conn->ops->set_sign_rsp &&
- conn->ops->is_sign_req(work,
- conn->ops->get_cmd_val(work))) {
- conn->ops->set_sign_rsp(work);
- }
- }
-
- ksmbd_conn_write(work);
- if (sess) {
- ksmbd_user_session_put(sess);
- work->sess = NULL;
- }
-}
-
-static void smb2_notify_cancel_deferred(struct work_struct *w)
-{
- struct notify_cancel_ctx *ctx =
- container_of(w, struct notify_cancel_ctx, work);
- struct ksmbd_conn *conn = ctx->in_work->conn;
-
- smb2_complete_notify_cancel(ctx->in_work);
- kfree(ctx);
- /*
- * The connection teardown waits for r_count before destroying
- * connection sessions and their proc entries.
- */
- ksmbd_conn_r_count_dec(conn);
-}
-
-static struct ksmbd_work *smb2_notify_cancel_claim(void **argv)
-{
- struct ksmbd_work *in_work = (struct ksmbd_work *)argv[0];
- struct ksmbd_file *fp = (struct ksmbd_file *)argv[1];
- bool claimed;
-
- spin_lock(&fp->f_lock);
- claimed = !list_empty(&in_work->notify_entry);
- if (claimed)
- list_del_init(&in_work->notify_entry);
- spin_unlock(&fp->f_lock);
-
- if (!claimed)
- return NULL;
-
- /* conn->request_lock is held by smb2_cancel() or connection teardown. */
- in_work->cancel_fn = NULL;
- kfree(in_work->cancel_argv);
- in_work->cancel_argv = NULL;
- return in_work;
-}
-
-static void smb2_complete_notify_cancel(struct ksmbd_work *in_work)
-{
- struct smb2_hdr *in_hdr = smb_get_msg(in_work->response_buf);
-
- in_hdr->Status = STATUS_CANCELLED;
- smb2_send_notify_cancelled(in_work);
- release_async_work(in_work);
- ksmbd_free_work_struct(in_work);
-}
-
-static void smb2_notify_cancel_fn(void **argv)
+/*
+ * Cancel handler for a pending CHANGE_NOTIFY. Called either by
+ * smb2_cancel() (conn->request_lock held, work->state already set to
+ * KSMBD_WORK_CANCELLED by the caller) or by
+ * set_close_state_blocked_works() (vfs_cache.c, fp->f_lock held,
+ * work->state already set to KSMBD_WORK_CLOSED by the caller) -- both
+ * callers hold a spinlock across this call, so it must not sleep.
+ * wake_up() only wakes the waiter in smb2_notify(); it does not touch
+ * fp->blocked_works itself, matching smb2_remove_blocked_lock()'s same
+ * non-mutating style for the equivalent byte-range-lock wait.
+ */
+static void smb2_notify_cancel(void **argv)
{
- struct ksmbd_work *in_work = smb2_notify_cancel_claim(argv);
- struct ksmbd_conn *conn;
- struct notify_cancel_ctx *ctx;
-
- if (!in_work)
- return;
- conn = in_work->conn;
+ struct ksmbd_notify_req *notify_req = argv[0];
- ctx = kmalloc_obj(*ctx, GFP_ATOMIC);
- if (!ctx) {
- /* Can't defer the response -- free without sending one. */
- list_del_init(&in_work->async_request_entry);
- in_work->asynchronous = false;
- if (in_work->async_id) {
- ksmbd_release_id(&conn->async_ida, in_work->async_id);
- in_work->async_id = 0;
- }
- ksmbd_free_work_struct(in_work);
- return;
- }
- ctx->in_work = in_work;
- INIT_WORK(&ctx->work, smb2_notify_cancel_deferred);
- /*
- * This deferred work can outlive the connection handler's receive loop.
- * Keep teardown from destroying the connection's sessions until the
- * deferred response has finished using them.
- */
- ksmbd_conn_r_count_inc(conn);
- schedule_work(&ctx->work);
+ wake_up(&notify_req->wait);
}
/**
@@ -11862,9 +12498,11 @@ int smb2_notify(struct ksmbd_work *work)
{
struct smb2_change_notify_req *req;
struct smb2_change_notify_rsp *rsp;
- struct ksmbd_work *in_work;
- struct smb2_hdr *in_hdr;
- struct ksmbd_file *fp;
+ struct ksmbd_notify_req notify_req;
+ struct ksmbd_file *fp = NULL;
+ void **argv = NULL;
+ bool async_work = false;
+ int err = 0;
ksmbd_debug(SMB, "Received smb2 notify\n");
@@ -11875,164 +12513,83 @@ int smb2_notify(struct ksmbd_work *work)
if (work->next_smb2_rcv_hdr_off && req->hdr.NextCommand) {
rsp->hdr.Status = STATUS_INTERNAL_ERROR;
- smb2_set_err_rsp(work);
- return -EIO;
- }
-
- /*
- * macOS backupd sends CHANGE_NOTIFY with FileId=FFFF...FFFF (share-root
- * sentinel) to watch for changes on the share root without holding an
- * open handle. Respond STATUS_PENDING + STATUS_NOTIFY_CLEANUP immediately;
- * without this, backupd aborts Time Machine setup on STATUS_FILE_CLOSED.
- */
- if (req->VolatileFileId == SMB2_NO_FID &&
- req->PersistentFileId == SMB2_NO_FID) {
- in_work = ksmbd_alloc_work_struct();
- if (!in_work || allocate_interim_rsp_buf(in_work)) {
- if (in_work)
- ksmbd_free_work_struct(in_work);
- rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES;
- smb2_set_err_rsp(work);
- return 0;
- }
- if (setup_async_work(work, NULL, NULL)) {
- ksmbd_free_work_struct(in_work);
- rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES;
- smb2_set_err_rsp(work);
- return 0;
- }
- smb2_send_interim_resp(work, STATUS_PENDING);
- in_work->conn = work->conn;
- in_hdr = smb_get_msg(in_work->response_buf);
- memcpy(in_hdr, ksmbd_resp_buf_next(work),
- __SMB2_HEADER_STRUCTURE_SIZE);
- in_hdr->Flags |= SMB2_FLAGS_ASYNC_COMMAND;
- in_hdr->Id.AsyncId = cpu_to_le64(work->async_id);
- smb2_set_err_rsp(in_work);
- in_hdr->Status = STATUS_NOTIFY_CLEANUP;
- in_work->async_id = work->async_id;
- work->async_id = 0;
- release_async_work(work);
- if (smb2_send_interim_work(in_work, work, false))
- ksmbd_debug(SMB, "failed to send notify cleanup\n");
- ksmbd_free_work_struct(in_work);
- work->send_no_response = 1;
- return 0;
+ err = -EIO;
+ goto out;
}
- /*
- * KSMBD does not implement a real change-notification backend.
- * Genuine SMB2 servers (and macOS smbfs) never complete a
- * CHANGE_NOTIFY spontaneously: it is satisfied only by a real
- * directory change, or with STATUS_NOTIFY_CLEANUP when the watched
- * handle is closed. Completing it early (e.g. on a timer) makes
- * Finder treat the cleanup as "directory changed" and re-enumerate
- * the directory forever, leaving items unopenable. Returning
- * STATUS_NOT_IMPLEMENTED here (like stock ksmbd) makes macOS smbfs
- * hard-freeze on unmount, so this must stay deferred.
- */
fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId);
if (!fp) {
rsp->hdr.Status = STATUS_FILE_CLOSED;
- smb2_set_err_rsp(work);
- return 0;
+ err = -ENOENT;
+ goto out;
}
- in_work = ksmbd_alloc_work_struct();
- if (!in_work || allocate_interim_rsp_buf(in_work)) {
- if (in_work)
- ksmbd_free_work_struct(in_work);
- ksmbd_fd_put(work, fp);
+ argv = kmalloc_obj(*argv, KSMBD_DEFAULT_GFP);
+ if (!argv) {
rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES;
- smb2_set_err_rsp(work);
- return 0;
- }
- /*
- * in_work is synthetic (not from the normal request-receiving
- * pipeline), so it has no request_buf of its own. It gets registered
- * into conn->async_requests below, and smb2_cancel() unconditionally
- * computes smb_get_msg(iter->request_buf) for every entry in that
- * list while searching for a match -- give it its own small buffer
- * (not an alias of response_buf: ksmbd_free_work_struct() kvfree()s
- * both separately, so aliasing them would double-free) so that stays
- * a harmless read instead of a near-NULL dereference.
- */
- in_work->request_buf = kzalloc(MAX_CIFS_SMALL_BUFFER_SIZE, KSMBD_DEFAULT_GFP);
- if (!in_work->request_buf) {
- ksmbd_free_work_struct(in_work);
- ksmbd_fd_put(work, fp);
- rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES;
- smb2_set_err_rsp(work);
- return 0;
+ err = -ENOMEM;
+ goto out;
}
- memcpy(smb_get_msg(in_work->request_buf), req,
- __SMB2_HEADER_STRUCTURE_SIZE);
+ init_waitqueue_head(&notify_req.wait);
+ argv[0] = &notify_req;
- if (setup_async_work(work, NULL, NULL)) {
- ksmbd_free_work_struct(in_work);
- ksmbd_fd_put(work, fp);
+ err = setup_async_work(work, smb2_notify_cancel, argv);
+ if (err) {
rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES;
- smb2_set_err_rsp(work);
- return 0;
+ goto out;
}
-
- smb2_send_interim_resp(work, STATUS_PENDING);
-
- /* Keep the async IDA alive until the deferred work is released. */
- in_work->conn = ksmbd_conn_get(work->conn);
- in_work->owns_conn_ref = true;
- in_work->encrypted = work->encrypted;
- in_hdr = smb_get_msg(in_work->response_buf);
- memcpy(in_hdr, ksmbd_resp_buf_next(work), __SMB2_HEADER_STRUCTURE_SIZE);
- in_hdr->Flags |= SMB2_FLAGS_ASYNC_COMMAND;
- in_hdr->Id.AsyncId = cpu_to_le64(work->async_id);
- smb2_set_err_rsp(in_work);
- in_hdr->Status = STATUS_NOTIFY_CLEANUP;
+ async_work = true;
/*
- * Transfer ownership of the async id to in_work; it stays reserved
- * until in_work is freed after the deferred response is sent on
- * close, so it can't be reused for an unrelated async response.
+ * Handle close holds the file-table write lock while it marks the
+ * handle closed and walks blocked_works. Hold the matching read lock
+ * across the state check and registration so close cannot finish its
+ * walk between the lookup above and this list insertion.
*/
- in_work->async_id = work->async_id;
- work->async_id = 0;
- release_async_work(work);
+ read_lock(&work->sess->file_table.lock);
+ if (fp->f_state != FP_INITED) {
+ read_unlock(&work->sess->file_table.lock);
+ rsp->hdr.Status = STATUS_NOTIFY_CLEANUP;
+ err = -ENOENT;
+ goto out;
+ }
+ spin_lock(&fp->f_lock);
+ list_add_tail(&work->fp_entry, &fp->blocked_works);
+ spin_unlock(&fp->f_lock);
+ read_unlock(&work->sess->file_table.lock);
- /*
- * work itself is about to be recycled by the normal request-processing
- * pipeline, so it can't stay the target of a future CANCEL -- register
- * in_work instead, reusing the same async_id, so a client-sent CANCEL
- * for this notify actually finds something to cancel instead of
- * silently doing nothing until the handle eventually closes.
- */
- in_work->asynchronous = true;
- in_work->cancel_argv = kmalloc_array(2, sizeof(void *), KSMBD_DEFAULT_GFP);
- if (in_work->cancel_argv) {
- in_work->cancel_argv[0] = in_work;
- in_work->cancel_argv[1] = fp;
- in_work->cancel_fn = smb2_notify_cancel_fn;
- }
-
- if (!ksmbd_conn_link_async_request(work->conn, in_work)) {
- kfree(in_work->cancel_argv);
- in_work->cancel_argv = NULL;
- in_work->cancel_fn = NULL;
- in_work->asynchronous = false;
- ksmbd_fd_put(work, fp);
- if (smb2_send_interim_work(in_work, work, false))
- ksmbd_debug(SMB, "failed to send notify cleanup\n");
- ksmbd_free_work_struct(in_work);
- work->send_no_response = 1;
- return 0;
+ smb2_send_interim_resp(work, STATUS_PENDING);
+
+ err = wait_event_interruptible(notify_req.wait,
+ READ_ONCE(work->state) != KSMBD_WORK_ACTIVE);
+ if (err && READ_ONCE(work->state) == KSMBD_WORK_ACTIVE) {
+ /*
+ * Woken by a signal, not a real cancel/close. There is no
+ * notification backend yet to report anything else against,
+ * so treat this the same as a client-side cancel.
+ */
+ WRITE_ONCE(work->state, KSMBD_WORK_CANCELLED);
}
spin_lock(&fp->f_lock);
- list_add_tail(&in_work->notify_entry, &fp->notify_pendings);
+ list_del_init(&work->fp_entry);
spin_unlock(&fp->f_lock);
- ksmbd_fd_put(work, fp);
+ rsp->hdr.Status = work->state == KSMBD_WORK_CLOSED ?
+ STATUS_NOTIFY_CLEANUP : STATUS_CANCELLED;
+ smb2_send_interim_resp(work, rsp->hdr.Status);
work->send_no_response = 1;
- return 0;
+
+out:
+ if (rsp->hdr.Status != STATUS_SUCCESS && !work->send_no_response)
+ smb2_set_err_rsp(work);
+ if (async_work)
+ release_async_work(work);
+ else
+ kfree(argv);
+ if (fp)
+ ksmbd_fd_put(work, fp);
+ return err;
}
/**
@@ -12227,14 +12784,15 @@ void smb3_set_sign_rsp(struct ksmbd_work *work)
struct channel *chann;
char signature[SMB2_CMACAES_SIZE];
struct kvec *iov;
- u16 command = conn->ops->get_cmd_val(work);
+ u16 command;
int n_vec;
char *signing_key;
hdr = ksmbd_resp_buf_curr(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);
@@ -12283,7 +12841,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);
@@ -12346,6 +12904,60 @@ bool smb3_is_transform_hdr(void *buf)
return trhdr->ProtocolId == SMB2_TRANSFORM_PROTO_NUM;
}
+/*
+ * The decryption key is selected by the transform SessionId, but
+ * the request is authorized under the session named in the
+ * decrypted SMB2 header. Per MS-SMB2 the two must match, so check
+ * the decrypted message and its compound chain before dispatch.
+ */
+int ksmbd_check_transform_session(struct ksmbd_work *work, u64 tr_sess_id)
+{
+ struct smb2_hdr *hdr = smb_get_msg(work->request_buf);
+ size_t msg_len = get_rfc1002_len(work->request_buf);
+ size_t off = 0;
+ bool first = true;
+
+ if (msg_len < sizeof(*hdr)) {
+ pr_err_ratelimited("Decrypted message is smaller than SMB2 header\n");
+ return -ECONNABORTED;
+ }
+
+ for (;;) {
+ u64 sid = le64_to_cpu(hdr->SessionId);
+ u32 next;
+
+ if (first) {
+ if (hdr->Flags & SMB2_FLAGS_RELATED_OPERATIONS) {
+ pr_err_ratelimited("RELATED_OPERATIONS set on first operation\n");
+ return -ECONNABORTED;
+ }
+ if (sid != tr_sess_id) {
+ pr_err_ratelimited("SessionId mismatch between transform and inner header\n");
+ return -ECONNABORTED;
+ }
+ first = false;
+ } else if (!(hdr->Flags & SMB2_FLAGS_RELATED_OPERATIONS) &&
+ sid != tr_sess_id) {
+ pr_err_ratelimited("SessionId mismatch in compound chain\n");
+ return -ECONNABORTED;
+ }
+
+ next = le32_to_cpu(hdr->NextCommand);
+ if (!next)
+ return 0;
+ if (next % 8) {
+ pr_err_ratelimited("NextCommand %u is not 8-byte aligned\n", next);
+ return -ECONNABORTED;
+ }
+ if (next > msg_len - off - sizeof(*hdr)) {
+ pr_err_ratelimited("NextCommand %u is out of the message\n", next);
+ return -ECONNABORTED;
+ }
+ off += next;
+ hdr = (struct smb2_hdr *)((u8 *)smb_get_msg(work->request_buf) + off);
+ }
+}
+
int smb3_decrypt_req(struct ksmbd_work *work)
{
char *buf = work->request_buf;
@@ -12362,6 +12974,12 @@ int smb3_decrypt_req(struct ksmbd_work *work)
return -ECONNABORTED;
}
+ if (tr_hdr->Flags != cpu_to_le16(TRANSFORM_FLAG_ENCRYPTED)) {
+ pr_err_ratelimited("Invalid transform flags 0x%04x\n",
+ le16_to_cpu(tr_hdr->Flags));
+ return -ECONNABORTED;
+ }
+
buf_data_size = pdu_length - sizeof(struct smb2_transform_hdr);
original_msg_size = le32_to_cpu(tr_hdr->OriginalMessageSize);
if (buf_data_size < sizeof(struct smb2_compression_hdr) ||
diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h
index ca8e27f7b712..8c6b170c2716 100644
--- a/fs/smb/server/smb2pdu.h
+++ b/fs/smb/server/smb2pdu.h
@@ -156,8 +156,7 @@ struct create_durable_rsp {
} __packed;
/*
- * See POSIX-SMB2 2.2.14.2.16
- * Link: https://gitlab.com/samba-team/smb3-posix-spec/-/blob/master/smb3_posix_extensions.md
+ * See POSIX-SMB2 2.1.3.2.1
*/
struct create_posix_rsp {
struct create_context_hdr ccontext;
@@ -169,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 {
@@ -199,7 +200,7 @@ struct file_sparse {
#define FILE_INTERNAL_INFORMATION_SIZE 8
#define FILE_EA_INFORMATION_SIZE 4
#define FILE_ACCESS_INFORMATION_SIZE 4
-#define FILE_NAME_INFORMATION_SIZE 9
+#define FILE_NAME_INFORMATION_SIZE 8
#define FILE_RENAME_INFORMATION_SIZE 10
#define FILE_LINK_INFORMATION_SIZE 11
#define FILE_NAMES_INFORMATION_SIZE 12
@@ -211,8 +212,6 @@ struct file_sparse {
#define FILE_ALL_INFORMATION_SIZE 104
#define FILE_ALLOCATION_INFORMATION_SIZE 19
#define FILE_END_OF_FILE_INFORMATION_SIZE 20
-#define FILE_ALTERNATE_NAME_INFORMATION_SIZE 8
-#define FILE_NORMALIZED_NAME_INFORMATION_SIZE 8
#define FILE_STREAM_INFORMATION_SIZE 32
#define FILE_PIPE_INFORMATION_SIZE 23
#define FILE_PIPE_LOCAL_INFORMATION_SIZE 24
@@ -259,7 +258,7 @@ struct smb2_file_alignment_info {
__le32 AlignmentRequirement;
} __packed;
-struct smb2_file_alt_name_info {
+struct smb2_file_name_info {
__le32 FileNameLength;
char FileName[];
} __packed;
@@ -351,6 +350,7 @@ struct create_sd_buf_req {
struct smb_ntsd ntsd;
} __packed;
+/* See POSIX-FSCC 2.2.1 */
struct smb2_posix_info {
__le32 NextEntryOffset;
__u32 Ignored;
@@ -364,20 +364,18 @@ struct smb2_posix_info {
__le64 Inode;
__le32 DeviceId;
__le32 Zero;
- /* beginning of POSIX Create Context Response */
+ /*
+ * Beginning of POSIX Create Context Response
+ * See POSIX-SMB2 2.1.3.2.1
+ */
__le32 HardLinks;
__le32 ReparseTag;
__le32 Mode;
/* SidBuffer contain two sids (UNIX user sid(16), UNIX group sid(16)) */
u8 SidBuffer[32];
+ /* End of POSIX Create Context Response */
__le32 name_len;
u8 name[];
- /*
- * var sized owner SID
- * var sized group SID
- * le32 filenamelength
- * u8 filename[]
- */
} __packed;
/* functions */
@@ -397,6 +395,7 @@ bool is_smb2_rsp(struct ksmbd_work *work);
u16 get_smb2_cmd_val(struct ksmbd_work *work);
void set_smb2_rsp_status(struct ksmbd_work *work, __le32 err);
int init_smb2_rsp_hdr(struct ksmbd_work *work);
+bool smb2_check_compound_fid(struct ksmbd_work *work);
int smb2_allocate_rsp_buf(struct ksmbd_work *work);
bool is_chained_smb2_message(struct ksmbd_work *work);
int init_smb2_neg_rsp(struct ksmbd_work *work);
@@ -420,6 +419,7 @@ struct channel *lookup_chann_list(struct ksmbd_session *sess,
void smb3_preauth_hash_rsp(struct ksmbd_work *work);
bool smb3_is_transform_hdr(void *buf);
int smb3_decrypt_req(struct ksmbd_work *work);
+int ksmbd_check_transform_session(struct ksmbd_work *work, u64 tr_sess_id);
int smb3_encrypt_resp(struct ksmbd_work *work);
bool smb3_11_final_sess_setup_resp(struct ksmbd_work *work);
int smb2_set_rsp_credits(struct ksmbd_work *work);
diff --git a/fs/smb/server/smb_common.c b/fs/smb/server/smb_common.c
index ec6ee56293ce..f9fb3741ada3 100644
--- a/fs/smb/server/smb_common.c
+++ b/fs/smb/server/smb_common.c
@@ -478,16 +478,17 @@ int ksmbd_populate_dot_dotdot_entries(struct ksmbd_work *work, int info_level,
if (i == 0) {
d_info->name = ".";
d_info->name_len = 1;
- dentry = dir->filp->f_path.dentry;
+ dentry = dget(dir->filp->f_path.dentry);
} else {
d_info->name = "..";
d_info->name_len = 2;
- dentry = dir->filp->f_path.dentry->d_parent;
+ dentry = dget_parent(dir->filp->f_path.dentry);
}
if (!match_pattern(d_info->name, d_info->name_len,
search_pattern)) {
dir->dot_dotdot[i] = 1;
+ dput(dentry);
continue;
}
@@ -496,6 +497,7 @@ int ksmbd_populate_dot_dotdot_entries(struct ksmbd_work *work, int info_level,
idmap,
dentry,
&ksmbd_kstat);
+ dput(dentry);
if (rc)
break;
@@ -523,8 +525,6 @@ int ksmbd_populate_dot_dotdot_entries(struct ksmbd_work *work, int info_level,
* @shortname: destination short filename
*
* Return: shortname length or 0 when source long name is '.' or '..'
- * TODO: Though this function conforms the restriction of 8.3 Filename spec,
- * but the result is different with Windows 7's one. need to check.
*/
int ksmbd_extract_shortname(struct ksmbd_conn *conn, const char *longname,
char *shortname)
@@ -588,7 +588,7 @@ int ksmbd_extract_shortname(struct ksmbd_conn *conn, const char *longname,
if (dot_present)
memcpy(out + baselen + 4, extension, 4);
else
- out[baselen + 4] = '\0';
+ out[baselen + 3] = '\0';
smbConvertToUTF16((__le16 *)shortname, out, PATH_MAX,
conn->local_nls, 0);
len = strlen(out) * 2;
diff --git a/fs/smb/server/smb_common.h b/fs/smb/server/smb_common.h
index 7b9c5cfcb63b..a9cc09e31721 100644
--- a/fs/smb/server/smb_common.h
+++ b/fs/smb/server/smb_common.h
@@ -133,6 +133,104 @@ struct file_id_both_directory_info {
char FileName[];
} __packed;
+/* MS-FSCC 2.4.23 FileIdExtdDirectoryInformation */
+struct file_id_extd_directory_info {
+ __le32 NextEntryOffset;
+ __u32 FileIndex;
+ __le64 CreationTime;
+ __le64 LastAccessTime;
+ __le64 LastWriteTime;
+ __le64 ChangeTime;
+ __le64 EndOfFile;
+ __le64 AllocationSize;
+ __le32 ExtFileAttributes;
+ __le32 FileNameLength;
+ __le32 EaSize;
+ __le32 ReparsePointTag;
+ __u8 FileId[16];
+ char FileName[];
+} __packed;
+
+/* MS-FSCC 2.4.19 FileId64ExtdDirectoryInformation */
+struct file_id_64_extd_directory_info {
+ __le32 NextEntryOffset;
+ __u32 FileIndex;
+ __le64 CreationTime;
+ __le64 LastAccessTime;
+ __le64 LastWriteTime;
+ __le64 ChangeTime;
+ __le64 EndOfFile;
+ __le64 AllocationSize;
+ __le32 ExtFileAttributes;
+ __le32 FileNameLength;
+ __le32 EaSize;
+ __le32 ReparsePointTag;
+ __le64 FileId;
+ char FileName[];
+} __packed;
+
+/* MS-FSCC 2.4.18 FileId64ExtdBothDirectoryInformation */
+struct file_id_64_extd_both_directory_info {
+ __le32 NextEntryOffset;
+ __u32 FileIndex;
+ __le64 CreationTime;
+ __le64 LastAccessTime;
+ __le64 LastWriteTime;
+ __le64 ChangeTime;
+ __le64 EndOfFile;
+ __le64 AllocationSize;
+ __le32 ExtFileAttributes;
+ __le32 FileNameLength;
+ __le32 EaSize;
+ __le32 ReparsePointTag;
+ __le64 FileId;
+ __u8 ShortNameLength;
+ __u8 Reserved1;
+ __u8 ShortName[24];
+ char FileName[];
+} __packed;
+
+/* MS-FSCC 2.4.21 FileIdAllExtdDirectoryInformation */
+struct file_id_all_extd_directory_info {
+ __le32 NextEntryOffset;
+ __u32 FileIndex;
+ __le64 CreationTime;
+ __le64 LastAccessTime;
+ __le64 LastWriteTime;
+ __le64 ChangeTime;
+ __le64 EndOfFile;
+ __le64 AllocationSize;
+ __le32 ExtFileAttributes;
+ __le32 FileNameLength;
+ __le32 EaSize;
+ __le32 ReparsePointTag;
+ __le64 FileId;
+ __u8 FileId128[16];
+ char FileName[];
+} __packed;
+
+/* MS-FSCC 2.4.20 FileIdAllExtdBothDirectoryInformation */
+struct file_id_all_extd_both_directory_info {
+ __le32 NextEntryOffset;
+ __u32 FileIndex;
+ __le64 CreationTime;
+ __le64 LastAccessTime;
+ __le64 LastWriteTime;
+ __le64 ChangeTime;
+ __le64 EndOfFile;
+ __le64 AllocationSize;
+ __le32 ExtFileAttributes;
+ __le32 FileNameLength;
+ __le32 EaSize;
+ __le32 ReparsePointTag;
+ __le64 FileId;
+ __u8 FileId128[16];
+ __u8 ShortNameLength;
+ __u8 Reserved1;
+ __u8 ShortName[24];
+ char FileName[];
+} __packed;
+
struct smb_version_ops {
u16 (*get_cmd_val)(struct ksmbd_work *swork);
void (*inc_reqs)(unsigned int cmd, __le32 status);
diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c
index 05f93a4d9d82..664f01263611 100644
--- a/fs/smb/server/smbacl.c
+++ b/fs/smb/server/smbacl.c
@@ -7,6 +7,7 @@
*/
#include <linux/fs.h>
+#include <kunit/visibility.h>
#include <linux/slab.h>
#include <linux/string.h>
#include <linux/mnt_idmapping.h>
@@ -476,10 +477,12 @@ static int parse_dacl(const 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,
@@ -545,6 +548,10 @@ static int parse_dacl(const 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. */
@@ -899,7 +906,7 @@ int parse_sec_desc(const struct mnt_idmap *idmap, struct smb_ntsd *pntsd,
int acl_len, struct smb_fattr *fattr)
{
int rc = 0;
- struct smb_sid *owner_sid_ptr, *group_sid_ptr;
+ struct smb_sid *owner_sid_ptr = NULL, *group_sid_ptr = NULL;
struct smb_acl *dacl_ptr; /* no need for SACL ptr */
char *end_of_acl = ((char *)pntsd) + acl_len;
__u32 dacloffset;
@@ -911,12 +918,7 @@ int parse_sec_desc(const struct mnt_idmap *idmap, struct smb_ntsd *pntsd,
if (acl_len < sizeof(struct smb_ntsd))
return -EINVAL;
- owner_sid_ptr = (struct smb_sid *)((char *)pntsd +
- le32_to_cpu(pntsd->osidoffset));
- group_sid_ptr = (struct smb_sid *)((char *)pntsd +
- le32_to_cpu(pntsd->gsidoffset));
dacloffset = le32_to_cpu(pntsd->dacloffset);
- dacl_ptr = (struct smb_acl *)((char *)pntsd + dacloffset);
ksmbd_debug(SMB,
"revision %d type 0x%x ooffset 0x%x goffset 0x%x sacloffset 0x%x dacloffset 0x%x\n",
pntsd->revision, pntsd->type, le32_to_cpu(pntsd->osidoffset),
@@ -932,9 +934,12 @@ int parse_sec_desc(const struct mnt_idmap *idmap, struct smb_ntsd *pntsd,
pntsd->type = cpu_to_le16(DACL_PRESENT);
if (pntsd->osidoffset) {
- if (le32_to_cpu(pntsd->osidoffset) < sizeof(struct smb_ntsd))
+ if (le32_to_cpu(pntsd->osidoffset) < sizeof(struct smb_ntsd) ||
+ le32_to_cpu(pntsd->osidoffset) >= acl_len)
return -EINVAL;
+ owner_sid_ptr = (struct smb_sid *)((char *)pntsd +
+ le32_to_cpu(pntsd->osidoffset));
rc = parse_sid(owner_sid_ptr, end_of_acl);
if (rc) {
pr_err("%s: Error %d parsing Owner SID\n", __func__, rc);
@@ -950,9 +955,12 @@ int parse_sec_desc(const struct mnt_idmap *idmap, struct smb_ntsd *pntsd,
}
if (pntsd->gsidoffset) {
- if (le32_to_cpu(pntsd->gsidoffset) < sizeof(struct smb_ntsd))
+ if (le32_to_cpu(pntsd->gsidoffset) < sizeof(struct smb_ntsd) ||
+ le32_to_cpu(pntsd->gsidoffset) >= acl_len)
return -EINVAL;
+ group_sid_ptr = (struct smb_sid *)((char *)pntsd +
+ le32_to_cpu(pntsd->gsidoffset));
rc = parse_sid(group_sid_ptr, end_of_acl);
if (rc) {
pr_err("%s: Error %d mapping Owner SID to gid\n",
@@ -974,9 +982,10 @@ int parse_sec_desc(const struct mnt_idmap *idmap, struct smb_ntsd *pntsd,
pntsd->type |= cpu_to_le16(DACL_PROTECTED);
if (dacloffset) {
- if (dacloffset < sizeof(struct smb_ntsd))
+ if (dacloffset < sizeof(struct smb_ntsd) || dacloffset >= acl_len)
return -EINVAL;
+ dacl_ptr = (struct smb_acl *)((char *)pntsd + dacloffset);
rc = parse_dacl(idmap, dacl_ptr, end_of_acl,
owner_sid_ptr, group_sid_ptr, fattr);
if (rc)
@@ -1096,9 +1105,12 @@ int build_sec_desc(const struct mnt_idmap *idmap,
struct smb_acl *ppdacl_ptr;
unsigned int dacl_offset = le32_to_cpu(ppntsd->dacloffset);
int ppdacl_size, ntacl_size = ppntsd_size - dacl_offset;
+ size_t dacl_struct_end;
if (!dacl_offset ||
- (dacl_offset + sizeof(struct smb_acl) > ppntsd_size))
+ check_add_overflow(dacl_offset, sizeof(struct smb_acl),
+ &dacl_struct_end) ||
+ dacl_struct_end > (size_t)ppntsd_size)
goto out;
ppdacl_ptr = (struct smb_acl *)((char *)ppntsd + dacl_offset);
@@ -1490,21 +1502,27 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path,
if (pdacl_size > acl_size || pdacl_size < sizeof(struct smb_acl))
goto err_out;
- if (!pdacl->num_aces) {
- if (!(pdacl_size - sizeof(struct smb_acl)) &&
- *pdaccess & ~(FILE_READ_CONTROL_LE | FILE_WRITE_DAC_LE)) {
- rc = -EACCES;
- goto err_out;
- }
- goto err_out;
- }
-
if (!uid)
sid_type = SIDUNIX_USER;
id_to_sid(uid, sid_type, &sid);
vfsuid = i_uid_into_vfsuid(idmap, d_inode(path->dentry));
is_owner = uid == from_kuid(&init_user_ns, vfsuid_into_kuid(vfsuid));
+ if (!pdacl->num_aces) {
+ /*
+ * An empty (present, zero-ACE) DACL grants no access to
+ * anyone except the object owner's implicit READ_CONTROL
+ * and WRITE_DAC (MS-DTYP 2.4.5). Deny every other caller,
+ * deny the owner any access beyond those two bits, and do
+ * not let trailing bytes after a zero-ACE DACL become an
+ * implicit grant.
+ */
+ if (!is_owner ||
+ (*pdaccess & ~(FILE_READ_CONTROL_LE | FILE_WRITE_DAC_LE)))
+ rc = -EACCES;
+ goto err_out;
+ }
+
if (*pdaccess & FILE_MAXIMAL_ACCESS_LE) {
ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl));
aces_size = pdacl_size - sizeof(struct smb_acl);
@@ -1665,10 +1683,12 @@ err_out:
kfree(pntsd);
return rc;
}
+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}};
@@ -1683,6 +1703,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)) {
@@ -1694,7 +1722,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 28d215807faa..3342cebab333 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,
diff --git a/fs/smb/server/tests/Kconfig b/fs/smb/server/tests/Kconfig
new file mode 100644
index 000000000000..ad7a4e94ceaa
--- /dev/null
+++ b/fs/smb/server/tests/Kconfig
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2026 Hang Nan <nanx95726@gmail.com>
+
+config SMB_SERVER_KUNIT_TESTS
+ tristate "KUnit tests for SMB3 server helpers" if !KUNIT_ALL_TESTS
+ depends on SMB_SERVER && SMB_KUNIT_TESTS && TMPFS_XATTR
+ default SMB_KUNIT_TESTS
+ help
+ This builds the KUnit tests for ksmbd server helpers. The tests
+ exercise internal server functionality and help detect regressions
+ in server-side behavior. They are intended for kernel developers
+ and are not suitable for production systems.
+
+ For more information on KUnit and unit tests in the kernel,
+ please read Documentation/dev-tools/kunit/index.rst.
diff --git a/fs/smb/server/tests/Makefile b/fs/smb/server/tests/Makefile
new file mode 100644
index 000000000000..8738ab0b0667
--- /dev/null
+++ b/fs/smb/server/tests/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2026 Hang Nan <nanx95726@gmail.com>
+
+obj-$(CONFIG_SMB_SERVER_KUNIT_TESTS) += smbacl_kunit.o
diff --git a/fs/smb/server/tests/smbacl_kunit.c b/fs/smb/server/tests/smbacl_kunit.c
new file mode 100644
index 000000000000..33496b4d31a3
--- /dev/null
+++ b/fs/smb/server/tests/smbacl_kunit.c
@@ -0,0 +1,301 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * KUnit tests for ksmbd security descriptor (DACL) handling.
+ *
+ * Copyright (C) 2026 Hang Nan <nanx95726@gmail.com>
+ *
+ * The tests pin the DACL declared-size boundary in smb_check_perm_dacl():
+ *
+ * - ksmbd_dacl_walk_must_stop_at_declared_size: a pure semantic harness
+ * that models the ACE walk. Walking to the end of the enclosing
+ * security descriptor (the pre-fix behaviour) selects an ACE that
+ * sits beyond struct smb_acl::size; stopping at the declared DACL
+ * size (the fixed behaviour) rejects it.
+ *
+ * - ksmbd_smb_check_perm_dacl_boundary and
+ * ksmbd_smb_check_perm_dacl_maximal_boundary: drive the real
+ * smb_check_perm_dacl() with a descriptor stored through ksmbd's own
+ * NTACL xattr path on a tmpfs file, and assert that a post-boundary
+ * ACE is not selected for either a regular or maximal access check.
+ */
+
+#include <kunit/test.h>
+#include <linux/fs.h>
+#include <linux/mm.h>
+#include <linux/shmem_fs.h>
+#include <linux/slab.h>
+
+#include "../smbacl.h"
+#include "../smb_common.h"
+#include "../vfs.h"
+
+struct ksmbd_acl_walk_result {
+ bool found;
+ bool allowed;
+ const struct smb_ace *selected;
+};
+
+static const struct smb_sid test_nonmatching_sid = {
+ 1, 5, {0, 0, 0, 0, 0, 5},
+ { cpu_to_le32(21), cpu_to_le32(1), cpu_to_le32(2),
+ cpu_to_le32(3), cpu_to_le32(9999) }
+};
+
+/*
+ * S-1-22-1-0: the SID id_to_sid(0, SIDUNIX_USER) resolves to, i.e. what
+ * smb_check_perm_dacl() looks for when called with uid == 0.
+ */
+static const struct smb_sid test_owner_sid = {
+ 1, 2, {0, 0, 0, 0, 0, 22},
+ { cpu_to_le32(1), cpu_to_le32(0) }
+};
+
+static int test_compare_sids(const struct smb_sid *a, const struct smb_sid *b)
+{
+ int i;
+
+ if (a->revision != b->revision || a->num_subauth != b->num_subauth)
+ return 1;
+ for (i = 0; i < NUM_AUTHS; i++) {
+ if (a->authority[i] != b->authority[i])
+ return 1;
+ }
+ for (i = 0; i < a->num_subauth; i++) {
+ if (a->sub_auth[i] != b->sub_auth[i])
+ return 1;
+ }
+ return 0;
+}
+
+static u16 test_ace_size(const struct smb_sid *sid)
+{
+ return offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE +
+ sid->num_subauth * sizeof(__le32);
+}
+
+static u16 fill_test_ace(struct smb_ace *ace, const struct smb_sid *sid,
+ u32 access_req)
+{
+ u16 size = test_ace_size(sid);
+
+ ace->type = ACCESS_ALLOWED_ACE_TYPE;
+ ace->flags = 0;
+ ace->size = cpu_to_le16(size);
+ ace->access_req = cpu_to_le32(access_req);
+ memcpy(&ace->sid, sid, size - offsetof(struct smb_ace, sid));
+ return size;
+}
+
+static struct ksmbd_acl_walk_result test_walk_dacl(struct smb_acl *pdacl,
+ int walk_boundary,
+ const struct smb_sid *target,
+ u32 requested)
+{
+ struct ksmbd_acl_walk_result result = {};
+ struct smb_ace *ace;
+ int aces_size;
+ int i;
+
+ ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl));
+ aces_size = walk_boundary - sizeof(struct smb_acl);
+ for (i = 0; i < le16_to_cpu(pdacl->num_aces); i++) {
+ u16 ace_size;
+
+ if (aces_size < offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE)
+ break;
+ ace_size = le16_to_cpu(ace->size);
+ if (ace_size > aces_size ||
+ ace_size < offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE)
+ break;
+ aces_size -= ace_size;
+
+ if (ace->sid.num_subauth > SID_MAX_SUB_AUTHORITIES ||
+ ace_size < offsetof(struct smb_ace, sid) + CIFS_SID_BASE_SIZE +
+ sizeof(__le32) * ace->sid.num_subauth)
+ break;
+
+ if (!test_compare_sids(target, &ace->sid)) {
+ result.found = true;
+ result.selected = ace;
+ result.allowed = !(requested & ~le32_to_cpu(ace->access_req));
+ return result;
+ }
+
+ ace = (struct smb_ace *)((char *)ace + ace_size);
+ }
+
+ return result;
+}
+
+static void ksmbd_dacl_walk_must_stop_at_declared_size(struct kunit *test)
+{
+ struct ksmbd_acl_walk_result declared, enclosing;
+ struct smb_acl *acl;
+ struct smb_ace *ace1, *fake;
+ u16 ace1_size, fake_size;
+ u16 pdacl_size;
+ u16 acl_size;
+
+ acl = kunit_kzalloc(test, 128, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, acl);
+
+ acl->revision = cpu_to_le16(2);
+ acl->num_aces = cpu_to_le16(2);
+
+ ace1 = (struct smb_ace *)((char *)acl + sizeof(*acl));
+ ace1_size = fill_test_ace(ace1, &test_nonmatching_sid, 0);
+ fake = (struct smb_ace *)((char *)ace1 + ace1_size);
+ fake_size = fill_test_ace(fake, &test_owner_sid, FILE_READ_DATA);
+
+ pdacl_size = sizeof(*acl) + ace1_size;
+ acl_size = pdacl_size + fake_size;
+ acl->size = cpu_to_le16(pdacl_size);
+
+ declared = test_walk_dacl(acl, pdacl_size, &test_owner_sid,
+ FILE_READ_DATA);
+ enclosing = test_walk_dacl(acl, acl_size, &test_owner_sid,
+ FILE_READ_DATA);
+
+ KUNIT_EXPECT_FALSE(test, declared.found);
+ KUNIT_EXPECT_FALSE(test, declared.allowed);
+
+ /* Demonstrates that the buggy acl_size boundary selects fake ACE #2. */
+ KUNIT_EXPECT_TRUE(test, enclosing.found);
+ KUNIT_EXPECT_TRUE(test, enclosing.allowed);
+}
+
+/*
+ * Build an NTSD whose DACL declares one ACE (pdacl->size) but actually
+ * contains two: the second ACE sits beyond the declared DACL boundary
+ * yet inside the enclosing security descriptor. The trailing ACE applies
+ * to S-1-22-1-0, which smb_check_perm_dacl() looks for when uid is zero.
+ */
+static struct smb_ntsd *build_boundary_ntsd(struct kunit *test,
+ const struct smb_sid *first_sid,
+ u32 first_access,
+ u32 trailing_access,
+ int *ntsd_size)
+{
+ struct smb_ntsd *pntsd;
+ struct smb_acl *pdacl;
+ struct smb_ace *ace;
+ u16 first_size = test_ace_size(first_sid);
+ u16 trailing_size = test_ace_size(&test_owner_sid);
+
+ *ntsd_size = sizeof(struct smb_ntsd) + sizeof(struct smb_acl) +
+ first_size + trailing_size;
+ pntsd = kunit_kzalloc(test, *ntsd_size, GFP_KERNEL);
+ if (!pntsd)
+ return NULL;
+
+ pntsd->revision = cpu_to_le16(SD_REVISION);
+ pntsd->type = cpu_to_le16(DACL_PRESENT);
+ pntsd->dacloffset = cpu_to_le32(sizeof(struct smb_ntsd));
+
+ pdacl = (struct smb_acl *)((char *)pntsd + sizeof(struct smb_ntsd));
+ pdacl->revision = cpu_to_le16(2);
+ pdacl->num_aces = cpu_to_le16(2);
+ pdacl->size = cpu_to_le16(sizeof(struct smb_acl) + first_size);
+
+ ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl));
+ fill_test_ace(ace, first_sid, first_access);
+
+ ace = (struct smb_ace *)((char *)ace + first_size);
+ fill_test_ace(ace, &test_owner_sid, trailing_access);
+
+ return pntsd;
+}
+
+static void ksmbd_smb_check_perm_dacl_boundary_test(struct kunit *test)
+{
+ struct file *file;
+ struct smb_ntsd *pntsd;
+ __le32 daccess = cpu_to_le32(FILE_READ_DATA);
+ int ntsd_size, rc;
+
+ pntsd = build_boundary_ntsd(test, &test_nonmatching_sid, 0,
+ FILE_READ_DATA, &ntsd_size);
+ KUNIT_ASSERT_NOT_NULL(test, pntsd);
+
+ file = shmem_file_setup("ksmbd-kunit-dacl", 0,
+ mk_vma_flags(VMA_NORESERVE_BIT));
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, file);
+
+ rc = ksmbd_vfs_set_sd_xattr(NULL, mnt_idmap(file->f_path.mnt),
+ &file->f_path, pntsd, ntsd_size,
+ false);
+ KUNIT_EXPECT_EQ(test, 0, rc);
+ if (rc)
+ goto out;
+
+ rc = smb_check_perm_dacl(NULL, &file->f_path, &daccess,
+ cpu_to_le32(FILE_READ_DATA), 0, false);
+
+ /*
+ * The post-boundary ACE (ACE #2, beyond pdacl->size) grants
+ * FILE_READ_DATA to the caller's SID, but it must not be
+ * selected: the walk stops at the declared DACL size and access
+ * is denied. Before the fix the walk used the enclosing
+ * descriptor length, selected ACE #2 and returned 0.
+ */
+ KUNIT_EXPECT_EQ(test, -EACCES, rc);
+out:
+ fput(file);
+}
+
+static void
+ksmbd_smb_check_perm_dacl_maximal_boundary_test(struct kunit *test)
+{
+ struct file *file;
+ struct smb_ntsd *pntsd;
+ __le32 daccess = FILE_MAXIMAL_ACCESS_LE;
+ int ntsd_size, rc;
+
+ /*
+ * The in-boundary ACE grants read access. The trailing ACE grants
+ * write access, which must not be included in the maximal access mask.
+ */
+ pntsd = build_boundary_ntsd(test, &test_owner_sid, FILE_READ_DATA,
+ FILE_WRITE_DATA, &ntsd_size);
+ KUNIT_ASSERT_NOT_NULL(test, pntsd);
+
+ file = shmem_file_setup("ksmbd-kunit-dacl-maximal", 0,
+ mk_vma_flags(VMA_NORESERVE_BIT));
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, file);
+
+ rc = ksmbd_vfs_set_sd_xattr(NULL, mnt_idmap(file->f_path.mnt),
+ &file->f_path, pntsd, ntsd_size,
+ false);
+ KUNIT_EXPECT_EQ(test, 0, rc);
+ if (rc)
+ goto out;
+
+ rc = smb_check_perm_dacl(NULL, &file->f_path, &daccess,
+ FILE_MAXIMAL_ACCESS_LE, 0, false);
+ KUNIT_EXPECT_EQ(test, 0, rc);
+ if (rc)
+ goto out;
+
+ KUNIT_EXPECT_TRUE(test, le32_to_cpu(daccess) & FILE_READ_DATA);
+ KUNIT_EXPECT_FALSE(test, le32_to_cpu(daccess) & FILE_WRITE_DATA);
+out:
+ fput(file);
+}
+
+static struct kunit_case ksmbd_smbacl_test_cases[] = {
+ KUNIT_CASE(ksmbd_dacl_walk_must_stop_at_declared_size),
+ KUNIT_CASE(ksmbd_smb_check_perm_dacl_boundary_test),
+ KUNIT_CASE(ksmbd_smb_check_perm_dacl_maximal_boundary_test),
+ {}
+};
+
+static struct kunit_suite ksmbd_smbacl_test_suite = {
+ .name = "ksmbd-smbacl",
+ .test_cases = ksmbd_smbacl_test_cases,
+};
+
+kunit_test_suite(ksmbd_smbacl_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ksmbd smbacl helpers");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
diff --git a/fs/smb/server/transport_ipc.c b/fs/smb/server/transport_ipc.c
index e550aa41ad2c..eb07cdf17410 100644
--- a/fs/smb/server/transport_ipc.c
+++ b/fs/smb/server/transport_ipc.c
@@ -353,6 +353,13 @@ static int ipc_server_config_on_startup(struct ksmbd_startup_request *req)
if (req->max_ip_connections)
server_conf.max_ip_connections = req->max_ip_connections;
+ if (req->max_sessions_per_connection)
+ server_conf.max_sessions_per_connection =
+ req->max_sessions_per_connection;
+ else
+ server_conf.max_sessions_per_connection =
+ KSMBD_DEFAULT_MAX_SESSIONS_PER_CONNECTION;
+
ret = ksmbd_set_netbios_name(req->netbios_name);
ret |= ksmbd_set_server_string(req->server_string);
ret |= ksmbd_set_work_group(req->work_group);
@@ -532,11 +539,25 @@ static int ipc_validate_msg(struct ipc_msg_table_entry *entry)
if (entry->msg_sz < sizeof(struct ksmbd_share_config_response))
return -EINVAL;
+ /*
+ * ksmbd-tools releases before 3.5.2 leave payload_sz zero.
+ * Derive it from the size of the message actually received so
+ * the checks below bound the payload for those releases too.
+ */
+ if (!resp->payload_sz)
+ resp->payload_sz = entry->msg_sz -
+ sizeof(struct ksmbd_share_config_response);
+
if (strnlen(resp->share_name, sizeof(resp->share_name)) ==
sizeof(resp->share_name))
return -EINVAL;
- if (resp->veto_list_sz > resp->payload_sz)
+ /*
+ * Pipe shares carry no veto list, but ksmbd-tools before 3.5.3
+ * copy a global "veto files" setting into the IPC$ response.
+ */
+ if (!(resp->flags & KSMBD_SHARE_FLAG_PIPE) &&
+ resp->veto_list_sz > resp->payload_sz)
return -EINVAL;
if (resp->flags != KSMBD_SHARE_FLAG_INVALID &&
diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c
index 4968cfc1a572..937d321fa364 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) {
@@ -77,6 +104,7 @@ static struct tcp_transport *alloc_transport(struct socket *client_sk)
if (client_sk->sk->sk_family == AF_INET6) {
memcpy(&conn->inet6_addr, &client_sk->sk->sk_v6_daddr, 16);
conn->inet_hash = ipv6_addr_hash(&client_sk->sk->sk_v6_daddr);
+ conn->is_ipv6 = true;
} else {
conn->inet_addr = inet_sk(client_sk->sk)->inet_daddr;
conn->inet_hash = ipv4_addr_hash(inet_sk(client_sk->sk)->inet_daddr);
@@ -85,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;
}
@@ -99,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);
@@ -106,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);
}
@@ -431,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_dfl_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_dfl_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;
+ }
+ }
+
+ /* 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;
+ }
+ }
- return kernel_sendmsg(TCP_TRANS(t)->sock, &smb_msg, tx->iov,
- tx->iov_cnt, tx->size);
+ /* 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_dfl_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)
@@ -451,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)
@@ -656,10 +937,15 @@ static void ksmbd_tcp_stop_listener(struct interface *iface)
void ksmbd_tcp_destroy(void)
{
struct interface *iface, *tmp;
+ LIST_HEAD(iface_list_to_free);
unregister_netdevice_notifier(&ksmbd_netdev_notifier);
- list_for_each_entry_safe(iface, tmp, &iface_list, entry) {
+ rtnl_lock();
+ list_splice_init(&iface_list, &iface_list_to_free);
+ rtnl_unlock();
+
+ list_for_each_entry_safe(iface, tmp, &iface_list_to_free, entry) {
ksmbd_tcp_stop_listener(iface);
list_del(&iface->entry);
kfree(iface->name);
@@ -715,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,
diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c
index 5cb161994e70..4ee72eda9bc0 100644
--- a/fs/smb/server/vfs.c
+++ b/fs/smb/server/vfs.c
@@ -5,6 +5,7 @@
*/
#include <crypto/sha2.h>
+#include <kunit/visibility.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/fs_struct.h>
@@ -61,6 +62,10 @@ static int ksmbd_vfs_path_lookup(struct ksmbd_share_config *share_conf,
struct dentry *d;
if (pathname[0] == '\0') {
+ /* The share root must never be removed. */
+ if (for_remove)
+ return -EINVAL;
+
pathname = share_conf->path;
root_share_path = NULL;
} else {
@@ -118,10 +123,11 @@ static int ksmbd_vfs_path_lookup(struct ksmbd_share_config *share_conf,
void ksmbd_vfs_query_maximal_access(const struct mnt_idmap *idmap,
struct dentry *dentry, __le32 *daccess)
{
- *daccess = cpu_to_le32(FILE_READ_ATTRIBUTES | READ_CONTROL);
+ *daccess = cpu_to_le32(FILE_READ_ATTRIBUTES | READ_CONTROL |
+ SYNCHRONIZE);
if (!inode_permission(idmap, d_inode(dentry), MAY_OPEN | MAY_WRITE))
- *daccess |= cpu_to_le32(WRITE_DAC | WRITE_OWNER | SYNCHRONIZE |
+ *daccess |= cpu_to_le32(WRITE_DAC | WRITE_OWNER |
FILE_WRITE_DATA | FILE_APPEND_DATA |
FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES |
FILE_DELETE_CHILD);
@@ -216,9 +222,9 @@ int ksmbd_vfs_mkdir(struct ksmbd_work *work, const char *name, umode_t mode)
return err;
}
-static ssize_t ksmbd_vfs_getcasexattr(const struct mnt_idmap *idmap,
- struct dentry *dentry, char *attr_name,
- int attr_name_len, char **attr_value)
+ssize_t ksmbd_vfs_getcasexattr(const struct mnt_idmap *idmap,
+ struct dentry *dentry, char *attr_name,
+ int attr_name_len, char **attr_value)
{
char *name, *xattr_list = NULL;
ssize_t value_len = -ENOENT, xattr_list_len;
@@ -268,7 +274,7 @@ static int ksmbd_vfs_stream_read(struct ksmbd_file *fp, char *buf, loff_t *pos,
return (int)v_len;
if (v_len <= *pos) {
- count = -EINVAL;
+ count = 0;
goto free_buf;
}
@@ -360,7 +366,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);
@@ -387,23 +393,21 @@ static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos,
const struct cred *saved_cred;
char *stream_buf = NULL, *wbuf;
const struct mnt_idmap *idmap = file_mnt_idmap(fp->filp);
- size_t size;
+ size_t size, write_end;
ssize_t v_len;
int err = 0;
ksmbd_debug(VFS, "write stream data pos : %llu, count : %zd\n",
*pos, count);
- if (*pos >= XATTR_SIZE_MAX) {
+ if (*pos < 0 || *pos >= XATTR_SIZE_MAX) {
pr_err("stream write position %lld is out of bounds\n", *pos);
return -EINVAL;
}
- size = *pos + count;
- if (size > XATTR_SIZE_MAX) {
- size = XATTR_SIZE_MAX;
- count = XATTR_SIZE_MAX - *pos;
- }
+ if (count > XATTR_SIZE_MAX - *pos)
+ return -EFBIG;
+ write_end = *pos + count;
saved_cred = override_creds(fp->filp->f_cred);
v_len = ksmbd_vfs_getcasexattr(idmap,
@@ -416,6 +420,8 @@ static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos,
err = v_len;
goto out_revert;
}
+ /* Preserve the tail of an existing stream on an in-place write. */
+ size = max_t(size_t, v_len, write_end);
if (v_len < size) {
wbuf = kvzalloc(size, KSMBD_DEFAULT_GFP);
@@ -444,7 +450,7 @@ out_revert:
if (err < 0)
goto out;
else
- fp->stream.pos = size;
+ fp->stream.pos = write_end;
err = 0;
out:
kvfree(stream_buf);
@@ -452,6 +458,62 @@ out:
}
/**
+ * ksmbd_vfs_stream_truncate() - change the length of a named stream
+ * @fp: stream file handle
+ * @newsize: new stream length
+ *
+ * Resize the stream xattr without changing the base inode size.
+ *
+ * Return: 0 on success, otherwise a negative error code
+ */
+int ksmbd_vfs_stream_truncate(struct ksmbd_file *fp, loff_t newsize)
+{
+ const struct cred *saved_cred;
+ const struct mnt_idmap *idmap = file_mnt_idmap(fp->filp);
+ char *stream_buf = NULL, *new_buf = NULL;
+ ssize_t v_len;
+ int err;
+
+ if (newsize < 0)
+ return -EINVAL;
+ if (newsize > XATTR_SIZE_MAX)
+ return -EFBIG;
+
+ saved_cred = override_creds(fp->filp->f_cred);
+ v_len = ksmbd_vfs_getcasexattr(idmap, fp->filp->f_path.dentry,
+ fp->stream.name, fp->stream.size,
+ &stream_buf);
+ if (v_len < 0) {
+ err = v_len;
+ goto out;
+ }
+ if (v_len == newsize) {
+ err = 0;
+ goto out;
+ }
+
+ new_buf = stream_buf;
+ if (newsize > v_len) {
+ new_buf = kvzalloc(newsize, KSMBD_DEFAULT_GFP);
+ if (!new_buf) {
+ err = -ENOMEM;
+ goto out;
+ }
+ if (v_len)
+ memcpy(new_buf, stream_buf, v_len);
+ }
+
+ err = ksmbd_vfs_setxattr(idmap, &fp->filp->f_path, fp->stream.name,
+ new_buf, newsize, 0, true);
+ if (new_buf != stream_buf)
+ kvfree(new_buf);
+out:
+ kvfree(stream_buf);
+ revert_creds(saved_cred);
+ return err;
+}
+
+/**
* ksmbd_vfs_write() - vfs helper for smb file write
* @work: work
* @fp: ksmbd file pointer
@@ -468,7 +530,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) {
@@ -490,7 +551,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");
@@ -515,7 +576,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);
@@ -841,8 +902,8 @@ ssize_t ksmbd_vfs_listxattr(struct dentry *dentry, char **list)
return size;
}
-static ssize_t ksmbd_vfs_xattr_len(const struct mnt_idmap *idmap,
- struct dentry *dentry, char *xattr_name)
+ssize_t ksmbd_vfs_xattr_len(const struct mnt_idmap *idmap,
+ struct dentry *dentry, char *xattr_name)
{
return vfs_getxattr(idmap, dentry, xattr_name, NULL, 0);
}
@@ -955,6 +1016,15 @@ int ksmbd_vfs_zero_data(struct ksmbd_work *work, struct ksmbd_file *fp,
char *zero_buf = NULL;
int err;
+ /*
+ * A directory handle can hold FILE_WRITE_DATA (the add-file right),
+ * so the caller access check does not exclude it. Writing zeroes to a
+ * directory makes no sense and kernel_write() would warn on the
+ * directory file (no FMODE_WRITE), so reject it here.
+ */
+ if (S_ISDIR(file_inode(fp->filp)->i_mode))
+ return -EINVAL;
+
smb_break_all_levII_oplock(work, fp, 1);
if (!work->tcon->posix_extensions) {
loff_t size = i_size_read(file_inode(fp->filp));
@@ -1671,6 +1741,7 @@ out:
kfree(def_smb_acl);
return rc;
}
+EXPORT_SYMBOL_IF_KUNIT(ksmbd_vfs_set_sd_xattr);
int ksmbd_vfs_get_sd_xattr(struct ksmbd_conn *conn,
const struct mnt_idmap *idmap,
@@ -1897,13 +1968,17 @@ int ksmbd_vfs_fill_dentry_attrs(struct ksmbd_work *work,
ssize_t ksmbd_vfs_casexattr_len(const struct mnt_idmap *idmap,
struct dentry *dentry, char *attr_name,
- int attr_name_len)
+ int attr_name_len, char *actual_name)
{
char *name, *xattr_list = NULL;
ssize_t value_len = -ENOENT, xattr_list_len;
xattr_list_len = ksmbd_vfs_listxattr(dentry, &xattr_list);
- if (xattr_list_len <= 0)
+ if (xattr_list_len < 0) {
+ value_len = xattr_list_len;
+ goto out;
+ }
+ if (!xattr_list_len)
goto out;
for (name = xattr_list; name - xattr_list < xattr_list_len;
@@ -1911,8 +1986,13 @@ ssize_t ksmbd_vfs_casexattr_len(const struct mnt_idmap *idmap,
ksmbd_debug(VFS, "%s, len %zd\n", name, strlen(name));
if (strncasecmp(attr_name, name, attr_name_len))
continue;
+ if (actual_name && strlen(name) + 1 != attr_name_len)
+ continue;
value_len = ksmbd_vfs_xattr_len(idmap, dentry, name);
+ /* The caller provides attr_name_len bytes for the actual name. */
+ if (value_len >= 0 && actual_name)
+ memcpy(actual_name, name, attr_name_len);
break;
}
@@ -2067,7 +2147,7 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work,
src_file_size = ksmbd_vfs_casexattr_len(
file_mnt_idmap(src_fp->filp),
src_fp->filp->f_path.dentry,
- src_fp->stream.name, src_fp->stream.size);
+ src_fp->stream.name, src_fp->stream.size, NULL);
revert_creds(saved_cred);
if (src_file_size < 0)
return src_file_size;
diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h
index 67cc48e583c8..21b0d29a3db0 100644
--- a/fs/smb/server/vfs.h
+++ b/fs/smb/server/vfs.h
@@ -83,6 +83,7 @@ int ksmbd_vfs_read(struct ksmbd_work *work, struct ksmbd_file *fp, size_t count,
int ksmbd_vfs_write(struct ksmbd_work *work, struct ksmbd_file *fp,
char *buf, size_t count, loff_t *pos, bool sync,
ssize_t *written);
+int ksmbd_vfs_stream_truncate(struct ksmbd_file *fp, loff_t newsize);
int ksmbd_vfs_fsync(struct ksmbd_work *work, u64 fid, u64 p_id);
int ksmbd_vfs_remove_file(struct ksmbd_work *work, const struct path *path);
int ksmbd_vfs_link(struct ksmbd_work *work,
@@ -108,9 +109,14 @@ ssize_t ksmbd_vfs_getxattr(const struct mnt_idmap *idmap,
struct dentry *dentry,
char *xattr_name,
char **xattr_buf);
+ssize_t ksmbd_vfs_xattr_len(const struct mnt_idmap *idmap,
+ struct dentry *dentry, char *xattr_name);
+ssize_t ksmbd_vfs_getcasexattr(const struct mnt_idmap *idmap,
+ struct dentry *dentry, char *attr_name,
+ int attr_name_len, char **attr_value);
ssize_t ksmbd_vfs_casexattr_len(const struct mnt_idmap *idmap,
struct dentry *dentry, char *attr_name,
- int attr_name_len);
+ int attr_name_len, char *actual_name);
int ksmbd_vfs_setxattr(const struct mnt_idmap *idmap,
const struct path *path, const char *attr_name,
void *attr_value, size_t attr_size, int flags,
diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c
index fd2c595f0486..7a0f13277ec0 100644
--- a/fs/smb/server/vfs_cache.c
+++ b/fs/smb/server/vfs_cache.c
@@ -11,6 +11,8 @@
#include <linux/kthread.h>
#include <linux/freezer.h>
#include <linux/dcache.h>
+#include <linux/hashtable.h>
+#include <linux/jiffies.h>
#include "glob.h"
#include "vfs_cache.h"
@@ -35,8 +37,82 @@ 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_percpu_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_percpu_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);
+
+/* 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);
@@ -605,7 +681,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);
@@ -613,104 +689,252 @@ 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
+ *
+ * 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 ||
+ cmp->pending)
+ 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) {
+ 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;
+}
+
+/**
+ * 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);
+ 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);
+ }
+ 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);
+ 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)
{
struct file *filp;
- struct ksmbd_lock *smb_lock, *tmp_lock;
- struct ksmbd_work *cn_work;
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;
+ /* 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);
- }
-
- /*
- * Complete any CHANGE_NOTIFY left pending on this handle now that
- * it is closed. KSMBD never completes CHANGE_NOTIFY spontaneously
- * (no real change-notification backend), only on close -- matching
- * genuine SMB2/macOS smbfs semantics and avoiding the Finder
- * "directory changed, re-enumerate everything" loop.
- *
- * smb2_notify() on another connection can be adding to
- * notify_pendings under fp->f_lock at the same time this handle is
- * closed, and a client-sent CANCEL can concurrently be racing to
- * claim the same entry via smb2_notify_cancel_fn() (smb2pdu.c).
- * Pop one entry at a time under the lock via list_del_init() rather
- * than a bulk list_splice_init(): list_del_init() leaves the node
- * self-linked ("empty"), which is what the cancel path checks under
- * the same lock to tell whether it lost the race -- a bulk splice
- * would instead relink every entry into a shared local list, so an
- * entry claimed here would still read as "not empty" to a racing
- * cancel_fn, and both sides could end up freeing the same work.
- * ksmbd_conn_write() can sleep (it takes conn's write mutex), so it
- * must not be called while fp->f_lock is held -- release the lock
- * before processing each popped entry, then reacquire it for the
- * next.
- */
- for (;;) {
- spin_lock(&fp->f_lock);
- if (list_empty(&fp->notify_pendings)) {
- spin_unlock(&fp->f_lock);
- break;
- }
- cn_work = list_first_entry(&fp->notify_pendings,
- struct ksmbd_work, notify_entry);
- list_del_init(&cn_work->notify_entry);
- spin_unlock(&fp->f_lock);
-
- ksmbd_conn_write(cn_work);
- /*
- * release_async_work() removes cn_work from
- * conn->async_requests, frees cancel_argv, and releases+zeroes
- * async_id -- all needed before ksmbd_free_work_struct(), which
- * only releases async_id itself if still nonzero (i.e. if this
- * hadn't already been done).
- */
- release_async_work(cn_work);
- ksmbd_free_work_struct(cn_work);
- }
-
- /*
- * Drop fp's strong reference on conn (taken in ksmbd_open_fd() /
- * ksmbd_reopen_durable_fd()). Durable fps that reached the
- * scavenger have already had fp->conn cleared by session_fd_check(),
- * in which case there is nothing to drop here.
+ * Drop fp's strong connection references. Durable fps that reached
+ * the scavenger have already had these cleared by session_fd_check().
*/
if (fp->conn) {
+ atomic_dec(&fp->conn->stats.open_files_count);
ksmbd_conn_put(fp->conn);
fp->conn = NULL;
}
+ if (fp->tcon) {
+ ksmbd_tree_connect_put(fp->tcon);
+ fp->tcon = NULL;
+ }
if (ksmbd_stream_fd(fp))
kfree(fp->stream.name);
+ ksmbd_share_config_put(fp->share_conf);
kfree(fp->owner.name);
kmem_cache_free(filp_cache, fp);
@@ -821,22 +1045,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)
@@ -1009,37 +1218,54 @@ bool ksmbd_has_other_active_fd(struct ksmbd_file *fp)
return ret;
}
-struct ksmbd_file *ksmbd_lookup_fd_app_instance_id(char *app_instance_id)
+bool ksmbd_app_instance_id_matches(struct ksmbd_work *work,
+ struct ksmbd_file *fp)
{
- struct ksmbd_file *fp = NULL;
- unsigned int id;
+ bool matches;
+
+ /* Keep the current connection and share alive across the comparison. */
+ spin_lock(&fp->f_lock);
+ matches = READ_ONCE(fp->f_state) == FP_INITED &&
+ fp->conn && fp->share_conf &&
+ !strcmp(fp->share_conf->name, work->tcon->share_conf->name) &&
+ memcmp(fp->conn->ClientGUID, work->conn->ClientGUID,
+ SMB2_CLIENT_GUID_SIZE);
+ spin_unlock(&fp->f_lock);
+
+ return matches;
+}
+
+struct ksmbd_file *
+ksmbd_lookup_fd_app_instance_id_next(char *app_instance_id, int *cursor)
+{
+ struct ksmbd_file *fp, *found = NULL;
+ int id = *cursor;
read_lock(&global_ft.lock);
- idr_for_each_entry(global_ft.idr, fp, id) {
- if (!fp->has_app_instance_id)
- continue;
- if (!memcmp(fp->app_instance_id, app_instance_id,
+ while ((fp = idr_get_next(global_ft.idr, &id))) {
+ *cursor = id + 1;
+ if (fp->has_app_instance_id &&
+ !memcmp(fp->app_instance_id, app_instance_id,
SMB2_CREATE_GUID_SIZE)) {
- fp = ksmbd_fp_get(fp);
- break;
+ found = ksmbd_fp_get(fp);
+ if (found)
+ break;
}
+ id++;
}
read_unlock(&global_ft.lock);
- return fp;
+ return found;
}
-int ksmbd_close_fd_app_instance_id(char *app_instance_id)
+int ksmbd_close_fd_app_instance_id(struct ksmbd_work *work,
+ struct ksmbd_file *fp, bool *closed)
{
struct ksmbd_file_table *ft;
- struct ksmbd_file *fp;
struct oplock_info *opinfo;
int n_to_drop = 0;
- fp = ksmbd_lookup_fd_app_instance_id(app_instance_id);
- if (!fp)
- return 0;
-
+ *closed = false;
opinfo = opinfo_get(fp);
if (!opinfo)
goto out;
@@ -1052,7 +1278,8 @@ int ksmbd_close_fd_app_instance_id(char *app_instance_id)
ft = &opinfo->sess->file_table;
write_lock(&ft->lock);
- if (fp->f_state == FP_INITED && has_file_id(fp->volatile_id)) {
+ if (fp->f_state == FP_INITED && has_file_id(fp->volatile_id) &&
+ ksmbd_app_instance_id_matches(work, fp)) {
idr_remove(ft->idr, fp->volatile_id);
fp->volatile_id = KSMBD_NO_FID;
n_to_drop = ksmbd_mark_fp_closed(fp);
@@ -1069,11 +1296,9 @@ int ksmbd_close_fd_app_instance_id(char *app_instance_id)
list_del_init(&fp->node);
up_write(&fp->f_ci->m_lock);
- if (atomic_sub_and_test(n_to_drop, &fp->refcount)) {
- if (fp->conn)
- atomic_dec(&fp->conn->stats.open_files_count);
+ *closed = true;
+ if (atomic_sub_and_test(n_to_drop, &fp->refcount))
__ksmbd_close_fd(NULL, fp);
- }
return 0;
out:
@@ -1223,11 +1448,6 @@ static int __open_id(struct ksmbd_file_table *ft, struct ksmbd_file *fp,
u64 id = 0;
int ret;
- if (type == OPEN_ID_TYPE_VOLATILE_ID && fd_limit_depleted()) {
- __open_id_set(fp, KSMBD_NO_FID, type);
- return -EMFILE;
- }
-
idr_preload(KSMBD_DEFAULT_GFP);
write_lock(&ft->lock);
ret = idr_alloc_cyclic(ft->idr, fp, KSMBD_START_FID, INT_MAX - 1,
@@ -1237,7 +1457,6 @@ static int __open_id(struct ksmbd_file_table *ft, struct ksmbd_file *fp,
ret = 0;
} else {
id = KSMBD_NO_FID;
- fd_limit_close();
}
__open_id_set(fp, id, type);
@@ -1257,30 +1476,35 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp)
struct ksmbd_file *fp;
int ret;
+ /* Durable reconnects reuse the slot held for the lifetime of fp. */
+ if (fd_limit_depleted())
+ return ERR_PTR(-EMFILE);
+
fp = kmem_cache_zalloc(filp_cache, KSMBD_DEFAULT_GFP);
if (!fp) {
pr_err("Failed to allocate memory\n");
- return ERR_PTR(-ENOMEM);
+ ret = -ENOMEM;
+ goto err_fd_limit;
}
INIT_LIST_HEAD(&fp->blocked_works);
INIT_LIST_HEAD(&fp->node);
INIT_LIST_HEAD(&fp->lock_list);
- INIT_LIST_HEAD(&fp->notify_pendings);
spin_lock_init(&fp->f_lock);
mutex_init(&fp->readdir_lock);
atomic_set(&fp->refcount, 1);
fp->filp = filp;
/*
- * fp owns a strong reference on fp->conn for as long as fp->conn is
- * non-NULL, so session_fd_check() and __ksmbd_close_fd() never
- * dereference a dangling pointer. Paired with ksmbd_conn_put() in
- * session_fd_check() (durable preserve), in __ksmbd_close_fd()
- * (final close), and on the error paths below.
+ * fp owns strong references on fp->conn and fp->tcon for as long as
+ * they are non-NULL. Paired with the puts in session_fd_check()
+ * (durable preserve), in __ksmbd_close_fd() (final close), and on the
+ * error paths below.
*/
fp->conn = ksmbd_conn_get(work->conn);
- fp->tcon = work->tcon;
+ fp->tcon = ksmbd_tree_connect_get(work->tcon);
+ fp->share_conf = work->tcon->share_conf;
+ atomic_inc(&fp->share_conf->refcount);
fp->volatile_id = KSMBD_NO_FID;
fp->persistent_id = KSMBD_NO_FID;
fp->f_state = FP_NEW;
@@ -1291,7 +1515,13 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp)
goto err_out;
}
- ret = __open_id(&work->sess->file_table, fp, OPEN_ID_TYPE_VOLATILE_ID);
+ down_read(&work->sess->tree_conns_lock);
+ if (work->tcon->t_state == TREE_CONNECTED)
+ ret = __open_id(&work->sess->file_table, fp,
+ OPEN_ID_TYPE_VOLATILE_ID);
+ else
+ ret = -ENOTCONN;
+ up_read(&work->sess->tree_conns_lock);
if (ret) {
ksmbd_inode_put(fp->f_ci);
goto err_out;
@@ -1301,9 +1531,13 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp)
return fp;
err_out:
- /* fp->conn was set and refcounted before every branch here. */
+ /* fp->conn and fp->tcon were refcounted before every branch here. */
+ ksmbd_tree_connect_put(fp->tcon);
ksmbd_conn_put(fp->conn);
+ ksmbd_share_config_put(fp->share_conf);
kmem_cache_free(filp_cache, fp);
+err_fd_limit:
+ fd_limit_close();
return ERR_PTR(ret);
}
@@ -1315,8 +1549,9 @@ err_out:
*
* Return: 0 on success. The FP_NEW -> FP_INITED transition is special:
* -ENOENT if teardown already unpublished @fp by advancing the state or
- * clearing the volatile id. Other state updates preserve the historical
- * fire-and-forget behavior.
+ * clearing the volatile id. On success it also takes a reference for the
+ * opening request, which must be released with ksmbd_fd_put(). Other state
+ * updates preserve the historical fire-and-forget behavior.
*/
int ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp,
unsigned int state)
@@ -1332,6 +1567,8 @@ int ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp,
ret = -ENOENT;
} else {
fp->f_state = state;
+ if (state == FP_INITED)
+ atomic_inc(&fp->refcount);
ret = 0;
}
write_unlock(&ft->lock);
@@ -1361,7 +1598,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,
@@ -1372,7 +1609,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;
@@ -1456,10 +1692,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);
@@ -1478,22 +1712,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)
@@ -1763,7 +1988,7 @@ 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;
+ struct ksmbd_tree_connect *tree_conn;
if (!is_reconnectable(fp))
return false;
@@ -1782,31 +2007,20 @@ 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;
+ tree_conn = fp->tcon;
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);
-
- 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);
- }
-
+ write_lock(&global_ft.lock);
fp->conn = NULL;
fp->tcon = NULL;
fp->volatile_id = KSMBD_NO_FID;
@@ -1814,34 +2028,30 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon,
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);
+ ksmbd_tree_connect_put(tree_conn);
return true;
}
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)
{
+ 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);
@@ -1852,6 +2062,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);
@@ -1864,6 +2076,8 @@ void ksmbd_free_global_file_table(void)
int ksmbd_validate_name_reconnect(struct ksmbd_share_config *share,
struct ksmbd_file *fp, char *name)
{
+ const char *share_path = share->real_path;
+ size_t share_path_sz = share->real_path_sz;
char *pathname, *ab_pathname;
int ret = 0;
@@ -1877,9 +2091,25 @@ int ksmbd_validate_name_reconnect(struct ksmbd_share_config *share,
return -EACCES;
}
- if (name && strcmp(&ab_pathname[share->path_sz + 1], name)) {
- ksmbd_debug(SMB, "invalid name reconnect %s\n", name);
- ret = -EINVAL;
+ if (name) {
+ size_t len = strlen(ab_pathname);
+
+ if (len == share_path_sz && !strncmp(ab_pathname, share_path, len)) {
+ /* the durable fp is the share root itself */
+ if (name[0])
+ ret = -EINVAL;
+ } else if (share_path_sz == 1 && share_path[0] == '/') {
+ if (ab_pathname[0] != '/' ||
+ strcmp(ab_pathname + 1, name))
+ ret = -EINVAL;
+ } else if (len <= share_path_sz ||
+ strncmp(ab_pathname, share_path, share_path_sz) ||
+ ab_pathname[share_path_sz] != '/' ||
+ strcmp(&ab_pathname[share_path_sz + 1], name)) {
+ ret = -EINVAL;
+ }
+ if (ret)
+ ksmbd_debug(SMB, "invalid name reconnect %s\n", name);
}
kfree(pathname);
@@ -1889,23 +2119,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_lock *smb_lock;
+ 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;
}
/*
@@ -1914,51 +2147,129 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp)
* concurrent teardown that iterates the table can observe a valid
* volatile_id with fp->conn == NULL and preserve a
* partially-initialized fp. fp owns a strong reference on the new
- * conn (see ksmbd_open_fd()); undo it on __open_id() failure.
+ * conn (see ksmbd_open_fd()); undo it on __open_id() failure. Serialize
+ * publication with tree disconnect just as ksmbd_open_fd() does.
*/
- fp->conn = ksmbd_conn_get(conn);
- fp->tcon = work->tcon;
- write_unlock(&global_ft.lock);
-
+ spin_lock(&fp->f_lock);
old_f_state = fp->f_state;
fp->f_state = FP_NEW;
+ old_share = fp->share_conf;
+ fp->share_conf = work->tcon->share_conf;
+ atomic_inc(&fp->share_conf->refcount);
+ fp->conn = ksmbd_conn_get(conn);
+ fp->tcon = ksmbd_tree_connect_get(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);
- if (!has_file_id(fp->volatile_id)) {
+ down_read(&work->sess->tree_conns_lock);
+ if (work->tcon->t_state == TREE_CONNECTED)
+ ret = __open_id(&work->sess->file_table, fp,
+ OPEN_ID_TYPE_VOLATILE_ID);
+ else
+ ret = -ENOTCONN;
+ up_read(&work->sess->tree_conns_lock);
+ if (ret) {
write_lock(&global_ft.lock);
+ spin_lock(&fp->f_lock);
fp->conn = NULL;
fp->tcon = NULL;
+ 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);
- fp->f_state = old_f_state;
- 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);
+ ksmbd_tree_connect_put(work->tcon);
+ ksmbd_share_config_put(work->tcon->share_conf);
+ if (ret != -ENOTCONN)
+ ret = -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)
continue;
op->conn = ksmbd_conn_get(fp->conn);
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;
kfree(fp->owner.name);
fp->owner.name = NULL;
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)
@@ -1975,14 +2286,19 @@ int ksmbd_init_file_table(struct ksmbd_file_table *ft)
void ksmbd_destroy_file_table(struct ksmbd_session *sess)
{
struct ksmbd_file_table *ft = &sess->file_table;
+ struct idr *idr;
if (!ft->idr)
return;
__close_file_table_ids(sess, NULL, session_fd_check, true);
- idr_destroy(ft->idr);
- kfree(ft->idr);
+ /* Stop procfs readers before freeing the detached file table. */
+ write_lock(&ft->lock);
+ idr = ft->idr;
ft->idr = NULL;
+ write_unlock(&ft->lock);
+ idr_destroy(idr);
+ kfree(idr);
}
int ksmbd_init_file_cache(void)
diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h
index 1884f6deb9d0..c00d92fdf94a 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"
@@ -34,20 +35,31 @@
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;
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;
ssize_t size;
@@ -102,6 +114,8 @@ struct ksmbd_file {
struct oplock_info __rcu *f_opinfo;
struct ksmbd_conn *conn;
struct ksmbd_tree_connect *tcon;
+ /* Own a share reference independently of tcon; rebind under f_lock. */
+ struct ksmbd_share_config *share_conf;
atomic_t refcount;
__le32 daccess;
@@ -157,17 +171,12 @@ 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;
unsigned int outstanding_pre_requests;
struct ksmbd_lock_sequence lock_seq[KSMBD_LOCK_SEQ_ARRAY_SIZE];
-
- /*
- * Pending CHANGE_NOTIFY completions for this handle, sent with
- * STATUS_NOTIFY_CLEANUP when the handle is closed.
- */
- struct list_head notify_pendings;
};
static inline void set_ctx_actor(struct dir_context *ctx,
@@ -213,8 +222,12 @@ void ksmbd_put_durable_fd(struct ksmbd_file *fp);
int ksmbd_invalidate_durable_fd(unsigned long long id);
bool ksmbd_has_other_active_fd(struct ksmbd_file *fp);
bool ksmbd_has_stream_without_delete_share(struct ksmbd_file *fp);
-struct ksmbd_file *ksmbd_lookup_fd_app_instance_id(char *app_instance_id);
-int ksmbd_close_fd_app_instance_id(char *app_instance_id);
+bool ksmbd_app_instance_id_matches(struct ksmbd_work *work,
+ struct ksmbd_file *fp);
+struct ksmbd_file *
+ksmbd_lookup_fd_app_instance_id_next(char *app_instance_id, int *cursor);
+int ksmbd_close_fd_app_instance_id(struct ksmbd_work *work,
+ struct ksmbd_file *fp, bool *closed);
struct ksmbd_file *ksmbd_lookup_fd_cguid(char *cguid);
struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry);
bool ksmbd_has_other_nonposix_open(struct dentry *dentry);
@@ -230,6 +243,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);
@@ -257,6 +271,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);
diff --git a/fs/smb/smbdirect/accept.c b/fs/smb/smbdirect/accept.c
index 1b30ca8476c1..85e37d11f43d 100644
--- a/fs/smb/smbdirect/accept.c
+++ b/fs/smb/smbdirect/accept.c
@@ -19,15 +19,33 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc,
const struct rdma_conn_param *param)
{
struct smbdirect_socket_parameters *sp = &sc->parameters;
- struct smbdirect_recv_io *recv_io;
+ struct smbdirect_recv_io *recv_io = NULL;
u8 peer_initiator_depth;
u8 peer_responder_resources;
struct rdma_conn_param conn_param;
__be32 ird_ord_hdr[2];
int ret;
- if (SMBDIRECT_CHECK_STATUS_WARN(sc, SMBDIRECT_SOCKET_CREATED))
- return -EINVAL;
+ /*
+ * Install the real event handler (and the event we expect next)
+ * before anything that can fail. On an early error the caller
+ * (smbdirect_listen_connect_request()) only schedules our deferred
+ * cleanup and returns 0 to the rdma_cm core, which keeps the cm_id, so
+ * the cm_id must already carry smbdirect_accept_rdma_event_handler():
+ * it turns any unexpected event (e.g. RDMA_CM_EVENT_DEVICE_REMOVAL)
+ * into a clean smbdirect_socket_schedule_cleanup(). Otherwise the
+ * placeholder smbdirect_socket_rdma_event_handler() installed by
+ * smbdirect_socket_init_accepting() stays in place and returns -ESTALE,
+ * which makes the rdma_cm core destroy the cm_id behind the back of our
+ * pending teardown.
+ */
+ sc->rdma.expected_event = RDMA_CM_EVENT_ESTABLISHED;
+ sc->rdma.cm_id->event_handler = smbdirect_accept_rdma_event_handler;
+
+ if (SMBDIRECT_CHECK_STATUS_WARN(sc, SMBDIRECT_SOCKET_CREATED)) {
+ ret = -EINVAL;
+ goto cleanup;
+ }
/*
* First set what the we as server are able to support
@@ -48,7 +66,7 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc,
smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_ERR,
"smbdirect_accept_init_params() failed %1pe\n",
SMBDIRECT_DEBUG_ERR_PTR(ret));
- goto init_params_failed;
+ goto cleanup;
}
ret = smbdirect_connection_create_qp(sc);
@@ -56,7 +74,7 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc,
smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_ERR,
"smbdirect_connection_create_qp() failed %1pe\n",
SMBDIRECT_DEBUG_ERR_PTR(ret));
- goto create_qp_failed;
+ goto cleanup;
}
ret = smbdirect_connection_create_mem_pools(sc);
@@ -64,7 +82,7 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc,
smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_ERR,
"smbdirect_connection_create_mem_pools() failed %1pe\n",
SMBDIRECT_DEBUG_ERR_PTR(ret));
- goto create_mem_failed;
+ goto cleanup;
}
recv_io = smbdirect_connection_get_recv_io(sc);
@@ -73,7 +91,7 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc,
smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_ERR,
"smbdirect_connection_get_recv_io() failed %1pe\n",
SMBDIRECT_DEBUG_ERR_PTR(ret));
- goto get_recv_io_failed;
+ goto cleanup;
}
recv_io->cqe.done = smbdirect_accept_negotiate_recv_done;
@@ -87,7 +105,7 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc,
smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_ERR,
"smbdirect_connection_post_recv_io() failed %1pe\n",
SMBDIRECT_DEBUG_ERR_PTR(ret));
- goto post_recv_io_failed;
+ goto cleanup;
}
/*
* From here recv_io is known to the RDMA QP and needs ib_drain_qp and
@@ -123,14 +141,12 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc,
/* explicitly set above */
WARN_ON_ONCE(sc->status != SMBDIRECT_SOCKET_RDMA_CONNECT_NEEDED);
sc->status = SMBDIRECT_SOCKET_RDMA_CONNECT_RUNNING;
- sc->rdma.expected_event = RDMA_CM_EVENT_ESTABLISHED;
- sc->rdma.cm_id->event_handler = smbdirect_accept_rdma_event_handler;
ret = rdma_accept(sc->rdma.cm_id, &conn_param);
if (ret) {
smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_ERR,
"rdma_accept() failed %1pe\n",
SMBDIRECT_DEBUG_ERR_PTR(ret));
- goto rdma_accept_failed;
+ goto cleanup;
}
/*
@@ -144,31 +160,25 @@ int smbdirect_accept_connect_request(struct smbdirect_socket *sc,
return 0;
-rdma_accept_failed:
+cleanup:
/*
- * The recv_io posted above is now owned by the QP (recv_io was set to
- * NULL after a successful post). smbdirect_connection_destroy_qp()
- * calls ib_drain_qp(), whose completion
- * (smbdirect_accept_negotiate_recv_done) returns the recv_io to the
- * free list via smbdirect_connection_put_recv_io(). It therefore MUST
- * run BEFORE smbdirect_connection_destroy_mem_pools(): otherwise the
- * posted recv_io is still outstanding when kmem_cache_destroy() runs
- * ("Slab cache still has objects") and is later freed into an
- * already-destroyed mempool (mempool_free_bulk NULL-ptr-deref).
+ * No RDMA state is torn down here, and in particular sc is not
+ * released: the caller (smbdirect_listen_connect_request()) owns that
+ * decision and schedules sc's teardown via
+ * smbdirect_socket_schedule_cleanup() on a non-zero return. All of the
+ * RDMA state - the QP, the memory pools and the cm_id itself - is then
+ * torn down asynchronously by smbdirect_socket_cleanup_work() ->
+ * smbdirect_socket_release() -> smbdirect_socket_destroy(), which
+ * drains the QP (returning any posted recv_io to the pool) before
+ * destroying the memory pools and destroys the cm_id with
+ * rdma_destroy_id(), all in the correct order.
+ *
+ * The only resource not reachable by that teardown is a recv_io that
+ * was taken from the pool but not yet posted to the QP (recv_io is set
+ * to NULL after a successful post), so hand it back here.
*/
- smbdirect_connection_destroy_qp(sc);
- smbdirect_connection_destroy_mem_pools(sc);
- return ret;
-post_recv_io_failed:
- /* post failed: recv_io was not accepted by the QP, still in hand */
if (recv_io)
smbdirect_connection_put_recv_io(recv_io);
-get_recv_io_failed:
- smbdirect_connection_destroy_mem_pools(sc);
-create_mem_failed:
- smbdirect_connection_destroy_qp(sc);
-create_qp_failed:
-init_params_failed:
return ret;
}
@@ -224,7 +234,7 @@ static void smbdirect_accept_negotiate_recv_done(struct ib_cq *cq, struct ib_wc
struct smbdirect_socket *sc = recv_io->socket;
unsigned long flags;
- if (unlikely(wc->status != IB_WC_SUCCESS || WARN_ON_ONCE(wc->opcode != IB_WC_RECV))) {
+ if (unlikely(wc->status != IB_WC_SUCCESS || wc->opcode != IB_WC_RECV)) {
if (wc->status != IB_WC_WR_FLUSH_ERR)
smbdirect_log_rdma_recv(sc, SMBDIRECT_LOG_ERR,
"wc->status=%s (%d) wc->opcode=%d\n",
@@ -302,6 +312,7 @@ static void smbdirect_accept_negotiate_recv_work(struct work_struct *work)
struct smbdirect_socket *sc =
container_of(work, struct smbdirect_socket, connect.work);
struct smbdirect_socket_parameters *sp = &sc->parameters;
+ struct smbdirect_socket *lsc;
struct smbdirect_recv_io *recv_io;
struct smbdirect_negotiate_req *nreq;
unsigned long flags;
@@ -464,29 +475,55 @@ static void smbdirect_accept_negotiate_recv_work(struct work_struct *work)
*/
sp->max_fragmented_send_size = max_fragmented_size;
- if (sc->accept.listener) {
- struct smbdirect_socket *lsc = sc->accept.listener;
- unsigned long flags;
+ /*
+ * Every accepting socket was created by
+ * smbdirect_listen_connect_request() and has a
+ * listener. If it's already cleared, the listener
+ * (or its purge_orphaned_work) is responsible for
+ * releasing us, so we must not send a negotiate
+ * response.
+ *
+ * The memory of the listener is freed via kfree_rcu(),
+ * see smbdirect_listen_orphan_socket().
+ */
+ rcu_read_lock();
+ lsc = READ_ONCE(sc->accept.listener);
+ if (!lsc) {
+ rcu_read_unlock();
+ return;
+ }
- spin_lock_irqsave(&lsc->listen.lock, flags);
- list_del(&sc->accept.list);
- list_add_tail(&sc->accept.list, &lsc->listen.ready);
+ spin_lock_irqsave(&lsc->listen.lock, flags);
+ /*
+ * The listener (or its purge_orphaned_work)
+ * may have cleared sc->accept.listener in the
+ * meantime and is responsible for releasing us.
+ *
+ * If we already failed, we're either
+ * already on the orphaned list or
+ * smbdirect_socket_cleanup_work() will
+ * move us there.
+ *
+ * In both cases we must not move us
+ * to the ready list.
+ */
+ if (sc->accept.listener == lsc && !READ_ONCE(sc->first_error)) {
+ list_move_tail(&sc->accept.list, &lsc->listen.ready);
wake_up(&lsc->listen.wait_queue);
- spin_unlock_irqrestore(&lsc->listen.lock, flags);
-
- /*
- * smbdirect_socket_accept() will call
- * smbdirect_accept_negotiate_finish(nsc, 0);
- *
- * So that we don't send the negotiation
- * response that grants credits to the peer
- * before the socket is accepted by the
- * application.
- */
- return;
}
+ spin_unlock_irqrestore(&lsc->listen.lock, flags);
+ rcu_read_unlock();
- ntstatus = le32_to_cpu(STATUS_SUCCESS);
+ /*
+ * smbdirect_socket_accept() will call
+ * smbdirect_accept_negotiate_finish(nsc, 0);
+ *
+ * So that we don't send the negotiation
+ * response that grants credits to the peer
+ * before the socket is accepted by the
+ * application.
+ */
+ return;
not_supported:
smbdirect_accept_negotiate_finish(sc, ntstatus);
@@ -799,7 +836,11 @@ static long smbdirect_socket_wait_for_accept(struct smbdirect_socket *lsc, long
if (ret < 0)
return ret;
- return 0;
+ /*
+ * Return the remaining timeout, so the caller can carry it
+ * over to the next smbdirect_socket_wait_for_accept() call.
+ */
+ return ret;
}
struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc,
@@ -809,6 +850,7 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc,
struct smbdirect_socket *nsc;
unsigned long flags;
+again:
if (lsc->status != SMBDIRECT_SOCKET_LISTENING) {
arg->err = -EINVAL;
return NULL;
@@ -820,7 +862,7 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc,
}
if (list_empty_careful(&lsc->listen.ready)) {
- int ret;
+ long ret;
if (timeo == 0) {
arg->err = -EAGAIN;
@@ -828,25 +870,78 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc,
}
ret = smbdirect_socket_wait_for_accept(lsc, timeo);
- if (ret) {
+ if (ret < 0) {
arg->err = ret;
return NULL;
}
+ /*
+ * Carry the remaining timeout over, so that a stream of
+ * failed connections that we orphan and skip (goto again)
+ * can't reset the caller's timeout and wait forever.
+ */
+ timeo = ret;
}
spin_lock_irqsave(&lsc->listen.lock, flags);
- nsc = list_first_entry_or_null(&lsc->listen.ready,
- struct smbdirect_socket,
- accept.list);
+ /*
+ * Recheck under the lock.
+ */
+ if (lsc->status != SMBDIRECT_SOCKET_LISTENING) {
+ arg->err = -EINVAL;
+ spin_unlock_irqrestore(&lsc->listen.lock, flags);
+ return NULL;
+ }
+ if (lsc->first_error) {
+ arg->err = lsc->first_error;
+ spin_unlock_irqrestore(&lsc->listen.lock, flags);
+ return NULL;
+ }
+
+ while ((nsc = list_first_entry_or_null(&lsc->listen.ready,
+ struct smbdirect_socket,
+ accept.list))) {
+ /*
+ * nsc may have failed after it was moved
+ * to the ready list, e.g. RDMA_CM_EVENT_DISCONNECTED
+ * already moved it to SMBDIRECT_SOCKET_DISCONNECTED.
+ * We must not overwrite that with
+ * SMBDIRECT_SOCKET_CONNECTED and hand
+ * out an already disconnected socket.
+ *
+ * Doing this under listen.lock means nsc
+ * still belongs to us, so we can just move
+ * a failed socket to the orphaned list,
+ * like smbdirect_listen_orphan_socket() does,
+ * and try the next one.
+ */
+ if (!READ_ONCE(nsc->first_error) &&
+ cmpxchg(&nsc->status,
+ SMBDIRECT_SOCKET_NEGOTIATE_RUNNING,
+ SMBDIRECT_SOCKET_CONNECTED) ==
+ SMBDIRECT_SOCKET_NEGOTIATE_RUNNING)
+ break;
+
+ smbdirect_log_rdma_event(nsc, SMBDIRECT_LOG_INFO,
+ "orphaning failed socket status=%s first_error=%1pe\n",
+ smbdirect_socket_status_string(nsc->status),
+ SMBDIRECT_DEBUG_ERR_PTR(nsc->first_error));
+ list_move_tail(&nsc->accept.list, &lsc->listen.orphaned);
+ queue_work(lsc->workqueues.cleanup, &lsc->listen.purge_orphaned_work);
+ }
if (nsc) {
- nsc->accept.listener = NULL;
+ WRITE_ONCE(nsc->accept.listener, NULL);
list_del_init_careful(&nsc->accept.list);
arg->is_empty = list_empty_careful(&lsc->listen.ready);
}
spin_unlock_irqrestore(&lsc->listen.lock, flags);
if (!nsc) {
- arg->err = -EAGAIN;
- return NULL;
+ /*
+ * If we only found failed sockets or no socket,
+ * we wait for the next one.
+ *
+ * A possible -EAGAIN/-ETIMEOUT is handled above.
+ */
+ goto again;
}
/*
@@ -855,12 +950,14 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc,
* so it didn't grant any credits to us.
*
* The caller expects a connected socket
- * now as there are no credits anyway.
+ * now as there are no credits anyway,
+ * above we already changed to SMBDIRECT_SOCKET_CONNECTED
+ * under the lsc->listen.lock and with cmpxchg.
*
- * Then we send the negotiation response in
- * order to grant credits to the peer.
+ * Now we send the negotiation response in
+ * order to grant credits to the peer,
+ * as the socket is now visible to the application layer.
*/
- nsc->status = SMBDIRECT_SOCKET_CONNECTED;
smbdirect_accept_negotiate_finish(nsc, 0);
return nsc;
diff --git a/fs/smb/smbdirect/connect.c b/fs/smb/smbdirect/connect.c
index 34a3e72c38fb..0d02f03063ac 100644
--- a/fs/smb/smbdirect/connect.c
+++ b/fs/smb/smbdirect/connect.c
@@ -581,7 +581,7 @@ static void smbdirect_connect_negotiate_recv_done(struct ib_cq *cq, struct ib_wc
struct smbdirect_socket *sc = recv_io->socket;
unsigned long flags;
- if (unlikely(wc->status != IB_WC_SUCCESS || WARN_ON_ONCE(wc->opcode != IB_WC_RECV))) {
+ if (unlikely(wc->status != IB_WC_SUCCESS || wc->opcode != IB_WC_RECV)) {
if (wc->status != IB_WC_WR_FLUSH_ERR)
smbdirect_log_rdma_recv(sc, SMBDIRECT_LOG_ERR,
"wc->status=%s (%d) wc->opcode=%d\n",
diff --git a/fs/smb/smbdirect/connection.c b/fs/smb/smbdirect/connection.c
index afd31fa12a36..e6c7b343fe87 100644
--- a/fs/smb/smbdirect/connection.c
+++ b/fs/smb/smbdirect/connection.c
@@ -83,9 +83,9 @@ static int smbdirect_connection_rdma_event_handler(struct rdma_cm_id *id,
* smbdirect_socket_schedule_cleanup[_status]() =>
* smbdirect_socket_cleanup_work().
*
- * As otherwise we'd set SMBDIRECT_SOCKET_DISCONNECTING,
- * but never ever get RDMA_CM_EVENT_DISCONNECTED and
- * never reach SMBDIRECT_SOCKET_DISCONNECTED.
+ * As otherwise we'd set SMBDIRECT_SOCKET_DISCONNECTING
+ * and call rdma_disconnect(), but never ever get
+ * RDMA_CM_EVENT_DISCONNECTED.
*/
if (event->event == RDMA_CM_EVENT_DEVICE_REMOVAL)
smbdirect_socket_schedule_cleanup_status(sc,
@@ -113,9 +113,9 @@ static int smbdirect_connection_rdma_event_handler(struct rdma_cm_id *id,
* smbdirect_socket_schedule_cleanup_status() =>
* smbdirect_socket_cleanup_work().
*
- * As otherwise we'd set SMBDIRECT_SOCKET_DISCONNECTING,
- * but never ever get RDMA_CM_EVENT_DISCONNECTED and
- * never reach SMBDIRECT_SOCKET_DISCONNECTED.
+ * As otherwise we'd set SMBDIRECT_SOCKET_DISCONNECTING
+ * and call rdma_disconnect(), but never ever get
+ * RDMA_CM_EVENT_DISCONNECTED.
*
* This is also a normal disconnect so
* SMBDIRECT_LOG_INFO should be good enough
@@ -1529,7 +1529,7 @@ void smbdirect_connection_recv_io_done(struct ib_cq *cq, struct ib_wc *wc)
u32 data_length;
u32 remaining_data_length;
- if (unlikely(wc->status != IB_WC_SUCCESS || WARN_ON_ONCE(wc->opcode != IB_WC_RECV))) {
+ if (unlikely(wc->status != IB_WC_SUCCESS || wc->opcode != IB_WC_RECV)) {
if (wc->status != IB_WC_WR_FLUSH_ERR)
smbdirect_log_rdma_recv(sc, SMBDIRECT_LOG_ERR,
"wc->status=%s (%d) wc->opcode=%d\n",
diff --git a/fs/smb/smbdirect/internal.h b/fs/smb/smbdirect/internal.h
index e9959e6dc13a..69d0c9f899de 100644
--- a/fs/smb/smbdirect/internal.h
+++ b/fs/smb/smbdirect/internal.h
@@ -74,6 +74,8 @@ void __smbdirect_socket_schedule_cleanup(struct smbdirect_socket *sc,
void smbdirect_socket_destroy_sync(struct smbdirect_socket *sc);
+void smbdirect_listen_orphan_socket(struct smbdirect_socket *sc);
+
int smbdirect_socket_wait_for_credits(struct smbdirect_socket *sc,
enum smbdirect_socket_status expected_status,
int unexpected_errno,
diff --git a/fs/smb/smbdirect/listen.c b/fs/smb/smbdirect/listen.c
index 2f78bcaedbf8..3c91f00d95ce 100644
--- a/fs/smb/smbdirect/listen.c
+++ b/fs/smb/smbdirect/listen.c
@@ -10,6 +10,78 @@
static int smbdirect_listen_rdma_event_handler(struct rdma_cm_id *id,
struct rdma_cm_event *event);
+/*
+ * This is called by a socket that failed while it
+ * is still on the pending or ready list of its
+ * listener, typically at the end of
+ * smbdirect_socket_cleanup_work(), when the
+ * disconnect was already started.
+ *
+ * It moves itself to the orphaned list and
+ * lets smbdirect_listen_purge_orphaned_work()
+ * release it. Otherwise it would stay on the
+ * pending or ready list until the listener is
+ * destroyed and fill up the backlog, so that
+ * no new connections would be accepted.
+ *
+ * It's fine to call this more than once.
+ */
+void smbdirect_listen_orphan_socket(struct smbdirect_socket *sc)
+{
+ struct smbdirect_socket *lsc;
+ unsigned long flags;
+
+ /*
+ * The memory of the listener is freed via
+ * kfree_rcu(), so it's safe to dereference it
+ * under rcu_read_lock(), even if sc->accept.listener
+ * is cleared and the listener is released concurrently.
+ */
+ rcu_read_lock();
+ lsc = READ_ONCE(sc->accept.listener);
+ if (!lsc) {
+ rcu_read_unlock();
+ return;
+ }
+
+ spin_lock_irqsave(&lsc->listen.lock, flags);
+ if (sc->accept.listener == lsc) {
+ list_move_tail(&sc->accept.list, &lsc->listen.orphaned);
+ queue_work(lsc->workqueues.cleanup, &lsc->listen.purge_orphaned_work);
+ }
+ spin_unlock_irqrestore(&lsc->listen.lock, flags);
+ rcu_read_unlock();
+}
+
+static void smbdirect_listen_purge_orphaned_work(struct work_struct *work)
+{
+ struct smbdirect_socket *lsc =
+ container_of(work, struct smbdirect_socket, listen.purge_orphaned_work);
+ struct smbdirect_socket *psc, *tsc;
+ LIST_HEAD(orphaned_list);
+ unsigned long flags;
+
+ /*
+ * Clearing accept.listener under listen.lock
+ * makes us responsible for releasing them.
+ */
+ spin_lock_irqsave(&lsc->listen.lock, flags);
+ list_splice_tail_init(&lsc->listen.orphaned, &orphaned_list);
+ list_for_each_entry(psc, &orphaned_list, accept.list)
+ WRITE_ONCE(psc->accept.listener, NULL);
+ spin_unlock_irqrestore(&lsc->listen.lock, flags);
+
+ /*
+ * We don't hold the listener's rdma_lock_handler()
+ * lock here, see smbdirect_socket_destroy()
+ * for why that's important.
+ */
+ list_for_each_entry_safe(psc, tsc, &orphaned_list, accept.list) {
+ list_del_init(&psc->accept.list);
+ smbdirect_socket_release(psc);
+ }
+}
+
int smbdirect_socket_listen(struct smbdirect_socket *sc, int backlog)
{
int ret;
@@ -47,6 +119,9 @@ int smbdirect_socket_listen(struct smbdirect_socket *sc, int backlog)
sc->rdma.cm_id->event_handler = smbdirect_listen_rdma_event_handler;
rdma_unlock_handler(sc->rdma.cm_id);
+ INIT_WORK(&sc->listen.purge_orphaned_work,
+ smbdirect_listen_purge_orphaned_work);
+
ret = rdma_listen(sc->rdma.cm_id, backlog);
if (ret) {
sc->first_error = ret;
@@ -214,6 +289,7 @@ static int smbdirect_listen_connect_request(struct smbdirect_socket *lsc,
size_t backlog = max_t(size_t, 1, lsc->listen.backlog);
size_t psockets;
size_t rsockets;
+ size_t osockets;
int ret;
if (!smbdirect_frwr_is_supported(&new_id->device->attrs)) {
@@ -254,14 +330,21 @@ static int smbdirect_listen_connect_request(struct smbdirect_socket *lsc,
spin_lock_irqsave(&lsc->listen.lock, flags);
psockets = list_count_nodes(&lsc->listen.pending);
rsockets = list_count_nodes(&lsc->listen.ready);
+ /*
+ * Orphaned sockets still count against
+ * the backlog until they are released by
+ * smbdirect_listen_purge_orphaned_work().
+ */
+ osockets = list_count_nodes(&lsc->listen.orphaned);
spin_unlock_irqrestore(&lsc->listen.lock, flags);
if (psockets > backlog ||
rsockets > backlog ||
- (psockets + rsockets) > backlog) {
+ osockets > backlog ||
+ (psockets + rsockets + osockets) > backlog) {
smbdirect_log_rdma_event(lsc, SMBDIRECT_LOG_ERR,
- "Backlog[%d][%zu] full pending[%zu] ready[%zu]\n",
- lsc->listen.backlog, backlog, psockets, rsockets);
+ "Backlog[%d][%zu] full pending[%zu] ready[%zu] orphaned[%zu]\n",
+ lsc->listen.backlog, backlog, psockets, rsockets, osockets);
return -EBUSY;
}
@@ -279,22 +362,54 @@ static int smbdirect_listen_connect_request(struct smbdirect_socket *lsc,
if (ret)
goto set_settings_failed;
+ /*
+ * Publish nsc on the pending list with accept.listener set before
+ * smbdirect_accept_connect_request() calls rdma_accept(). Once the
+ * connection can establish, smbdirect_accept_negotiate_recv_work()
+ * may run and it must observe accept.listener, otherwise nsc would
+ * be left stranded on the pending list forever.
+ *
+ * The listener's handler_mutex is held while we're called, so
+ * smbdirect_socket_destroy() of the listener can't reach nsc on the
+ * pending list before we're done.
+ *
+ * From here nsc is published on the pending list: on failure of
+ * smbdirect_accept_connect_request() below we schedule nsc's teardown,
+ * which orphans nsc off the listener so
+ * smbdirect_listen_purge_orphaned_work() -> smbdirect_socket_release()
+ * releases it.
+ */
spin_lock_irqsave(&lsc->listen.lock, flags);
list_add_tail(&nsc->accept.list, &lsc->listen.pending);
- nsc->accept.listener = lsc;
+ WRITE_ONCE(nsc->accept.listener, lsc);
spin_unlock_irqrestore(&lsc->listen.lock, flags);
ret = smbdirect_accept_connect_request(nsc, &event->param.conn);
- if (ret)
- goto accept_connect_failed;
+ if (ret) {
+ /*
+ * The rdma_cm core holds both the listener's and nsc's
+ * id_priv->handler_mutex across this CONNECT_REQUEST handler,
+ * so neither can go away under us here and nsc cannot receive
+ * any rdma event: it is safe to hand nsc to its teardown.
+ *
+ * That teardown must be deferred though: nsc->rdma.cm_id
+ * (= new_id) has its handler_mutex held by us, so it cannot be
+ * destroyed synchronously from here.
+ * smbdirect_socket_schedule_cleanup() only queues work;
+ * smbdirect_socket_cleanup_work() then orphans nsc off the
+ * listener and smbdirect_listen_purge_orphaned_work() releases
+ * it, destroying nsc's cm_id with rdma_destroy_id().
+ */
+ smbdirect_socket_schedule_cleanup(nsc, ret);
+ }
+ /*
+ * Always return 0 so the rdma_cm core keeps new_id: nsc owns it now.
+ * On success it stays connected; on failure its deferred teardown
+ * above destroys it.
+ */
return 0;
-accept_connect_failed:
- spin_lock_irqsave(&lsc->listen.lock, flags);
- list_del_init(&nsc->accept.list);
- nsc->accept.listener = NULL;
- spin_unlock_irqrestore(&lsc->listen.lock, flags);
set_settings_failed:
set_params_failed:
/*
diff --git a/fs/smb/smbdirect/socket.c b/fs/smb/smbdirect/socket.c
index bb02df6158b9..e270e36b3145 100644
--- a/fs/smb/smbdirect/socket.c
+++ b/fs/smb/smbdirect/socket.c
@@ -319,6 +319,15 @@ void __smbdirect_socket_schedule_cleanup(struct smbdirect_socket *sc,
* (smbdirect_socket_destroy) to reap.
*/
if (sc->listen.backlog != -1) { /* was a listener */
+ /*
+ * We don't move them to the orphaned list here,
+ * each child does that itself at the end of its
+ * smbdirect_socket_cleanup_work(), see
+ * smbdirect_listen_orphan_socket(). Only that checks
+ * accept.listener, which is still NULL for a child
+ * that smbdirect_listen_connect_request() is still
+ * setting up and will release itself on failure.
+ */
spin_lock_irqsave(&sc->listen.lock, flags);
list_splice_init(&sc->listen.ready, &sc->listen.pending);
list_for_each_entry_safe(psc, tsc, &sc->listen.pending, accept.list)
@@ -427,6 +436,15 @@ static void smbdirect_socket_cleanup_work(struct work_struct *work)
* instances of one class -- harmless, but lockdep cannot tell).
*/
if (sc->listen.backlog != -1) { /* was a listener */
+ /*
+ * We don't move them to the orphaned list here,
+ * each child does that itself at the end of its
+ * smbdirect_socket_cleanup_work(), see
+ * smbdirect_listen_orphan_socket(). Only that checks
+ * accept.listener, which is still NULL for a child
+ * that smbdirect_listen_connect_request() is still
+ * setting up and will release itself on failure.
+ */
spin_lock_irqsave(&sc->listen.lock, flags);
list_splice_init(&sc->listen.ready, &sc->listen.pending);
list_for_each_entry_safe(psc, tsc, &sc->listen.pending, accept.list)
@@ -486,6 +504,18 @@ static void smbdirect_socket_cleanup_work(struct work_struct *work)
* in order to notice the broken connection.
*/
smbdirect_socket_wake_up_all(sc);
+
+ /*
+ * If we're still on the pending or ready list
+ * of a listener, we started the disconnect
+ * as far as possible above, so we move ourself
+ * to the orphaned list of the listener,
+ * which will release us.
+ *
+ * This is a no-op internally if
+ * sc->accept.listener is NULL.
+ */
+ smbdirect_listen_orphan_socket(sc);
}
static void smbdirect_socket_destroy(struct smbdirect_socket *sc)
@@ -511,7 +541,13 @@ static void smbdirect_socket_destroy(struct smbdirect_socket *sc)
if (sc->status == SMBDIRECT_SOCKET_DESTROYED)
return;
- WARN_ONCE(sc->status != SMBDIRECT_SOCKET_DISCONNECTED,
+ /*
+ * smbdirect_socket_destroy_sync() doesn't wait
+ * for RDMA_CM_EVENT_DISCONNECTED, so we may still
+ * be in SMBDIRECT_SOCKET_DISCONNECTING
+ * (or already reached SMBDIRECT_SOCKET_DISCONNECTED)
+ */
+ WARN_ONCE(sc->status < SMBDIRECT_SOCKET_DISCONNECTING,
"status=%s first_error=%1pe",
smbdirect_socket_status_string(sc->status),
SMBDIRECT_DEBUG_ERR_PTR(sc->first_error));
@@ -538,10 +574,37 @@ static void smbdirect_socket_destroy(struct smbdirect_socket *sc)
disable_work_sync(&sc->recv_io.posted.refill_work);
disable_work_sync(&sc->idle.immediate_work);
disable_delayed_work_sync(&sc->idle.timer_work);
+ disable_work_sync(&sc->listen.purge_orphaned_work);
if (sc->rdma.cm_id)
rdma_lock_handler(sc->rdma.cm_id);
+ /*
+ * We hold the handler lock, so the rdma event
+ * handlers can't change the status anymore.
+ *
+ * If RDMA_CM_EVENT_DISCONNECTED didn't arrive yet,
+ * we just stop waiting for it here.
+ *
+ * We already disabled disconnect_work above
+ * and before we call rdma_unlock_handler below
+ * we call smbdirect_connection_destroy_qp which
+ * sets sc->ib.qp = NULL.
+ *
+ * Between rdma_unlock_handler() and
+ * rdma_destroy_id() below there's a small
+ * windows where RDMA_CM_EVENT_DISCONNECTED
+ * could still arrive.
+ *
+ * But smbdirect_connection_rdma_event_handler
+ * will be a noop when calling smbdirect_socket_schedule_cleanup*
+ * ib_drain_qp() also won't be called.
+ */
+ if (sc->status < SMBDIRECT_SOCKET_DISCONNECTED) {
+ sc->status = SMBDIRECT_SOCKET_DISCONNECTED;
+ smbdirect_socket_wake_up_all(sc);
+ }
+
if (sc->ib.qp) {
smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO,
"drain qp\n");
@@ -575,6 +638,19 @@ static void smbdirect_socket_destroy(struct smbdirect_socket *sc)
spin_lock_irqsave(&sc->listen.lock, flags);
list_splice_tail_init(&sc->listen.ready, &pending_list);
list_splice_tail_init(&sc->listen.pending, &pending_list);
+ /*
+ * purge_orphaned_work is already disabled above,
+ * so we also need to release the orphaned sockets.
+ *
+ * Clearing accept.listener under listen.lock
+ * makes us responsible for releasing them and
+ * prevents them from moving themselves to
+ * the orphaned list via
+ * smbdirect_listen_orphan_socket().
+ */
+ list_splice_tail_init(&sc->listen.orphaned, &pending_list);
+ list_for_each_entry(psc, &pending_list, accept.list)
+ WRITE_ONCE(psc->accept.listener, NULL);
spin_unlock_irqrestore(&sc->listen.lock, flags);
/* It's not possible for upper layer to get to reassembly */
@@ -618,7 +694,6 @@ static void smbdirect_socket_destroy(struct smbdirect_socket *sc)
"release %zu pending sockets\n", psockets);
list_for_each_entry_safe(psc, tsc, &pending_list, accept.list) {
list_del_init(&psc->accept.list);
- psc->accept.listener = NULL;
smbdirect_socket_release(psc);
}
if (sc->listen.backlog != -1) /* was a listener */
@@ -679,17 +754,21 @@ void smbdirect_socket_destroy_sync(struct smbdirect_socket *sc)
"destroying rdma session\n");
if (sc->status < SMBDIRECT_SOCKET_DISCONNECTING)
smbdirect_socket_cleanup_work(&sc->disconnect_work);
- if (sc->status < SMBDIRECT_SOCKET_DISCONNECTED) {
- smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO,
- "wait for transport being disconnected\n");
- wait_event(sc->status_wait, sc->status == SMBDIRECT_SOCKET_DISCONNECTED);
- smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO,
- "waited for transport being disconnected\n");
- }
/*
- * Once we reached SMBDIRECT_SOCKET_DISCONNECTED,
- * we should call smbdirect_socket_destroy()
+ * We don't wait for RDMA_CM_EVENT_DISCONNECTED,
+ * rdma_disconnect() was already called by
+ * smbdirect_socket_cleanup_work() if needed
+ * and smbdirect_socket_destroy() drains the qp,
+ * destroys it and calls rdma_destroy_id(), which
+ * only waits for a currently running event handler
+ * and makes sure no further events are delivered.
+ * The rest of the disconnect protocol is handled
+ * by the rdma core asynchronously.
+ *
+ * Waiting for RDMA_CM_EVENT_DISCONNECTED could
+ * take very long or forever, e.g. if the peer
+ * just disappeared.
*/
smbdirect_socket_destroy(sc);
smbdirect_log_rdma_event(sc, SMBDIRECT_LOG_INFO,
@@ -741,7 +820,17 @@ static void smbdirect_socket_release_destroy(struct kref *kref)
* in DESTROYED state, before we free the memory.
*/
smbdirect_socket_destroy_sync(sc);
- kfree(sc);
+
+ /*
+ * Only a listener (backlog != -1) is ever dereferenced
+ * via sc->accept.listener under rcu_read_lock(), see
+ * smbdirect_listen_orphan_socket(). Other sockets can be
+ * freed immediately.
+ */
+ if (sc->listen.backlog != -1) /* was a listener */
+ kfree_rcu(sc, refs.rcu);
+ else
+ kfree(sc);
}
void smbdirect_socket_release(struct smbdirect_socket *sc)
diff --git a/fs/smb/smbdirect/socket.h b/fs/smb/smbdirect/socket.h
index c09eddd8ad16..14114a2567a2 100644
--- a/fs/smb/smbdirect/socket.h
+++ b/fs/smb/smbdirect/socket.h
@@ -151,6 +151,12 @@ struct smbdirect_socket {
* the disconnect refcount.
*/
struct kref destroy;
+
+ /*
+ * smbdirect_socket_release_destroy() uses
+ * kfree_rcu(), see accept.listener.
+ */
+ struct rcu_head rcu;
} refs;
/* RDMA related */
@@ -216,6 +222,16 @@ struct smbdirect_socket {
* only be > 0.
*/
int backlog;
+ /*
+ * Sockets on pending or ready that failed
+ * move themselves to orphaned and queue
+ * purge_orphaned_work, which releases them.
+ * So that they are freed while the listener
+ * is still alive. They still count against
+ * the backlog until they are released.
+ */
+ struct list_head orphaned;
+ struct work_struct purge_orphaned_work;
} listen;
/*
@@ -226,7 +242,30 @@ struct smbdirect_socket {
* connection.
*/
struct {
+ /*
+ * This is only set, protected by
+ * listener->listen.lock, while the socket
+ * is owned by the listener (on its pending,
+ * ready or orphaned list). The one who clears
+ * it is responsible for releasing the socket.
+ *
+ * The memory of a struct smbdirect_socket is
+ * freed via kfree_rcu(), so the listener
+ * can be dereferenced under rcu_read_lock(),
+ * even if accept.listener is cleared and
+ * the listener is released concurrently.
+ */
struct smbdirect_socket *listener;
+ /*
+ * Linkage on one of the listener's
+ * listen.{pending,ready,orphaned} lists.
+ *
+ * accept.list and accept.listener are set
+ * together under listener->listen.lock before
+ * smbdirect_accept_connect_request() is called,
+ * so whenever the socket is on one of those lists
+ * accept.listener is that listener (never NULL).
+ */
struct list_head list;
} accept;
@@ -588,6 +627,9 @@ static __always_inline void smbdirect_socket_init(struct smbdirect_socket *sc)
spin_lock_init(&sc->listen.lock);
INIT_LIST_HEAD(&sc->listen.pending);
INIT_LIST_HEAD(&sc->listen.ready);
+ INIT_LIST_HEAD(&sc->listen.orphaned);
+ INIT_WORK(&sc->listen.purge_orphaned_work, __smbdirect_socket_disabled_work);
+ disable_work_sync(&sc->listen.purge_orphaned_work);
sc->listen.backlog = -1; /* not a listener */
init_waitqueue_head(&sc->listen.wait_queue);