diff options
| author | Linus Torvalds <torvalds@linux-foundation.org> | 2026-10-25 15:47:41 -1000 |
|---|---|---|
| committer | Linus Torvalds <torvalds@linux-foundation.org> | 2026-10-25 15:47:41 -1000 |
| commit | 4e1b4fdf6113b4eb9564bdf3b76ee3c3132da7d4 (patch) | |
| tree | 0f88f96f5977b697047fd793ed8f86a982a10252 | |
| parent | 9e089e670e9ad1caf4a5b2b63e01b2dc076efbe3 (diff) | |
| parent | 2c36c4a86104d763ee798d4a89e2d3384b4cd626 (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
...
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, <->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(¬ify_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(¬ify_req.wait); + argv[0] = ¬ify_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); |
