diff options
Diffstat (limited to 'fs/smb/server')
44 files changed, 9608 insertions, 4816 deletions
diff --git a/fs/smb/server/Kconfig b/fs/smb/server/Kconfig index 12594879cb64..221ec9717a83 100644 --- a/fs/smb/server/Kconfig +++ b/fs/smb/server/Kconfig @@ -7,14 +7,12 @@ config SMB_SERVER select NLS_UTF8 select NLS_UCS2_UTILS select CRYPTO - select CRYPTO_ECB + select CRYPTO_LIB_AES_CBC_MACS select CRYPTO_LIB_ARC4 - select CRYPTO_LIB_DES select CRYPTO_LIB_MD5 select CRYPTO_LIB_SHA256 select CRYPTO_LIB_SHA512 select CRYPTO_LIB_UTILS - select CRYPTO_CMAC select CRYPTO_AEAD2 select CRYPTO_CCM select CRYPTO_GCM @@ -48,8 +46,9 @@ if SMB_SERVER config SMB_SERVER_SMBDIRECT bool "Support for SMB Direct protocol" - depends on SMB_SERVER=m && INFINIBAND && INFINIBAND_ADDR_TRANS || SMB_SERVER=y && INFINIBAND=y && INFINIBAND_ADDR_TRANS=y - select SG_POOL + depends on SMB_SERVER && INFINIBAND && INFINIBAND_ADDR_TRANS + depends on SMB_SERVER=m || INFINIBAND=y + select SMBDIRECT default n help diff --git a/fs/smb/server/Makefile b/fs/smb/server/Makefile index 6407ba6b9340..a3e9306055e8 100644 --- a/fs/smb/server/Makefile +++ b/fs/smb/server/Makefile @@ -10,7 +10,7 @@ ksmbd-y := unicode.o auth.o vfs.o vfs_cache.o server.o ndr.o \ mgmt/tree_connect.o mgmt/user_session.o smb_common.o \ transport_tcp.o transport_ipc.o smbacl.o smb2pdu.o \ smb2ops.o smb2misc.o ksmbd_spnego_negtokeninit.asn1.o \ - ksmbd_spnego_negtokentarg.asn1.o asn1.o + ksmbd_spnego_negtokentarg.asn1.o asn1.o compress.o $(obj)/asn1.o: $(obj)/ksmbd_spnego_negtokeninit.asn1.h $(obj)/ksmbd_spnego_negtokentarg.asn1.h diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c index 5fe8c667c6b1..1b24eb3f3998 100644 --- a/fs/smb/server/auth.c +++ b/fs/smb/server/auth.c @@ -11,8 +11,8 @@ #include <linux/writeback.h> #include <linux/uio.h> #include <linux/xattr.h> -#include <crypto/hash.h> #include <crypto/aead.h> +#include <crypto/aes-cbc-macs.h> #include <crypto/md5.h> #include <crypto/sha2.h> #include <crypto/utils.h> @@ -24,7 +24,6 @@ #include <linux/fips.h> #include <crypto/arc4.h> -#include <crypto/des.h> #include "server.h" #include "smb_common.h" @@ -122,6 +121,8 @@ static int calc_ntlmv2_hash(struct ksmbd_conn *conn, struct ksmbd_session *sess, out: kfree(uniname); kfree(domain); + if (ret) /* Done by hmac_md5_final() already if ret == 0 */ + memzero_explicit(&ctx, sizeof(ctx)); return ret; } @@ -133,15 +134,17 @@ out: * @blen: NTLMv2 blob length * @domain_name: domain name * @cryptkey: session crypto key + * @sess_key: derived session key output buffer * * Return: 0 on success, error number on error */ int ksmbd_auth_ntlmv2(struct ksmbd_conn *conn, struct ksmbd_session *sess, struct ntlmv2_resp *ntlmv2, int blen, char *domain_name, - char *cryptkey) + char *cryptkey, char *sess_key) { char ntlmv2_hash[CIFS_ENCPWD_SIZE]; char ntlmv2_rsp[CIFS_HMAC_MD5_HASH_SIZE]; + char base_key[SMB2_NTLMV2_SESSKEY_SIZE]; struct hmac_md5_ctx ctx; int rc; @@ -164,12 +167,21 @@ int ksmbd_auth_ntlmv2(struct ksmbd_conn *conn, struct ksmbd_session *sess, /* Generate the session key */ hmac_md5_usingrawkey(ntlmv2_hash, CIFS_HMAC_MD5_HASH_SIZE, ntlmv2_rsp, CIFS_HMAC_MD5_HASH_SIZE, - sess->sess_key); + base_key); if (crypto_memneq(ntlmv2->ntlmv2_hash, ntlmv2_rsp, - CIFS_HMAC_MD5_HASH_SIZE)) - return -EINVAL; - return 0; + CIFS_HMAC_MD5_HASH_SIZE)) { + rc = -EINVAL; + goto out; + } + + memcpy(sess_key, base_key, sizeof(base_key)); + rc = 0; +out: + memzero_explicit(ntlmv2_hash, sizeof(ntlmv2_hash)); + memzero_explicit(ntlmv2_rsp, sizeof(ntlmv2_rsp)); + memzero_explicit(base_key, sizeof(base_key)); + return rc; } /** @@ -179,12 +191,13 @@ int ksmbd_auth_ntlmv2(struct ksmbd_conn *conn, struct ksmbd_session *sess, * @blob_len: length of the @authblob message * @conn: connection * @sess: session of connection + * @sess_key: derived session key output buffer * * Return: 0 on success, error number on error */ int ksmbd_decode_ntlmssp_auth_blob(struct authenticate_message *authblob, int blob_len, struct ksmbd_conn *conn, - struct ksmbd_session *sess) + struct ksmbd_session *sess, char *sess_key) { char *domain_name; unsigned int nt_off, dn_off; @@ -224,8 +237,10 @@ int ksmbd_decode_ntlmssp_auth_blob(struct authenticate_message *authblob, ret = ksmbd_auth_ntlmv2(conn, sess, (struct ntlmv2_resp *)((char *)authblob + nt_off), nt_len - CIFS_ENCPWD_SIZE, - domain_name, conn->ntlmssp.cryptkey); + domain_name, conn->ntlmssp.cryptkey, sess_key); kfree(domain_name); + if (ret) + return ret; /* The recovered secondary session key */ if (conn->ntlmssp.client_flags & NTLMSSP_NEGOTIATE_KEY_XCH) { @@ -245,8 +260,8 @@ int ksmbd_decode_ntlmssp_auth_blob(struct authenticate_message *authblob, if (!ctx_arc4) return -ENOMEM; - arc4_setkey(ctx_arc4, sess->sess_key, SMB2_NTLMV2_SESSKEY_SIZE); - arc4_crypt(ctx_arc4, sess->sess_key, + arc4_setkey(ctx_arc4, sess_key, SMB2_NTLMV2_SESSKEY_SIZE); + arc4_crypt(ctx_arc4, sess_key, (char *)authblob + sess_key_off, sess_key_len); kfree_sensitive(ctx_arc4); } @@ -388,7 +403,8 @@ ksmbd_build_ntlmssp_challenge_blob(struct challenge_message *chgblob, #ifdef CONFIG_SMB_SERVER_KERBEROS5 int ksmbd_krb5_authenticate(struct ksmbd_session *sess, char *in_blob, - int in_len, char *out_blob, int *out_len) + int in_len, char *out_blob, int *out_len, + char *sess_key) { struct ksmbd_spnego_authen_response *resp; struct ksmbd_login_response_ext *resp_ext = NULL; @@ -424,6 +440,7 @@ int ksmbd_krb5_authenticate(struct ksmbd_session *sess, char *in_blob, resp_ext = ksmbd_ipc_login_request_ext(resp->login_response.account); user = ksmbd_alloc_user(&resp->login_response, resp_ext); + kvfree(resp_ext); if (!user) { ksmbd_debug(AUTH, "login failure\n"); retval = -ENOMEM; @@ -436,25 +453,28 @@ int ksmbd_krb5_authenticate(struct ksmbd_session *sess, char *in_blob, } else { if (!ksmbd_compare_user(sess->user, user)) { ksmbd_debug(AUTH, "different user tried to reuse session\n"); - retval = -EPERM; + retval = -EKEYREJECTED; ksmbd_free_user(user); goto out; } ksmbd_free_user(user); } - memcpy(sess->sess_key, resp->payload, resp->session_key_len); + memcpy(sess_key, resp->payload, resp->session_key_len); 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; retval = 0; out: - kvfree(resp); + kvfree_sensitive(resp, sizeof(*resp) + resp->session_key_len + + resp->spnego_blob_len); return retval; } #else int ksmbd_krb5_authenticate(struct ksmbd_session *sess, char *in_blob, - int in_len, char *out_blob, int *out_len) + int in_len, char *out_blob, int *out_len, + char *sess_key) { return -EOPNOTSUPP; } @@ -490,46 +510,21 @@ void ksmbd_sign_smb2_pdu(struct ksmbd_conn *conn, char *key, struct kvec *iov, * @sig: signature value generated for client request packet * */ -int ksmbd_sign_smb3_pdu(struct ksmbd_conn *conn, char *key, struct kvec *iov, - int n_vec, char *sig) +void ksmbd_sign_smb3_pdu(struct ksmbd_conn *conn, char *key, struct kvec *iov, + int n_vec, char *sig) { - struct ksmbd_crypto_ctx *ctx; - int rc, i; - - ctx = ksmbd_crypto_ctx_find_cmacaes(); - if (!ctx) { - ksmbd_debug(AUTH, "could not crypto alloc cmac\n"); - return -ENOMEM; - } - - rc = crypto_shash_setkey(CRYPTO_CMACAES_TFM(ctx), - key, - SMB2_CMACAES_SIZE); - if (rc) - goto out; + struct aes_cmac_key cmac_key __cleanup(aes_cmac_zeroize_key); + struct aes_cmac_ctx cmac_ctx; + int i; - rc = crypto_shash_init(CRYPTO_CMACAES(ctx)); - if (rc) { - ksmbd_debug(AUTH, "cmaces init error %d\n", rc); - goto out; - } + /* This cannot fail, since we always pass a valid key length. */ + static_assert(SMB2_CMACAES_SIZE == AES_KEYSIZE_128); + aes_cmac_preparekey(&cmac_key, key, SMB2_CMACAES_SIZE); - for (i = 0; i < n_vec; i++) { - rc = crypto_shash_update(CRYPTO_CMACAES(ctx), - iov[i].iov_base, - iov[i].iov_len); - if (rc) { - ksmbd_debug(AUTH, "cmaces update error %d\n", rc); - goto out; - } - } - - rc = crypto_shash_final(CRYPTO_CMACAES(ctx), sig); - if (rc) - ksmbd_debug(AUTH, "cmaces generation error %d\n", rc); -out: - ksmbd_release_crypto_ctx(ctx); - return rc; + aes_cmac_init(&cmac_ctx, &cmac_key); + for (i = 0; i < n_vec; i++) + aes_cmac_update(&cmac_ctx, iov[i].iov_base, iov[i].iov_len); + aes_cmac_final(&cmac_ctx, sig); } struct derivation { @@ -538,7 +533,7 @@ struct derivation { bool binding; }; -static void generate_key(struct ksmbd_conn *conn, struct ksmbd_session *sess, +static void generate_key(struct ksmbd_conn *conn, const char *sess_key, struct kvec label, struct kvec context, __u8 *key, unsigned int key_size) { @@ -549,7 +544,7 @@ static void generate_key(struct ksmbd_conn *conn, struct ksmbd_session *sess, unsigned char prfhash[SMB2_HMACSHA256_SIZE]; struct hmac_sha256_ctx ctx; - hmac_sha256_init_usingrawkey(&ctx, sess->sess_key, + hmac_sha256_init_usingrawkey(&ctx, sess_key, SMB2_NTLMV2_SESSKEY_SIZE); hmac_sha256_update(&ctx, i, 4); hmac_sha256_update(&ctx, label.iov_base, label.iov_len); @@ -565,6 +560,7 @@ static void generate_key(struct ksmbd_conn *conn, struct ksmbd_session *sess, hmac_sha256_final(&ctx, prfhash); memcpy(key, prfhash, key_size); + memzero_explicit(prfhash, sizeof(prfhash)); } static int generate_smb3signingkey(struct ksmbd_session *sess, @@ -572,29 +568,28 @@ static int generate_smb3signingkey(struct ksmbd_session *sess, const struct derivation *signing) { struct channel *chann; - char *key; + char *key, *sess_key; chann = lookup_chann_list(sess, conn); if (!chann) return 0; - if (conn->dialect >= SMB30_PROT_ID && signing->binding) + if (conn->dialect >= SMB30_PROT_ID && signing->binding) { key = chann->smb3signingkey; - else + sess_key = chann->sess_key; + } else { key = sess->smb3signingkey; + sess_key = sess->sess_key; + } - generate_key(conn, sess, signing->label, signing->context, key, + generate_key(conn, sess_key, signing->label, signing->context, key, SMB3_SIGN_KEY_SIZE); if (!(conn->dialect >= SMB30_PROT_ID && signing->binding)) memcpy(chann->smb3signingkey, key, SMB3_SIGN_KEY_SIZE); - ksmbd_debug(AUTH, "dumping generated AES signing keys\n"); + ksmbd_debug(AUTH, "generated SMB3 signing key\n"); ksmbd_debug(AUTH, "Session Id %llu\n", sess->id); - ksmbd_debug(AUTH, "Session Key %*ph\n", - SMB2_NTLMV2_SESSKEY_SIZE, sess->sess_key); - ksmbd_debug(AUTH, "Signing Key %*ph\n", - SMB3_SIGN_KEY_SIZE, key); return 0; } @@ -644,31 +639,17 @@ static void generate_smb3encryptionkey(struct ksmbd_conn *conn, struct ksmbd_session *sess, const struct derivation_twin *ptwin) { - generate_key(conn, sess, ptwin->encryption.label, + generate_key(conn, sess->sess_key, ptwin->encryption.label, ptwin->encryption.context, sess->smb3encryptionkey, SMB3_ENC_DEC_KEY_SIZE); - generate_key(conn, sess, ptwin->decryption.label, + generate_key(conn, sess->sess_key, ptwin->decryption.label, ptwin->decryption.context, sess->smb3decryptionkey, SMB3_ENC_DEC_KEY_SIZE); - ksmbd_debug(AUTH, "dumping generated AES encryption keys\n"); + ksmbd_debug(AUTH, "generated SMB3 encryption/decryption keys\n"); ksmbd_debug(AUTH, "Cipher type %d\n", conn->cipher_type); ksmbd_debug(AUTH, "Session Id %llu\n", sess->id); - ksmbd_debug(AUTH, "Session Key %*ph\n", - SMB2_NTLMV2_SESSKEY_SIZE, sess->sess_key); - if (conn->cipher_type == SMB2_ENCRYPTION_AES256_CCM || - conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) { - ksmbd_debug(AUTH, "ServerIn Key %*ph\n", - SMB3_GCM256_CRYPTKEY_SIZE, sess->smb3encryptionkey); - ksmbd_debug(AUTH, "ServerOut Key %*ph\n", - SMB3_GCM256_CRYPTKEY_SIZE, sess->smb3decryptionkey); - } else { - ksmbd_debug(AUTH, "ServerIn Key %*ph\n", - SMB3_GCM128_CRYPTKEY_SIZE, sess->smb3encryptionkey); - ksmbd_debug(AUTH, "ServerOut Key %*ph\n", - SMB3_GCM128_CRYPTKEY_SIZE, sess->smb3decryptionkey); - } } void ksmbd_gen_smb30_encryptionkey(struct ksmbd_conn *conn, @@ -740,8 +721,21 @@ static int ksmbd_get_encryption_key(struct ksmbd_work *work, __u64 ses_id, if (enc) sess = work->sess; - else - sess = ksmbd_session_lookup_all(work->conn, ses_id); + else { + /* + * A previous-session replacement leaves the old encryption key in + * place. Use it to authenticate an encrypted request, then let + * session validation reject the expired session. This preserves the + * encrypted STATUS_USER_SESSION_DELETED response without reviving + * the session. + */ + sess = ksmbd_session_lookup_all_states(work->conn, ses_id); + if (sess && sess->state != SMB2_SESSION_VALID && + (sess->state != SMB2_SESSION_EXPIRED || !sess->enc)) { + ksmbd_user_session_put(sess); + sess = NULL; + } + } if (!sess) return -EINVAL; @@ -838,6 +832,189 @@ static struct scatterlist *ksmbd_init_sg(struct kvec *iov, unsigned int nvec, return sg; } +/** + * ksmbd_init_rdma_sg() - build an AEAD scatterlist for an RDMA payload + * @buf: payload buffer + * @buflen: payload length + * @tag: authentication tag buffer + * @taglen: authentication tag length + * + * Split vmalloc-backed payloads at page boundaries and append the detached + * authentication tag as the final scatterlist entry. + * + * Return: allocated scatterlist, or NULL on allocation failure + */ +static struct scatterlist *ksmbd_init_rdma_sg(void *buf, + unsigned int buflen, + u8 *tag, + unsigned int taglen) +{ + struct scatterlist *sg; + unsigned int nr_data = 1, nr_entries, i = 0; + void *data = buf; + int len = buflen; + + if (is_vmalloc_addr(buf)) + nr_data = DIV_ROUND_UP(offset_in_page(buf) + buflen, PAGE_SIZE); + nr_entries = nr_data + 1; + + sg = kmalloc_objs(struct scatterlist, nr_entries, KSMBD_DEFAULT_GFP); + if (!sg) + return NULL; + + sg_init_table(sg, nr_entries); + if (!is_vmalloc_addr(buf)) { + smb2_sg_set_buf(&sg[i++], buf, buflen); + } else { + while (len) { + unsigned int bytes = min_t(unsigned int, + PAGE_SIZE - offset_in_page(data), len); + + sg_set_page(&sg[i++], vmalloc_to_page(data), bytes, + offset_in_page(data)); + data += bytes; + len -= bytes; + } + } + smb2_sg_set_buf(&sg[i], tag, taglen); + return sg; +} + +/** + * ksmbd_crypt_rdma() - encrypt or decrypt an SMB Direct data buffer + * @conn: connection containing the negotiated cipher + * @key: session encryption or decryption key + * @buf: RDMA payload, transformed in place + * @buflen: payload length (the authentication tag is carried out of band) + * @nonce: transform nonce + * @nonce_len: nonce length + * @tag: authentication tag output for encryption, input for decryption + * @tag_len: authentication tag length + * @enc: true to encrypt, false to decrypt + * + * SMB2_RDMA_CRYPTO_TRANSFORM carries the nonce and authentication tag in the + * SMB2 message while only the payload is transferred through RDMA. Therefore + * this uses AEAD without the normal SMB3 transform header as associated data. + * + * Return: 0 on success, otherwise a negative errno + */ +int ksmbd_crypt_rdma(struct ksmbd_conn *conn, const u8 *key, + void *buf, unsigned int buflen, const u8 *nonce, + unsigned int nonce_len, u8 *tag, unsigned int tag_len, + bool enc) +{ + struct ksmbd_crypto_ctx *ctx; + struct crypto_aead *tfm; + struct aead_request *req = NULL; + struct scatterlist *sg = NULL; + unsigned int iv_len, crypt_len; + u8 auth_tag[SMB2_SIGNATURE_SIZE] = {}; + u8 *iv = NULL; + u16 cipher = le16_to_cpu(conn->cipher_type); + int rc; + DECLARE_CRYPTO_WAIT(wait); + + if (!buflen || !tag_len || tag_len > SMB2_SIGNATURE_SIZE) { + pr_err("RDMA %s rejected: cipher=0x%04x payload=%u nonce=%u tag=%u\n", + enc ? "encryption" : "decryption", cipher, buflen, + nonce_len, tag_len); + return -EINVAL; + } + if (!enc) + memcpy(auth_tag, tag, tag_len); + + if (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || + conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) { + if (nonce_len != SMB3_AES_GCM_NONCE) { + pr_err("RDMA %s rejected: cipher=0x%04x invalid nonce=%u expected=%u\n", + enc ? "encryption" : "decryption", cipher, + nonce_len, SMB3_AES_GCM_NONCE); + return -EINVAL; + } + ctx = ksmbd_crypto_ctx_find_gcm(); + } else { + if (nonce_len != SMB3_AES_CCM_NONCE) { + pr_err("RDMA %s rejected: cipher=0x%04x invalid nonce=%u expected=%u\n", + enc ? "encryption" : "decryption", cipher, + nonce_len, SMB3_AES_CCM_NONCE); + return -EINVAL; + } + ctx = ksmbd_crypto_ctx_find_ccm(); + } + if (!ctx) { + pr_err("RDMA %s failed: cipher=0x%04x crypto context unavailable\n", + enc ? "encryption" : "decryption", cipher); + return -ENOMEM; + } + + tfm = (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || + conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) ? + CRYPTO_GCM(ctx) : CRYPTO_CCM(ctx); + if (conn->cipher_type == SMB2_ENCRYPTION_AES256_CCM || + conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) + rc = crypto_aead_setkey(tfm, key, SMB3_GCM256_CRYPTKEY_SIZE); + else + rc = crypto_aead_setkey(tfm, key, SMB3_GCM128_CRYPTKEY_SIZE); + if (rc) + goto out; + + rc = crypto_aead_setauthsize(tfm, tag_len); + if (rc) + goto out; + + req = aead_request_alloc(tfm, KSMBD_DEFAULT_GFP); + if (!req) { + rc = -ENOMEM; + goto out; + } + + sg = ksmbd_init_rdma_sg(buf, buflen, auth_tag, tag_len); + if (!sg) { + rc = -ENOMEM; + goto out; + } + + iv_len = crypto_aead_ivsize(tfm); + iv = kzalloc(iv_len, KSMBD_DEFAULT_GFP); + if (!iv) { + rc = -ENOMEM; + goto out; + } + if (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || + conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) { + memcpy(iv, nonce, nonce_len); + } else { + iv[0] = 3; + memcpy(iv + 1, nonce, nonce_len); + } + + crypt_len = buflen + (enc ? 0 : tag_len); + aead_request_set_crypt(req, sg, sg, crypt_len, iv); + aead_request_set_ad(req, 0); + aead_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG | + CRYPTO_TFM_REQ_MAY_SLEEP, + crypto_req_done, &wait); + rc = crypto_wait_req(enc ? crypto_aead_encrypt(req) : + crypto_aead_decrypt(req), &wait); + if (!rc && enc) + memcpy(tag, auth_tag, tag_len); +out: + kfree(iv); + kfree(sg); + aead_request_free(req); + ksmbd_release_crypto_ctx(ctx); + if (rc) + pr_err("RDMA %s failed: cipher=0x%04x payload=%u nonce=%u tag=%u rc=%d\n", + enc ? "encryption" : "decryption", cipher, buflen, + nonce_len, tag_len, rc); + else + ksmbd_debug(RDMA, + "RDMA %s completed: cipher=0x%04x payload=%u nonce=%u tag=%u\n", + enc ? "encryption" : "decryption", cipher, buflen, + nonce_len, tag_len); + return rc; +} + int ksmbd_crypt_message(struct ksmbd_work *work, struct kvec *iov, unsigned int nvec, int enc) { @@ -845,6 +1022,7 @@ int ksmbd_crypt_message(struct ksmbd_work *work, struct kvec *iov, struct smb2_transform_hdr *tr_hdr = smb_get_msg(iov[0].iov_base); unsigned int assoc_data_len = sizeof(struct smb2_transform_hdr) - 20; int rc; + DECLARE_CRYPTO_WAIT(wait); struct scatterlist *sg; u8 sign[SMB2_SIGNATURE_SIZE] = {}; u8 key[SMB3_ENC_DEC_KEY_SIZE]; @@ -871,7 +1049,8 @@ int ksmbd_crypt_message(struct ksmbd_work *work, struct kvec *iov, ctx = ksmbd_crypto_ctx_find_ccm(); if (!ctx) { pr_err("crypto alloc failed\n"); - return -ENOMEM; + rc = -ENOMEM; + goto zeroize_key; } if (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || @@ -931,12 +1110,12 @@ int ksmbd_crypt_message(struct ksmbd_work *work, struct kvec *iov, aead_request_set_crypt(req, sg, sg, crypt_len, iv); aead_request_set_ad(req, assoc_data_len); - aead_request_set_callback(req, CRYPTO_TFM_REQ_MAY_SLEEP, NULL, NULL); + aead_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG | + CRYPTO_TFM_REQ_MAY_SLEEP, + crypto_req_done, &wait); - if (enc) - rc = crypto_aead_encrypt(req); - else - rc = crypto_aead_decrypt(req); + rc = crypto_wait_req(enc ? crypto_aead_encrypt(req) : + crypto_aead_decrypt(req), &wait); if (rc) goto free_iv; @@ -951,5 +1130,8 @@ free_req: aead_request_free(req); free_ctx: ksmbd_release_crypto_ctx(ctx); +zeroize_key: + memzero_explicit(key, sizeof(key)); + memzero_explicit(sign, sizeof(sign)); return rc; } diff --git a/fs/smb/server/auth.h b/fs/smb/server/auth.h index 6d351d61b0e5..7ce9c42d58f1 100644 --- a/fs/smb/server/auth.h +++ b/fs/smb/server/auth.h @@ -38,24 +38,29 @@ struct kvec; int ksmbd_crypt_message(struct ksmbd_work *work, struct kvec *iov, unsigned int nvec, int enc); +int ksmbd_crypt_rdma(struct ksmbd_conn *conn, const u8 *key, + void *buf, unsigned int buflen, const u8 *nonce, + unsigned int nonce_len, u8 *tag, unsigned int tag_len, + bool enc); void ksmbd_copy_gss_neg_header(void *buf); int ksmbd_auth_ntlmv2(struct ksmbd_conn *conn, struct ksmbd_session *sess, struct ntlmv2_resp *ntlmv2, int blen, char *domain_name, - char *cryptkey); + char *cryptkey, char *sess_key); int ksmbd_decode_ntlmssp_auth_blob(struct authenticate_message *authblob, int blob_len, struct ksmbd_conn *conn, - struct ksmbd_session *sess); + struct ksmbd_session *sess, char *sess_key); int ksmbd_decode_ntlmssp_neg_blob(struct negotiate_message *negblob, int blob_len, struct ksmbd_conn *conn); unsigned int ksmbd_build_ntlmssp_challenge_blob(struct challenge_message *chgblob, struct ksmbd_conn *conn); int ksmbd_krb5_authenticate(struct ksmbd_session *sess, char *in_blob, - int in_len, char *out_blob, int *out_len); + int in_len, char *out_blob, int *out_len, + char *sess_key); void ksmbd_sign_smb2_pdu(struct ksmbd_conn *conn, char *key, struct kvec *iov, int n_vec, char *sig); -int ksmbd_sign_smb3_pdu(struct ksmbd_conn *conn, char *key, struct kvec *iov, - int n_vec, char *sig); +void ksmbd_sign_smb3_pdu(struct ksmbd_conn *conn, char *key, struct kvec *iov, + int n_vec, char *sig); int ksmbd_gen_smb30_signingkey(struct ksmbd_session *sess, struct ksmbd_conn *conn); int ksmbd_gen_smb311_signingkey(struct ksmbd_session *sess, diff --git a/fs/smb/server/compress.c b/fs/smb/server/compress.c new file mode 100644 index 000000000000..5162fb84c755 --- /dev/null +++ b/fs/smb/server/compress.c @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SMB2 compression support for ksmbd. + * + * Receive and send SMB 3.1.1 compression transforms using the common helpers. + * + * Copyright (C) 2026 Namjae Jeon <linkinjeon@kernel.org> + */ +#include <linux/slab.h> + +#include "compress.h" +#include "smb_common.h" +#include "../common/compress/lz77.h" + +#define SMB_COMPRESS_MIN_LEN PAGE_SIZE + +static int __ksmbd_decompress_request(struct ksmbd_conn *conn, + void *request_buf, void **out_buf) +{ + struct smb2_compression_hdr *hdr; + unsigned int pdu_size = get_rfc1002_len(request_buf); + u32 orig_size, offset, out_size; + u32 max_allowed_pdu_size; + char *out; + int rc; + + if (pdu_size < sizeof(struct smb2_compression_hdr)) + return -EINVAL; + + if (conn->dialect != SMB311_PROT_ID || + conn->compress_algorithm == SMB3_COMPRESS_NONE) + return -EINVAL; + + hdr = smb_get_msg(request_buf); + if (hdr->ProtocolId != SMB2_COMPRESSION_TRANSFORM_ID) + return -EINVAL; + + orig_size = le32_to_cpu(hdr->OriginalCompressedSegmentSize); + /* + * For chained transforms the top-level header is only eight bytes; the + * Flags field overlays the first payload header. Reject unknown Flags + * and unnegotiated chained mode before allocating the output buffer. + */ + if (hdr->Flags == cpu_to_le16(SMB2_COMPRESSION_FLAG_CHAINED)) { + if (!conn->compress_chained) + return -EINVAL; + out_size = orig_size; + } else if (hdr->Flags == cpu_to_le16(SMB2_COMPRESSION_FLAG_NONE)) { + offset = le32_to_cpu(hdr->Offset); + if (offset > pdu_size - sizeof(*hdr) || + check_add_overflow(orig_size, offset, &out_size)) + return -EINVAL; + } else { + return -EINVAL; + } + + max_allowed_pdu_size = ksmbd_max_allowed_pdu_size(conn); + if (out_size < sizeof(struct smb2_pdu) || + out_size > max_allowed_pdu_size || + out_size > MAX_STREAM_PROT_LEN) + return -EINVAL; + + out = kvmalloc(out_size + 4 + 1, KSMBD_DEFAULT_GFP); + if (!out) + return -ENOMEM; + + *(__be32 *)out = cpu_to_be32(out_size); + rc = smb_compression_decompress(conn->compress_algorithm, + conn->compress_chained, + conn->compress_pattern, + (char *)hdr, pdu_size, out + 4, out_size); + if (rc) { + kvfree(out); + return rc; + } + + *out_buf = out; + return 0; +} + +/** + * ksmbd_decompress_request() - replace a compressed request with its SMB2 PDU + * @conn: connection which owns the current RFC1002 request buffer + * + * Derive the uncompressed size from the transform variant, enforce ksmbd's + * normal message limits, and ask the common decoder to validate every payload. + * On success, replace conn->request_buf with a regular RFC1002-framed SMB2 + * message so the rest of the request path needs no compression awareness. + * + * Return: 0 on success, otherwise a negative errno. + */ +int ksmbd_decompress_request(struct ksmbd_conn *conn) +{ + void *out_buf; + int rc; + + rc = __ksmbd_decompress_request(conn, conn->request_buf, &out_buf); + if (rc) + return rc; + + kvfree(conn->request_buf); + conn->request_buf = out_buf; + return 0; +} + +/** + * ksmbd_decompress_work_request() - decompress an encrypted work request + * @work: work item whose request buffer contains a compression transform + * + * SMB3 encrypts a compressed message by applying compression first and + * encryption second. The receive loop can therefore only decode the + * compression transform before work allocation for an unencrypted request; + * an encrypted request must be decompressed after its encryption layer has + * been removed. + * + * Return: 0 on success, otherwise a negative errno. + */ +int ksmbd_decompress_work_request(struct ksmbd_work *work) +{ + void *out_buf; + int rc; + + rc = __ksmbd_decompress_request(work->conn, work->request_buf, + &out_buf); + if (rc) + return rc; + + kvfree(work->request_buf); + work->request_buf = out_buf; + return 0; +} + +/** + * ksmbd_compress_response() - compress an eligible ksmbd response + * @work: request work item containing the response iov + * + * Compression transforms describe one contiguous SMB2 message, while ksmbd + * builds responses from multiple iov entries. Flatten the response first, + * produce the negotiated transform, and replace the response iov only when the + * result is smaller than the original message. + * + * Encrypted and compound responses are intentionally left unchanged. The + * caller may still continue sending the original response when this function + * returns zero. + * + * Return: 1 if the response was replaced, 0 if compression was skipped, or a + * negative errno on failure. + */ +int ksmbd_compress_response(struct ksmbd_work *work) +{ + struct smb2_compression_hdr *chdr; + struct smb2_hdr *req_hdr; + u32 src_len, dst_len, compressed_pdu_len, max_dst_len; + u8 *src = NULL, *out = NULL, *p; + int i, rc; + + if (!work->compress_response || work->encrypted || + work->conn->compress_algorithm != SMB3_COMPRESS_LZ77) + return 0; + + req_hdr = smb_get_msg(work->request_buf); + if (req_hdr->NextCommand || work->next_smb2_rcv_hdr_off || + work->next_smb2_rsp_hdr_off) + return 0; + + src_len = get_rfc1002_len(work->iov[0].iov_base); + if (src_len < SMB_COMPRESS_MIN_LEN) + return 0; + + src = kvmalloc(src_len, KSMBD_DEFAULT_GFP); + if (!src) + return -ENOMEM; + + p = src; + /* iov[0] contains only the RFC1002 length; the SMB2 PDU starts at iov[1]. */ + for (i = 1; i < work->iov_cnt; i++) { + if (work->iov[i].iov_len > src + src_len - p) { + rc = -EINVAL; + goto out; + } + memcpy(p, work->iov[i].iov_base, work->iov[i].iov_len); + p += work->iov[i].iov_len; + } + if (p != src + src_len) { + rc = -EINVAL; + goto out; + } + + max_dst_len = smb_lz77_compressed_alloc_size(src_len) + + sizeof(struct smb2_compression_hdr) + + 3 * sizeof(struct smb2_compression_payload_hdr) + + 2 * sizeof(struct smb2_compression_pattern_v1); + out = kvzalloc(sizeof(__be32) + max_dst_len, + KSMBD_DEFAULT_GFP); + if (!out) { + rc = -ENOMEM; + goto out; + } + + if (work->conn->compress_chained) { + dst_len = max_dst_len; + rc = smb_compression_compress_chained(SMB3_COMPRESS_LZ77, + work->conn->compress_pattern, + src, src_len, + out + sizeof(__be32), + &dst_len); + if (rc == -EMSGSIZE || dst_len >= src_len) { + rc = 0; + goto out; + } + if (rc) + goto out; + compressed_pdu_len = dst_len; + } else { + /* + * Peers which did not negotiate chained compression still use + * the original 16-byte unchained transform format. + */ + dst_len = smb_lz77_compressed_alloc_size(src_len); + rc = smb_lz77_compress(src, src_len, + out + sizeof(__be32) + sizeof(*chdr), + &dst_len); + if (rc == -EMSGSIZE || + dst_len + sizeof(*chdr) >= src_len) { + rc = 0; + goto out; + } + if (rc) + goto out; + + compressed_pdu_len = sizeof(*chdr) + dst_len; + chdr = (struct smb2_compression_hdr *)(out + sizeof(__be32)); + chdr->ProtocolId = SMB2_COMPRESSION_TRANSFORM_ID; + chdr->OriginalCompressedSegmentSize = cpu_to_le32(src_len); + chdr->CompressionAlgorithm = SMB3_COMPRESS_LZ77; + chdr->Flags = cpu_to_le16(SMB2_COMPRESSION_FLAG_NONE); + chdr->Offset = 0; + } + + *(__be32 *)out = cpu_to_be32(compressed_pdu_len); + + /* + * Keep the transform in work->compress_buf until send completion. + * Existing response iovs can then be replaced without changing their + * individual ownership rules. + */ + work->compress_buf = out; + work->iov[0].iov_base = out; + work->iov[0].iov_len = sizeof(__be32); + work->iov[1].iov_base = out + sizeof(__be32); + work->iov[1].iov_len = compressed_pdu_len; + work->iov_cnt = 2; + work->iov_idx = 1; + out = NULL; + rc = 1; +out: + kvfree(out); + kvfree(src); + return rc; +} diff --git a/fs/smb/server/compress.h b/fs/smb/server/compress.h new file mode 100644 index 000000000000..13df2eb221e8 --- /dev/null +++ b/fs/smb/server/compress.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * SMB2 compression support for ksmbd. + * + * Copyright (C) 2026 Namjae Jeon <linkinjeon@kernel.org> + */ +#ifndef __KSMBD_COMPRESS_H__ +#define __KSMBD_COMPRESS_H__ + +#include "connection.h" +#include "../common/compress/compress.h" + +int ksmbd_decompress_request(struct ksmbd_conn *conn); +int ksmbd_decompress_work_request(struct ksmbd_work *work); +int ksmbd_compress_response(struct ksmbd_work *work); + +#endif /* __KSMBD_COMPRESS_H__ */ diff --git a/fs/smb/server/connection.c b/fs/smb/server/connection.c index 1bb2081c492c..d211861ff86f 100644 --- a/fs/smb/server/connection.c +++ b/fs/smb/server/connection.c @@ -11,7 +11,10 @@ #include "server.h" #include "smb_common.h" #include "mgmt/ksmbd_ida.h" +#include "mgmt/user_session.h" #include "connection.h" +#include "vfs_cache.h" +#include "compress.h" #include "transport_tcp.h" #include "transport_rdma.h" #include "misc.h" @@ -19,6 +22,8 @@ static DEFINE_MUTEX(init_lock); static struct ksmbd_conn_ops default_conn_ops; +static struct delayed_work session_expiration_work; +static bool stopping_session_expiration_work; DEFINE_HASHTABLE(conn_list, CONN_HASH_BITS); DECLARE_RWSEM(conn_list_lock); @@ -26,33 +31,107 @@ DECLARE_RWSEM(conn_list_lock); #ifdef CONFIG_PROC_FS static struct proc_dir_entry *proc_clients; +static const char *ksmbd_conn_state_string(struct ksmbd_conn *conn) +{ + switch (READ_ONCE(conn->status)) { + case KSMBD_SESS_NEW: + return "new"; + case KSMBD_SESS_GOOD: + return "good"; + case KSMBD_SESS_EXITING: + return "exiting"; + case KSMBD_SESS_NEED_RECONNECT: + return "reconnect"; + case KSMBD_SESS_NEED_NEGOTIATE: + return "negotiate"; + case KSMBD_SESS_NEED_SETUP: + return "setup"; + case KSMBD_SESS_RELEASING: + return "releasing"; + default: + return "unknown"; + } +} + +static const char *ksmbd_conn_transport_string(struct ksmbd_conn *conn) +{ + if (conn->transport->ops->rdma_read || conn->transport->ops->rdma_write) + return "smbdirect"; + return "tcp"; +} + +static void proc_show_conn_feature(struct seq_file *m, bool *separator, + bool enabled, const char *name) +{ + if (!enabled) + return; + seq_printf(m, "%s%s", *separator ? "," : "", name); + *separator = true; +} + +static void proc_show_conn_features(struct seq_file *m, + struct ksmbd_conn *conn) +{ + bool separator = false; + + proc_show_conn_feature(m, &separator, + conn->sign || conn->signing_negotiated, "sign"); + proc_show_conn_feature(m, &separator, conn->cipher_type, "encrypt"); + proc_show_conn_feature(m, &separator, + conn->compress_algorithm != SMB3_COMPRESS_NONE, + "compress"); + proc_show_conn_feature(m, &separator, conn->rdma_transform_ids, + "rdma-transform"); + proc_show_conn_feature(m, &separator, conn->posix_ext_supported, "posix"); + if (!separator) + seq_puts(m, "none"); +} + static int proc_show_clients(struct seq_file *m, void *v) { struct ksmbd_conn *conn; struct timespec64 now, t; int i; - seq_printf(m, "#%-20s %-10s %-10s %-10s %-10s %-10s\n", - "<name>", "<dialect>", "<credits>", "<open files>", - "<requests>", "<last active>"); - down_read(&conn_list_lock); hash_for_each(conn_list, i, conn, hlist) { + unsigned int outstanding_credits, total_credits; + unsigned long id; + void *entry; + unsigned int sessions = 0; + jiffies_to_timespec64(jiffies - conn->last_active, &t); ktime_get_real_ts64(&now); t = timespec64_sub(now, t); + + spin_lock(&conn->credits_lock); + outstanding_credits = conn->outstanding_credits; + total_credits = conn->total_credits; + spin_unlock(&conn->credits_lock); + + rcu_read_lock(); + xa_for_each(&conn->sessions, id, entry) + sessions++; + rcu_read_unlock(); #if IS_ENABLED(CONFIG_IPV6) if (!conn->inet_addr) - seq_printf(m, "%-20pI6c", &conn->inet6_addr); + seq_printf(m, "client:\t%pI6c\n", &conn->inet6_addr); else #endif - seq_printf(m, "%-20pI4", &conn->inet_addr); - seq_printf(m, " 0x%-10x %-10u %-12d %-10d %ptT\n", - conn->dialect, - conn->total_credits, - atomic_read(&conn->stats.open_files_count), - atomic_read(&conn->req_running), - &t); + seq_printf(m, "client:\t%pI4\n", &conn->inet_addr); + seq_printf(m, "transport:\t%s\n", ksmbd_conn_transport_string(conn)); + seq_printf(m, "state:\t%s\n", ksmbd_conn_state_string(conn)); + seq_printf(m, "dialect:\t0x%04x\n", conn->dialect); + seq_printf(m, "credits:\t%u/%u\n", outstanding_credits, + total_credits); + seq_printf(m, "sessions:\t%u\n", sessions); + seq_printf(m, "open_files:\t%d\n", + atomic_read(&conn->stats.open_files_count)); + seq_printf(m, "requests:\t%lld\n", + atomic64_read(&conn->stats.request_served)); + seq_puts(m, "features:\t"); + proc_show_conn_features(m, conn); + seq_printf(m, "\nlast_active:\t%ptT\n\n", &t); } up_read(&conn_list_lock); return 0; @@ -79,6 +158,95 @@ static int create_proc_clients(void) { return 0; } static void delete_proc_clients(void) {} #endif +static struct workqueue_struct *ksmbd_conn_wq; + +static void ksmbd_session_expiration_worker(struct work_struct *work); + +int ksmbd_conn_wq_init(void) +{ + ksmbd_conn_wq = alloc_workqueue("ksmbd-conn-release", + WQ_UNBOUND | WQ_MEM_RECLAIM, 0); + if (!ksmbd_conn_wq) + return -ENOMEM; + + WRITE_ONCE(stopping_session_expiration_work, false); + INIT_DELAYED_WORK(&session_expiration_work, + ksmbd_session_expiration_worker); + queue_delayed_work(ksmbd_conn_wq, &session_expiration_work, + KSMBD_SESSION_EXPIRATION_INTERVAL); + return 0; +} + +void ksmbd_conn_wq_destroy(void) +{ + if (ksmbd_conn_wq) { + WRITE_ONCE(stopping_session_expiration_work, true); + cancel_delayed_work_sync(&session_expiration_work); + destroy_workqueue(ksmbd_conn_wq); + ksmbd_conn_wq = NULL; + } +} + +/* + * __ksmbd_conn_release_work() - perform the final, once-per-struct cleanup + * of a ksmbd_conn whose refcount has just dropped to zero. + * + * This is the common release path used by ksmbd_conn_put() for the embedded + * state that outlives the connection thread: async_ida and the attached + * transport (which owns the socket and iov for TCP). Called from a workqueue + * so that sleep-allowed teardown (sock_release -> tcp_close -> + * lock_sock_nested) never runs from an RCU softirq callback (free_opinfo_rcu) + * or any other non-sleeping putter context. + */ +static void __ksmbd_conn_release_work(struct work_struct *work) +{ + struct ksmbd_conn *conn = + container_of(work, struct ksmbd_conn, release_work); + + ida_destroy(&conn->async_ida); + conn->transport->ops->free_transport(conn->transport); + kfree_sensitive(conn); +} + +/** + * ksmbd_conn_get() - take a reference on @conn and return it. + * + * @conn: connection instance to get a reference to + * + * Returns @conn unchanged so callers can write + * "fp->conn = ksmbd_conn_get(work->conn);" in one expression. Returns NULL + * if @conn is NULL. + */ +struct ksmbd_conn *ksmbd_conn_get(struct ksmbd_conn *conn) +{ + if (!conn) + return NULL; + + atomic_inc(&conn->refcnt); + return conn; +} + +/** + * ksmbd_conn_put() - drop a reference and, if it was the last, queue the + * release onto ksmbd_conn_wq so it runs from process context. + * + * @conn: connection instance to put a reference to + * + * Callable from any context including RCU softirq callbacks and non-sleeping + * locks; the actual release is deferred to the workqueue. ksmbd_conn_wq is + * created in ksmbd_server_init() before any conn can be allocated and is + * destroyed in ksmbd_server_exit() after rcu_barrier(), so it is always + * non-NULL while a conn reference is held. + */ +void ksmbd_conn_put(struct ksmbd_conn *conn) +{ + if (!conn) + return; + + if (atomic_dec_and_test(&conn->refcnt)) + queue_work(ksmbd_conn_wq, &conn->release_work); +} + /** * ksmbd_conn_free() - free resources of the connection instance * @@ -93,13 +261,20 @@ void ksmbd_conn_free(struct ksmbd_conn *conn) hash_del(&conn->hlist); up_write(&conn_list_lock); + /* + * request_buf / preauth_info / mechToken are only ever accessed by the + * connection handler thread that owns @conn. ksmbd_conn_free() is + * called from the transport free_transport() path when that thread is + * exiting, so it is safe to release them unconditionally even when + * ksmbd_conn_put() below is not the final putter (oplock / ksmbd_file + * holders only retain the conn pointer, not these per-thread buffers). + */ xa_destroy(&conn->sessions); kvfree(conn->request_buf); - kfree(conn->preauth_info); - if (atomic_dec_and_test(&conn->refcnt)) { - conn->transport->ops->free_transport(conn->transport); - kfree(conn); - } + kfree_sensitive(conn->preauth_info); + kfree(conn->mechToken); + ksmbd_preauth_session_destroy(conn); + ksmbd_conn_put(conn); } /** @@ -116,6 +291,7 @@ struct ksmbd_conn *ksmbd_conn_alloc(void) return NULL; conn->need_neg = true; + conn->creation_time = jiffies; ksmbd_conn_set_new(conn); conn->local_nls = load_nls("utf8"); if (!conn->local_nls) @@ -126,16 +302,26 @@ struct ksmbd_conn *ksmbd_conn_alloc(void) conn->um = ERR_PTR(-EOPNOTSUPP); if (IS_ERR(conn->um)) conn->um = NULL; + INIT_WORK(&conn->release_work, __ksmbd_conn_release_work); atomic_set(&conn->req_running, 0); atomic_set(&conn->r_count, 0); atomic_set(&conn->refcnt, 1); conn->total_credits = 1; conn->outstanding_credits = 0; + /* + * The command sequence window starts as the set { 0 } when the + * connection is established. + */ + conn->seq_low = 0; + conn->seq_high = 1; + __set_bit(0, conn->seq_bitmap); + init_waitqueue_head(&conn->req_running_q); init_waitqueue_head(&conn->r_count_q); INIT_LIST_HEAD(&conn->requests); INIT_LIST_HEAD(&conn->async_requests); + INIT_LIST_HEAD(&conn->preauth_sess_table); spin_lock_init(&conn->request_lock); spin_lock_init(&conn->credits_lock); ida_init(&conn->async_ida); @@ -204,6 +390,26 @@ void ksmbd_conn_try_dequeue_request(struct ksmbd_work *work) wake_up_all(&conn->req_running_q); } +static void ksmbd_conn_cancel_async_requests(struct ksmbd_conn *conn) +{ + struct ksmbd_work *work, *tmp; + + ksmbd_debug(CONN, "Cancel pending async requests on releasing connection\n"); + spin_lock(&conn->request_lock); + list_for_each_entry_safe(work, tmp, &conn->async_requests, + async_request_entry) { + if (cmpxchg(&work->state, KSMBD_WORK_ACTIVE, + KSMBD_WORK_CANCELLED) != KSMBD_WORK_ACTIVE) + continue; + + ksmbd_debug(CONN, "Cancel async request id %d\n", + work->async_id); + if (work->cancel_fn) + work->cancel_fn(work->cancel_argv); + } + spin_unlock(&conn->request_lock); +} + void ksmbd_conn_lock(struct ksmbd_conn *conn) { mutex_lock(&conn->srv_mutex); @@ -214,39 +420,80 @@ void ksmbd_conn_unlock(struct ksmbd_conn *conn) mutex_unlock(&conn->srv_mutex); } -void ksmbd_all_conn_set_status(u64 sess_id, u32 status) +static bool ksmbd_session_is_bound_to_conn(struct ksmbd_session *sess, + struct ksmbd_conn *conn) +{ + bool found; + + rcu_read_lock(); + found = xa_load(&conn->sessions, sess->id) == sess; + rcu_read_unlock(); + if (found) + return true; + + down_read(&sess->chann_lock); + found = xa_load(&sess->ksmbd_chann_list, (long)conn); + up_read(&sess->chann_lock); + return found; +} + +void ksmbd_all_conn_set_status(struct ksmbd_session *sess, u32 status) { struct ksmbd_conn *conn; int bkt; down_read(&conn_list_lock); hash_for_each(conn_list, bkt, conn, hlist) { - if (conn->binding || xa_load(&conn->sessions, sess_id)) - WRITE_ONCE(conn->status, status); + if (ksmbd_session_is_bound_to_conn(sess, conn)) { + spin_lock(&conn->request_lock); + if (!ksmbd_conn_exiting(conn) && + !ksmbd_conn_releasing(conn)) + WRITE_ONCE(conn->status, status); + spin_unlock(&conn->request_lock); + } } up_read(&conn_list_lock); } +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)) { + 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) + conn->transport->ops->shutdown(conn->transport); +} + void ksmbd_conn_wait_idle(struct ksmbd_conn *conn) { wait_event(conn->req_running_q, atomic_read(&conn->req_running) < 2); } -int ksmbd_conn_wait_idle_sess_id(struct ksmbd_conn *curr_conn, u64 sess_id) +int ksmbd_conn_wait_idle_sess(struct ksmbd_conn *curr_conn, + struct ksmbd_session *sess) { struct ksmbd_conn *conn; int rc, retry_count = 0, max_timeout = 120; - int rcount = 1, bkt; + int rcount, bkt; retry_idle: if (retry_count >= max_timeout) return -EIO; + /* A blocked byte-range lock cannot drain until teardown wakes it. */ + ksmbd_wake_session_blocked_works(sess); + down_read(&conn_list_lock); hash_for_each(conn_list, bkt, conn, hlist) { - if (conn->binding || xa_load(&conn->sessions, sess_id)) { - if (conn == curr_conn) - rcount = 2; + if (ksmbd_session_is_bound_to_conn(sess, conn)) { + rcount = (conn == curr_conn) ? 2 : 1; if (atomic_read(&conn->req_running) >= rcount) { rc = wait_event_timeout(conn->req_running_q, atomic_read(&conn->req_running) < rcount, @@ -264,7 +511,8 @@ retry_idle: return 0; } -int ksmbd_conn_write(struct ksmbd_work *work) +static int __ksmbd_conn_write(struct ksmbd_work *work, + struct ksmbd_transport_write *tx) { struct ksmbd_conn *conn = work->conn; int sent; @@ -280,12 +528,14 @@ int ksmbd_conn_write(struct ksmbd_work *work) if (!work->iov_idx) return -EINVAL; + tx->iov = work->iov; + tx->iov_cnt = work->iov_cnt; + tx->size = get_rfc1002_len(work->iov[0].iov_base) + 4; + tx->need_invalidate_rkey = work->need_invalidate_rkey; + tx->remote_key = work->remote_key; + ksmbd_conn_lock(conn); - sent = conn->transport->ops->writev(conn->transport, work->iov, - work->iov_cnt, - get_rfc1002_len(work->iov[0].iov_base) + 4, - work->need_invalidate_rkey, - work->remote_key); + sent = conn->transport->ops->writev(conn->transport, tx); ksmbd_conn_unlock(conn); if (sent < 0) { @@ -296,6 +546,22 @@ int ksmbd_conn_write(struct ksmbd_work *work) return 0; } +int ksmbd_conn_write(struct ksmbd_work *work) +{ + struct ksmbd_transport_write tx = {}; + + return __ksmbd_conn_write(work, &tx); +} + +int ksmbd_conn_write_eor(struct ksmbd_work *work) +{ + struct ksmbd_transport_write tx = { + .msg_flags = MSG_EOR, + }; + + return __ksmbd_conn_write(work, &tx); +} + int ksmbd_conn_rdma_read(struct ksmbd_conn *conn, void *buf, unsigned int buflen, struct smbdirect_buffer_descriptor_v1 *desc, @@ -335,6 +601,20 @@ bool ksmbd_conn_alive(struct ksmbd_conn *conn) if (kthread_should_stop()) return false; + /* + * Stale connections that have not completed NEGOTIATE and SESSION_SETUP + * must be disconnected. Do not race a request that is currently + * completing authentication. + */ + if (!atomic_read(&conn->req_running) && + time_after(jiffies, conn->creation_time + + KSMBD_UNAUTHENTICATED_CONN_TIMEOUT) && + (READ_ONCE(conn->need_neg) || + !ksmbd_conn_has_valid_or_expired_session(conn))) { + ksmbd_debug(CONN, "Connection setup timed out\n"); + return false; + } + if (atomic_read(&conn->stats.open_files_count) > 0) return true; @@ -352,9 +632,56 @@ bool ksmbd_conn_alive(struct ksmbd_conn *conn) return true; } +static void ksmbd_session_expiration_worker(struct work_struct *work) +{ + struct ksmbd_conn *conn, *target; + int bkt; + + if (!ksmbd_server_running()) + goto reschedule; + + ksmbd_expire_sessions(); + + /* + * An old connection without a Valid or Expired session must be + * disconnected. Process one connection at a time without holding + * conn_list_lock across transport shutdown. + */ +again: + target = NULL; + down_read(&conn_list_lock); + hash_for_each(conn_list, bkt, conn, hlist) { + if (ksmbd_conn_exiting(conn) || ksmbd_conn_releasing(conn) || + atomic_read(&conn->req_running) || + time_before_eq(jiffies, conn->creation_time + + KSMBD_UNAUTHENTICATED_CONN_TIMEOUT) || + (!READ_ONCE(conn->need_neg) && + ksmbd_conn_has_valid_or_expired_session(conn))) + continue; + + target = ksmbd_conn_get(conn); + break; + } + up_read(&conn_list_lock); + + if (target) { + ksmbd_debug(CONN, "Connection setup timed out\n"); + ksmbd_conn_abort(target); + ksmbd_conn_put(target); + goto again; + } + +reschedule: + if (!READ_ONCE(stopping_session_expiration_work)) + queue_delayed_work(ksmbd_conn_wq, &session_expiration_work, + KSMBD_SESSION_EXPIRATION_INTERVAL); +} + /* "+2" for BCC field (ByteCount, 2 bytes) */ #define SMB1_MIN_SUPPORTED_PDU_SIZE (sizeof(struct smb_hdr) + 2) #define SMB2_MIN_SUPPORTED_PDU_SIZE (sizeof(struct smb2_pdu)) +#define SMB2_TRANSFORM_MIN_SUPPORTED_PDU_SIZE \ + (sizeof(struct smb2_transform_hdr) + sizeof(struct smb2_hdr)) /** * ksmbd_conn_handler_loop() - session thread to listen on new smb requests @@ -369,15 +696,13 @@ int ksmbd_conn_handler_loop(void *p) struct ksmbd_conn *conn = (struct ksmbd_conn *)p; struct ksmbd_transport *t = conn->transport; unsigned int pdu_size, max_allowed_pdu_size, max_req; + __le32 proto; char hdr_buf[4] = {0,}; int size; mutex_init(&conn->srv_mutex); __module_get(THIS_MODULE); - if (t->ops->prepare && t->ops->prepare(t)) - goto out; - max_req = server_conf.max_inflight_req; conn->last_active = jiffies; set_freezable(); @@ -402,11 +727,7 @@ recheck: pdu_size = get_rfc1002_len(hdr_buf); ksmbd_debug(CONN, "RFC1002 header %u bytes\n", pdu_size); - if (ksmbd_conn_good(conn)) - max_allowed_pdu_size = - SMB3_MAX_MSGSIZE + conn->vals->max_write_size; - else - max_allowed_pdu_size = SMB3_MAX_MSGSIZE; + max_allowed_pdu_size = ksmbd_max_allowed_pdu_size(conn); if (pdu_size > max_allowed_pdu_size) { pr_err_ratelimited("PDU length(%u) exceeded maximum allowed pdu size(%u) on connection(%d)\n", @@ -449,15 +770,29 @@ recheck: continue; } - if (!ksmbd_smb_request(conn)) - break; - if (((struct smb2_hdr *)smb_get_msg(conn->request_buf))->ProtocolId == - SMB2_PROTO_NUMBER) { - if (pdu_size < SMB2_MIN_SUPPORTED_PDU_SIZE) + SMB2_COMPRESSION_TRANSFORM_ID) { + /* + * Convert the transform into a normal RFC1002-framed SMB2 + * request before protocol validation and work allocation. + */ + if (ksmbd_decompress_request(conn)) break; + pdu_size = get_rfc1002_len(conn->request_buf); } + if (!ksmbd_smb_request(conn)) + break; + + proto = *(__le32 *)smb_get_msg(conn->request_buf); + if (proto == SMB2_PROTO_NUMBER && + pdu_size < SMB2_MIN_SUPPORTED_PDU_SIZE) + break; + + if (proto == SMB2_TRANSFORM_PROTO_NUM && + pdu_size < SMB2_TRANSFORM_MIN_SUPPORTED_PDU_SIZE) + break; + if (!default_conn_ops.process_fn) { pr_err("No connection request callback\n"); break; @@ -469,8 +804,8 @@ recheck: } } -out: ksmbd_conn_set_releasing(conn); + 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)); wait_event(conn->r_count_q, atomic_read(&conn->r_count) == 0); @@ -507,8 +842,7 @@ void ksmbd_conn_r_count_dec(struct ksmbd_conn *conn) if (!atomic_dec_return(&conn->r_count) && waitqueue_active(&conn->r_count_q)) wake_up(&conn->r_count_q); - if (atomic_dec_and_test(&conn->refcnt)) - kfree(conn); + ksmbd_conn_put(conn); } int ksmbd_conn_transport_init(void) @@ -529,30 +863,63 @@ int ksmbd_conn_transport_init(void) } out: mutex_unlock(&init_lock); - create_proc_clients(); + if (create_proc_clients()) + pr_warn("Unable to create clients procfs entry\n"); return ret; } static void stop_sessions(void) { - struct ksmbd_conn *conn; + struct ksmbd_conn *conn, *target; struct ksmbd_transport *t; + bool any; int bkt; + /* + * Serialised via init_lock; no concurrent stop_sessions() can + * touch conn->stop_called, so writing it under the read lock is + * safe. + */ again: + target = NULL; + any = false; down_read(&conn_list_lock); hash_for_each(conn_list, bkt, conn, hlist) { - t = conn->transport; - ksmbd_conn_set_exiting(conn); - if (t->ops->shutdown) { - up_read(&conn_list_lock); + any = true; + if (conn->stop_called) + continue; + atomic_inc(&conn->refcnt); + conn->stop_called = true; + /* + * Mark the connection EXITING while still holding the + * read lock so the selection and the status transition + * happen together. Do not regress a connection that has + * already advanced to RELEASING on its own (e.g. the + * handler exited its receive loop for an unrelated + * reason). + */ + spin_lock(&conn->request_lock); + if (!ksmbd_conn_releasing(conn)) + ksmbd_conn_set_exiting(conn); + spin_unlock(&conn->request_lock); + target = conn; + break; + } + up_read(&conn_list_lock); + + if (target) { + t = target->transport; + if (t->ops->shutdown) t->ops->shutdown(t); - down_read(&conn_list_lock); + if (atomic_dec_and_test(&target->refcnt)) { + ida_destroy(&target->async_ida); + t->ops->free_transport(t); + kfree_sensitive(target); } + goto again; } - up_read(&conn_list_lock); - if (!hash_empty(conn_list)) { + if (any) { msleep(100); goto again; } @@ -565,6 +932,5 @@ void ksmbd_conn_transport_destroy(void) ksmbd_tcp_destroy(); ksmbd_rdma_stop_listening(); stop_sessions(); - ksmbd_rdma_destroy(); mutex_unlock(&init_lock); } diff --git a/fs/smb/server/connection.h b/fs/smb/server/connection.h index 1e2587036bca..371f17b4f02a 100644 --- a/fs/smb/server/connection.h +++ b/fs/smb/server/connection.h @@ -16,14 +16,26 @@ #include <linux/kthread.h> #include <linux/nls.h> #include <linux/unicode.h> +#include <linux/workqueue.h> +#include <linux/bitmap.h> #include "smb_common.h" #include "ksmbd_work.h" struct smbdirect_buffer_descriptor_v1; +struct ksmbd_session; #define KSMBD_SOCKET_BACKLOG 16 +/* + * Size of the per-connection SMB2 command sequence window. This mirrors + * SMB2_MAX_CREDITS, the maximum number of credits (and therefore the + * maximum number of outstanding sequence numbers) that can be granted on + * a connection. It must be a power of two so the window can be indexed as + * a ring. + */ +#define KSMBD_CMD_SEQ_WINDOW 8192 + enum { KSMBD_SESS_NEW = 0, KSMBD_SESS_GOOD, @@ -49,6 +61,7 @@ struct ksmbd_conn { struct mutex srv_mutex; int status; unsigned int cli_cap; + bool stop_called; union { __be32 inet_addr; #if IS_ENABLED(CONFIG_IPV6) @@ -64,6 +77,7 @@ struct ksmbd_conn { struct rw_semaphore session_lock; /* smb session 1 per user */ struct xarray sessions; + unsigned long creation_time; unsigned long last_active; /* How many request are running currently */ atomic_t req_running; @@ -72,6 +86,16 @@ struct ksmbd_conn { unsigned int total_credits; unsigned int outstanding_credits; spinlock_t credits_lock; + /* + * Connection command sequence window. [seq_low, seq_high) is the + * range of granted sequence numbers (message IDs). seq_bitmap marks + * the ones in that range that have been granted but + * not yet consumed by a received request. All three are protected by + * credits_lock. + */ + u64 seq_low; + u64 seq_high; + DECLARE_BITMAP(seq_bitmap, KSMBD_CMD_SEQ_WINDOW); wait_queue_head_t req_running_q; wait_queue_head_t r_count_q; /* Lock to protect requests list*/ @@ -113,12 +137,21 @@ struct ksmbd_conn { __le16 cipher_type; __le16 compress_algorithm; + /* Negotiated SMB 3.1.1 compression capabilities. */ + bool compress_chained; + bool compress_pattern; + /* Bitmap indexed by SMB2_RDMA_TRANSFORM_* IDs. */ + unsigned long rdma_transform_ids; + bool rdma_transform_negotiated; bool posix_ext_supported; bool signing_negotiated; __le16 signing_algorithm; bool binding; atomic_t refcnt; bool is_aapl; + bool aapl_readdir_attr; /* READDIR_ATTR negotiated */ + bool aapl_readdir_attr_v2; /* V2 specifically */ + struct work_struct release_work; }; struct ksmbd_conn_ops { @@ -126,15 +159,22 @@ struct ksmbd_conn_ops { int (*terminate_fn)(struct ksmbd_conn *conn); }; +struct ksmbd_transport_write { + struct kvec *iov; + int iov_cnt; + int size; + bool need_invalidate_rkey; + unsigned int remote_key; + int msg_flags; +}; + struct ksmbd_transport_ops { - int (*prepare)(struct ksmbd_transport *t); void (*disconnect)(struct ksmbd_transport *t); void (*shutdown)(struct ksmbd_transport *t); int (*read)(struct ksmbd_transport *t, char *buf, unsigned int size, int max_retries); - int (*writev)(struct ksmbd_transport *t, struct kvec *iovs, int niov, - int size, bool need_invalidate_rkey, - unsigned int remote_key); + int (*writev)(struct ksmbd_transport *t, + const struct ksmbd_transport_write *tx); int (*rdma_read)(struct ksmbd_transport *t, void *buf, unsigned int len, struct smbdirect_buffer_descriptor_v1 *desc, @@ -153,6 +193,8 @@ struct ksmbd_transport { #define KSMBD_TCP_RECV_TIMEOUT (7 * HZ) #define KSMBD_TCP_SEND_TIMEOUT (5 * HZ) +#define KSMBD_SESSION_EXPIRATION_INTERVAL (5 * HZ) +#define KSMBD_UNAUTHENTICATED_CONN_TIMEOUT (45 * HZ) #define KSMBD_TCP_PEER_SOCKADDR(c) ((struct sockaddr *)&((c)->peer_addr)) #define CONN_HASH_BITS 12 @@ -161,11 +203,18 @@ extern struct rw_semaphore conn_list_lock; bool ksmbd_conn_alive(struct ksmbd_conn *conn); void ksmbd_conn_wait_idle(struct ksmbd_conn *conn); -int ksmbd_conn_wait_idle_sess_id(struct ksmbd_conn *curr_conn, u64 sess_id); +int ksmbd_conn_wait_idle_sess(struct ksmbd_conn *curr_conn, + struct ksmbd_session *sess); struct ksmbd_conn *ksmbd_conn_alloc(void); void ksmbd_conn_free(struct ksmbd_conn *conn); +struct ksmbd_conn *ksmbd_conn_get(struct ksmbd_conn *conn); +void ksmbd_conn_put(struct ksmbd_conn *conn); +void ksmbd_conn_abort(struct ksmbd_conn *conn); +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_eor(struct ksmbd_work *work); int ksmbd_conn_rdma_read(struct ksmbd_conn *conn, void *buf, unsigned int buflen, struct smbdirect_buffer_descriptor_v1 *desc, @@ -191,11 +240,25 @@ void ksmbd_conn_r_count_dec(struct ksmbd_conn *conn); * This is a hack. We will move status to a proper place once we land * a multi-sessions support. */ +static inline bool ksmbd_conn_new(struct ksmbd_conn *conn) +{ + return READ_ONCE(conn->status) == KSMBD_SESS_NEW; +} + static inline bool ksmbd_conn_good(struct ksmbd_conn *conn) { return READ_ONCE(conn->status) == KSMBD_SESS_GOOD; } +static inline unsigned int +ksmbd_max_allowed_pdu_size(struct ksmbd_conn *conn) +{ + if (ksmbd_conn_good(conn)) + return SMB3_MAX_MSGSIZE + conn->vals->max_write_size; + + return SMB3_MAX_MSGSIZE; +} + static inline bool ksmbd_conn_need_negotiate(struct ksmbd_conn *conn) { return READ_ONCE(conn->status) == KSMBD_SESS_NEED_NEGOTIATE; @@ -256,5 +319,5 @@ static inline void ksmbd_conn_set_releasing(struct ksmbd_conn *conn) WRITE_ONCE(conn->status, KSMBD_SESS_RELEASING); } -void ksmbd_all_conn_set_status(u64 sess_id, u32 status); +void ksmbd_all_conn_set_status(struct ksmbd_session *sess, u32 status); #endif /* __CONNECTION_H__ */ diff --git a/fs/smb/server/crypto_ctx.c b/fs/smb/server/crypto_ctx.c index 8fd9713b00b7..2fe7d3300480 100644 --- a/fs/smb/server/crypto_ctx.c +++ b/fs/smb/server/crypto_ctx.c @@ -28,14 +28,6 @@ static inline void free_aead(struct crypto_aead *aead) crypto_free_aead(aead); } -static void free_shash(struct shash_desc *shash) -{ - if (shash) { - crypto_free_shash(shash->tfm); - kfree(shash); - } -} - static struct crypto_aead *alloc_aead(int id) { struct crypto_aead *tfm = NULL; @@ -60,37 +52,10 @@ static struct crypto_aead *alloc_aead(int id) return tfm; } -static struct shash_desc *alloc_shash_desc(int id) -{ - struct crypto_shash *tfm = NULL; - struct shash_desc *shash; - - switch (id) { - case CRYPTO_SHASH_CMACAES: - tfm = crypto_alloc_shash("cmac(aes)", 0, 0); - break; - default: - return NULL; - } - - if (IS_ERR(tfm)) - return NULL; - - shash = kzalloc(sizeof(*shash) + crypto_shash_descsize(tfm), - KSMBD_DEFAULT_GFP); - if (!shash) - crypto_free_shash(tfm); - else - shash->tfm = tfm; - return shash; -} - static void ctx_free(struct ksmbd_crypto_ctx *ctx) { int i; - for (i = 0; i < CRYPTO_SHASH_MAX; i++) - free_shash(ctx->desc[i]); for (i = 0; i < CRYPTO_AEAD_MAX; i++) free_aead(ctx->ccmaes[i]); kfree(ctx); @@ -153,29 +118,6 @@ void ksmbd_release_crypto_ctx(struct ksmbd_crypto_ctx *ctx) ctx_free(ctx); } -static struct ksmbd_crypto_ctx *____crypto_shash_ctx_find(int id) -{ - struct ksmbd_crypto_ctx *ctx; - - if (id >= CRYPTO_SHASH_MAX) - return NULL; - - ctx = ksmbd_find_crypto_ctx(); - if (ctx->desc[id]) - return ctx; - - ctx->desc[id] = alloc_shash_desc(id); - if (ctx->desc[id]) - return ctx; - ksmbd_release_crypto_ctx(ctx); - return NULL; -} - -struct ksmbd_crypto_ctx *ksmbd_crypto_ctx_find_cmacaes(void) -{ - return ____crypto_shash_ctx_find(CRYPTO_SHASH_CMACAES); -} - static struct ksmbd_crypto_ctx *____crypto_aead_ctx_find(int id) { struct ksmbd_crypto_ctx *ctx; diff --git a/fs/smb/server/crypto_ctx.h b/fs/smb/server/crypto_ctx.h index b9476ed520ae..b22c6e086f03 100644 --- a/fs/smb/server/crypto_ctx.h +++ b/fs/smb/server/crypto_ctx.h @@ -6,41 +6,24 @@ #ifndef __CRYPTO_CTX_H__ #define __CRYPTO_CTX_H__ -#include <crypto/hash.h> #include <crypto/aead.h> enum { - CRYPTO_SHASH_CMACAES = 0, - CRYPTO_SHASH_MAX, -}; - -enum { CRYPTO_AEAD_AES_GCM = 16, CRYPTO_AEAD_AES_CCM, CRYPTO_AEAD_MAX, }; -enum { - CRYPTO_BLK_ECBDES = 32, - CRYPTO_BLK_MAX, -}; - struct ksmbd_crypto_ctx { struct list_head list; - struct shash_desc *desc[CRYPTO_SHASH_MAX]; struct crypto_aead *ccmaes[CRYPTO_AEAD_MAX]; }; -#define CRYPTO_CMACAES(c) ((c)->desc[CRYPTO_SHASH_CMACAES]) - -#define CRYPTO_CMACAES_TFM(c) ((c)->desc[CRYPTO_SHASH_CMACAES]->tfm) - #define CRYPTO_GCM(c) ((c)->ccmaes[CRYPTO_AEAD_AES_GCM]) #define CRYPTO_CCM(c) ((c)->ccmaes[CRYPTO_AEAD_AES_CCM]) void ksmbd_release_crypto_ctx(struct ksmbd_crypto_ctx *ctx); -struct ksmbd_crypto_ctx *ksmbd_crypto_ctx_find_cmacaes(void); struct ksmbd_crypto_ctx *ksmbd_crypto_ctx_find_gcm(void); struct ksmbd_crypto_ctx *ksmbd_crypto_ctx_find_ccm(void); void ksmbd_crypto_destroy(void); diff --git a/fs/smb/server/ksmbd_netlink.h b/fs/smb/server/ksmbd_netlink.h index 8ccd57fd904b..2673522c76bc 100644 --- a/fs/smb/server/ksmbd_netlink.h +++ b/fs/smb/server/ksmbd_netlink.h @@ -113,7 +113,8 @@ struct ksmbd_startup_request { __u32 max_connections; /* Number of maximum simultaneous connections */ __s8 bind_interfaces_only; __u32 max_ip_connections; /* Number of maximum connection per ip address */ - __s8 reserved[499]; /* Reserved room */ + __s8 aapl_model[32]; /* AAPL model string for Finder icon, e.g. "Xserve" */ + __s8 reserved[467]; /* Reserved room */ __u32 ifc_list_sz; /* interfaces list size */ __s8 ____payload[]; } __packed; @@ -285,6 +286,7 @@ 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 */ }; @@ -377,6 +379,10 @@ enum KSMBD_TREE_CONN_STATUS { #define KSMBD_SHARE_FLAG_UPDATE BIT(14) #define KSMBD_SHARE_FLAG_CROSSMNT BIT(15) #define KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY BIT(16) +#define KSMBD_SHARE_FLAG_HIDE_UNREADABLE BIT(17) +#define KSMBD_SHARE_FLAG_TIME_MACHINE BIT(18) +/* Keep BIT(19) reserved for the existing ksmbd-tools WIDE_LINKS flag. */ +#define KSMBD_SHARE_FLAG_ENCRYPT_DATA BIT(20) /* * Tree connect request flags. diff --git a/fs/smb/server/ksmbd_work.c b/fs/smb/server/ksmbd_work.c index ab4958dc3eb0..d307aefe0aec 100644 --- a/fs/smb/server/ksmbd_work.c +++ b/fs/smb/server/ksmbd_work.c @@ -11,11 +11,42 @@ #include "server.h" #include "connection.h" #include "ksmbd_work.h" +#include "vfs_cache.h" #include "mgmt/ksmbd_ida.h" static struct kmem_cache *work_cache; static struct workqueue_struct *ksmbd_wq; +static int ksmbd_reserve_iov(struct ksmbd_work *work, int need_iov_cnt) +{ + struct kvec *new; + int new_alloc_cnt = work->iov_alloc_cnt; + + if (work->iov_alloc_cnt >= work->iov_cnt + need_iov_cnt) + return 0; + + do { + new_alloc_cnt += KSMBD_WORK_INLINE_IOVS; + } while (new_alloc_cnt < work->iov_cnt + need_iov_cnt); + + if (work->iov == work->iov_inline) { + new = kzalloc_objs(*new, new_alloc_cnt, KSMBD_DEFAULT_GFP); + if (!new) + return -ENOMEM; + + memcpy(new, work->iov_inline, sizeof(work->iov_inline)); + } else { + new = krealloc(work->iov, sizeof(*new) * new_alloc_cnt, + KSMBD_DEFAULT_GFP | __GFP_ZERO); + if (!new) + return -ENOMEM; + } + + work->iov = new; + work->iov_alloc_cnt = new_alloc_cnt; + return 0; +} + struct ksmbd_work *ksmbd_alloc_work_struct(void) { struct ksmbd_work *work = kmem_cache_zalloc(work_cache, KSMBD_DEFAULT_GFP); @@ -26,14 +57,10 @@ 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 = 4; - work->iov = kzalloc_objs(struct kvec, work->iov_alloc_cnt, - KSMBD_DEFAULT_GFP); - if (!work->iov) { - kmem_cache_free(work_cache, work); - work = NULL; - } + work->iov_alloc_cnt = ARRAY_SIZE(work->iov_inline); + work->iov = work->iov_inline; } return work; } @@ -53,11 +80,16 @@ void ksmbd_free_work_struct(struct ksmbd_work *work) } kfree(work->tr_buf); + kvfree(work->compress_buf); kvfree(work->request_buf); - kfree(work->iov); + if (work->iov != work->iov_inline) + kfree(work->iov); 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); } @@ -116,19 +148,9 @@ static int __ksmbd_iov_pin_rsp(struct ksmbd_work *work, void *ib, int len, return -ENOMEM; } - if (work->iov_alloc_cnt < work->iov_cnt + need_iov_cnt) { - struct kvec *new; - - work->iov_alloc_cnt += 4; - new = krealloc(work->iov, - sizeof(struct kvec) * work->iov_alloc_cnt, - KSMBD_DEFAULT_GFP | __GFP_ZERO); - if (!new) { - kfree(ar); - work->iov_alloc_cnt -= 4; - return -ENOMEM; - } - work->iov = new; + if (ksmbd_reserve_iov(work, need_iov_cnt)) { + kfree(ar); + return -ENOMEM; } /* Plus rfc_length size on first iov */ diff --git a/fs/smb/server/ksmbd_work.h b/fs/smb/server/ksmbd_work.h index d36393ff8310..0844aa929f55 100644 --- a/fs/smb/server/ksmbd_work.h +++ b/fs/smb/server/ksmbd_work.h @@ -12,6 +12,9 @@ struct ksmbd_conn; struct ksmbd_session; struct ksmbd_tree_connect; +struct ksmbd_file; + +#define KSMBD_WORK_INLINE_IOVS 4 enum { KSMBD_WORK_ACTIVE = 0, @@ -42,6 +45,7 @@ struct ksmbd_work { int iov_alloc_cnt; int iov_cnt; int iov_idx; + struct kvec iov_inline[KSMBD_WORK_INLINE_IOVS]; /* Next cmd hdr in compound req buf*/ int next_smb2_rcv_hdr_off; @@ -57,25 +61,41 @@ struct ksmbd_work { u64 compound_fid; u64 compound_pfid; u64 compound_sid; + __le32 compound_status; const struct cred *saved_cred; /* Number of granted credits */ unsigned int credits_granted; + /* + * Credit charge added to conn->outstanding_credits at receive time + * for the SMB2 PDU currently being processed, pending release. Zero + * once the charge has been returned (on the response or error path). + */ + unsigned short credit_charge; + /* response smb header size */ unsigned int response_sz; void *tr_buf; + /* Contiguous SMB2 compression transform owned by this work item. */ + void *compress_buf; - unsigned char state; + unsigned int state; /* No response for cancelled request */ bool send_no_response:1; /* Request is encrypted */ bool encrypted:1; + /* READ response should be wrapped in a compression transform. */ + 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; + bool session_setup_reauth:1; unsigned int remote_key; /* cancel works */ @@ -83,12 +103,21 @@ struct ksmbd_work { void **cancel_argv; void (*cancel_fn)(void **argv); + /* + * Refcounted open associated with the SMB2 command currently being + * processed. + */ + struct ksmbd_file *request_open; + __le16 request_open_chseq; + struct work_struct work; /* List head at conn->requests */ struct list_head request_entry; /* List head at conn->async_requests */ struct list_head async_request_entry; struct list_head fp_entry; + /* List head at ksmbd_file->notify_pendings */ + struct list_head notify_entry; }; /** diff --git a/fs/smb/server/mgmt/share_config.c b/fs/smb/server/mgmt/share_config.c index 53f44ff4d376..cc9f18ede80d 100644 --- a/fs/smb/server/mgmt/share_config.c +++ b/fs/smb/server/mgmt/share_config.c @@ -9,6 +9,7 @@ #include <linux/rwsem.h> #include <linux/parser.h> #include <linux/namei.h> +#include <linux/fs_struct.h> #include <linux/sched.h> #include <linux/mm.h> @@ -28,6 +29,63 @@ struct ksmbd_veto_pattern { struct list_head list; }; +#ifdef CONFIG_PROC_FS +static const struct ksmbd_const_name ksmbd_share_flag_names[] = { + {KSMBD_SHARE_FLAG_AVAILABLE, "available"}, + {KSMBD_SHARE_FLAG_BROWSEABLE, "browseable"}, + {KSMBD_SHARE_FLAG_WRITEABLE, "writeable"}, + {KSMBD_SHARE_FLAG_READONLY, "read-only"}, + {KSMBD_SHARE_FLAG_GUEST_OK, "guest-ok"}, + {KSMBD_SHARE_FLAG_GUEST_ONLY, "guest-only"}, + {KSMBD_SHARE_FLAG_STORE_DOS_ATTRS, "store-dos-attrs"}, + {KSMBD_SHARE_FLAG_OPLOCKS, "oplocks"}, + {KSMBD_SHARE_FLAG_PIPE, "pipe"}, + {KSMBD_SHARE_FLAG_HIDE_DOT_FILES, "hide-dot-files"}, + {KSMBD_SHARE_FLAG_INHERIT_OWNER, "inherit-owner"}, + {KSMBD_SHARE_FLAG_STREAMS, "streams"}, + {KSMBD_SHARE_FLAG_FOLLOW_SYMLINKS, "follow-symlinks"}, + {KSMBD_SHARE_FLAG_ACL_XATTR, "acl-xattr"}, + {KSMBD_SHARE_FLAG_UPDATE, "update"}, + {KSMBD_SHARE_FLAG_CROSSMNT, "crossmnt"}, + {KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY, "continuous-availability"}, + {KSMBD_SHARE_FLAG_ENCRYPT_DATA, "encrypt-data"}, +}; + +static int proc_show_shares(struct seq_file *m, void *v) +{ + struct ksmbd_share_config *share; + int i; + + down_read(&shares_table_lock); + hash_for_each(shares_table, i, share, hlist) { + seq_printf(m, "name:\t%s\n", share->name); + seq_printf(m, "type:\t%s\n", + test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE) ? + "pipe" : "disk"); + seq_printf(m, "tree_connects:\t%d\n", + atomic_read(&share->tree_connections)); + seq_printf(m, "file_mask:\t0%07o\n", share->create_mask); + seq_printf(m, "directory_mask:\t0%07o\n", share->directory_mask); + seq_puts(m, "flags:\t"); + ksmbd_proc_show_flag_names(m, ksmbd_share_flag_names, + ARRAY_SIZE(ksmbd_share_flag_names), + share->flags); + seq_puts(m, "\n\n"); + } + up_read(&shares_table_lock); + return 0; +} + +int create_proc_shares(void) +{ + if (!ksmbd_proc_create("shares", proc_show_shares, NULL)) + return -ENOMEM; + return 0; +} +#else +int create_proc_shares(void) { return 0; } +#endif + static unsigned int share_name_hash(const char *name) { return jhash(name, strlen(name), 0); @@ -88,9 +146,9 @@ static struct ksmbd_share_config *__share_lookup(const char *name) static int parse_veto_list(struct ksmbd_share_config *share, char *veto_list, - int veto_list_sz) + size_t veto_list_sz) { - int sz = 0; + size_t sz; if (!veto_list_sz) return 0; @@ -98,7 +156,7 @@ static int parse_veto_list(struct ksmbd_share_config *share, while (veto_list_sz > 0) { struct ksmbd_veto_pattern *p; - sz = strlen(veto_list); + sz = strnlen(veto_list, veto_list_sz); if (!sz) break; @@ -106,7 +164,7 @@ static int parse_veto_list(struct ksmbd_share_config *share, if (!p) return -ENOMEM; - p->pattern = kstrdup(veto_list, KSMBD_DEFAULT_GFP); + p->pattern = kstrndup(veto_list, sz, KSMBD_DEFAULT_GFP); if (!p->pattern) { kfree(p); return -ENOMEM; @@ -114,6 +172,9 @@ static int parse_veto_list(struct ksmbd_share_config *share, list_add(&p->list, &share->veto_list); + if (sz == veto_list_sz) + break; + veto_list += sz + 1; veto_list_sz -= (sz + 1); } @@ -156,17 +217,37 @@ static struct ksmbd_share_config *share_config_request(struct ksmbd_work *work, share->flags = resp->flags; atomic_set(&share->refcount, 1); + ksmbd_share_tree_conn_init(share); INIT_LIST_HEAD(&share->veto_list); share->name = kstrdup(name, KSMBD_DEFAULT_GFP); + if (!share->name) { + kill_share(share); + share = NULL; + goto out; + } if (!test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE)) { - int path_len = PATH_MAX; + size_t path_len; - if (resp->payload_sz) + if (resp->payload_sz <= resp->veto_list_sz) { + ret = -EINVAL; + } else { path_len = resp->payload_sz - resp->veto_list_sz; - - share->path = kstrndup(ksmbd_share_config_path(resp), path_len, - KSMBD_DEFAULT_GFP); + if (resp->veto_list_sz) + path_len--; + + if (!path_len) { + ret = -EINVAL; + } else { + share->path = kstrndup( + ksmbd_share_config_path(resp), + path_len, KSMBD_DEFAULT_GFP); + if (!share->path) + ret = -ENOMEM; + else + ret = 0; + } + } if (share->path) { share->path_sz = strlen(share->path); while (share->path_sz > 1 && @@ -179,9 +260,10 @@ static struct ksmbd_share_config *share_config_request(struct ksmbd_work *work, share->force_directory_mode = resp->force_directory_mode; share->force_uid = resp->force_uid; share->force_gid = resp->force_gid; - ret = parse_veto_list(share, - KSMBD_SHARE_CONFIG_VETO_LIST(resp), - resp->veto_list_sz); + if (!ret) + ret = parse_veto_list(share, + KSMBD_SHARE_CONFIG_VETO_LIST(resp), + resp->veto_list_sz); if (!ret && share->path) { if (__ksmbd_override_fsids(work, share)) { kill_share(share); @@ -189,7 +271,8 @@ static struct ksmbd_share_config *share_config_request(struct ksmbd_work *work, goto out; } - ret = kern_path(share->path, 0, &share->vfs_path); + scoped_with_init_fs() + ret = kern_path(share->path, 0, &share->vfs_path); ksmbd_revert_fsids(work); if (ret) { ksmbd_debug(SMB, "failed to access '%s'\n", @@ -199,7 +282,7 @@ static struct ksmbd_share_config *share_config_request(struct ksmbd_work *work, share->path = NULL; } } - if (ret || !share->name) { + if (ret) { kill_share(share); share = NULL; goto out; diff --git a/fs/smb/server/mgmt/share_config.h b/fs/smb/server/mgmt/share_config.h index d4ac2dd4de20..d157545fe7d1 100644 --- a/fs/smb/server/mgmt/share_config.h +++ b/fs/smb/server/mgmt/share_config.h @@ -24,6 +24,9 @@ struct ksmbd_share_config { struct path vfs_path; atomic_t refcount; +#ifdef CONFIG_PROC_FS + atomic_t tree_connections; +#endif struct hlist_node hlist; unsigned short create_mask; unsigned short directory_mask; @@ -60,6 +63,27 @@ static inline int test_share_config_flag(struct ksmbd_share_config *share, return share->flags & flag; } +#ifdef CONFIG_PROC_FS +static inline void ksmbd_share_tree_conn_init(struct ksmbd_share_config *share) +{ + atomic_set(&share->tree_connections, 0); +} + +static inline void ksmbd_share_tree_conn_inc(struct ksmbd_share_config *share) +{ + atomic_inc(&share->tree_connections); +} + +static inline void ksmbd_share_tree_conn_dec(struct ksmbd_share_config *share) +{ + atomic_dec(&share->tree_connections); +} +#else +static inline void ksmbd_share_tree_conn_init(struct ksmbd_share_config *share) {} +static inline void ksmbd_share_tree_conn_inc(struct ksmbd_share_config *share) {} +static inline void ksmbd_share_tree_conn_dec(struct ksmbd_share_config *share) {} +#endif + void ksmbd_share_config_del(struct ksmbd_share_config *share); void __ksmbd_share_config_put(struct ksmbd_share_config *share); @@ -74,4 +98,5 @@ struct ksmbd_share_config *ksmbd_share_config_get(struct ksmbd_work *work, const char *name); bool ksmbd_share_veto_filename(struct ksmbd_share_config *share, const char *filename); +int create_proc_shares(void); #endif /* __SHARE_CONFIG_MANAGEMENT_H__ */ diff --git a/fs/smb/server/mgmt/tree_connect.c b/fs/smb/server/mgmt/tree_connect.c index a72d7e42a6c2..dd1db3554cae 100644 --- a/fs/smb/server/mgmt/tree_connect.c +++ b/fs/smb/server/mgmt/tree_connect.c @@ -82,12 +82,15 @@ ksmbd_tree_conn_connect(struct ksmbd_work *work, const char *share_name) down_write(&sess->tree_conns_lock); ret = xa_err(xa_store(&sess->tree_conns, tree_conn->id, tree_conn, KSMBD_DEFAULT_GFP)); + if (!ret) + atomic_inc(&tree_conn->refcount); up_write(&sess->tree_conns_lock); if (ret) { status.ret = -ENOMEM; goto out_error; } ksmbd_counter_inc(KSMBD_COUNTER_TREE_CONNS); + ksmbd_share_tree_conn_inc(sc); kvfree(resp); return status; @@ -102,8 +105,10 @@ out_error: void ksmbd_tree_connect_put(struct ksmbd_tree_connect *tcon) { - if (atomic_dec_and_test(&tcon->refcount)) + if (atomic_dec_and_test(&tcon->refcount)) { + ksmbd_share_config_put(tcon->share_conf); kfree(tcon); + } } static int __ksmbd_tree_conn_disconnect(struct ksmbd_session *sess, @@ -113,10 +118,12 @@ static int __ksmbd_tree_conn_disconnect(struct ksmbd_session *sess, ret = ksmbd_ipc_tree_disconnect_request(sess->id, tree_conn->id); ksmbd_release_tree_conn_id(sess, tree_conn->id); - ksmbd_share_config_put(tree_conn->share_conf); ksmbd_counter_dec(KSMBD_COUNTER_TREE_CONNS); - if (atomic_dec_and_test(&tree_conn->refcount)) + ksmbd_share_tree_conn_dec(tree_conn->share_conf); + if (atomic_dec_and_test(&tree_conn->refcount)) { + ksmbd_share_config_put(tree_conn->share_conf); kfree(tree_conn); + } return ret; } @@ -124,6 +131,12 @@ int ksmbd_tree_conn_disconnect(struct ksmbd_session *sess, struct ksmbd_tree_connect *tree_conn) { down_write(&sess->tree_conns_lock); + if (tree_conn->t_state == TREE_DISCONNECTED || + xa_load(&sess->tree_conns, tree_conn->id) != tree_conn) { + up_write(&sess->tree_conns_lock); + return -ENOENT; + } + tree_conn->t_state = TREE_DISCONNECTED; xa_erase(&sess->tree_conns, tree_conn->id); up_write(&sess->tree_conns_lock); diff --git a/fs/smb/server/mgmt/user_config.c b/fs/smb/server/mgmt/user_config.c index a3183fe5c536..5efc3d7455b9 100644 --- a/fs/smb/server/mgmt/user_config.c +++ b/fs/smb/server/mgmt/user_config.c @@ -26,8 +26,9 @@ struct ksmbd_user *ksmbd_login_user(const char *account) resp_ext = ksmbd_ipc_login_request_ext(account); user = ksmbd_alloc_user(resp, resp_ext); + kvfree(resp_ext); out: - kvfree(resp); + kvfree_sensitive(resp, sizeof(*resp)); return user; } @@ -36,6 +37,17 @@ struct ksmbd_user *ksmbd_alloc_user(struct ksmbd_login_response *resp, { struct ksmbd_user *user; + /* + * resp->hash_sz is a __u16 taken from the mountd IPC login response but + * resp->hash[] is only KSMBD_REQ_MAX_HASH_SZ bytes. A malformed or + * malicious response can set hash_sz far beyond that (up to 65535), + * making the memcpy() below read past the response object + * (slab-out-of-bounds in ksmbd_alloc_user()). Reject any oversized + * hash rather than trust the length. + */ + if (resp->hash_sz > sizeof(resp->hash)) + return NULL; + user = kmalloc_obj(struct ksmbd_user, KSMBD_DEFAULT_GFP); if (!user) return NULL; @@ -56,12 +68,6 @@ struct ksmbd_user *ksmbd_alloc_user(struct ksmbd_login_response *resp, goto err_free; if (resp_ext) { - if (resp_ext->ngroups > NGROUPS_MAX) { - pr_err("ngroups(%u) from login response exceeds max groups(%d)\n", - resp_ext->ngroups, NGROUPS_MAX); - goto err_free; - } - user->sgid = kmemdup(resp_ext->____payload, resp_ext->ngroups * sizeof(gid_t), KSMBD_DEFAULT_GFP); @@ -76,7 +82,7 @@ struct ksmbd_user *ksmbd_alloc_user(struct ksmbd_login_response *resp, err_free: kfree(user->name); - kfree(user->passkey); + kfree_sensitive(user->passkey); kfree(user); return NULL; } @@ -86,7 +92,7 @@ void ksmbd_free_user(struct ksmbd_user *user) ksmbd_ipc_logout_request(user->name, user->flags); kfree(user->sgid); kfree(user->name); - kfree(user->passkey); + kfree_sensitive(user->passkey); kfree(user); } diff --git a/fs/smb/server/mgmt/user_session.c b/fs/smb/server/mgmt/user_session.c index 39be2d2be86c..44dc3f800cd4 100644 --- a/fs/smb/server/mgmt/user_session.c +++ b/fs/smb/server/mgmt/user_session.c @@ -22,6 +22,7 @@ static DEFINE_IDA(session_ida); #define SESSION_HASH_BITS 12 +#define KSMBD_MAX_PENDING_SESSIONS 1 static DEFINE_HASHTABLE(sessions_table, SESSION_HASH_BITS); static DECLARE_RWSEM(sessions_table_lock); @@ -78,6 +79,28 @@ static const char *session_user_name(struct ksmbd_session *session) return session->user->name; } +static const char *session_account_type(struct ksmbd_session *session) +{ + if (user_guest(session->user)) + return "guest"; + if (ksmbd_anonymous_user(session->user)) + return "anonymous"; + return "user"; +} + +static unsigned int session_open_file_count(struct ksmbd_session *session) +{ + struct ksmbd_file *fp; + unsigned int count = 0; + unsigned int id; + + read_lock(&session->file_table.lock); + idr_for_each_entry(session->file_table.idr, fp, id) + count++; + read_unlock(&session->file_table.lock); + return count; +} + static int show_proc_session(struct seq_file *m, void *v) { struct ksmbd_session *sess; @@ -90,97 +113,91 @@ static int show_proc_session(struct seq_file *m, void *v) sess = (struct ksmbd_session *)m->private; ksmbd_user_session_get(sess); + seq_printf(m, "user:\t%s\n", session_user_name(sess)); + seq_printf(m, "account_type:\t%s\n", + session_account_type(sess)); + seq_printf(m, "id:\t%llu\n", sess->id); + seq_printf(m, "state:\t%s\n", session_state_string(sess)); + seq_printf(m, "dialect:\t0x%04x\n", sess->dialect); + seq_printf(m, "last_active_seconds:\t%lu\n", + jiffies_to_msecs(jiffies - sess->last_active) / MSEC_PER_SEC); + seq_printf(m, "open_files:\t%u\n", + session_open_file_count(sess)); + i = 0; down_read(&sess->chann_lock); xa_for_each(&sess->ksmbd_chann_list, id, chan) { + const char *name; + #if IS_ENABLED(CONFIG_IPV6) if (chan->conn->inet_addr) - seq_printf(m, "%-20s\t%pI4\n", "client", + seq_printf(m, "client:\t%pI4\n", &chan->conn->inet_addr); else - seq_printf(m, "%-20s\t%pI6c\n", "client", + seq_printf(m, "client:\t%pI6c\n", &chan->conn->inet6_addr); #else - seq_printf(m, "%-20s\t%pI4\n", "client", + seq_printf(m, "client:\t%pI4\n", &chan->conn->inet_addr); #endif - seq_printf(m, "%-20s\t%s\n", "user", session_user_name(sess)); - seq_printf(m, "%-20s\t%llu\n", "id", sess->id); - seq_printf(m, "%-20s\t%s\n", "state", - session_state_string(sess)); - - seq_printf(m, "%-20s\t", "capabilities"); + seq_puts(m, "capabilities:\t"); ksmbd_proc_show_flag_names(m, ksmbd_sess_cap_const_names, ARRAY_SIZE(ksmbd_sess_cap_const_names), chan->conn->vals->req_capabilities); + seq_putc(m, '\n'); + seq_printf(m, "posix_extensions:\t%s\n", + chan->conn->posix_ext_supported ? "yes" : "no"); if (sess->sign) { - seq_printf(m, "%-20s\t", "signing"); - ksmbd_proc_show_const_name(m, "%s\t", - ksmbd_signing_const_names, - ARRAY_SIZE(ksmbd_signing_const_names), - le16_to_cpu(chan->conn->signing_algorithm)); - } else if (sess->enc) { - seq_printf(m, "%-20s\t", "encryption"); - ksmbd_proc_show_const_name(m, "%s\t", - ksmbd_cipher_const_names, - ARRAY_SIZE(ksmbd_cipher_const_names), - le16_to_cpu(chan->conn->cipher_type)); + unsigned int algorithm = + le16_to_cpu(chan->conn->signing_algorithm); + + name = ksmbd_proc_const_name(ksmbd_signing_const_names, + ARRAY_SIZE(ksmbd_signing_const_names), + algorithm); + if (name) + seq_printf(m, "signing:\t%s\n", name); + else + seq_printf(m, "signing:\t0x%04x\n", + algorithm); + } + if (sess->enc) { + unsigned int cipher = le16_to_cpu(chan->conn->cipher_type); + + name = ksmbd_proc_const_name(ksmbd_cipher_const_names, + ARRAY_SIZE(ksmbd_cipher_const_names), + cipher); + if (name) + seq_printf(m, "encryption:\t%s\n", name); + else + seq_printf(m, "encryption:\t0x%04x\n", + cipher); } i++; } up_read(&sess->chann_lock); - seq_printf(m, "%-20s\t%d\n", "channels", i); + seq_printf(m, "channels:\t%d\n", i); i = 0; down_read(&sess->tree_conns_lock); xa_for_each(&sess->tree_conns, id, tree_conn) { share_conf = tree_conn->share_conf; - seq_printf(m, "%-20s\t%s\t%8d", "share", - share_conf->name, tree_conn->id); - if (test_share_config_flag(share_conf, KSMBD_SHARE_FLAG_PIPE)) - seq_printf(m, " %s ", "pipe"); - else - seq_printf(m, " %s ", "disk"); - seq_putc(m, '\n'); + seq_printf(m, "share:\t%s\n", share_conf->name); + seq_printf(m, "tree_id:\t%d\n", tree_conn->id); + seq_printf(m, "share_type:\t%s\n", + test_share_config_flag(share_conf, KSMBD_SHARE_FLAG_PIPE) ? + "pipe" : "disk"); + i++; } up_read(&sess->tree_conns_lock); + seq_printf(m, "tree_connects:\t%d\n", i); ksmbd_user_session_put(sess); return 0; } -void ksmbd_proc_show_flag_names(struct seq_file *m, - const struct ksmbd_const_name *table, - int count, - unsigned int flags) -{ - int i; - - for (i = 0; i < count; i++) { - if (table[i].const_value & flags) - seq_printf(m, "0x%08x\t", table[i].const_value); - } - seq_putc(m, '\n'); -} - -void ksmbd_proc_show_const_name(struct seq_file *m, - const char *format, - const struct ksmbd_const_name *table, - int count, - unsigned int const_value) -{ - int i; - - for (i = 0; i < count; i++) { - if (table[i].const_value & const_value) - seq_printf(m, format, table[i].name); - } - seq_putc(m, '\n'); -} - static int create_proc_session(struct ksmbd_session *sess) { char name[30]; @@ -188,6 +205,8 @@ static int create_proc_session(struct ksmbd_session *sess) snprintf(name, sizeof(name), "sessions/%llu", sess->id); sess->proc_entry = ksmbd_proc_create(name, show_proc_session, sess); + if (!sess->proc_entry) + return -ENOMEM; return 0; } @@ -204,9 +223,6 @@ static int show_proc_sessions(struct seq_file *m, void *v) int i; unsigned long id; - seq_printf(m, "#%-40s %-15s %-10s %-10s\n", - "<client>", "<user>", "<sess_id>", "<state>"); - down_read(&sessions_table_lock); hash_for_each(sessions_table, i, session, hlist) { down_read(&session->chann_lock); @@ -216,13 +232,13 @@ static int show_proc_sessions(struct seq_file *m, void *v) #if IS_ENABLED(CONFIG_IPV6) if (!chan->conn->inet_addr) - seq_printf(m, " %-40pI6c", &chan->conn->inet6_addr); + seq_printf(m, "client:\t%pI6c\n", &chan->conn->inet6_addr); else #endif - seq_printf(m, " %-40pI4", &chan->conn->inet_addr); - seq_printf(m, " %-15s %-10llu %-10s\n", - session_user_name(session), - session->id, + seq_printf(m, "client:\t%pI4\n", &chan->conn->inet_addr); + seq_printf(m, "user:\t%s\n", session_user_name(session)); + seq_printf(m, "id:\t%llu\n", session->id); + seq_printf(m, "state:\t%s\n\n", session_state_string(session)); ksmbd_user_session_put(session); @@ -255,7 +271,7 @@ 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); - kfree(chann); + kfree_sensitive(chann); } xa_destroy(&sess->ksmbd_chann_list); @@ -308,8 +324,11 @@ static int __rpc_method(char *rpc_name) if (!strcmp(rpc_name, "\\lsarpc") || !strcmp(rpc_name, "lsarpc")) return KSMBD_RPC_LSARPC_METHOD_INVOKE; + if (!strcmp(rpc_name, "\\mdssvc") || !strcmp(rpc_name, "mdssvc")) + return -ENOENT; + pr_err("Unsupported RPC: %s\n", rpc_name); - return 0; + return -ENOENT; } int ksmbd_session_rpc_open(struct ksmbd_session *sess, char *rpc_name) @@ -319,8 +338,8 @@ int ksmbd_session_rpc_open(struct ksmbd_session *sess, char *rpc_name) int method, id; method = __rpc_method(rpc_name); - if (!method) - return -EINVAL; + if (method < 0) + return method; entry = kzalloc_obj(struct ksmbd_session_rpc, KSMBD_DEFAULT_GFP); if (!entry) @@ -382,18 +401,23 @@ void ksmbd_session_destroy(struct ksmbd_session *sess) return; delete_proc_session(sess); - + ksmbd_tree_conn_session_logoff(sess); + ksmbd_destroy_file_table(sess); if (sess->user) ksmbd_free_user(sess->user); - - ksmbd_tree_conn_session_logoff(sess); - ksmbd_destroy_file_table(&sess->file_table); ksmbd_launch_ksmbd_durable_scavenger(); ksmbd_session_rpc_clear_list(sess); free_channel_list(sess); - kfree(sess->Preauth_HashValue); + kfree_sensitive(sess->Preauth_HashValue); ksmbd_release_id(&session_ida, sess->id); - kfree(sess); + ida_destroy(&sess->tree_conn_ida); + kfree_sensitive(sess); +} + +static void ksmbd_session_remove_from_table(struct ksmbd_session *sess) +{ + hash_del(&sess->hlist); + ksmbd_counter_dec(KSMBD_COUNTER_SESSIONS); } struct ksmbd_session *__session_lookup(unsigned long long id) @@ -409,35 +433,153 @@ struct ksmbd_session *__session_lookup(unsigned long long id) return NULL; } -static void ksmbd_expire_session(struct ksmbd_conn *conn) +static bool ksmbd_too_many_session_setups(struct ksmbd_conn *conn) { unsigned long id; struct ksmbd_session *sess; + unsigned int pending = 0; down_write(&sessions_table_lock); down_write(&conn->session_lock); xa_for_each(&conn->sessions, id, sess) { + if (READ_ONCE(sess->state) != SMB2_SESSION_IN_PROGRESS) + continue; + if (atomic_read(&sess->refcnt) <= 1 && - (sess->state != SMB2_SESSION_VALID || - time_after(jiffies, - sess->last_active + SMB2_SESSION_TIMEOUT))) { + time_after(jiffies, sess->last_active + + KSMBD_UNAUTHENTICATED_CONN_TIMEOUT)) { xa_erase(&conn->sessions, sess->id); - hash_del(&sess->hlist); + ksmbd_session_remove_from_table(sess); ksmbd_session_destroy(sess); continue; } + pending++; } up_write(&conn->session_lock); up_write(&sessions_table_lock); + return pending >= KSMBD_MAX_PENDING_SESSIONS; } int ksmbd_session_register(struct ksmbd_conn *conn, struct ksmbd_session *sess) { + int ret; + sess->dialect = conn->dialect; memcpy(sess->ClientGUID, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE); - ksmbd_expire_session(conn); - return xa_err(xa_store(&conn->sessions, sess->id, sess, KSMBD_DEFAULT_GFP)); + /* 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)); + if (ret) { + down_write(&sessions_table_lock); + ksmbd_session_remove_from_table(sess); + up_write(&sessions_table_lock); + ksmbd_user_session_put(sess); + } + + return ret; +} + +void ksmbd_session_unregister(struct ksmbd_conn *conn, + struct ksmbd_session *sess) +{ + struct ksmbd_conn *session_conns[KSMBD_MAX_CHANNELS]; + struct channel *chann; + unsigned long index; + unsigned int nr_conns = 0, i; + bool removed = false; + + down_write(&sessions_table_lock); + if (!hlist_unhashed(&sess->hlist)) { + /* Keep each channel connection stable under sessions_table_lock. */ + down_read(&sess->chann_lock); + xa_for_each(&sess->ksmbd_chann_list, index, chann) { + if (nr_conns == ARRAY_SIZE(session_conns)) + break; + session_conns[nr_conns++] = chann->conn; + } + up_read(&sess->chann_lock); + + ksmbd_session_remove_from_table(sess); + 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); + for (i = 0; i < nr_conns; i++) { + if (session_conns[i] == conn) + continue; + down_write(&session_conns[i]->session_lock); + if (xa_load(&session_conns[i]->sessions, sess->id) == sess) + xa_erase(&session_conns[i]->sessions, sess->id); + up_write(&session_conns[i]->session_lock); + } + up_write(&sessions_table_lock); + + if (removed) + ksmbd_user_session_put(sess); +} + +bool ksmbd_conn_has_valid_or_expired_session(struct ksmbd_conn *conn) +{ + struct ksmbd_session *sess; + unsigned long id; + int state, bkt; + bool found = false; + + down_read(&conn->session_lock); + xa_for_each(&conn->sessions, id, sess) { + state = READ_ONCE(sess->state); + if (state == SMB2_SESSION_VALID || + state == SMB2_SESSION_EXPIRED) { + found = true; + break; + } + } + up_read(&conn->session_lock); + if (found) + return true; + + /* A session bound through SMB3 multichannel is not in conn->sessions. */ + down_read(&sessions_table_lock); + hash_for_each(sessions_table, bkt, sess, hlist) { + state = READ_ONCE(sess->state); + if (state != SMB2_SESSION_VALID && + state != SMB2_SESSION_EXPIRED) + continue; + + down_read(&sess->chann_lock); + found = xa_load(&sess->ksmbd_chann_list, (long)conn); + up_read(&sess->chann_lock); + if (found) + break; + } + up_read(&sessions_table_lock); + return found; +} + +void ksmbd_expire_sessions(void) +{ + struct ksmbd_session *sess; + u64 now = ktime_get_real_seconds(); + int bkt; + + down_read(&sessions_table_lock); + hash_for_each(sessions_table, bkt, sess, hlist) { + if (READ_ONCE(sess->state) != SMB2_SESSION_VALID || + !sess->kerberos_expiry || now < sess->kerberos_expiry) + continue; + + if (cmpxchg(&sess->state, SMB2_SESSION_VALID, + SMB2_SESSION_EXPIRED) == SMB2_SESSION_VALID) + ksmbd_counter_inc(KSMBD_COUNTER_SESSION_TIMEOUTS); + } + up_read(&sessions_table_lock); } static int ksmbd_chann_del(struct ksmbd_conn *conn, struct ksmbd_session *sess) @@ -450,47 +592,36 @@ static int ksmbd_chann_del(struct ksmbd_conn *conn, struct ksmbd_session *sess) if (!chann) return -ENOENT; - kfree(chann); + kfree_sensitive(chann); return 0; } -void ksmbd_sessions_deregister(struct ksmbd_conn *conn) +void ksmbd_conn_sessions_cleanup(struct ksmbd_conn *conn) { struct ksmbd_session *sess; unsigned long id; + struct hlist_node *tmp; + int bkt; down_write(&sessions_table_lock); - if (conn->binding) { - int bkt; - struct hlist_node *tmp; - - hash_for_each_safe(sessions_table, bkt, tmp, sess, hlist) { - if (!ksmbd_chann_del(conn, sess) && - xa_empty(&sess->ksmbd_chann_list)) { - hash_del(&sess->hlist); - 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); - } + hash_for_each_safe(sessions_table, bkt, tmp, sess, hlist) { + if (!ksmbd_chann_del(conn, sess) && + xa_empty(&sess->ksmbd_chann_list)) { + ksmbd_session_remove_from_table(sess); + 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); } } down_write(&conn->session_lock); xa_for_each(&conn->sessions, id, sess) { - unsigned long chann_id; - struct channel *chann; - - xa_for_each(&sess->ksmbd_chann_list, chann_id, chann) { - if (chann->conn != conn) - ksmbd_conn_set_exiting(chann->conn); - } - ksmbd_chann_del(conn, sess); if (xa_empty(&sess->ksmbd_chann_list)) { xa_erase(&conn->sessions, sess->id); - hash_del(&sess->hlist); + ksmbd_session_remove_from_table(sess); if (atomic_dec_and_test(&sess->refcnt)) ksmbd_session_destroy(sess); } @@ -543,14 +674,35 @@ struct ksmbd_session *ksmbd_session_lookup_slowpath(unsigned long long id) return sess; } -struct ksmbd_session *ksmbd_session_lookup_all(struct ksmbd_conn *conn, - unsigned long long id) +struct ksmbd_session *ksmbd_session_lookup_all_states(struct ksmbd_conn *conn, + unsigned long long id) { struct ksmbd_session *sess; + bool channel_found; sess = ksmbd_session_lookup(conn, id); - if (!sess && conn->binding) + if (!sess) { sess = ksmbd_session_lookup_slowpath(id); + if (!sess) + return NULL; + + down_read(&sess->chann_lock); + channel_found = xa_load(&sess->ksmbd_chann_list, (long)conn); + up_read(&sess->chann_lock); + if (!channel_found) { + ksmbd_user_session_put(sess); + sess = NULL; + } + } + return sess; +} + +struct ksmbd_session *ksmbd_session_lookup_all(struct ksmbd_conn *conn, + unsigned long long id) +{ + struct ksmbd_session *sess; + + sess = ksmbd_session_lookup_all_states(conn, id); if (sess && sess->state != SMB2_SESSION_VALID) { ksmbd_user_session_put(sess); sess = NULL; @@ -591,6 +743,17 @@ struct preauth_session *ksmbd_preauth_session_alloc(struct ksmbd_conn *conn, return sess; } +void ksmbd_preauth_session_destroy(struct ksmbd_conn *conn) +{ + struct preauth_session *sess, *tmp; + + list_for_each_entry_safe(sess, tmp, &conn->preauth_sess_table, + preauth_entry) { + list_del(&sess->preauth_entry); + kfree(sess); + } +} + void destroy_previous_session(struct ksmbd_conn *conn, struct ksmbd_user *user, u64 id) { @@ -611,16 +774,28 @@ void destroy_previous_session(struct ksmbd_conn *conn, memcmp(user->passkey, prev_user->passkey, user->passkey_sz)) goto out; - ksmbd_all_conn_set_status(id, KSMBD_SESS_NEED_RECONNECT); - err = ksmbd_conn_wait_idle_sess_id(conn, id); + down_write(&prev_sess->chann_lock); + if (prev_sess->tearing_down) { + up_write(&prev_sess->chann_lock); + goto out; + } + prev_sess->tearing_down = true; + up_write(&prev_sess->chann_lock); + + ksmbd_all_conn_set_status(prev_sess, KSMBD_SESS_NEED_RECONNECT); + err = ksmbd_conn_wait_idle_sess(conn, prev_sess); if (err) { - ksmbd_all_conn_set_status(id, KSMBD_SESS_NEED_SETUP); + down_write(&prev_sess->chann_lock); + prev_sess->tearing_down = false; + up_write(&prev_sess->chann_lock); + ksmbd_all_conn_set_status(prev_sess, KSMBD_SESS_GOOD); goto out; } - ksmbd_destroy_file_table(&prev_sess->file_table); + ksmbd_destroy_file_table(prev_sess); + prev_sess->kerberos_expiry = 0; prev_sess->state = SMB2_SESSION_EXPIRED; - ksmbd_all_conn_set_status(id, KSMBD_SESS_NEED_SETUP); + ksmbd_all_conn_set_status(prev_sess, KSMBD_SESS_NEED_SETUP); ksmbd_launch_ksmbd_durable_scavenger(); out: up_write(&conn->session_lock); @@ -667,6 +842,8 @@ static struct ksmbd_session *__session_create(int protocol) if (!sess) return NULL; + ida_init(&sess->tree_conn_ida); + if (ksmbd_init_file_table(&sess->file_table)) goto error; @@ -686,14 +863,13 @@ static struct ksmbd_session *__session_create(int protocol) if (ret) goto error; - ida_init(&sess->tree_conn_ida); - down_write(&sessions_table_lock); hash_add(sessions_table, &sess->hlist, sess->id); + ksmbd_counter_inc(KSMBD_COUNTER_SESSIONS); up_write(&sessions_table_lock); - create_proc_session(sess); - ksmbd_counter_inc(KSMBD_COUNTER_SESSIONS); + if (create_proc_session(sess)) + pr_warn_ratelimited("Unable to create session %llu procfs entry\n", sess->id); return sess; error: diff --git a/fs/smb/server/mgmt/user_session.h b/fs/smb/server/mgmt/user_session.h index 6aebd385be84..217258551d6d 100644 --- a/fs/smb/server/mgmt/user_session.h +++ b/fs/smb/server/mgmt/user_session.h @@ -19,6 +19,7 @@ struct ksmbd_file_table; struct channel { + char sess_key[CIFS_KEY_SIZE]; __u8 smb3signingkey[SMB3_SIGN_KEY_SIZE]; struct ksmbd_conn *conn; }; @@ -41,11 +42,13 @@ struct ksmbd_session { bool sign; bool enc; + bool tearing_down; int state; __u8 *Preauth_HashValue; char sess_key[CIFS_KEY_SIZE]; + u64 kerberos_expiry; struct hlist_node hlist; struct rw_semaphore chann_lock; @@ -69,6 +72,8 @@ struct ksmbd_session { struct rw_semaphore rpc_lock; }; +#define KSMBD_MAX_CHANNELS 32 + static inline int test_session_flag(struct ksmbd_session *sess, int bit) { return sess->flags & bit; @@ -95,14 +100,21 @@ bool is_ksmbd_session_in_connection(struct ksmbd_conn *conn, unsigned long long id); int ksmbd_session_register(struct ksmbd_conn *conn, struct ksmbd_session *sess); -void ksmbd_sessions_deregister(struct ksmbd_conn *conn); +void ksmbd_session_unregister(struct ksmbd_conn *conn, + struct ksmbd_session *sess); +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); struct ksmbd_session *__session_lookup(unsigned long long id); struct ksmbd_session *ksmbd_session_lookup_all(struct ksmbd_conn *conn, unsigned long long id); +struct ksmbd_session *ksmbd_session_lookup_all_states(struct ksmbd_conn *conn, + unsigned long long id); void destroy_previous_session(struct ksmbd_conn *conn, struct ksmbd_user *user, u64 id); struct preauth_session *ksmbd_preauth_session_alloc(struct ksmbd_conn *conn, u64 sess_id); +void ksmbd_preauth_session_destroy(struct ksmbd_conn *conn); struct preauth_session *ksmbd_preauth_session_lookup(struct ksmbd_conn *conn, unsigned long long id); diff --git a/fs/smb/server/misc.c b/fs/smb/server/misc.c index a543ec9d3581..2dd91c9e956a 100644 --- a/fs/smb/server/misc.c +++ b/fs/smb/server/misc.c @@ -121,7 +121,9 @@ int parse_stream_name(char *filename, char **stream_name, int *s_type) char *stream_type; char *s_name; int rc = 0; + bool has_stream_type = false; + *stream_name = NULL; s_name = filename; filename = strsep(&s_name, ":"); ksmbd_debug(SMB, "filename : %s, streams : %s\n", filename, s_name); @@ -137,14 +139,20 @@ int parse_stream_name(char *filename, char **stream_name, int *s_type) ksmbd_debug(SMB, "stream name : %s, stream type : %s\n", s_name, stream_type); - if (!strncasecmp("$data", stream_type, 5)) + if (!strncasecmp("$data", stream_type, 5)) { *s_type = DATA_STREAM; - else if (!strncasecmp("$index_allocation", stream_type, 17)) + has_stream_type = true; + } else if (!strncasecmp("$index_allocation", stream_type, 17)) { *s_type = DIR_STREAM; - else + has_stream_type = true; + } else { rc = -ENOENT; + } } + if (has_stream_type && !s_name[0] && *s_type == DATA_STREAM) + goto out; + *stream_name = s_name; out: return rc; @@ -283,39 +291,6 @@ char *ksmbd_extract_sharename(struct unicode_map *um, const char *treename) return ksmbd_casefold_sharename(um, name); } -/** - * convert_to_unix_name() - convert windows name to unix format - * @share: ksmbd_share_config pointer - * @name: file name that is relative to share - * - * Return: converted name on success, otherwise NULL - */ -char *convert_to_unix_name(struct ksmbd_share_config *share, const char *name) -{ - int no_slash = 0, name_len, path_len; - char *new_name; - - if (name[0] == '/') - name++; - - path_len = share->path_sz; - name_len = strlen(name); - new_name = kmalloc(path_len + name_len + 2, KSMBD_DEFAULT_GFP); - if (!new_name) - return new_name; - - memcpy(new_name, share->path, path_len); - if (new_name[path_len - 1] != '/') { - new_name[path_len] = '/'; - no_slash = 1; - } - - memcpy(new_name + path_len + no_slash, name, name_len); - path_len += name_len + no_slash; - new_name[path_len] = 0x00; - return new_name; -} - char *ksmbd_convert_dir_info_name(struct ksmbd_dir_info *d_info, const struct nls_table *local_nls, int *conv_len) diff --git a/fs/smb/server/misc.h b/fs/smb/server/misc.h index 13423696ae8c..1faaddd0f5f7 100644 --- a/fs/smb/server/misc.h +++ b/fs/smb/server/misc.h @@ -25,7 +25,6 @@ void ksmbd_strip_last_slash(char *path); void ksmbd_conv_path_to_windows(char *path); char *ksmbd_casefold_sharename(struct unicode_map *um, const char *name); char *ksmbd_extract_sharename(struct unicode_map *um, const char *treename); -char *convert_to_unix_name(struct ksmbd_share_config *share, const char *name); #define KSMBD_DIR_INFO_ALIGNMENT 8 struct ksmbd_dir_info; @@ -44,7 +43,7 @@ struct ksmbd_const_name { const char *name; }; -void ksmbd_proc_init(void); +int ksmbd_proc_init(void); void ksmbd_proc_cleanup(void); void ksmbd_proc_reset(void); struct proc_dir_entry *ksmbd_proc_create(const char *name, @@ -54,13 +53,10 @@ void ksmbd_proc_show_flag_names(struct seq_file *m, const struct ksmbd_const_name *table, int count, unsigned int flags); -void ksmbd_proc_show_const_name(struct seq_file *m, - const char *format, - const struct ksmbd_const_name *table, - int count, - unsigned int const_value); +const char *ksmbd_proc_const_name(const struct ksmbd_const_name *table, + int count, unsigned int const_value); #else -static inline void ksmbd_proc_init(void) {} +static inline int ksmbd_proc_init(void) { return 0; } static inline void ksmbd_proc_cleanup(void) {} static inline void ksmbd_proc_reset(void) {} #endif diff --git a/fs/smb/server/oplock.c b/fs/smb/server/oplock.c index 09d9878db9cb..1b8c3482d1e4 100644 --- a/fs/smb/server/oplock.c +++ b/fs/smb/server/oplock.c @@ -5,6 +5,7 @@ */ #include <linux/moduleparam.h> +#include <linux/err.h> #include "glob.h" #include "oplock.h" @@ -15,10 +16,58 @@ #include "mgmt/user_session.h" #include "mgmt/share_config.h" #include "mgmt/tree_connect.h" +#include "server.h" static LIST_HEAD(lease_table_list); static DEFINE_RWLOCK(lease_list_lock); +#define SMB2_LEASE_STATE_MASK_LE (SMB2_LEASE_READ_CACHING_LE | \ + SMB2_LEASE_HANDLE_CACHING_LE | \ + SMB2_LEASE_WRITE_CACHING_LE) + +static bool lease_state_valid(__le32 state) +{ + return !(state & ~SMB2_LEASE_STATE_MASK_LE); +} + +static __le32 lease_state_grantable(__le32 state) +{ + if (state == SMB2_LEASE_READ_CACHING_LE || + state == (SMB2_LEASE_READ_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE) || + state == (SMB2_LEASE_READ_CACHING_LE | SMB2_LEASE_WRITE_CACHING_LE) || + state == SMB2_LEASE_STATE_MASK_LE) + return state; + + return 0; +} + +static bool lease_v2_flags_valid(__le32 flags) +{ + return !(flags & ~SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE); +} + +static bool lease_has_parent_key(struct lease *lease) +{ + return lease->flags & SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE; +} + +static bool lease_break_in_progress(struct lease *lease) +{ + struct oplock_info *opinfo; + bool ret = false; + + spin_lock(&lease->lock); + list_for_each_entry(opinfo, &lease->open_list, lease_entry) { + if (opinfo->op_state == OPLOCK_ACK_WAIT) { + ret = true; + break; + } + } + spin_unlock(&lease->lock); + + return ret; +} + /** * alloc_opinfo() - allocate a new opinfo object for oplock info * @work: smb work @@ -30,7 +79,6 @@ static DEFINE_RWLOCK(lease_list_lock); static struct oplock_info *alloc_opinfo(struct ksmbd_work *work, u64 id, __u16 Tid) { - struct ksmbd_conn *conn = work->conn; struct ksmbd_session *sess = work->sess; struct oplock_info *opinfo; @@ -39,63 +87,96 @@ static struct oplock_info *alloc_opinfo(struct ksmbd_work *work, return NULL; opinfo->sess = sess; - opinfo->conn = conn; + opinfo->conn = ksmbd_conn_get(work->conn); opinfo->level = SMB2_OPLOCK_LEVEL_NONE; opinfo->op_state = OPLOCK_STATE_NONE; + spin_lock_init(&opinfo->state_lock); opinfo->pending_break = 0; opinfo->fid = id; opinfo->Tid = Tid; INIT_LIST_HEAD(&opinfo->op_entry); + INIT_LIST_HEAD(&opinfo->lease_entry); init_waitqueue_head(&opinfo->oplock_q); init_waitqueue_head(&opinfo->oplock_brk); atomic_set(&opinfo->refcount, 1); atomic_set(&opinfo->breaking_cnt, 0); - atomic_inc(&opinfo->conn->refcnt); return opinfo; } -static void lease_add_list(struct oplock_info *opinfo) +static void lease_get(struct lease *lease) +{ + atomic_inc(&lease->refcount); +} + +static void lease_put(struct lease *lease) { - struct lease_table *lb = opinfo->o_lease->l_lb; + if (lease && atomic_dec_and_test(&lease->refcount)) + kfree(lease); +} +static void lease_add_table(struct lease *lease, struct lease_table *lb) +{ + lease_get(lease); + lease->l_lb = lb; spin_lock(&lb->lb_lock); - list_add_rcu(&opinfo->lease_entry, &lb->lease_list); + list_add_rcu(&lease->l_entry, &lb->lease_list); spin_unlock(&lb->lb_lock); } -static void lease_del_list(struct oplock_info *opinfo) +static void lease_del_table(struct lease *lease) { - struct lease_table *lb = opinfo->o_lease->l_lb; + struct lease_table *lb = lease->l_lb; if (!lb) return; spin_lock(&lb->lb_lock); - if (list_empty(&opinfo->lease_entry)) { + if (list_empty(&lease->l_entry)) { spin_unlock(&lb->lb_lock); return; } - list_del_init(&opinfo->lease_entry); - opinfo->o_lease->l_lb = NULL; + list_del_init(&lease->l_entry); + lease->l_lb = NULL; spin_unlock(&lb->lb_lock); + + lease_put(lease); } -static void lb_add(struct lease_table *lb) +static struct lease_table *alloc_lease_table(struct oplock_info *opinfo) { - write_lock(&lease_list_lock); - list_add(&lb->l_entry, &lease_table_list); - write_unlock(&lease_list_lock); + struct lease_table *lb; + + lb = kmalloc_obj(struct lease_table, KSMBD_DEFAULT_GFP); + if (!lb) + return NULL; + + 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 int alloc_lease(struct oplock_info *opinfo, struct lease_ctx_info *lctx) +static struct lease *alloc_lease(struct lease_ctx_info *lctx, + struct ksmbd_inode *ci) { struct lease *lease; lease = kmalloc_obj(struct lease, KSMBD_DEFAULT_GFP); if (!lease) - return -ENOMEM; + return NULL; memcpy(lease->lease_key, lctx->lease_key, SMB2_LEASE_KEY_SIZE); lease->state = lctx->req_state; @@ -105,30 +186,87 @@ static int alloc_lease(struct oplock_info *opinfo, struct lease_ctx_info *lctx) lease->is_dir = lctx->is_dir; memcpy(lease->parent_lease_key, lctx->parent_lease_key, SMB2_LEASE_KEY_SIZE); lease->version = lctx->version; - lease->epoch = le16_to_cpu(lctx->epoch) + 1; - INIT_LIST_HEAD(&opinfo->lease_entry); - opinfo->o_lease = lease; + lease->epoch = lctx->version == 2 ? le16_to_cpu(lctx->epoch) + 1 : 0; + lease->ci = ci; + lease->reuse_epoch = false; + lease->l_lb = NULL; + INIT_LIST_HEAD(&lease->l_entry); + INIT_LIST_HEAD(&lease->open_list); + spin_lock_init(&lease->lock); + atomic_set(&lease->refcount, 1); + + return lease; +} - return 0; +static void lease_add_open(struct lease *lease, struct oplock_info *opinfo) +{ + spin_lock(&lease->lock); + list_add(&opinfo->lease_entry, &lease->open_list); + spin_unlock(&lease->lock); } -static void free_lease(struct oplock_info *opinfo) +static void lease_del_open(struct oplock_info *opinfo) { - struct lease *lease; + struct lease *lease = opinfo->o_lease; + bool remove_table = false; - lease = opinfo->o_lease; - kfree(lease); + if (!lease) + return; + + spin_lock(&lease->lock); + if (!list_empty(&opinfo->lease_entry)) { + list_del_init(&opinfo->lease_entry); + remove_table = list_empty(&lease->open_list); + } + spin_unlock(&lease->lock); + + if (remove_table) { + write_lock(&lease_list_lock); + lease_del_table(lease); + write_unlock(&lease_list_lock); + } } -static void free_opinfo(struct oplock_info *opinfo) +static void free_lease(struct oplock_info *opinfo) +{ + lease_put(opinfo->o_lease); +} + +static void __free_opinfo(struct oplock_info *opinfo) { if (opinfo->is_lease) free_lease(opinfo); - if (opinfo->conn && atomic_dec_and_test(&opinfo->conn->refcnt)) - kfree(opinfo->conn); + ksmbd_conn_put(opinfo->conn); kfree(opinfo); } +static void free_opinfo_rcu(struct rcu_head *rcu) +{ + struct oplock_info *opinfo = container_of(rcu, struct oplock_info, rcu); + + __free_opinfo(opinfo); +} + +static void free_opinfo(struct oplock_info *opinfo) +{ + call_rcu(&opinfo->rcu, free_opinfo_rcu); +} + +void lease_update_oplock_levels(struct lease *lease) +{ + struct oplock_info *opinfo; + __u8 level; + + if (!lease) + return; + + level = smb2_map_lease_to_oplock(lease->state); + spin_lock(&lease->lock); + list_for_each_entry(opinfo, &lease->open_list, lease_entry) + opinfo->level = level; + spin_unlock(&lease->lock); +} + struct oplock_info *opinfo_get(struct ksmbd_file *fp) { struct oplock_info *opinfo; @@ -142,10 +280,24 @@ struct oplock_info *opinfo_get(struct ksmbd_file *fp) return opinfo; } -static struct oplock_info *opinfo_get_list(struct ksmbd_inode *ci) +struct oplock_snapshot { + bool durable_open; + bool durable_detached; + unsigned long long fid; +}; + +static struct oplock_info *opinfo_get_list(struct ksmbd_inode *ci, + struct ksmbd_file *skip_fp, + struct oplock_snapshot *snapshot) { struct oplock_info *opinfo; + if (snapshot) { + snapshot->durable_open = false; + snapshot->durable_detached = false; + snapshot->fid = KSMBD_NO_FID; + } + down_read(&ci->m_lock); opinfo = list_first_entry_or_null(&ci->m_op_list, struct oplock_info, op_entry); @@ -159,6 +311,16 @@ static struct oplock_info *opinfo_get_list(struct ksmbd_inode *ci) opinfo = NULL; } } + + if (opinfo && snapshot && opinfo->o_fp && + opinfo->o_fp != skip_fp && + READ_ONCE(opinfo->o_fp->is_durable)) { + snapshot->durable_open = true; + snapshot->durable_detached = + !READ_ONCE(opinfo->o_fp->conn) || + !READ_ONCE(opinfo->o_fp->tcon); + snapshot->fid = opinfo->fid; + } } up_read(&ci->m_lock); @@ -176,9 +338,21 @@ void opinfo_put(struct oplock_info *opinfo) free_opinfo(opinfo); } -static void opinfo_add(struct oplock_info *opinfo) +static bool ksmbd_inode_has_lease(struct ksmbd_inode *ci) { - struct ksmbd_inode *ci = opinfo->o_fp->f_ci; + struct oplock_info *opinfo = opinfo_get_list(ci, NULL, NULL); + bool is_lease; + + if (!opinfo) + return false; + is_lease = opinfo->is_lease; + opinfo_put(opinfo); + return is_lease; +} + +static void opinfo_add(struct oplock_info *opinfo, struct ksmbd_file *fp) +{ + struct ksmbd_inode *ci = fp->f_ci; down_write(&ci->m_lock); list_add(&opinfo->op_entry, &ci->m_op_list); @@ -189,11 +363,9 @@ static void opinfo_del(struct oplock_info *opinfo) { struct ksmbd_inode *ci = opinfo->o_fp->f_ci; - if (opinfo->is_lease) { - write_lock(&lease_list_lock); - lease_del_list(opinfo); - write_unlock(&lease_list_lock); - } + if (opinfo->is_lease) + lease_del_open(opinfo); + down_write(&ci->m_lock); list_del(&opinfo->op_entry); up_write(&ci->m_lock); @@ -242,8 +414,10 @@ int opinfo_write_to_read(struct oplock_info *opinfo) } opinfo->level = SMB2_OPLOCK_LEVEL_II; - if (opinfo->is_lease) + if (opinfo->is_lease) { lease->state = lease->new_state; + lease_update_oplock_levels(lease); + } return 0; } @@ -258,7 +432,7 @@ int opinfo_read_handle_to_read(struct oplock_info *opinfo) struct lease *lease = opinfo->o_lease; lease->state = lease->new_state; - opinfo->level = SMB2_OPLOCK_LEVEL_II; + lease_update_oplock_levels(lease); return 0; } @@ -280,8 +454,10 @@ int opinfo_write_to_none(struct oplock_info *opinfo) return -EINVAL; } opinfo->level = SMB2_OPLOCK_LEVEL_NONE; - if (opinfo->is_lease) + if (opinfo->is_lease) { lease->state = lease->new_state; + lease_update_oplock_levels(lease); + } return 0; } @@ -302,8 +478,10 @@ int opinfo_read_to_none(struct oplock_info *opinfo) return -EINVAL; } opinfo->level = SMB2_OPLOCK_LEVEL_NONE; - if (opinfo->is_lease) + if (opinfo->is_lease) { lease->state = lease->new_state; + lease_update_oplock_levels(lease); + } return 0; } @@ -324,10 +502,7 @@ int lease_read_to_write(struct oplock_info *opinfo) lease->new_state = SMB2_LEASE_NONE_LE; lease->state |= SMB2_LEASE_WRITE_CACHING_LE; - if (lease->state & SMB2_LEASE_HANDLE_CACHING_LE) - opinfo->level = SMB2_OPLOCK_LEVEL_BATCH; - else - opinfo->level = SMB2_OPLOCK_LEVEL_EXCLUSIVE; + lease_update_oplock_levels(lease); return 0; } @@ -349,15 +524,7 @@ static int lease_none_upgrade(struct oplock_info *opinfo, __le32 new_state) lease->new_state = SMB2_LEASE_NONE_LE; lease->state = new_state; - if (lease->state & SMB2_LEASE_HANDLE_CACHING_LE) - if (lease->state & SMB2_LEASE_WRITE_CACHING_LE) - opinfo->level = SMB2_OPLOCK_LEVEL_BATCH; - else - opinfo->level = SMB2_OPLOCK_LEVEL_II; - else if (lease->state & SMB2_LEASE_WRITE_CACHING_LE) - opinfo->level = SMB2_OPLOCK_LEVEL_EXCLUSIVE; - else if (lease->state & SMB2_LEASE_READ_CACHING_LE) - opinfo->level = SMB2_OPLOCK_LEVEL_II; + lease_update_oplock_levels(lease); return 0; } @@ -380,14 +547,23 @@ void close_id_del_oplock(struct ksmbd_file *fp) opinfo_del(opinfo); rcu_assign_pointer(fp->f_opinfo, NULL); - if (opinfo->op_state == OPLOCK_ACK_WAIT) { - opinfo->op_state = OPLOCK_CLOSING; - wake_up_interruptible_all(&opinfo->oplock_q); - if (opinfo->is_lease) { - atomic_set(&opinfo->breaking_cnt, 0); - wake_up_interruptible_all(&opinfo->oplock_brk); - } - } + spin_lock(&opinfo->state_lock); + if (opinfo->op_state == OPLOCK_ACK_WAIT && opinfo->is_lease) + atomic_set(&opinfo->breaking_cnt, 0); + /* + * An opinfo that has been removed from the inode list is terminal. Keep + * this transition and releasing pending_break under state_lock. a breaker + * takes the same lock before it acquires pending_break or sets ACK_WAIT. + */ + opinfo->op_state = OPLOCK_CLOSING; + clear_bit_unlock(0, &opinfo->pending_break); + spin_unlock(&opinfo->state_lock); + wake_up_interruptible_all(&opinfo->oplock_q); + if (opinfo->is_lease) + wake_up_interruptible_all(&opinfo->oplock_brk); + /* memory barrier is needed for wake_up_bit() */ + smp_mb__after_atomic(); + wake_up_bit(&opinfo->pending_break, 0); opinfo_count_dec(fp); atomic_dec(&opinfo->refcount); @@ -464,8 +640,12 @@ static inline int compare_guid_key(struct oplock_info *opinfo, const char *guid1, const char *key1) { const char *guid2, *key2; + struct ksmbd_conn *conn; - guid2 = opinfo->conn->ClientGUID; + conn = READ_ONCE(opinfo->conn); + if (!conn) + 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)) @@ -484,7 +664,7 @@ static inline int compare_guid_key(struct oplock_info *opinfo, * Return: oplock(lease) object on success, otherwise NULL */ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci, - char *client_guid, + const char *client_guid, struct lease_ctx_info *lctx) { int ret; @@ -507,7 +687,12 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci, ret = compare_guid_key(opinfo, client_guid, lctx->lease_key); if (ret) { + if (!atomic_inc_not_zero(&opinfo->refcount)) + continue; + if (m_opinfo) + opinfo_put(m_opinfo); m_opinfo = opinfo; + /* skip upgrading lease about breaking lease */ if (atomic_read(&opinfo->breaking_cnt)) continue; @@ -529,8 +714,11 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci, if (lctx->req_state == (SMB2_LEASE_READ_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE)) { - lease->epoch++; - lease->state = lctx->req_state; + if (lease->state != lctx->req_state) { + lease->epoch++; + lease->state = lctx->req_state; + lease_update_oplock_levels(lease); + } } } @@ -546,7 +734,7 @@ static struct oplock_info *same_client_has_lease(struct ksmbd_inode *ci, return m_opinfo; } -static void wait_for_break_ack(struct oplock_info *opinfo) +static bool wait_for_break_ack(struct oplock_info *opinfo) { int rc = 0; @@ -557,11 +745,22 @@ static void wait_for_break_ack(struct oplock_info *opinfo) /* is this a timeout ? */ if (!rc) { - if (opinfo->is_lease) + 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) @@ -572,17 +771,49 @@ static void wake_up_oplock_break(struct oplock_info *opinfo) wake_up_bit(&opinfo->pending_break, 0); } +static bool oplock_break_set_ack_wait(struct oplock_info *opinfo) +{ + bool ret = false; + + spin_lock(&opinfo->state_lock); + if (opinfo->op_state != OPLOCK_CLOSING) { + opinfo->op_state = OPLOCK_ACK_WAIT; + ret = true; + } + spin_unlock(&opinfo->state_lock); + + return ret; +} + static int oplock_break_pending(struct oplock_info *opinfo, int req_op_level) { - while (test_and_set_bit(0, &opinfo->pending_break)) { + for (;;) { + bool closing; + + spin_lock(&opinfo->state_lock); + closing = opinfo->op_state == OPLOCK_CLOSING; + if (!closing && !test_and_set_bit(0, &opinfo->pending_break)) { + spin_unlock(&opinfo->state_lock); + break; + } + spin_unlock(&opinfo->state_lock); + if (closing) + return -ENOENT; + + if (opinfo->is_lease) + opinfo->o_lease->reuse_epoch = true; + wait_on_bit(&opinfo->pending_break, 0, TASK_UNINTERRUPTIBLE); /* Not immediately break to none. */ opinfo->open_trunc = 0; - if (opinfo->op_state == OPLOCK_CLOSING) + spin_lock(&opinfo->state_lock); + closing = opinfo->op_state == OPLOCK_CLOSING; + spin_unlock(&opinfo->state_lock); + if (closing) return -ENOENT; - else if (opinfo->level <= req_op_level) { + if (opinfo->level <= req_op_level) { if (opinfo->is_lease == false) return 1; @@ -608,6 +839,17 @@ static int oplock_break_pending(struct oplock_info *opinfo, int req_op_level) return 0; } +static bool lease_break_needed(struct oplock_info *opinfo, int req_op_level, + bool open_trunc) +{ + struct lease *lease = opinfo->o_lease; + + if (open_trunc) + return lease->state != SMB2_LEASE_NONE_LE; + + return opinfo->level > req_op_level; +} + /** * __smb2_oplock_break_noti() - send smb2 oplock break cmd from conn * to client @@ -679,28 +921,72 @@ static void __smb2_oplock_break_noti(struct work_struct *wk) out: ksmbd_free_work_struct(work); ksmbd_conn_r_count_dec(conn); + ksmbd_conn_put(conn); +} + +/* + * 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. + */ +static struct ksmbd_conn *smb2_oplock_break_conn_get(struct oplock_info *opinfo, + struct ksmbd_inode *ci) +{ + struct ksmbd_conn *conn; + + down_read(&ci->m_lock); + conn = READ_ONCE(opinfo->conn); + if (conn && !ksmbd_conn_releasing(conn)) + conn = ksmbd_conn_get(conn); + else + conn = NULL; + up_read(&ci->m_lock); + + return conn; } /** * 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) +static int smb2_oplock_break_noti(struct oplock_info *opinfo, + struct ksmbd_inode *ci) { - struct ksmbd_conn *conn = opinfo->conn; + struct ksmbd_conn *conn; struct oplock_break_info *br_info; int ret = 0; - struct ksmbd_work *work = ksmbd_alloc_work_struct(); + struct ksmbd_work *work; - if (!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; + } 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; } @@ -709,6 +995,7 @@ static int smb2_oplock_break_noti(struct oplock_info *opinfo) br_info->open_trunc = opinfo->open_trunc; work->request_buf = (char *)br_info; + /* Transfer the reference acquired by smb2_oplock_break_conn_get(). */ work->conn = conn; work->sess = opinfo->sess; @@ -717,7 +1004,8 @@ static int smb2_oplock_break_noti(struct oplock_info *opinfo) INIT_WORK(&work->work, __smb2_oplock_break_noti); ksmbd_queue_work(work); - wait_for_break_ack(opinfo); + 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) @@ -783,54 +1071,106 @@ static void __smb2_lease_break_noti(struct work_struct *wk) out: ksmbd_free_work_struct(work); ksmbd_conn_r_count_dec(conn); + ksmbd_conn_put(conn); +} + +/* + * 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 lease *lease = opinfo->o_lease; + struct lease_table *lb; + struct ksmbd_conn *conn; + + /* 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 (conn || lease->version != 2) + return conn; + + /* 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); + + return conn; } /** * 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) +static int smb2_lease_break_noti(struct oplock_info *opinfo, bool sync, + bool inc_epoch) { - struct ksmbd_conn *conn = opinfo->conn; + struct ksmbd_conn *conn; struct ksmbd_work *work; struct lease_break_info *br_info; struct lease *lease = opinfo->o_lease; + conn = smb2_lease_break_conn_get(opinfo); + if (!conn) + return ksmbd_invalidate_durable_fd(opinfo->fid); + work = ksmbd_alloc_work_struct(); - if (!work) + if (!work) { + ksmbd_conn_put(conn); return -ENOMEM; + } 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) - br_info->epoch = cpu_to_le16(++lease->epoch); - else + if (lease->version == 2) { + if (inc_epoch) + lease->epoch++; + br_info->epoch = cpu_to_le16(lease->epoch); + } else { br_info->epoch = 0; + } 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) { - INIT_WORK(&work->work, __smb2_lease_break_noti); - ksmbd_queue_work(work); - wait_for_break_ack(opinfo); + 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->level = SMB2_OPLOCK_LEVEL_NONE; opinfo->o_lease->state = SMB2_LEASE_NONE_LE; + lease_update_oplock_levels(opinfo->o_lease); } } return 0; @@ -853,10 +1193,12 @@ static void wait_lease_breaking(struct oplock_info *opinfo) } } -static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, - struct ksmbd_work *in_work) +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) { int err = 0; + bool sent_interim = false; /* Need to break exclusive/batch oplock, write lease or overwrite_if */ ksmbd_debug(OPLOCK, @@ -865,18 +1207,36 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, if (brk_opinfo->is_lease) { struct lease *lease = brk_opinfo->o_lease; + bool open_trunc = brk_opinfo->open_trunc; + bool was_pending = test_bit(0, &brk_opinfo->pending_break); + bool wait_ack; + bool inc_epoch = true; + + if (in_work && was_pending) { + setup_async_work(in_work, NULL, NULL); + smb2_send_interim_resp(in_work, STATUS_PENDING); + release_async_work(in_work); + sent_interim = true; + } - atomic_inc(&brk_opinfo->breaking_cnt); err = oplock_break_pending(brk_opinfo, req_op_level); if (err) return err < 0 ? err : 0; + if (was_pending) + open_trunc = brk_opinfo->open_trunc; - if (brk_opinfo->open_trunc) { +again: + atomic_inc(&brk_opinfo->breaking_cnt); + if (open_trunc) { /* * Create overwrite break trigger the lease break to * none. */ lease->new_state = SMB2_LEASE_NONE_LE; + } else if (share_break && + lease->state & SMB2_LEASE_HANDLE_CACHING_LE) { + lease->new_state = + lease->state & ~SMB2_LEASE_HANDLE_CACHING_LE; } else { if (lease->state & SMB2_LEASE_WRITE_CACHING_LE) { if (lease->state & SMB2_LEASE_HANDLE_CACHING_LE) @@ -898,44 +1258,131 @@ static int oplock_break(struct oplock_info *brk_opinfo, int req_op_level, if (lease->state & (SMB2_LEASE_WRITE_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE)) { - if (in_work) { - setup_async_work(in_work, NULL, NULL); - smb2_send_interim_resp(in_work, STATUS_PENDING); - release_async_work(in_work); + if (!oplock_break_set_ack_wait(brk_opinfo)) { + atomic_dec_if_positive(&brk_opinfo->breaking_cnt); + wake_up_oplock_break(brk_opinfo); + return -ENOENT; } - - brk_opinfo->op_state = OPLOCK_ACK_WAIT; } else atomic_dec(&brk_opinfo->breaking_cnt); + + wait_ack = !(open_trunc && + lease->state == (SMB2_LEASE_READ_CACHING_LE | + SMB2_LEASE_HANDLE_CACHING_LE)); + if (lease->reuse_epoch) { + inc_epoch = false; + lease->reuse_epoch = false; + } + err = smb2_lease_break_noti(brk_opinfo, sync_lease_break, inc_epoch); + 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) + err = -ENOENT; + + if (wait_ack) + wait_lease_breaking(brk_opinfo); + /* + * A share-mode conflict break only drops the conflicting + * caching bit; the triggering open fails with a sharing + * violation, so keep it to a single break. + * + * Otherwise chain another break while the lease is still + * incompatible with this open (req_op_level), or while a + * truncating waiter that arrived during the break still needs + * the lease dropped to none. open_trunc snapshotted for this + * break stays cleared, so the next state is computed from the + * lease state and the cascade steps down (e.g. RH->R->none) + * instead of collapsing straight to none. + */ + if (wait_ack && !err && !share_break && + (lease_break_needed(brk_opinfo, req_op_level, open_trunc) || + (brk_opinfo->open_trunc && + lease->state != SMB2_LEASE_NONE_LE))) + goto again; + + wake_up_oplock_break(brk_opinfo); + return err; } else { err = oplock_break_pending(brk_opinfo, req_op_level); if (err) return err < 0 ? err : 0; if (brk_opinfo->level == SMB2_OPLOCK_LEVEL_BATCH || - brk_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) - brk_opinfo->op_state = OPLOCK_ACK_WAIT; + brk_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) { + if (!oplock_break_set_ack_wait(brk_opinfo)) { + wake_up_oplock_break(brk_opinfo); + 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); + } } - if (brk_opinfo->is_lease) - err = smb2_lease_break_noti(brk_opinfo); - else - err = smb2_oplock_break_noti(brk_opinfo); + err = smb2_oplock_break_noti(brk_opinfo, ci); ksmbd_debug(OPLOCK, "oplock granted = %d\n", brk_opinfo->level); if (brk_opinfo->op_state == OPLOCK_CLOSING) - err = -ENOENT; + err = -EAGAIN; wake_up_oplock_break(brk_opinfo); - wait_lease_breaking(brk_opinfo); - return err; } +struct oplock_break_entry { + struct list_head list; + struct oplock_info *opinfo; +}; + +static int oplock_break_add(struct list_head *head, struct oplock_info *opinfo) +{ + struct oplock_break_entry *ent; + + ent = kmalloc_obj(struct oplock_break_entry, KSMBD_DEFAULT_GFP); + if (!ent) + return -ENOMEM; + + ent->opinfo = opinfo; + list_add_tail(&ent->list, head); + return 0; +} + +static void oplock_break_drain_none(struct list_head *head, + struct ksmbd_inode *ci) +{ + struct oplock_break_entry *ent, *tmp; + + list_for_each_entry_safe(ent, tmp, head, list) { + oplock_break(ent->opinfo, ci, SMB2_OPLOCK_LEVEL_NONE, NULL, + false, false); + list_del(&ent->list); + opinfo_put(ent->opinfo); + kfree(ent); + } +} + void destroy_lease_table(struct ksmbd_conn *conn) { struct lease_table *lb, *lbtmp; - struct oplock_info *opinfo; + struct lease *lease, *ltmp; write_lock(&lease_list_lock); if (list_empty(&lease_table_list)) { @@ -947,25 +1394,18 @@ void destroy_lease_table(struct ksmbd_conn *conn) if (conn && memcmp(lb->client_guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE)) continue; -again: - rcu_read_lock(); - list_for_each_entry_rcu(opinfo, &lb->lease_list, - lease_entry) { - rcu_read_unlock(); - lease_del_list(opinfo); - goto again; - } - rcu_read_unlock(); + list_for_each_entry_safe(lease, ltmp, &lb->lease_list, l_entry) + lease_del_table(lease); list_del(&lb->l_entry); - kfree(lb); + free_lease_table(lb); } write_unlock(&lease_list_lock); } -int find_same_lease_key(struct ksmbd_session *sess, struct ksmbd_inode *ci, +int find_same_lease_key(struct ksmbd_conn *conn, struct ksmbd_inode *ci, struct lease_ctx_info *lctx) { - struct oplock_info *opinfo; + struct lease *lease; int err = 0; struct lease_table *lb; @@ -979,7 +1419,7 @@ int find_same_lease_key(struct ksmbd_session *sess, struct ksmbd_inode *ci, } list_for_each_entry(lb, &lease_table_list, l_entry) { - if (!memcmp(lb->client_guid, sess->ClientGUID, + if (!memcmp(lb->client_guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE)) goto found; } @@ -988,76 +1428,42 @@ int find_same_lease_key(struct ksmbd_session *sess, struct ksmbd_inode *ci, return 0; found: - rcu_read_lock(); - list_for_each_entry_rcu(opinfo, &lb->lease_list, lease_entry) { - if (!atomic_inc_not_zero(&opinfo->refcount)) + list_for_each_entry(lease, &lb->lease_list, l_entry) { + if (lease->ci == ci) continue; - rcu_read_unlock(); - if (opinfo->o_fp->f_ci == ci) - goto op_next; - err = compare_guid_key(opinfo, sess->ClientGUID, - lctx->lease_key); - if (err) { + if (!memcmp(lease->lease_key, lctx->lease_key, + SMB2_LEASE_KEY_SIZE)) { err = -EINVAL; ksmbd_debug(OPLOCK, "found same lease key is already used in other files\n"); - opinfo_put(opinfo); goto out; } -op_next: - opinfo_put(opinfo); - rcu_read_lock(); } - rcu_read_unlock(); out: read_unlock(&lease_list_lock); return err; } -static void copy_lease(struct oplock_info *op1, struct oplock_info *op2) -{ - struct lease *lease1 = op1->o_lease; - struct lease *lease2 = op2->o_lease; - - op2->level = op1->level; - lease2->state = lease1->state; - memcpy(lease2->lease_key, lease1->lease_key, - SMB2_LEASE_KEY_SIZE); - lease2->duration = lease1->duration; - lease2->flags = lease1->flags; - lease2->epoch = lease1->epoch; - lease2->version = lease1->version; -} - -static int add_lease_global_list(struct oplock_info *opinfo) +static void add_lease_global_list(struct lease *lease, struct ksmbd_conn *conn, + struct lease_table *new_lb) { struct lease_table *lb; - read_lock(&lease_list_lock); + write_lock(&lease_list_lock); list_for_each_entry(lb, &lease_table_list, l_entry) { - if (!memcmp(lb->client_guid, opinfo->conn->ClientGUID, + if (!memcmp(lb->client_guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE)) { - opinfo->o_lease->l_lb = lb; - lease_add_list(opinfo); - read_unlock(&lease_list_lock); - return 0; + lease_add_table(lease, lb); + write_unlock(&lease_list_lock); + free_lease_table(new_lb); + return; } } - read_unlock(&lease_list_lock); - lb = kmalloc_obj(struct lease_table, KSMBD_DEFAULT_GFP); - if (!lb) - return -ENOMEM; - - memcpy(lb->client_guid, opinfo->conn->ClientGUID, - SMB2_CLIENT_GUID_SIZE); - INIT_LIST_HEAD(&lb->lease_list); - spin_lock_init(&lb->lb_lock); - opinfo->o_lease->l_lb = lb; - lease_add_list(opinfo); - lb_add(lb); - return 0; + lease_add_table(lease, new_lb); + list_add(&new_lb->l_entry, &lease_table_list); + write_unlock(&lease_list_lock); } static void set_oplock_level(struct oplock_info *opinfo, int level, @@ -1082,8 +1488,9 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp, { struct oplock_info *opinfo; struct ksmbd_inode *p_ci = NULL; + LIST_HEAD(brk_list); - if (lctx->version != 2) + if (lctx && lctx->version != 2) return; p_ci = ksmbd_inode_lookup_lock(fp->filp->f_path.dentry->d_parent); @@ -1096,9 +1503,10 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp, continue; if (opinfo->o_lease->state != SMB2_OPLOCK_LEVEL_NONE && - (!(lctx->flags & SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE) || - !compare_guid_key(opinfo, fp->conn->ClientGUID, - lctx->parent_lease_key))) { + (!lctx || + (!(lctx->flags & SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE) || + !compare_guid_key(opinfo, fp->conn->ClientGUID, + lctx->parent_lease_key)))) { if (!atomic_inc_not_zero(&opinfo->refcount)) continue; @@ -1107,12 +1515,14 @@ void smb_send_parent_lease_break_noti(struct ksmbd_file *fp, continue; } - oplock_break(opinfo, SMB2_OPLOCK_LEVEL_NONE, NULL); - opinfo_put(opinfo); + if (oplock_break_add(&brk_list, opinfo)) + opinfo_put(opinfo); } } up_read(&p_ci->m_lock); + oplock_break_drain_none(&brk_list, p_ci); + ksmbd_inode_put(p_ci); } @@ -1120,13 +1530,16 @@ void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp) { struct oplock_info *opinfo; struct ksmbd_inode *p_ci = NULL; + LIST_HEAD(brk_list); rcu_read_lock(); opinfo = rcu_dereference(fp->f_opinfo); - rcu_read_unlock(); - if (!opinfo || !opinfo->is_lease || opinfo->o_lease->version != 2) + if (!opinfo || !opinfo->is_lease || opinfo->o_lease->version != 2) { + rcu_read_unlock(); return; + } + rcu_read_unlock(); p_ci = ksmbd_inode_lookup_lock(fp->filp->f_path.dentry->d_parent); if (!p_ci) @@ -1146,12 +1559,14 @@ void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp) continue; } - oplock_break(opinfo, SMB2_OPLOCK_LEVEL_NONE, NULL); - opinfo_put(opinfo); + if (oplock_break_add(&brk_list, opinfo)) + opinfo_put(opinfo); } } up_read(&p_ci->m_lock); + oplock_break_drain_none(&brk_list, p_ci); + ksmbd_inode_put(p_ci); } @@ -1164,25 +1579,31 @@ void smb_lazy_parent_lease_break_close(struct ksmbd_file *fp) * @tid: Tree id of connection * @lctx: lease context information on file open * @share_ret: share mode + * @replay: whether this is a replayed CREATE request * * Return: 0 on success, otherwise error */ 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) + struct lease_ctx_info *lctx, int share_ret, bool replay) { - struct ksmbd_session *sess = work->sess; int err = 0; + int break_level = SMB2_OPLOCK_LEVEL_II; struct oplock_info *opinfo = NULL, *prev_opinfo = NULL; struct ksmbd_inode *ci = fp->f_ci; + struct lease_table *new_lb = NULL; + struct oplock_snapshot prev_op_snapshot; bool prev_op_has_lease; + bool prev_durable_open = false; + bool prev_durable_detached = false; + unsigned long long prev_fid = KSMBD_NO_FID; + bool new_lease = false; + bool break_needed; __le32 prev_op_state = 0; /* Only v2 leases handle the directory */ if (S_ISDIR(file_inode(fp->filp)->i_mode)) { - if (!lctx || lctx->version != 2 || - (lctx->flags != SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE && - !lctx->epoch)) + if (!lctx || lctx->version != 2) return 0; } @@ -1191,20 +1612,35 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, return -ENOMEM; if (lctx) { - err = alloc_lease(opinfo, lctx); - if (err) + opinfo->o_lease = alloc_lease(lctx, ci); + if (!opinfo->o_lease) { + err = -ENOMEM; goto err_out; + } opinfo->is_lease = 1; + new_lease = true; } /* ci does not have any oplock */ if (!opinfo_count(fp)) goto set_lev; - /* grant none-oplock if second open is trunc */ - if (fp->attrib_only && fp->cdoption != FILE_OVERWRITE_IF_LE && + /* + * A stat open that only requests metadata access must not break the + * existing caching state. READ_CONTROL (reading the security + * descriptor) does not conflict with a lease, but it does conflict + * with an oplock, so only treat a read-control-only open as a stat + * open when the existing holder is a lease. + */ + if (fp->cdoption != FILE_OVERWRITE_IF_LE && fp->cdoption != FILE_OVERWRITE_LE && - fp->cdoption != FILE_SUPERSEDE_LE) { + fp->cdoption != FILE_SUPERSEDE_LE && + (fp->attrib_only || + (!(fp->daccess & ~(FILE_READ_ATTRIBUTES_LE | + FILE_WRITE_ATTRIBUTES_LE | + FILE_SYNCHRONIZE_LE | + FILE_READ_CONTROL_LE)) && + ksmbd_inode_has_lease(ci)))) { req_op_level = SMB2_OPLOCK_LEVEL_NONE; goto set_lev; } @@ -1213,17 +1649,19 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, struct oplock_info *m_opinfo; /* is lease already granted ? */ - m_opinfo = same_client_has_lease(ci, sess->ClientGUID, + m_opinfo = same_client_has_lease(ci, work->conn->ClientGUID, lctx); if (m_opinfo) { - copy_lease(m_opinfo, opinfo); - if (atomic_read(&m_opinfo->breaking_cnt)) - opinfo->o_lease->flags = - SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; + lease_put(opinfo->o_lease); + lease_get(m_opinfo->o_lease); + opinfo->o_lease = m_opinfo->o_lease; + opinfo->level = m_opinfo->level; + new_lease = false; + opinfo_put(m_opinfo); goto out; } } - prev_opinfo = opinfo_get_list(ci); + prev_opinfo = opinfo_get_list(ci, fp, &prev_op_snapshot); if (!prev_opinfo || (prev_opinfo->level == SMB2_OPLOCK_LEVEL_NONE && lctx)) { opinfo_put(prev_opinfo); @@ -1232,6 +1670,20 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, prev_op_has_lease = prev_opinfo->is_lease; if (prev_op_has_lease) prev_op_state = prev_opinfo->o_lease->state; + /* + * A replay received while this open is waiting for an oplock or lease + * break must not observe an intermediate level and proceed as a new + * open. This check has to precede break_needed. an oplock may already + * have been downgraded from Batch to II while its acknowledgement is + * still pending. + */ + if (replay && + (test_bit(0, &prev_opinfo->pending_break) || + prev_opinfo->op_state == OPLOCK_ACK_WAIT)) { + err = -EINPROGRESS; + opinfo_put(prev_opinfo); + goto err_out; + } if (share_ret < 0 && prev_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE) { @@ -1240,16 +1692,44 @@ int smb_grant_oplock(struct ksmbd_work *work, int req_op_level, u64 pid, goto err_out; } - if (prev_opinfo->level != SMB2_OPLOCK_LEVEL_BATCH && - prev_opinfo->level != SMB2_OPLOCK_LEVEL_EXCLUSIVE) { + break_needed = prev_opinfo->level == SMB2_OPLOCK_LEVEL_BATCH || + prev_opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE || + (share_ret < 0 && prev_op_has_lease && + (prev_op_state & SMB2_LEASE_HANDLE_CACHING_LE)); + if (!break_needed) { opinfo_put(prev_opinfo); goto op_break_not_needed; } - err = oplock_break(prev_opinfo, SMB2_OPLOCK_LEVEL_II, work); + prev_durable_open = prev_op_snapshot.durable_open; + prev_durable_detached = prev_op_snapshot.durable_detached; + prev_fid = prev_op_snapshot.fid; + + 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)) + ksmbd_invalidate_durable_fd(prev_fid); opinfo_put(prev_opinfo); - if (err == -ENOENT) + if (err == -EAGAIN) { + share_ret = ksmbd_smb_check_shared_mode(fp->filp, fp); + if (share_ret < 0) { + err = share_ret; + goto err_out; + } goto set_lev; + } + if (err == -ENOENT) { + /* + * A pending durable CREATE can lose the previous oplock when + * its holder closes the file. In that case grant the original + * request its full caching state. Other opens still need the + * normal shared-open downgrade below. + */ + if (!prev_durable_open && + req_op_level != SMB2_OPLOCK_LEVEL_NONE) + req_op_level = SMB2_OPLOCK_LEVEL_II; + goto set_lev; + } /* Check all oplock was freed by close */ else if (err < 0) goto err_out; @@ -1277,20 +1757,35 @@ set_lev: set_oplock_level(opinfo, req_op_level, lctx); out: - rcu_assign_pointer(fp->f_opinfo, opinfo); + /* + * Keep the original publication order so concurrent opens can + * still observe the in-flight grant via ci->m_op_list, but make + * everything after opinfo_add() no-fail by preallocating any new + * lease_table first. + */ opinfo->o_fp = fp; - - opinfo_count_inc(fp); - opinfo_add(opinfo); - if (opinfo->is_lease) { - err = add_lease_global_list(opinfo); - if (err) + if (new_lease) { + new_lb = alloc_lease_table(opinfo); + if (!new_lb) { + err = -ENOMEM; goto err_out; + } } + opinfo_count_inc(fp); + opinfo_add(opinfo, fp); + + if (new_lease) + add_lease_global_list(opinfo->o_lease, opinfo->conn, new_lb); + if (opinfo->is_lease) + lease_add_open(opinfo->o_lease, opinfo); + + rcu_assign_pointer(fp->f_opinfo, opinfo); + return 0; err_out: - free_opinfo(opinfo); + kfree(new_lb); + opinfo_put(opinfo); return err; } @@ -1300,38 +1795,51 @@ err_out: * @fp: ksmbd file pointer * @is_trunc: truncate on open */ -static void smb_break_all_write_oplock(struct ksmbd_work *work, +static bool smb_break_all_write_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, int is_trunc) { struct oplock_info *brk_opinfo; + bool sent_break = false; - brk_opinfo = opinfo_get_list(fp->f_ci); + brk_opinfo = opinfo_get_list(fp->f_ci, NULL, NULL); if (!brk_opinfo) - return; + return false; if (brk_opinfo->level != SMB2_OPLOCK_LEVEL_BATCH && brk_opinfo->level != SMB2_OPLOCK_LEVEL_EXCLUSIVE) { opinfo_put(brk_opinfo); - return; + return false; } brk_opinfo->open_trunc = is_trunc; - oplock_break(brk_opinfo, SMB2_OPLOCK_LEVEL_II, work); + oplock_break(brk_opinfo, fp->f_ci, SMB2_OPLOCK_LEVEL_II, work, false, + false); + sent_break = true; opinfo_put(brk_opinfo); + + return sent_break; } /** - * smb_break_all_levII_oplock() - send level2 oplock or read lease break command + * __smb_break_all_levII_oplock() - send level2 oplock or read lease break command * from server to client - * @work: smb work - * @fp: ksmbd file pointer - * @is_trunc: truncate on open + * @work: smb work + * @fp: ksmbd file pointer + * @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 */ -void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, - int is_trunc) +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) { struct oplock_info *op, *brk_op; + struct oplock_break_entry *ent, *tmp; struct ksmbd_inode *ci; struct ksmbd_conn *conn = work->conn; + bool sent_interim = false; + LIST_HEAD(brk_list); if (!test_share_config_flag(work->tcon->share_conf, KSMBD_SHARE_FLAG_OPLOCKS)) @@ -1353,14 +1861,8 @@ void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, continue; } - if (brk_op->is_lease && (brk_op->o_lease->state & - (~(SMB2_LEASE_READ_CACHING_LE | - SMB2_LEASE_HANDLE_CACHING_LE)))) { - ksmbd_debug(OPLOCK, "unexpected lease state(0x%x)\n", - brk_op->o_lease->state); - goto next; - } else if (brk_op->level != - SMB2_OPLOCK_LEVEL_II) { + if (!brk_op->is_lease && + brk_op->level != SMB2_OPLOCK_LEVEL_II) { ksmbd_debug(OPLOCK, "unexpected oplock(0x%x)\n", brk_op->level); goto next; @@ -1379,16 +1881,68 @@ void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, SMB2_LEASE_KEY_SIZE)) goto next; brk_op->open_trunc = is_trunc; - oplock_break(brk_op, SMB2_OPLOCK_LEVEL_NONE, NULL); + + /* + * Defer the break until ci->m_lock is released: oplock_break() + * may block waiting for the lease break acknowledgment, and the + * close that wakes that wait needs ci->m_lock for write. + */ + if (!oplock_break_add(&brk_list, brk_op)) + continue; next: opinfo_put(brk_op); } up_read(&ci->m_lock); + list_for_each_entry_safe(ent, tmp, &brk_list, list) { + brk_op = ent->opinfo; + + if (!brk_op->is_lease && !send_oplock_break) { + brk_op->level = SMB2_OPLOCK_LEVEL_NONE; + spin_lock(&brk_op->state_lock); + if (brk_op->op_state != OPLOCK_CLOSING) + brk_op->op_state = OPLOCK_STATE_NONE; + spin_unlock(&brk_op->state_lock); + } else { + oplock_break(brk_op, ci, + brk_op->is_lease && !is_trunc ? + SMB2_OPLOCK_LEVEL_II : SMB2_OPLOCK_LEVEL_NONE, + send_interim && !sent_interim ? work : NULL, + false, sync_lease_break); + } + sent_interim = true; + list_del(&ent->list); + opinfo_put(brk_op); + kfree(ent); + } + if (op) opinfo_put(op); } +void smb_break_all_levII_oplock(struct ksmbd_work *work, struct ksmbd_file *fp, + int is_trunc) +{ + __smb_break_all_levII_oplock(work, fp, is_trunc, true, true, false); +} + +void smb_break_all_levII_oplock_rename(struct ksmbd_work *work, struct ksmbd_file *fp) +{ + __smb_break_all_levII_oplock(work, fp, 0, true, true, true); +} + +void smb_break_all_levII_oplock_no_interim(struct ksmbd_work *work, + struct ksmbd_file *fp, int is_trunc) +{ + __smb_break_all_levII_oplock(work, fp, is_trunc, false, true, false); +} + +void smb_break_all_levII_oplock_for_delete(struct ksmbd_work *work, + struct ksmbd_file *fp) +{ + __smb_break_all_levII_oplock(work, fp, 0, false, false, false); +} + /** * smb_break_all_oplock() - break both batch/exclusive and level2 oplock * @work: smb work @@ -1396,12 +1950,14 @@ next: */ void smb_break_all_oplock(struct ksmbd_work *work, struct ksmbd_file *fp) { + bool sent_break; + if (!test_share_config_flag(work->tcon->share_conf, KSMBD_SHARE_FLAG_OPLOCKS)) return; - smb_break_all_write_oplock(work, fp, 1); - smb_break_all_levII_oplock(work, fp, 1); + sent_break = smb_break_all_write_oplock(work, fp, 1); + __smb_break_all_levII_oplock(work, fp, 1, !sent_break, true, false); } /** @@ -1412,15 +1968,13 @@ void smb_break_all_oplock(struct ksmbd_work *work, struct ksmbd_file *fp) */ __u8 smb2_map_lease_to_oplock(__le32 lease_state) { - if (lease_state == (SMB2_LEASE_HANDLE_CACHING_LE | - SMB2_LEASE_READ_CACHING_LE | - SMB2_LEASE_WRITE_CACHING_LE)) { + if ((lease_state & SMB2_LEASE_WRITE_CACHING_LE) && + (lease_state & SMB2_LEASE_HANDLE_CACHING_LE)) { return SMB2_OPLOCK_LEVEL_BATCH; - } else if (lease_state != SMB2_LEASE_WRITE_CACHING_LE && - lease_state & SMB2_LEASE_WRITE_CACHING_LE) { - if (!(lease_state & SMB2_LEASE_HANDLE_CACHING_LE)) - return SMB2_OPLOCK_LEVEL_EXCLUSIVE; - } else if (lease_state & SMB2_LEASE_READ_CACHING_LE) { + } else if (lease_state & SMB2_LEASE_WRITE_CACHING_LE) { + return SMB2_OPLOCK_LEVEL_EXCLUSIVE; + } else if (lease_state & (SMB2_LEASE_READ_CACHING_LE | + SMB2_LEASE_HANDLE_CACHING_LE)) { return SMB2_OPLOCK_LEVEL_II; } return 0; @@ -1435,14 +1989,19 @@ void create_lease_buf(u8 *rbuf, struct lease *lease) { if (lease->version == 2) { struct create_lease_v2 *buf = (struct create_lease_v2 *)rbuf; + __le32 flags = 0; memset(buf, 0, sizeof(struct create_lease_v2)); memcpy(buf->lcontext.LeaseKey, lease->lease_key, SMB2_LEASE_KEY_SIZE); - buf->lcontext.LeaseFlags = lease->flags; + if (lease_has_parent_key(lease)) + flags |= SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE; + if (lease_break_in_progress(lease)) + flags |= SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; + buf->lcontext.LeaseFlags = flags; buf->lcontext.Epoch = cpu_to_le16(lease->epoch); buf->lcontext.LeaseState = lease->state; - if (lease->flags == SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE) + if (lease_has_parent_key(lease)) memcpy(buf->lcontext.ParentLeaseKey, lease->parent_lease_key, SMB2_LEASE_KEY_SIZE); buf->ccontext.DataOffset = cpu_to_le16(offsetof @@ -1460,7 +2019,9 @@ void create_lease_buf(u8 *rbuf, struct lease *lease) memset(buf, 0, sizeof(struct create_lease)); memcpy(buf->lcontext.LeaseKey, lease->lease_key, SMB2_LEASE_KEY_SIZE); - buf->lcontext.LeaseFlags = lease->flags; + if (lease_break_in_progress(lease)) + buf->lcontext.LeaseFlags = + SMB2_LEASE_FLAG_BREAK_IN_PROGRESS_LE; buf->lcontext.LeaseState = lease->state; buf->ccontext.DataOffset = cpu_to_le16(offsetof (struct create_lease, lcontext)); @@ -1488,12 +2049,14 @@ struct lease_ctx_info *parse_lease_state(void *open_req) struct lease_ctx_info *lreq; cc = smb2_find_context_vals(req, SMB2_CREATE_REQUEST_LEASE, 4); - if (IS_ERR_OR_NULL(cc)) + if (IS_ERR(cc)) + return ERR_CAST(cc); + if (!cc) return NULL; lreq = kzalloc_obj(struct lease_ctx_info, KSMBD_DEFAULT_GFP); if (!lreq) - return NULL; + return ERR_PTR(-ENOMEM); if (sizeof(struct lease_context_v2) == le32_to_cpu(cc->DataLength)) { struct create_lease_v2 *lc = (struct create_lease_v2 *)cc; @@ -1507,11 +2070,15 @@ struct lease_ctx_info *parse_lease_state(void *open_req) lreq->flags = lc->lcontext.LeaseFlags; lreq->epoch = lc->lcontext.Epoch; lreq->duration = lc->lcontext.LeaseDuration; + if (!lease_state_valid(lreq->req_state) || + !lease_v2_flags_valid(lreq->flags)) + goto err_out; + lreq->req_state = lease_state_grantable(lreq->req_state); if (lreq->flags == SMB2_LEASE_FLAG_PARENT_LEASE_KEY_SET_LE) memcpy(lreq->parent_lease_key, lc->lcontext.ParentLeaseKey, SMB2_LEASE_KEY_SIZE); lreq->version = 2; - } else { + } else if (sizeof(struct lease_context) == le32_to_cpu(cc->DataLength)) { struct create_lease *lc = (struct create_lease *)cc; if (le16_to_cpu(cc->DataOffset) + le32_to_cpu(cc->DataLength) < @@ -1520,14 +2087,18 @@ struct lease_ctx_info *parse_lease_state(void *open_req) memcpy(lreq->lease_key, lc->lcontext.LeaseKey, SMB2_LEASE_KEY_SIZE); lreq->req_state = lc->lcontext.LeaseState; - lreq->flags = lc->lcontext.LeaseFlags; + lreq->flags = 0; lreq->duration = lc->lcontext.LeaseDuration; + if (!lease_state_valid(lreq->req_state)) + goto err_out; + lreq->req_state = lease_state_grantable(lreq->req_state); lreq->version = 1; - } + } else + goto err_out; return lreq; err_out: kfree(lreq); - return NULL; + return ERR_PTR(-EINVAL); } /** @@ -1552,6 +2123,9 @@ struct create_context *smb2_find_context_vals(void *open_req, const char *tag, i * CreateContextsOffset and CreateContextsLength are guaranteed to * be valid because of ksmbd_smb2_check_message(). */ + if (!req->CreateContextsOffset || !req->CreateContextsLength) + return NULL; + cc = (struct create_context *)((char *)req + le32_to_cpu(req->CreateContextsOffset)); remain_len = le32_to_cpu(req->CreateContextsLength); @@ -1620,12 +2194,12 @@ void create_durable_v2_rsp_buf(char *cc, struct ksmbd_file *fp) struct create_durable_rsp_v2 *buf; buf = (struct create_durable_rsp_v2 *)cc; - memset(buf, 0, sizeof(struct create_durable_rsp)); + memset(buf, 0, sizeof(*buf)); buf->ccontext.DataOffset = cpu_to_le16(offsetof - (struct create_durable_rsp, Data)); + (struct create_durable_rsp_v2, dcontext)); buf->ccontext.DataLength = cpu_to_le32(8); buf->ccontext.NameOffset = cpu_to_le16(offsetof - (struct create_durable_rsp, Name)); + (struct create_durable_rsp_v2, Name)); buf->ccontext.NameLength = cpu_to_le16(4); /* SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2 is "DH2Q" */ buf->Name[0] = 'D'; @@ -1746,6 +2320,90 @@ void create_posix_rsp_buf(char *cc, struct ksmbd_file *fp) SIDUNIX_GROUP, (struct smb_sid *)&buf->SidBuffer[28]); } +/** + * create_aapl_rsp_buf() - build AAPL kAAPL_SERVER_QUERY response + * @cc: buffer to write the create context into (AAPL_RSP_MAX_SIZE bytes) + * @vol_caps: volume capability flags (SMB2_CRTCTX_AAPL_* volume bits) + * @req_bitmap: the client's request bitmap, echoed back in reply_bitmap + * + * Response format follows the layout observed from macOS's own smbd, and + * matches the client-side parsing in AAPL's published public client kernel + * source (public client behavior reference, kAAPL_SERVER_QUERY + * case): reply_bitmap, then server_caps/vol_caps/model-info fields present + * only when their reply_bitmap bit is set: + * reply_bitmap = req_bitmap masked to the fields we support + * server_caps = AAPL_SERVER_CAPS_KSMBD when requested + * vol_caps = caller-supplied + * model string = server_conf.aapl_model (default "Xserve") in UTF-16LE, + * when SMB2_CRTCTX_AAPL_MODEL_INFO requested + * + * Sending reply_bitmap with MODEL_INFO set but no model string causes + * smbfs.kext to enter a broken disconnect path requiring a macOS reboot. + * @readdir_attr_v2: advertise SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2 + * instead of the V1 bit + */ +void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap, + bool readdir_attr_v2) +{ + struct create_aapl_rsp *buf; + u64 reply_bitmap; + u64 server_caps; + u32 data_len; + + buf = (struct create_aapl_rsp *)cc; + memset(buf, 0, AAPL_RSP_MAX_SIZE); + + reply_bitmap = req_bitmap & (SMB2_CRTCTX_AAPL_SERVER_CAPS | + SMB2_CRTCTX_AAPL_VOLUME_CAPS | + SMB2_CRTCTX_AAPL_MODEL_INFO); + + /* base data: cmd(4)+reserved(4)+reply_bitmap(8)+server_caps(8)+vol_caps(8) */ + data_len = 32; + if (reply_bitmap & SMB2_CRTCTX_AAPL_MODEL_INFO) + data_len += 4 + 4 + AAPL_MODEL_UTF16_BYTES; /* pad2+model_bytes+string */ + + buf->ccontext.DataOffset = cpu_to_le16(offsetof(struct create_aapl_rsp, cmd)); + buf->ccontext.DataLength = cpu_to_le32(data_len); + buf->ccontext.NameOffset = cpu_to_le16(offsetof(struct create_aapl_rsp, Name)); + buf->ccontext.NameLength = cpu_to_le16(SMB2_CREATE_AAPL_LEN); + buf->Name[0] = 'A'; + buf->Name[1] = 'A'; + buf->Name[2] = 'P'; + buf->Name[3] = 'L'; + + buf->cmd = cpu_to_le32(SMB2_CRTCTX_AAPL_SERVER_QUERY); + buf->reply_bitmap = cpu_to_le64(reply_bitmap); + server_caps = AAPL_SERVER_CAPS_KSMBD; + if (readdir_attr_v2) + server_caps = (server_caps & ~SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR) | + SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2; + buf->server_caps = (reply_bitmap & SMB2_CRTCTX_AAPL_SERVER_CAPS) ? + cpu_to_le64(server_caps) : 0; + buf->vol_caps = (reply_bitmap & SMB2_CRTCTX_AAPL_VOLUME_CAPS) ? + cpu_to_le64(vol_caps) : 0; + + if (reply_bitmap & SMB2_CRTCTX_AAPL_MODEL_INFO) { + __le32 *p = (__le32 *)((u8 *)buf + sizeof(*buf)); + __le16 *model_str = (__le16 *)(p + 2); + const char *src = server_conf.aapl_model[0] ? + server_conf.aapl_model : "Xserve"; + int i, model_bytes = 0; + + /* Convert ASCII model string to UTF-16LE in-place */ + for (i = 0; src[i] && i < AAPL_MODEL_MAX_CHARS; i++) { + model_str[i] = cpu_to_le16((unsigned char)src[i]); + model_bytes += 2; + } + + p[0] = 0; /* pad2 */ + p[1] = cpu_to_le32(model_bytes); + + /* Update DataLength to reflect actual model string size */ + buf->ccontext.DataLength = + cpu_to_le32(data_len - AAPL_MODEL_UTF16_BYTES + model_bytes); + } +} + /* * Find lease object(opinfo) for given lease key/fid from lease * break/file close path. @@ -1761,8 +2419,8 @@ struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn, char *lease_key) { struct oplock_info *opinfo = NULL, *ret_op = NULL; + struct lease *lease; struct lease_table *lt; - int ret; read_lock(&lease_list_lock); list_for_each_entry(lt, &lease_table_list, l_entry) { @@ -1775,29 +2433,29 @@ struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn, return NULL; found: - rcu_read_lock(); - list_for_each_entry_rcu(opinfo, <->lease_list, lease_entry) { - if (!atomic_inc_not_zero(&opinfo->refcount)) + list_for_each_entry(lease, <->lease_list, l_entry) { + if (memcmp(lease->lease_key, lease_key, SMB2_LEASE_KEY_SIZE)) continue; - rcu_read_unlock(); - if (!opinfo->op_state || opinfo->op_state == OPLOCK_CLOSING) - goto op_next; - if (!(opinfo->o_lease->state & - (SMB2_LEASE_HANDLE_CACHING_LE | - SMB2_LEASE_WRITE_CACHING_LE))) - goto op_next; - ret = compare_guid_key(opinfo, conn->ClientGUID, - lease_key); - if (ret) { - ksmbd_debug(OPLOCK, "found opinfo\n"); + if (!(lease->state & (SMB2_LEASE_HANDLE_CACHING_LE | + SMB2_LEASE_WRITE_CACHING_LE))) + break; + + spin_lock(&lease->lock); + list_for_each_entry(opinfo, &lease->open_list, lease_entry) { + if (!opinfo->op_state || + opinfo->op_state == OPLOCK_CLOSING) + continue; + if (!atomic_inc_not_zero(&opinfo->refcount)) + continue; ret_op = opinfo; + } + spin_unlock(&lease->lock); + if (ret_op) { + ksmbd_debug(OPLOCK, "found opinfo\n"); goto out; } -op_next: - opinfo_put(opinfo); - rcu_read_lock(); + break; } - rcu_read_unlock(); out: read_unlock(&lease_list_lock); @@ -1808,6 +2466,7 @@ int smb2_check_durable_oplock(struct ksmbd_conn *conn, struct ksmbd_share_config *share, struct ksmbd_file *fp, struct lease_ctx_info *lctx, + struct ksmbd_user *user, char *name) { struct oplock_info *opinfo = opinfo_get(fp); @@ -1816,6 +2475,18 @@ int smb2_check_durable_oplock(struct ksmbd_conn *conn, if (!opinfo) return 0; + if (ksmbd_has_other_active_fd(fp)) { + ksmbd_debug(SMB, "Durable handle reconnect failed: competing open\n"); + ret = -EBADF; + goto out; + } + + if (ksmbd_vfs_compare_durable_owner(fp, user) == false) { + ksmbd_debug(SMB, "Durable handle reconnect failed: owner mismatch\n"); + ret = -EBADF; + goto out; + } + if (opinfo->is_lease == false) { if (lctx) { pr_err("create context include lease\n"); diff --git a/fs/smb/server/oplock.h b/fs/smb/server/oplock.h index 9a56eaadd0dd..b08d21758e07 100644 --- a/fs/smb/server/oplock.h +++ b/fs/smb/server/oplock.h @@ -34,6 +34,7 @@ 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; @@ -49,7 +50,13 @@ struct lease { int version; unsigned short epoch; bool is_dir; + bool reuse_epoch; + struct ksmbd_inode *ci; struct lease_table *l_lb; + struct list_head l_entry; + struct list_head open_list; + spinlock_t lock; + atomic_t refcount; }; struct oplock_info { @@ -59,6 +66,7 @@ struct oplock_info { struct ksmbd_file *o_fp; int level; int op_state; + spinlock_t state_lock; unsigned long pending_break; u64 fid; atomic_t breaking_cnt; @@ -69,8 +77,9 @@ struct oplock_info { struct lease *o_lease; struct list_head op_entry; struct list_head lease_entry; - wait_queue_head_t oplock_q; /* Other server threads */ - wait_queue_head_t oplock_brk; /* oplock breaking wait */ + wait_queue_head_t oplock_q; /* Other server threads */ + wait_queue_head_t oplock_brk; /* oplock breaking wait */ + struct rcu_head rcu; }; struct lease_break_info { @@ -88,9 +97,14 @@ struct oplock_break_info { 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); + struct lease_ctx_info *lctx, int share_ret, bool replay); void smb_break_all_levII_oplock(struct ksmbd_work *work, - struct ksmbd_file *fp, int is_trunc); + struct ksmbd_file *fp, int is_trunc); +void smb_break_all_levII_oplock_rename(struct ksmbd_work *work, struct ksmbd_file *fp); +void smb_break_all_levII_oplock_no_interim(struct ksmbd_work *work, + struct ksmbd_file *fp, int is_trunc); +void smb_break_all_levII_oplock_for_delete(struct ksmbd_work *work, + struct ksmbd_file *fp); int opinfo_write_to_read(struct oplock_info *opinfo); int opinfo_read_handle_to_read(struct oplock_info *opinfo); int opinfo_write_to_none(struct oplock_info *opinfo); @@ -104,6 +118,7 @@ void opinfo_put(struct oplock_info *opinfo); void create_lease_buf(u8 *rbuf, struct lease *lease); struct lease_ctx_info *parse_lease_state(void *open_req); __u8 smb2_map_lease_to_oplock(__le32 lease_state); +void lease_update_oplock_levels(struct lease *lease); int lease_read_to_write(struct oplock_info *opinfo); /* Durable related functions */ @@ -112,10 +127,12 @@ void create_durable_v2_rsp_buf(char *cc, struct ksmbd_file *fp); void create_mxac_rsp_buf(char *cc, int maximal_access); void create_disk_id_rsp_buf(char *cc, __u64 file_id, __u64 vol_id); void create_posix_rsp_buf(char *cc, struct ksmbd_file *fp); +void create_aapl_rsp_buf(char *cc, __u64 vol_caps, __u64 req_bitmap, + bool readdir_attr_v2); struct create_context *smb2_find_context_vals(void *open_req, const char *tag, int tag_len); struct oplock_info *lookup_lease_in_table(struct ksmbd_conn *conn, char *lease_key); -int find_same_lease_key(struct ksmbd_session *sess, struct ksmbd_inode *ci, +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 smb_send_parent_lease_break_noti(struct ksmbd_file *fp, @@ -125,5 +142,6 @@ int smb2_check_durable_oplock(struct ksmbd_conn *conn, struct ksmbd_share_config *share, struct ksmbd_file *fp, struct lease_ctx_info *lctx, + struct ksmbd_user *user, char *name); #endif /* __KSMBD_OPLOCK_H */ diff --git a/fs/smb/server/proc.c b/fs/smb/server/proc.c index 101a2cc45a44..19f0f2cfbf54 100644 --- a/fs/smb/server/proc.c +++ b/fs/smb/server/proc.c @@ -11,10 +11,12 @@ #include <linux/seq_file.h> #include "misc.h" +#include "connection.h" #include "server.h" #include "stats.h" #include "smb_common.h" #include "smb2pdu.h" +#include "vfs_cache.h" static struct proc_dir_entry *ksmbd_proc_fs; struct ksmbd_counters ksmbd_counters; @@ -27,6 +29,42 @@ struct proc_dir_entry *ksmbd_proc_create(const char *name, show, v); } +void ksmbd_proc_show_flag_names(struct seq_file *m, + const struct ksmbd_const_name *table, + int count, unsigned int flags) +{ + unsigned int remaining = flags; + bool separator = false; + int i; + + for (i = 0; i < count; i++) { + unsigned int flag = table[i].const_value; + + if (!flag || (remaining & flag) != flag) + continue; + seq_printf(m, "%s%s", separator ? "," : "", table[i].name); + separator = true; + remaining &= ~flag; + } + + if (remaining) + seq_printf(m, "%s0x%08x", separator ? "," : "", remaining); + else if (!separator) + seq_puts(m, "none"); +} + +const char *ksmbd_proc_const_name(const struct ksmbd_const_name *table, + int count, unsigned int const_value) +{ + int i; + + for (i = 0; i < count; i++) { + if (table[i].const_value == const_value) + return table[i].name; + } + return NULL; +} + struct ksmbd_const_smb2_process_req { unsigned int const_value; const char *name; @@ -54,32 +92,129 @@ static const struct ksmbd_const_smb2_process_req smb2_process_req[KSMBD_COUNTER_ {le16_to_cpu(SMB2_OPLOCK_BREAK), "SMB2_OPLOCK_BREAK"}, }; +static const char *ksmbd_server_state_string(void) +{ + switch (READ_ONCE(server_conf.state)) { + case SERVER_STATE_STARTING_UP: + return "starting"; + case SERVER_STATE_RUNNING: + return "running"; + case SERVER_STATE_RESETTING: + return "resetting"; + case SERVER_STATE_SHUTTING_DOWN: + return "shutdown"; + default: + return "unknown"; + } +} + +static const char *ksmbd_signing_mode_string(void) +{ + switch (server_conf.signing) { + case KSMBD_CONFIG_OPT_DISABLED: + return "disabled"; + case KSMBD_CONFIG_OPT_MANDATORY: + return "mandatory"; + case KSMBD_CONFIG_OPT_AUTO: + return "auto"; + default: + return "unknown"; + } +} + +static void proc_show_runtime_totals(struct seq_file *m) +{ + struct ksmbd_conn *conn; + unsigned int clients = 0; + unsigned int open_files = 0; + int i; + + down_read(&conn_list_lock); + hash_for_each(conn_list, i, conn, hlist) { + clients++; + open_files += atomic_read(&conn->stats.open_files_count); + } + up_read(&conn_list_lock); + + seq_printf(m, "clients:\t%u\n", clients); + seq_printf(m, "open_files:\t%u\n", open_files); +} + static int proc_show_ksmbd_stats(struct seq_file *m, void *v) { int i; seq_puts(m, "Server\n"); - seq_printf(m, "name: %s\n", ksmbd_server_string()); - seq_printf(m, "netbios: %s\n", ksmbd_netbios_name()); - seq_printf(m, "work group: %s\n", ksmbd_work_group()); - seq_printf(m, "min protocol: %s\n", ksmbd_get_protocol_string(server_conf.min_protocol)); - seq_printf(m, "max protocol: %s\n", ksmbd_get_protocol_string(server_conf.max_protocol)); - seq_printf(m, "flags: 0x%08x\n", server_conf.flags); - seq_printf(m, "share_fake_fscaps: 0x%08x\n", + seq_printf(m, "state:\t%s\n", ksmbd_server_state_string()); + seq_printf(m, "name:\t%s\n", ksmbd_server_string()); + seq_printf(m, "netbios:\t%s\n", ksmbd_netbios_name()); + seq_printf(m, "work_group:\t%s\n", ksmbd_work_group()); + seq_printf(m, "min_protocol:\t%s\n", ksmbd_get_protocol_string(server_conf.min_protocol)); + seq_printf(m, "max_protocol:\t%s\n", ksmbd_get_protocol_string(server_conf.max_protocol)); + seq_printf(m, "flags:\t0x%08x\n", server_conf.flags); + seq_printf(m, "tcp_port:\t%u\n", server_conf.tcp_port); + seq_printf(m, "signing:\t%s\n", ksmbd_signing_mode_string()); + seq_printf(m, "signing_enforced:\t%s\n", + server_conf.enforced_signing ? "yes" : "no"); + seq_printf(m, "bind_interfaces_only:\t%s\n", + server_conf.bind_interfaces_only ? "yes" : "no"); + 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_inflight_requests:\t%u\n", + server_conf.max_inflight_req); + seq_printf(m, "deadtime_seconds:\t%lu\n", server_conf.deadtime / HZ); + seq_printf(m, "ipc_timeout_seconds:\t%u\n", server_conf.ipc_timeout / HZ); + if (server_conf.ipc_last_active) + seq_printf(m, "ipc_last_active_seconds:\t%lu\n", + jiffies_to_msecs(jiffies - server_conf.ipc_last_active) / + MSEC_PER_SEC); + else + seq_puts(m, "ipc_last_active_seconds:\tnever\n"); + seq_printf(m, "durable_scavenger:\t%s\n", + ksmbd_durable_scavenger_active() ? "running" : "stopped"); + seq_printf(m, "share_fake_fscaps:\t0x%08x\n", server_conf.share_fake_fscaps); - seq_printf(m, "sessions: %lld\n", + proc_show_runtime_totals(m); + seq_printf(m, "sessions:\t%lld\n", ksmbd_counter_sum(KSMBD_COUNTER_SESSIONS)); - seq_printf(m, "tree connects: %lld\n", + seq_printf(m, "session_timeouts:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_SESSION_TIMEOUTS)); + seq_printf(m, "tree_connects:\t%lld\n", ksmbd_counter_sum(KSMBD_COUNTER_TREE_CONNS)); - seq_printf(m, "read bytes: %lld\n", + seq_printf(m, "requests:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_REQUESTS)); + seq_printf(m, "read_bytes:\t%lld\n", ksmbd_counter_sum(KSMBD_COUNTER_READ_BYTES)); - seq_printf(m, "written bytes: %lld\n", + seq_printf(m, "written_bytes:\t%lld\n", ksmbd_counter_sum(KSMBD_COUNTER_WRITE_BYTES)); seq_puts(m, "\nSMB2\n"); for (i = 0; i < KSMBD_COUNTER_MAX_REQS; i++) - seq_printf(m, "%-20s:\t%lld\n", smb2_process_req[i].name, + seq_printf(m, "%s:\t%lld\n", smb2_process_req[i].name, ksmbd_counter_sum(KSMBD_COUNTER_FIRST_REQ + i)); + + seq_puts(m, "\nSMB2 status\n"); + seq_printf(m, "success:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_STATUS_SUCCESS)); + seq_printf(m, "informational:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_STATUS_INFORMATIONAL)); + seq_printf(m, "warning:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_STATUS_WARNING)); + seq_printf(m, "error:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_STATUS_ERROR)); + seq_printf(m, "access_denied:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_ERROR_ACCESS_DENIED)); + seq_printf(m, "not_found:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_ERROR_NOT_FOUND)); + seq_printf(m, "invalid_parameter:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_ERROR_INVALID_PARAMETER)); + seq_printf(m, "sharing_violation:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_ERROR_SHARING_VIOLATION)); + seq_printf(m, "not_supported:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_ERROR_NOT_SUPPORTED)); + seq_printf(m, "other:\t%lld\n", + ksmbd_counter_sum(KSMBD_COUNTER_ERROR_OTHER)); return 0; } @@ -106,14 +241,14 @@ void ksmbd_proc_reset(void) percpu_counter_set(&ksmbd_counters.counters[i], 0); } -void ksmbd_proc_init(void) +int ksmbd_proc_init(void) { int i; - int retval; + int retval = -ENOMEM; ksmbd_proc_fs = proc_mkdir("fs/ksmbd", NULL); if (!ksmbd_proc_fs) - return; + return retval; if (!proc_mkdir_mode("sessions", 0400, ksmbd_proc_fs)) goto err_out; @@ -124,11 +259,14 @@ void ksmbd_proc_init(void) goto err_out; } - if (!ksmbd_proc_create("server", proc_show_ksmbd_stats, NULL)) + if (!ksmbd_proc_create("server", proc_show_ksmbd_stats, NULL)) { + retval = -ENOMEM; goto err_out; + } ksmbd_proc_reset(); - return; + return 0; err_out: ksmbd_proc_cleanup(); + return retval; } diff --git a/fs/smb/server/server.c b/fs/smb/server/server.c index 422d9d978285..0827c8c51006 100644 --- a/fs/smb/server/server.c +++ b/fs/smb/server/server.c @@ -15,6 +15,7 @@ #include "server.h" #include "smb_common.h" +#include "smb2pdu.h" #include "../common/smb2status.h" #include "connection.h" #include "transport_ipc.h" @@ -22,6 +23,9 @@ #include "crypto_ctx.h" #include "auth.h" #include "stats.h" +#include "compress.h" +#include "mgmt/share_config.h" +#include "mgmt/tree_connect.h" int ksmbd_debug_types; @@ -111,6 +115,7 @@ static int __process_request(struct ksmbd_work *work, struct ksmbd_conn *conn, { struct smb_version_cmds *cmds; u16 command; + bool signed_req; int ret; if (check_conn_state(work)) @@ -137,7 +142,14 @@ andx_again: return SERVER_HANDLER_ABORT; } - if (work->sess && conn->ops->is_sign_req(work, command)) { + signed_req = conn->ops->is_sign_req && conn->ops->is_sign_req(work, command); + if (work->sess && work->sess->sign && !work->encrypted && + !signed_req) { + conn->ops->set_rsp_status(work, STATUS_ACCESS_DENIED); + return SERVER_HANDLER_ABORT; + } + + if (work->sess && signed_req) { ret = conn->ops->check_sign_req(work); if (!ret) { conn->ops->set_rsp_status(work, STATUS_ACCESS_DENIED); @@ -146,8 +158,11 @@ andx_again: } ret = cmds->proc(work); - if (conn->ops->inc_reqs) - conn->ops->inc_reqs(command); + if (conn->ops->inc_reqs) { + struct smb2_hdr *rsp = ksmbd_resp_buf_curr(work); + + conn->ops->inc_reqs(command, rsp->Status); + } if (ret < 0) ksmbd_debug(CONN, "Failed to process %u [%d]\n", command, ret); @@ -173,9 +188,33 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, if (conn->ops->is_transform_hdr && conn->ops->is_transform_hdr(work->request_buf)) { rc = conn->ops->decrypt_req(work); - if (rc < 0) + if (rc < 0) { + ksmbd_conn_abort(conn); return; + } work->encrypted = true; + + /* + * SMB3 applies compression before encryption. The receive loop + * handles a plain compression transform before allocating work, but + * an encrypted request exposes that transform only after decryption. + */ + if (((struct smb2_hdr *)smb_get_msg(work->request_buf))->ProtocolId == + SMB2_COMPRESSION_TRANSFORM_ID) { + rc = ksmbd_decompress_work_request(work); + if (rc < 0) { + ksmbd_conn_abort(conn); + return; + } + } + + /* The decrypted payload must now be a complete SMB2 request. */ + if (((struct smb2_hdr *)smb_get_msg(work->request_buf))->ProtocolId != + SMB2_PROTO_NUMBER || + get_rfc1002_len(work->request_buf) < sizeof(struct smb2_pdu)) { + ksmbd_conn_abort(conn); + return; + } } if (conn->ops->allocate_rsp_buf(work)) @@ -195,9 +234,22 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, if (rc == -EINVAL) conn->ops->set_rsp_status(work, STATUS_INVALID_PARAMETER); + else if (rc == -EKEYEXPIRED) + conn->ops->set_rsp_status(work, + STATUS_NETWORK_SESSION_EXPIRED); else conn->ops->set_rsp_status(work, STATUS_USER_SESSION_DELETED); + if (conn->ops->is_sign_req(work, conn->ops->get_cmd_val(work))) { + struct smb2_hdr *rsp_hdr; + + rsp_hdr = ksmbd_resp_buf_curr(work); + if (rc == -EKEYEXPIRED && work->sess && + conn->ops->set_sign_rsp) + conn->ops->set_sign_rsp(work); + else + rsp_hdr->Flags |= SMB2_FLAGS_SIGNED; + } goto send; } else if (rc > 0) { rc = conn->ops->get_ksmbd_tcon(work); @@ -210,12 +262,23 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, STATUS_NETWORK_NAME_DELETED); goto send; } + + if (work->tcon && + test_share_config_flag(work->tcon->share_conf, + KSMBD_SHARE_FLAG_ENCRYPT_DATA) && + !work->encrypted) { + conn->ops->set_rsp_status(work, + STATUS_ACCESS_DENIED); + goto send; + } } } rc = __process_request(work, conn, &command); - if (rc == SERVER_HANDLER_ABORT) + if (rc == SERVER_HANDLER_ABORT) { + smb2_complete_request_open(work); break; + } /* * Call smb2_set_rsp_credits() function to set number of credits @@ -228,22 +291,55 @@ static void __handle_ksmbd_work(struct ksmbd_work *work, if (rc < 0) { conn->ops->set_rsp_status(work, STATUS_INVALID_PARAMETER); + smb2_complete_request_open(work); goto send; } } + smb2_complete_request_open(work); + is_chained = is_chained_smb2_message(work); if (work->sess && (work->sess->sign || smb3_11_final_sess_setup_resp(work) || - conn->ops->is_sign_req(work, command))) - conn->ops->set_sign_rsp(work); + conn->ops->is_sign_req(work, command))) { + if (command == SMB2_SESSION_SETUP_HE && + work->sess->dialect >= SMB30_PROT_ID && + conn->dialect < SMB30_PROT_ID) + smb3_set_sign_rsp(work); + else + conn->ops->set_sign_rsp(work); + } } while (is_chained == true); send: + smb2_complete_request_open(work); + /* + * Release any credit charge still outstanding for this request. On + * the normal path smb2_set_rsp_credits() already returned it, but the + * abort, error and send-no-response paths skip that call, so the + * charge would otherwise leak and eventually exhaust the connection's + * outstanding credit window. + */ + if (work->credit_charge) { + spin_lock(&conn->credits_lock); + conn->outstanding_credits -= work->credit_charge; + work->credit_charge = 0; + spin_unlock(&conn->credits_lock); + } + if (work->tcon) ksmbd_tree_connect_put(work->tcon); smb3_preauth_hash_rsp(work); + /* + * Preauthentication hashes cover the original SMB2 response. Apply the + * transport compression wrapper only after updating the hash. + */ + if (work->compress_response) { + rc = ksmbd_compress_response(work); + if (rc < 0) + ksmbd_debug(CONN, "Failed to compress response: %d\n", rc); + } if (work->sess && work->sess->enc && work->encrypted && conn->ops->encrypt_resp) { rc = conn->ops->encrypt_resp(work); @@ -318,7 +414,7 @@ static int ksmbd_server_process_request(struct ksmbd_conn *conn) static int ksmbd_server_terminate_conn(struct ksmbd_conn *conn) { - ksmbd_sessions_deregister(conn); + ksmbd_conn_sessions_cleanup(conn); destroy_lease_table(conn); return 0; } @@ -535,11 +631,16 @@ static int ksmbd_server_shutdown(void) { WRITE_ONCE(server_conf.state, SERVER_STATE_SHUTTING_DOWN); - ksmbd_proc_cleanup(); class_unregister(&ksmbd_control_class); ksmbd_workqueue_destroy(); ksmbd_ipc_release(); ksmbd_conn_transport_destroy(); + /* + * ksmbd_conn_transport_destroy() calls delete_proc_clients() and destroys + * sessions. ksmbd_session_destroy() removes each session's proc entry. + * Keep the procfs tree alive until these entries have been removed. + */ + ksmbd_proc_cleanup(); ksmbd_crypto_destroy(); ksmbd_free_global_file_table(); destroy_lease_table(NULL); @@ -559,18 +660,25 @@ static int __init ksmbd_server_init(void) return ret; } - ksmbd_proc_init(); - create_proc_sessions(); + ret = ksmbd_proc_init(); + if (ret) + goto err_unregister; + + if (create_proc_sessions()) + pr_warn("Unable to create sessions procfs entry\n"); + + if (create_proc_shares()) + pr_warn("Unable to create shares procfs entry\n"); ksmbd_server_tcp_callbacks_init(); ret = server_conf_init(); if (ret) - goto err_unregister; + goto err_proc_cleanup; ret = ksmbd_work_pool_init(); if (ret) - goto err_unregister; + goto err_proc_cleanup; ret = ksmbd_init_file_cache(); if (ret) @@ -596,8 +704,14 @@ static int __init ksmbd_server_init(void) if (ret) goto err_crypto_destroy; + ret = ksmbd_conn_wq_init(); + if (ret) + goto err_workqueue_destroy; + return 0; +err_workqueue_destroy: + ksmbd_workqueue_destroy(); err_crypto_destroy: ksmbd_crypto_destroy(); err_release_inode_hash: @@ -610,6 +724,8 @@ err_exit_file_cache: ksmbd_exit_file_cache(); err_destroy_work_pools: ksmbd_work_pool_destroy(); +err_proc_cleanup: + ksmbd_proc_cleanup(); err_unregister: class_unregister(&ksmbd_control_class); @@ -623,16 +739,20 @@ static void __exit ksmbd_server_exit(void) { ksmbd_server_shutdown(); rcu_barrier(); + /* + * ksmbd_conn_put() defers the final release onto ksmbd_conn_wq, + * so drain it after rcu_barrier() has fired any pending RCU + * callbacks that may have queued a release. + */ + ksmbd_conn_wq_destroy(); ksmbd_release_inode_hash(); } MODULE_AUTHOR("Namjae Jeon <linkinjeon@kernel.org>"); MODULE_DESCRIPTION("Linux kernel CIFS/SMB SERVER"); MODULE_LICENSE("GPL"); -MODULE_SOFTDEP("pre: ecb"); MODULE_SOFTDEP("pre: nls"); MODULE_SOFTDEP("pre: aes"); -MODULE_SOFTDEP("pre: cmac"); MODULE_SOFTDEP("pre: aead2"); MODULE_SOFTDEP("pre: ccm"); MODULE_SOFTDEP("pre: gcm"); diff --git a/fs/smb/server/server.h b/fs/smb/server/server.h index b8a7317be86b..4d4d268b59d5 100644 --- a/fs/smb/server/server.h +++ b/fs/smb/server/server.h @@ -48,6 +48,8 @@ struct ksmbd_server_config { char *conf[SERVER_CONF_WORK_GROUP + 1]; struct task_struct *dh_task; bool bind_interfaces_only; + /* AAPL model string for Finder icon, e.g. "Xserve" */ + char aapl_model[32]; }; extern struct ksmbd_server_config server_conf; diff --git a/fs/smb/server/smb2misc.c b/fs/smb/server/smb2misc.c index a1ddca21c47b..532dea7be0b3 100644 --- a/fs/smb/server/smb2misc.c +++ b/fs/smb/server/smb2misc.c @@ -261,8 +261,12 @@ calc_size_exit: static inline int smb2_query_info_req_len(struct smb2_query_info_req *h) { - return le32_to_cpu(h->InputBufferLength) + - le32_to_cpu(h->OutputBufferLength); + return le32_to_cpu(h->InputBufferLength); +} + +static inline int smb2_query_info_resp_len(struct smb2_query_info_req *h) +{ + return le32_to_cpu(h->OutputBufferLength); } static inline int smb2_set_info_req_len(struct smb2_set_info_req *h) @@ -297,9 +301,10 @@ static inline int smb2_ioctl_resp_len(struct smb2_ioctl_req *h) le32_to_cpu(h->MaxOutputResponse); } -static int smb2_validate_credit_charge(struct ksmbd_conn *conn, +static int smb2_validate_credit_charge(struct ksmbd_work *work, struct smb2_hdr *hdr) { + struct ksmbd_conn *conn = work->conn; unsigned int req_len = 0, expect_resp_len = 0, calc_credit_num, max_len; unsigned short credit_charge = le16_to_cpu(hdr->CreditCharge); void *__hdr = hdr; @@ -308,6 +313,7 @@ static int smb2_validate_credit_charge(struct ksmbd_conn *conn, switch (hdr->Command) { case SMB2_QUERY_INFO: req_len = smb2_query_info_req_len(__hdr); + expect_resp_len = smb2_query_info_resp_len(__hdr); break; case SMB2_SET_INFO: req_len = smb2_set_info_req_len(__hdr); @@ -356,14 +362,85 @@ static int smb2_validate_credit_charge(struct ksmbd_conn *conn, ksmbd_debug(SMB, "Limits exceeding the maximum allowable outstanding requests, given : %u, pending : %u\n", credit_charge, conn->outstanding_credits); ret = 1; - } else + } else { conn->outstanding_credits += credit_charge; + work->credit_charge = credit_charge; + } spin_unlock(&conn->credits_lock); return ret; } +/* + * Verify that the sequence number(s) consumed by an incoming request fall + * within the connection's command sequence window and are not a replay, then + * remove them from the window. Returns 0 if the request + * may proceed, or 1 if it is invalid and the connection must be torn down. + */ +static int smb2_check_sequence_number(struct ksmbd_work *work, + struct smb2_hdr *hdr) +{ + struct ksmbd_conn *conn = work->conn; + u64 mid = le64_to_cpu(hdr->MessageId); + unsigned short charge; + u64 i; + int ret = 0; + + /* An SMB2 CANCEL consumes no sequence number. */ + if (hdr->Command == SMB2_CANCEL) + return 0; + + /* + * A multi-credit request consumes CreditCharge consecutive sequence + * numbers; every other request consumes exactly one. + */ + charge = le16_to_cpu(hdr->CreditCharge); + if (!(conn->vals->req_capabilities & SMB2_GLOBAL_CAP_LARGE_MTU) || + charge == 0) + charge = 1; + + /* The 64-bit sequence number space must not wrap. */ + if (mid + charge < mid) { + pr_err("SMB2 sequence number wrapped (mid %llu charge %u)\n", + mid, charge); + return 1; + } + + spin_lock(&conn->credits_lock); + + /* The whole range must lie within the granted window... */ + if (mid < conn->seq_low || mid + charge > conn->seq_high) { + ksmbd_debug(SMB, + "MessageId %llu (charge %u) outside command window [%llu, %llu)\n", + mid, charge, conn->seq_low, conn->seq_high); + ret = 1; + goto out; + } + + /* ...and none of it may have been consumed already (replay). */ + for (i = mid; i < mid + charge; i++) { + if (!test_bit(i & (KSMBD_CMD_SEQ_WINDOW - 1), conn->seq_bitmap)) { + ksmbd_debug(SMB, + "replayed sequence number %llu (mid %llu charge %u)\n", + i, mid, charge); + ret = 1; + goto out; + } + } + + /* Consume the sequence numbers and slide the low edge forward. */ + for (i = mid; i < mid + charge; i++) + __clear_bit(i & (KSMBD_CMD_SEQ_WINDOW - 1), conn->seq_bitmap); + while (conn->seq_low < conn->seq_high && + !test_bit(conn->seq_low & (KSMBD_CMD_SEQ_WINDOW - 1), + conn->seq_bitmap)) + conn->seq_low++; +out: + spin_unlock(&conn->credits_lock); + return ret; +} + int ksmbd_smb2_check_message(struct ksmbd_work *work) { struct smb2_pdu *pdu = ksmbd_req_buf_next(work); @@ -399,6 +476,11 @@ int ksmbd_smb2_check_message(struct ksmbd_work *work) return 1; } + if (len < __SMB2_HEADER_STRUCTURE_SIZE + sizeof(__le16)) { + ksmbd_debug(SMB, "Message is too small for StructureSize2\n"); + return 1; + } + if (smb2_req_struct_sizes[command] != pdu->StructureSize2) { if (!(command == SMB2_OPLOCK_BREAK_HE && (le16_to_cpu(pdu->StructureSize2) == OP_BREAK_STRUCT_SIZE_20 || @@ -460,9 +542,19 @@ int ksmbd_smb2_check_message(struct ksmbd_work *work) validate_credit: if ((work->conn->vals->req_capabilities & SMB2_GLOBAL_CAP_LARGE_MTU) && - smb2_validate_credit_charge(work->conn, hdr)) + smb2_validate_credit_charge(work, hdr)) return 1; + /* + * A sequence number violation (out of window or a replay) is a + * protocol error. tear the connection down rather than + * keep accepting requests on it. + */ + if (smb2_check_sequence_number(work, hdr)) { + ksmbd_conn_set_exiting(work->conn); + return 1; + } + return 0; } diff --git a/fs/smb/server/smb2ops.c b/fs/smb/server/smb2ops.c index c9a32ee096b5..4578291fb172 100644 --- a/fs/smb/server/smb2ops.c +++ b/fs/smb/server/smb2ops.c @@ -37,6 +37,7 @@ static struct smb_version_values smb21_server_values = { .create_mxac_size = sizeof(struct create_mxac_rsp), .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, }; static struct smb_version_values smb30_server_values = { @@ -64,6 +65,7 @@ static struct smb_version_values smb30_server_values = { .create_mxac_size = sizeof(struct create_mxac_rsp), .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, }; static struct smb_version_values smb302_server_values = { @@ -91,6 +93,7 @@ static struct smb_version_values smb302_server_values = { .create_mxac_size = sizeof(struct create_mxac_rsp), .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, }; static struct smb_version_values smb311_server_values = { @@ -118,6 +121,7 @@ static struct smb_version_values smb311_server_values = { .create_mxac_size = sizeof(struct create_mxac_rsp), .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, }; static struct smb_version_ops smb2_0_server_ops = { @@ -266,8 +270,10 @@ void init_smb3_02_server(struct ksmbd_conn *conn) if (server_conf.flags & KSMBD_GLOBAL_FLAG_SMB3_MULTICHANNEL) conn->vals->req_capabilities |= SMB2_GLOBAL_CAP_MULTI_CHANNEL; - if (server_conf.flags & KSMBD_GLOBAL_FLAG_DURABLE_HANDLE) - conn->vals->req_capabilities |= SMB2_GLOBAL_CAP_PERSISTENT_HANDLES; + /* + * Durable handles are in-memory only. Do not advertise persistent + * handles until CA recovery and fencing are implemented. + */ } /** @@ -290,10 +296,7 @@ int init_smb3_11_server(struct ksmbd_conn *conn) if (server_conf.flags & KSMBD_GLOBAL_FLAG_SMB3_MULTICHANNEL) conn->vals->req_capabilities |= SMB2_GLOBAL_CAP_MULTI_CHANNEL; - if (server_conf.flags & KSMBD_GLOBAL_FLAG_DURABLE_HANDLE) - conn->vals->req_capabilities |= SMB2_GLOBAL_CAP_PERSISTENT_HANDLES; - - INIT_LIST_HEAD(&conn->preauth_sess_table); + /* See init_smb3_02_server(): persistent handles require CA recovery. */ return 0; } @@ -326,6 +329,13 @@ void init_smb2_max_trans_size(unsigned int sz) void init_smb2_max_credits(unsigned int sz) { + /* + * The command sequence window (and its backing bitmap) can track at + * most SMB2_MAX_CREDITS outstanding sequence numbers, so the number of + * credits granted on a connection must not exceed that. + */ + if (sz > SMB2_MAX_CREDITS) + sz = SMB2_MAX_CREDITS; smb21_server_values.max_credits = sz; smb30_server_values.max_credits = sz; smb302_server_values.max_credits = sz; diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c index 743c629fe7ec..6b8809f67b92 100644 --- a/fs/smb/server/smb2pdu.c +++ b/fs/smb/server/smb2pdu.c @@ -9,14 +9,18 @@ #include <net/addrconf.h> #include <linux/syscalls.h> #include <linux/namei.h> +#include <linux/fs_struct.h> #include <linux/statfs.h> #include <linux/ethtool.h> #include <linux/falloc.h> #include <linux/mount.h> #include <linux/filelock.h> +#include <linux/fileattr.h> +#include <linux/timekeeping.h> +#include <linux/unaligned.h> #include "glob.h" -#include "smbfsctl.h" +#include "../common/smbfsctl.h" #include "oplock.h" #include "smbacl.h" @@ -24,7 +28,6 @@ #include "asn1.h" #include "connection.h" #include "transport_ipc.h" -#include "../common/smbdirect/smbdirect.h" #include "transport_rdma.h" #include "vfs.h" #include "vfs_cache.h" @@ -42,6 +45,7 @@ #include "ndr.h" #include "stats.h" #include "transport_tcp.h" +#include "compress.h" static void __wbuf(struct ksmbd_work *work, void **req, void **rsp) { @@ -54,30 +58,21 @@ 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)) -/** - * check_session_id() - check for valid session id in smb header - * @conn: connection instance - * @id: session id from smb header - * - * Return: 1 if valid session id, otherwise 0 - */ -static inline bool check_session_id(struct ksmbd_conn *conn, u64 id) -{ - struct ksmbd_session *sess; +#define SMB2_CREATE_FILE_ATTRIBUTE_MASK \ + (FILE_ATTRIBUTE_MASK & ~(FILE_ATTRIBUTE_INTEGRITY_STREAM | \ + FILE_ATTRIBUTE_NO_SCRUB_DATA)) - if (id == 0 || id == -1) - return false; +/* Windows reports automatic write-time updates at roughly 15 ms resolution. */ +#define KSMBD_WRITE_TIME_RESOLUTION (15ULL * 10000) - sess = ksmbd_session_lookup_all(conn, id); - if (sess) { - ksmbd_user_session_put(sess); - return true; - } - pr_err("Invalid user session id: %llu\n", id); - return false; -} +/* MAXFILESIZE in [MS-FSA] 2.1.5.3 Server Requests a Write. */ +#define SMB2_MAX_FILE_SIZE 0xfffffff0000ULL struct channel *lookup_chann_list(struct ksmbd_session *sess, struct ksmbd_conn *conn) { @@ -90,6 +85,50 @@ struct channel *lookup_chann_list(struct ksmbd_session *sess, struct ksmbd_conn return chann; } +static int register_session_channel(struct ksmbd_session *sess, + struct ksmbd_conn *conn, + const char *sess_key) +{ + struct channel *chann, *old; + unsigned long index; + unsigned int count = 0; + int rc = 0; + + down_write(&sess->chann_lock); + if (sess->tearing_down) { + rc = -ESHUTDOWN; + goto out; + } + + if (xa_load(&sess->ksmbd_chann_list, (long)conn)) + goto out; + + xa_for_each(&sess->ksmbd_chann_list, index, chann) + count++; + if (count >= KSMBD_MAX_CHANNELS) { + rc = -ENOSPC; + goto out; + } + + chann = kmalloc_obj(struct channel, KSMBD_DEFAULT_GFP); + if (!chann) { + rc = -ENOMEM; + goto out; + } + + chann->conn = 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)) { + kfree_sensitive(chann); + rc = xa_err(old); + } +out: + up_write(&sess->chann_lock); + return rc; +} + /** * smb2_get_ksmbd_tcon() - get tree connection information using a tree id. * @work: smb work @@ -126,6 +165,8 @@ int smb2_get_ksmbd_tcon(struct ksmbd_work *work) pr_err("The first operation in the compound does not have tcon\n"); return -EINVAL; } + if (work->tcon->t_state != TREE_CONNECTED) + return -ENOENT; if (tree_id != UINT_MAX && work->tcon->id != tree_id) { pr_err("tree id(%u) is different with id(%u) in first operation\n", tree_id, work->tcon->id); @@ -243,6 +284,13 @@ void set_smb2_rsp_status(struct ksmbd_work *work, __le32 err) { struct smb2_hdr *rsp_hdr; + if (work->next_smb2_rcv_hdr_off) { + rsp_hdr = ksmbd_resp_buf_next(work); + rsp_hdr->Status = err; + smb2_set_err_rsp(work); + return; + } + rsp_hdr = smb_get_msg(work->response_buf); rsp_hdr->Status = err; @@ -327,6 +375,7 @@ int smb2_set_rsp_credits(struct ksmbd_work *work) struct ksmbd_conn *conn = work->conn; unsigned short credits_requested, aux_max; unsigned short credit_charge, credits_granted = 0; + u64 window_room, i; if (work->send_no_response) return 0; @@ -349,6 +398,7 @@ int smb2_set_rsp_credits(struct ksmbd_work *work) conn->total_credits -= credit_charge; conn->outstanding_credits -= credit_charge; + work->credit_charge = 0; credits_requested = max_t(unsigned short, le16_to_cpu(req_hdr->CreditRequest), 1); @@ -362,11 +412,26 @@ int smb2_set_rsp_credits(struct ksmbd_work *work) aux_max = 1; else aux_max = conn->vals->max_credits - conn->total_credits; + + /* + * The command sequence window must not grow beyond + * KSMBD_CMD_SEQ_WINDOW sequence numbers ahead of the oldest one still + * outstanding. Cap the grant by the room left in the window so that + * credits are withheld until the client consumes the low end (and so + * that seq_bitmap stays usable as a ring). + */ + window_room = conn->seq_low + KSMBD_CMD_SEQ_WINDOW - conn->seq_high; + aux_max = min_t(unsigned short, aux_max, window_room); credits_granted = min_t(unsigned short, credits_requested, aux_max); conn->total_credits += credits_granted; work->credits_granted += credits_granted; + /* Extend the sequence window to cover the newly granted credits. */ + for (i = conn->seq_high; i < conn->seq_high + credits_granted; i++) + __set_bit(i & (KSMBD_CMD_SEQ_WINDOW - 1), conn->seq_bitmap); + conn->seq_high += credits_granted; + if (!req_hdr->NextCommand) { /* Update CreditRequest in last request */ hdr->CreditRequest = cpu_to_le16(work->credits_granted); @@ -402,6 +467,59 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work) 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 { + 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; + } + } 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 + * that fails (e.g. a standalone request with a bad session id) + * must not seed the status for a following related command, + * which has to be evaluated on its own (and may legitimately + * fail with a different status such as INVALID_PARAMETER). The + * compound session id is still tracked so a following related + * command can validate it. + */ + if (req->Flags & SMB2_FLAGS_RELATED_OPERATIONS) + work->compound_status = rsp->Status; } len = get_rfc1002_len(work->response_buf) - work->next_smb2_rsp_hdr_off; @@ -427,6 +545,7 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work) ksmbd_debug(SMB, "related flag should be set\n"); work->compound_fid = KSMBD_NO_FID; work->compound_pfid = KSMBD_NO_FID; + work->compound_status = STATUS_SUCCESS; } memset((char *)rsp_hdr, 0, sizeof(struct smb2_hdr) + 2); rsp_hdr->ProtocolId = SMB2_PROTO_NUMBER; @@ -438,6 +557,8 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work) */ rsp_hdr->Flags = (SMB2_FLAGS_SERVER_TO_REDIR | SMB2_FLAGS_RELATED_OPERATIONS); + if (rcv_hdr->Flags & SMB2_FLAGS_REPLAY_OPERATION) + rsp_hdr->Flags |= SMB2_FLAGS_REPLAY_OPERATION; rsp_hdr->NextCommand = 0; rsp_hdr->MessageId = rcv_hdr->MessageId; rsp_hdr->Id.SyncId.ProcessId = rcv_hdr->Id.SyncId.ProcessId; @@ -446,6 +567,19 @@ static void init_chained_smb2_rsp(struct ksmbd_work *work) memcpy(rsp_hdr->Signature, rcv_hdr->Signature, 16); } +static bool smb2_compound_has_failed(struct ksmbd_work *work, + struct smb2_hdr *rsp) +{ + if (!work->next_smb2_rcv_hdr_off || + has_file_id(work->compound_fid) || + work->compound_status == STATUS_SUCCESS) + return false; + + rsp->Status = work->compound_status; + smb2_set_err_rsp(work); + return true; +} + /** * is_chained_smb2_message() - check for chained command * @work: smb work containing smb request buffer @@ -517,6 +651,8 @@ int init_smb2_rsp_hdr(struct ksmbd_work *work) * Message is response. We don't grant oplock yet. */ rsp_hdr->Flags = (SMB2_FLAGS_SERVER_TO_REDIR); + if (rcv_hdr->Flags & SMB2_FLAGS_REPLAY_OPERATION) + rsp_hdr->Flags |= SMB2_FLAGS_REPLAY_OPERATION; rsp_hdr->NextCommand = 0; rsp_hdr->MessageId = rcv_hdr->MessageId; rsp_hdr->Id.SyncId.ProcessId = rcv_hdr->Id.SyncId.ProcessId; @@ -527,6 +663,192 @@ int init_smb2_rsp_hdr(struct ksmbd_work *work) return 0; } +static __le16 smb3_hdr_channel_sequence(struct smb2_hdr *hdr) +{ + return ((struct smb3_hdr_req *)hdr)->ChannelSequence; +} + +static bool smb3_hdr_replay(struct smb2_hdr *hdr) +{ + return hdr->Flags & SMB2_FLAGS_REPLAY_OPERATION; +} + +static int smb3_verify_channel_sequence(struct ksmbd_work *work, + struct ksmbd_file *fp, + struct smb2_hdr *hdr, + bool allow_stale) +{ + __le16 chseq_le; + u16 chseq, old_chseq; + int ret = 0; + + if (work->conn->dialect < SMB30_PROT_ID) + return 0; + + chseq_le = smb3_hdr_channel_sequence(hdr); + chseq = le16_to_cpu(chseq_le); + + spin_lock(&fp->f_lock); + old_chseq = le16_to_cpu(fp->channel_sequence); + if (smb3_hdr_replay(hdr)) { + if (chseq == old_chseq && fp->outstanding_pre_requests == 0) { + fp->outstanding_requests++; + } else if ((u16)(chseq - old_chseq) <= 0x7fff && + fp->outstanding_pre_requests == 0) { + fp->outstanding_pre_requests += fp->outstanding_requests; + fp->outstanding_requests = 1; + fp->channel_sequence = chseq_le; + } else if (allow_stale) { + fp->outstanding_pre_requests++; + } else { + ret = -EAGAIN; + } + } else { + if (chseq == old_chseq) { + fp->outstanding_requests++; + } else if ((u16)(chseq - old_chseq) <= 0x7fff) { + fp->outstanding_pre_requests += fp->outstanding_requests; + fp->outstanding_requests = 1; + fp->channel_sequence = chseq_le; + } else if (allow_stale) { + fp->outstanding_pre_requests++; + } else { + ret = -EAGAIN; + } + } + spin_unlock(&fp->f_lock); + + return ret; +} + +static void smb3_complete_channel_sequence(struct ksmbd_work *work, + struct ksmbd_file *fp, + __le16 chseq_le) +{ + u16 chseq; + + if (work->conn->dialect < SMB30_PROT_ID) + return; + + chseq = le16_to_cpu(chseq_le); + + spin_lock(&fp->f_lock); + if (chseq == le16_to_cpu(fp->channel_sequence)) { + if (fp->outstanding_requests) + fp->outstanding_requests--; + } else { + if (fp->outstanding_pre_requests) + fp->outstanding_pre_requests--; + } + spin_unlock(&fp->f_lock); +} + +static int smb2_set_request_open(struct ksmbd_work *work, struct ksmbd_file *fp, + struct smb2_hdr *hdr, bool verify_chseq, + bool allow_stale_chseq) +{ + struct ksmbd_file *open; + int ret; + + smb2_complete_request_open(work); + + open = ksmbd_file_get(fp); + if (!open) + return -ESTALE; + + if (verify_chseq) { + ret = smb3_verify_channel_sequence(work, fp, hdr, + allow_stale_chseq); + if (ret) { + ksmbd_fd_put(work, open); + return ret; + } + work->request_open_chseq_tracked = true; + } + + work->request_open = open; + work->request_open_chseq = smb3_hdr_channel_sequence(hdr); + return 0; +} + +void smb2_complete_request_open(struct ksmbd_work *work) +{ + struct ksmbd_file *open = work->request_open; + + if (!open) + return; + + if (work->request_open_chseq_tracked) + smb3_complete_channel_sequence(work, open, + work->request_open_chseq); + + work->request_open = NULL; + work->request_open_chseq_tracked = false; + ksmbd_fd_put(work, open); +} + +static bool smb2_lock_sequence_applicable(struct ksmbd_work *work, + struct ksmbd_file *fp) +{ + return fp->is_resilient || fp->is_durable || fp->is_persistent || + (work->conn->dialect >= SMB30_PROT_ID && + (work->conn->vals->req_capabilities & + SMB2_GLOBAL_CAP_MULTI_CHANNEL)); +} + +static bool smb2_verify_lock_sequence(struct ksmbd_work *work, + struct ksmbd_file *fp, + struct smb2_lock_req *req) +{ + u32 val, index; + u8 sequence; + bool replay = false; + + if (work->conn->dialect == SMB20_PROT_ID || + !smb2_lock_sequence_applicable(work, fp)) + return false; + + val = le32_to_cpu(req->LockSequenceNumber); + sequence = val & 0xf; + index = val >> 4; + if (!index || index > KSMBD_LOCK_SEQ_ARRAY_SIZE) + return false; + + spin_lock(&fp->f_lock); + if (fp->lock_seq[index - 1].valid) { + if (fp->lock_seq[index - 1].sequence == sequence) + replay = true; + else + fp->lock_seq[index - 1].valid = false; + } + spin_unlock(&fp->f_lock); + + return replay; +} + +static void smb2_update_lock_sequence(struct ksmbd_work *work, + struct ksmbd_file *fp, + struct smb2_lock_req *req) +{ + u32 val, index; + u8 sequence; + + if (work->conn->dialect == SMB20_PROT_ID || + !smb2_lock_sequence_applicable(work, fp)) + return; + + val = le32_to_cpu(req->LockSequenceNumber); + sequence = val & 0xf; + index = val >> 4; + if (!index || index > KSMBD_LOCK_SEQ_ARRAY_SIZE) + return; + + spin_lock(&fp->f_lock); + fp->lock_seq[index - 1].valid = true; + fp->lock_seq[index - 1].sequence = sequence; + spin_unlock(&fp->f_lock); +} + /** * smb2_allocate_rsp_buf() - allocate smb2 response buffer * @work: smb work containing smb request buffer @@ -554,7 +876,8 @@ int smb2_allocate_rsp_buf(struct ksmbd_work *work) req = smb_get_msg(work->request_buf); if ((req->InfoType == SMB2_O_INFO_FILE && (req->FileInfoClass == FILE_FULL_EA_INFORMATION || - req->FileInfoClass == FILE_ALL_INFORMATION)) || + req->FileInfoClass == FILE_ALL_INFORMATION || + req->FileInfoClass == FILE_NORMALIZED_NAME_INFORMATION)) || req->InfoType == SMB2_O_INFO_SECURITY) sz = large_sz; } @@ -571,6 +894,53 @@ int smb2_allocate_rsp_buf(struct ksmbd_work *work) return 0; } +static bool smb2_session_expired_cmd_allowed(struct ksmbd_work *work, + unsigned int cmd) +{ + struct smb2_lock_req *req; + unsigned int len, lock_count, i; + + if (cmd == SMB2_CANCEL_HE || cmd == SMB2_CLOSE_HE || + cmd == SMB2_LOGOFF_HE) + return true; + if (cmd != SMB2_LOCK_HE) + return false; + + req = ksmbd_req_buf_next(work); + if (req->hdr.NextCommand) + len = le32_to_cpu(req->hdr.NextCommand); + else { + len = get_rfc1002_len(work->request_buf); + if (len < work->next_smb2_rcv_hdr_off) + return false; + len -= work->next_smb2_rcv_hdr_off; + } + + lock_count = le16_to_cpu(req->LockCount); + if (!lock_count || len < offsetof(struct smb2_lock_req, locks) || + lock_count > (len - offsetof(struct smb2_lock_req, locks)) / + sizeof(struct smb2_lock_element)) + return false; + + for (i = 0; i < lock_count; i++) { + if (le32_to_cpu(req->locks[i].Flags) != SMB2_LOCKFLAG_UNLOCK) + return false; + } + return true; +} + +static bool smb2_session_kerberos_expired(struct ksmbd_session *sess) +{ + if (!sess->kerberos_expiry || + ktime_get_real_seconds() < sess->kerberos_expiry) + return false; + + if (cmpxchg(&sess->state, SMB2_SESSION_VALID, + SMB2_SESSION_EXPIRED) == SMB2_SESSION_VALID) + ksmbd_counter_inc(KSMBD_COUNTER_SESSION_TIMEOUTS); + return true; +} + /** * smb2_check_user_session() - check for valid session for a user * @work: smb work containing smb request buffer @@ -585,19 +955,36 @@ int smb2_check_user_session(struct ksmbd_work *work) unsigned long long sess_id; /* - * SMB2_ECHO, SMB2_NEGOTIATE, SMB2_SESSION_SETUP command do not - * require a session id, so no need to validate user session's for - * these commands. + * SMB2_NEGOTIATE and SMB2_SESSION_SETUP do not require a session id. + * SMB2_ECHO may omit it, but an echo carrying a session id still needs + * the session attached to work so that its signature can be checked and + * the response can be signed, including after Kerberos expiry. */ - if (cmd == SMB2_ECHO_HE || cmd == SMB2_NEGOTIATE_HE || - cmd == SMB2_SESSION_SETUP_HE) + if (cmd == SMB2_NEGOTIATE_HE || cmd == SMB2_SESSION_SETUP_HE) return 0; + sess_id = le64_to_cpu(req_hdr->SessionId); + if (cmd == SMB2_ECHO_HE) { + /* + * ECHO remains valid without a live session, including after + * LOGOFF. Attach an existing session only to authenticate a signed + * ECHO and sign its response; a stale SessionId is not an error. + */ + if (!work->next_smb2_rcv_hdr_off && sess_id) + work->sess = ksmbd_session_lookup_all_states(conn, sess_id); + if (work->sess) { + if (!smb2_session_kerberos_expired(work->sess) && + work->sess->state != SMB2_SESSION_VALID) { + ksmbd_user_session_put(work->sess); + work->sess = NULL; + } + } + return 0; + } + if (!ksmbd_conn_good(conn)) return -EIO; - sess_id = le64_to_cpu(req_hdr->SessionId); - /* * If request is not the first in Compound request, * Just validate session id in header with work->sess->id. @@ -612,13 +999,41 @@ int smb2_check_user_session(struct ksmbd_work *work) sess_id, work->sess->id); return -EINVAL; } + smb2_session_kerberos_expired(work->sess); + if (work->sess->state != SMB2_SESSION_VALID) { + pr_err("compound request on a non-valid session (state %d)\n", + work->sess->state); + if (smb2_session_kerberos_expired(work->sess) && + smb2_session_expired_cmd_allowed(work, cmd)) + return 1; + return smb2_session_kerberos_expired(work->sess) ? + -EKEYEXPIRED : -EINVAL; + } return 1; } /* Check for validity of user session */ - work->sess = ksmbd_session_lookup_all(conn, sess_id); - if (work->sess) + work->sess = ksmbd_session_lookup_all_states(conn, sess_id); + if (work->sess) { + if (smb2_session_kerberos_expired(work->sess)) { + return smb2_session_expired_cmd_allowed(work, cmd) ? + 1 : -EKEYEXPIRED; + } + if (work->sess->state != SMB2_SESSION_VALID) { + /* + * Keep the reference for an encrypted request so the caller can + * return STATUS_USER_SESSION_DELETED encrypted with the old key. + */ + if (work->encrypted && + work->sess->state == SMB2_SESSION_EXPIRED && + work->sess->enc) + return -ENOENT; + ksmbd_user_session_put(work->sess); + work->sess = NULL; + return -ENOENT; + } return 1; + } ksmbd_debug(SMB, "Invalid user session, Uid %llu\n", sess_id); return -ENOENT; } @@ -658,6 +1073,24 @@ smb2_get_name(const char *src, const int maxlen, struct nls_table *local_nls) return name; } +/* Link a fully initialized async work item unless the connection is closing. */ +static bool ksmbd_conn_link_async_request(struct ksmbd_conn *conn, + struct ksmbd_work *work) +{ + bool linked = false; + + spin_lock(&conn->request_lock); + if (!ksmbd_conn_exiting(conn) && !ksmbd_conn_releasing(conn)) { + if (list_empty(&work->async_request_entry)) + list_add_tail(&work->async_request_entry, + &conn->async_requests); + linked = true; + } + spin_unlock(&conn->request_lock); + + return linked; +} + int setup_async_work(struct ksmbd_work *work, void (*fn)(void **), void **arg) { struct ksmbd_conn *conn = work->conn; @@ -670,20 +1103,22 @@ int setup_async_work(struct ksmbd_work *work, void (*fn)(void **), void **arg) } work->asynchronous = true; work->async_id = id; - - ksmbd_debug(SMB, - "Send interim Response to inform async request id : %d\n", - work->async_id); - work->cancel_fn = fn; work->cancel_argv = arg; - if (list_empty(&work->async_request_entry)) { - spin_lock(&conn->request_lock); - list_add_tail(&work->async_request_entry, &conn->async_requests); - spin_unlock(&conn->request_lock); + if (!ksmbd_conn_link_async_request(conn, work)) { + work->asynchronous = false; + work->async_id = 0; + work->cancel_fn = NULL; + work->cancel_argv = NULL; + ksmbd_release_id(&conn->async_ida, id); + return -ESHUTDOWN; } + ksmbd_debug(SMB, + "Send interim Response to inform async request id : %d\n", + work->async_id); + return 0; } @@ -705,6 +1140,108 @@ void release_async_work(struct ksmbd_work *work) } } +static int smb2_send_interim_work(struct ksmbd_work *in_work, + struct ksmbd_work *work, bool eor) +{ + int err = 0; + + in_work->encrypted = work->encrypted; + if (work->encrypted && work->sess && work->sess->enc && + work->conn->ops->encrypt_resp) { + in_work->sess = work->sess; + err = work->conn->ops->encrypt_resp(in_work); + in_work->sess = NULL; + } + if (err) + return err; + + return eor ? ksmbd_conn_write_eor(in_work) : + ksmbd_conn_write(in_work); +} + +static int smb2_send_interim_prefix_work(struct ksmbd_work *work) +{ + struct ksmbd_work *in_work; + unsigned int len, copied = 0; + char *dst; + int err = -ENOMEM; + int i; + + len = get_rfc1002_len(work->iov[0].iov_base); + in_work = ksmbd_alloc_work_struct(); + if (!in_work) + return err; + + in_work->response_buf = kvzalloc(len + 4, KSMBD_DEFAULT_GFP); + if (!in_work->response_buf) + goto out; + in_work->response_sz = len + 4; + in_work->conn = work->conn; + dst = in_work->response_buf + 4; + for (i = 1; i <= work->iov_idx; i++) { + if (work->iov[i].iov_len > len - copied) { + err = -EINVAL; + goto out; + } + memcpy(dst + copied, work->iov[i].iov_base, + work->iov[i].iov_len); + copied += work->iov[i].iov_len; + } + if (copied != len) { + err = -EINVAL; + goto out; + } + + err = ksmbd_iov_pin_rsp(in_work, dst, len); + if (!err) + err = smb2_send_interim_work(in_work, work, true); +out: + ksmbd_free_work_struct(in_work); + return err; +} + +static void smb2_send_interim_compound_prefix(struct ksmbd_work *work) +{ + struct smb2_hdr *req_hdr; + struct smb2_hdr *rsp_hdr; + int err; + + if (!work->next_smb2_rcv_hdr_off || + !work->next_smb2_rsp_hdr_off || + work->curr_smb2_rsp_hdr_off == work->next_smb2_rsp_hdr_off || + !work->iov_idx) + return; + + req_hdr = ksmbd_req_buf_next(work); + /* Detach only the final async command from the completed prefix. */ + if (req_hdr->NextCommand) + return; + + /* + * The responses before the async command are sent as a standalone + * compound response. The last response in this prefix must terminate + * the chain. + */ + rsp_hdr = ksmbd_resp_buf_curr(work); + rsp_hdr->NextCommand = 0; + if ((rsp_hdr->Flags & SMB2_FLAGS_SIGNED) && work->sess && + work->conn->ops->set_sign_rsp) + work->conn->ops->set_sign_rsp(work); + + err = smb2_send_interim_prefix_work(work); + if (err) + ksmbd_debug(SMB, "failed to send compound interim prefix: %d\n", + err); + + work->iov_idx = 0; + work->iov_cnt = 0; + work->curr_smb2_rsp_hdr_off = work->next_smb2_rsp_hdr_off; + *(__be32 *)work->response_buf = 0; + + rsp_hdr = ksmbd_resp_buf_next(work); + rsp_hdr->Flags &= ~SMB2_FLAGS_RELATED_OPERATIONS; +} + void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status) { struct smb2_hdr *rsp_hdr; @@ -719,6 +1256,9 @@ void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status) return; } + if (status == STATUS_PENDING) + smb2_send_interim_compound_prefix(work); + in_work->conn = work->conn; memcpy(smb_get_msg(in_work->response_buf), ksmbd_resp_buf_next(work), __SMB2_HEADER_STRUCTURE_SIZE); @@ -729,7 +1269,8 @@ void smb2_send_interim_resp(struct ksmbd_work *work, __le32 status) smb2_set_err_rsp(in_work); rsp_hdr->Status = status; - ksmbd_conn_write(in_work); + if (smb2_send_interim_work(in_work, work, true)) + ksmbd_debug(SMB, "failed to send interim response\n"); ksmbd_free_work_struct(in_work); } @@ -769,9 +1310,6 @@ static int smb2_get_dos_mode(struct kstat *stat, int attribute) } else { attr = (attribute & 0x00005137) | FILE_ATTRIBUTE_ARCHIVE; attr &= ~(FILE_ATTRIBUTE_DIRECTORY); - if (S_ISREG(stat->mode) && (server_conf.share_fake_fscaps & - FILE_SUPPORTS_SPARSE_FILES)) - attr |= FILE_ATTRIBUTE_SPARSE_FILE; if (smb2_get_reparse_tag_special_file(stat->mode)) attr |= FILE_ATTRIBUTE_REPARSE_POINT; @@ -802,6 +1340,61 @@ static void build_encrypt_ctxt(struct smb2_encryption_neg_context *pneg_ctxt, pneg_ctxt->Ciphers[0] = cipher_type; } +static void build_compress_ctxt(struct smb2_compression_capabilities_context *pneg_ctxt, + __le16 compress_algorithm, bool compress_chained, + bool compress_pattern) +{ + /* + * Return only algorithms implemented by ksmbd. Pattern_V1 is advertised + * as a second ID when the client also enabled chained transforms. + */ + pneg_ctxt->ContextType = SMB2_COMPRESSION_CAPABILITIES; + pneg_ctxt->DataLength = cpu_to_le16(compress_pattern ? 12 : 10); + pneg_ctxt->Reserved = cpu_to_le32(0); + pneg_ctxt->CompressionAlgorithmCount = + cpu_to_le16(compress_pattern ? 2 : 1); + pneg_ctxt->Padding = cpu_to_le16(0); + pneg_ctxt->Flags = compress_chained ? + SMB2_COMPRESSION_CAPABILITIES_FLAG_CHAINED : + SMB2_COMPRESSION_CAPABILITIES_FLAG_NONE; + pneg_ctxt->CompressionAlgorithms[0] = compress_algorithm; + pneg_ctxt->CompressionAlgorithms[1] = compress_pattern ? + SMB3_COMPRESS_PATTERN : 0; + pneg_ctxt->CompressionAlgorithms[2] = 0; + pneg_ctxt->CompressionAlgorithms[3] = 0; +} + +/** + * build_rdma_ctx() - build an RDMA transform negotiate response context + * @ctxt: response context header to populate + * @transform_ids: bitmap of transforms common to the client and server + * + * Return: encoded negotiate context length + */ +static int build_rdma_ctx(struct smb2_neg_context *ctxt, + unsigned long transform_ids) +{ + struct smb2_rdma_transform_capabilities_context *pneg_ctxt; + int count = 0; + + pneg_ctxt = (void *)ctxt; + pneg_ctxt->ContextType = SMB2_RDMA_TRANSFORM_CAPABILITIES; + pneg_ctxt->Reserved = 0; + pneg_ctxt->Reserved1 = 0; + pneg_ctxt->Reserved2 = 0; + if (transform_ids & BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION)) + pneg_ctxt->RDMATransformIds[count++] = + cpu_to_le16(SMB2_RDMA_TRANSFORM_ENCRYPTION); + if (!count) + pneg_ctxt->RDMATransformIds[count++] = + cpu_to_le16(SMB2_RDMA_TRANSFORM_NONE); + + pneg_ctxt->TransformCount = cpu_to_le16(count); + pneg_ctxt->DataLength = cpu_to_le16(8 + count * sizeof(__le16)); + return sizeof(struct smb2_neg_context) + + le16_to_cpu(pneg_ctxt->DataLength); +} + static void build_sign_cap_ctxt(struct smb2_signing_capabilities *pneg_ctxt, __le16 sign_algo) { @@ -863,8 +1456,31 @@ static unsigned int assemble_neg_contexts(struct ksmbd_conn *conn, ctxt_size += sizeof(struct smb2_encryption_neg_context) + 2; } - /* compression context not yet supported */ - WARN_ON(conn->compress_algorithm != SMB3_COMPRESS_NONE); + if (conn->compress_algorithm != SMB3_COMPRESS_NONE) { + ctxt_size = round_up(ctxt_size, 8); + ksmbd_debug(SMB, + "assemble SMB2_COMPRESSION_CAPABILITIES context\n"); + build_compress_ctxt((struct smb2_compression_capabilities_context *) + (pneg_ctxt + ctxt_size), + conn->compress_algorithm, + conn->compress_chained, + conn->compress_pattern); + neg_ctxt_cnt++; + ctxt_size += sizeof(struct smb2_neg_context) + + (conn->compress_pattern ? 12 : 10); + } + + if (conn->rdma_transform_negotiated) { + struct smb2_neg_context *rdma_ctxt; + + ctxt_size = round_up(ctxt_size, 8); + ksmbd_debug(SMB, + "assemble SMB2_RDMA_TRANSFORM_CAPABILITIES context\n"); + rdma_ctxt = (void *)(pneg_ctxt + ctxt_size); + ctxt_size += build_rdma_ctx(rdma_ctxt, + conn->rdma_transform_ids); + neg_ctxt_cnt++; + } if (conn->posix_ext_supported) { ctxt_size = round_up(ctxt_size, 8); @@ -968,10 +1584,67 @@ bool smb3_encryption_negotiated(struct ksmbd_conn *conn) conn->cipher_type; } -static void decode_compress_ctxt(struct ksmbd_conn *conn, - struct smb2_compression_capabilities_context *pneg_ctxt) +static __le32 decode_compress_ctxt(struct ksmbd_conn *conn, + struct smb2_compression_capabilities_context *pneg_ctxt, + int ctxt_len) { + int alg_cnt, algs_size, i; + __le16 *algs; + + if (sizeof(struct smb2_neg_context) + 10 > ctxt_len) { + pr_err("Invalid SMB2_COMPRESSION_CAPABILITIES context length\n"); + return STATUS_INVALID_PARAMETER; + } + conn->compress_algorithm = SMB3_COMPRESS_NONE; + conn->compress_chained = false; + conn->compress_pattern = false; + + alg_cnt = le16_to_cpu(pneg_ctxt->CompressionAlgorithmCount); + if (!alg_cnt) + return STATUS_INVALID_PARAMETER; + + if (pneg_ctxt->Flags != SMB2_COMPRESSION_CAPABILITIES_FLAG_NONE && + pneg_ctxt->Flags != SMB2_COMPRESSION_CAPABILITIES_FLAG_CHAINED) + return STATUS_INVALID_PARAMETER; + + algs_size = alg_cnt * sizeof(__le16); + if (sizeof(struct smb2_neg_context) + 8 + algs_size > ctxt_len) { + pr_err("Invalid compression algorithm count(%d)\n", alg_cnt); + return STATUS_INVALID_PARAMETER; + } + + /* + * CompressionAlgorithms[] is declared as a fixed 4-element array, but + * the actual element count is variable (clients such as Windows may + * advertise more). The on-wire length was validated above, so walk the + * algorithms through a pointer to avoid a fixed-array bounds check. + */ + algs = pneg_ctxt->CompressionAlgorithms; + for (i = 0; i < alg_cnt; i++) { + __le16 alg = algs[i]; + + /* + * LZ77 is the required general-purpose codec. Pattern_V1 is an + * optional chained payload type and cannot stand alone. + */ + if (alg == SMB3_COMPRESS_LZ77) { + conn->compress_algorithm = alg; + conn->compress_chained = + pneg_ctxt->Flags == + SMB2_COMPRESSION_CAPABILITIES_FLAG_CHAINED; + ksmbd_debug(SMB, "Compression Algorithm ID = 0x%x\n", + le16_to_cpu(alg)); + } else if (alg == SMB3_COMPRESS_PATTERN) { + conn->compress_pattern = true; + } + } + + if (conn->compress_algorithm == SMB3_COMPRESS_NONE || + !conn->compress_chained) + conn->compress_pattern = false; + + return STATUS_SUCCESS; } static void decode_sign_cap_ctxt(struct ksmbd_conn *conn, @@ -1009,6 +1682,46 @@ static void decode_sign_cap_ctxt(struct ksmbd_conn *conn, } } +/** + * decode_rdma_ctx() - decode an RDMA transform negotiate request context + * @conn: connection being negotiated + * @ctxt: request context header to decode + * @ctxt_len: total context length, including the negotiate context header + * + * Record transforms supported by both peers only for SMB Direct connections. + * + * Return: NT status describing the decode result + */ +static __le32 decode_rdma_ctx(struct ksmbd_conn *conn, + struct smb2_neg_context *ctxt, int ctxt_len) +{ + struct smb2_rdma_transform_capabilities_context *pneg_ctxt; + unsigned int count, i; + + pneg_ctxt = (void *)ctxt; + /* RDMA transforms are a node capability, not just a transport capability. */ + if (!ksmbd_rdma_enabled()) + return STATUS_SUCCESS; + + if (ctxt_len < sizeof(*pneg_ctxt)) + return STATUS_INVALID_PARAMETER; + + count = le16_to_cpu(pneg_ctxt->TransformCount); + if (!count || count > + (ctxt_len - sizeof(*pneg_ctxt)) / sizeof(__le16)) + return STATUS_INVALID_PARAMETER; + + conn->rdma_transform_negotiated = true; + conn->rdma_transform_ids = 0; + for (i = 0; i < count; i++) { + u16 id = le16_to_cpu(pneg_ctxt->RDMATransformIds[i]); + + if (id == SMB2_RDMA_TRANSFORM_ENCRYPTION) + conn->rdma_transform_ids |= BIT(id); + } + return STATUS_SUCCESS; +} + static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn, struct smb2_negotiate_req *req, unsigned int len_of_smb) @@ -1019,6 +1732,7 @@ static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn, unsigned int offset = le32_to_cpu(req->NegotiateContextOffset); 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; ksmbd_debug(SMB, "decoding %d negotiate contexts\n", neg_ctxt_cnt); if (len_of_smb <= offset) { @@ -1064,14 +1778,30 @@ static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn, } else if (pctx->ContextType == SMB2_COMPRESSION_CAPABILITIES) { ksmbd_debug(SMB, "deassemble SMB2_COMPRESSION_CAPABILITIES context\n"); - if (conn->compress_algorithm) + if (compress_ctxt_cnt++) { + status = STATUS_INVALID_PARAMETER; break; + } - decode_compress_ctxt(conn, - (struct smb2_compression_capabilities_context *)pctx); + status = decode_compress_ctxt(conn, + (struct smb2_compression_capabilities_context *) + pctx, ctxt_len); + if (status != STATUS_SUCCESS) + break; } else if (pctx->ContextType == SMB2_NETNAME_NEGOTIATE_CONTEXT_ID) { ksmbd_debug(SMB, "deassemble SMB2_NETNAME_NEGOTIATE_CONTEXT_ID context\n"); + } else if (pctx->ContextType == SMB2_RDMA_TRANSFORM_CAPABILITIES) { + ksmbd_debug(SMB, + "deassemble SMB2_RDMA_TRANSFORM_CAPABILITIES context\n"); + if (ksmbd_rdma_enabled() && + rdma_transform_ctxt_cnt++) { + status = STATUS_INVALID_PARAMETER; + break; + } + status = decode_rdma_ctx(conn, pctx, ctxt_len); + if (status != STATUS_SUCCESS) + break; } else if (pctx->ContextType == SMB2_POSIX_EXTENSIONS_AVAILABLE) { ksmbd_debug(SMB, "deassemble SMB2_POSIX_EXTENSIONS_AVAILABLE context\n"); @@ -1096,6 +1826,8 @@ static __le32 deassemble_neg_contexts(struct ksmbd_conn *conn, * smb2_handle_negotiate() - handler for smb2 negotiate command * @work: smb work containing smb request buffer * + * The caller holds conn->srv_mutex. + * * Return: 0 */ int smb2_handle_negotiate(struct ksmbd_work *work) @@ -1109,13 +1841,6 @@ int smb2_handle_negotiate(struct ksmbd_work *work) ksmbd_debug(SMB, "Received negotiate request\n"); conn->need_neg = false; - if (ksmbd_conn_good(conn)) { - pr_err("conn->tcp_status is already in CifsGood State\n"); - work->send_no_response = 1; - return rc; - } - - ksmbd_conn_lock(conn); smb2_buf_len = get_rfc1002_len(work->request_buf); smb2_neg_size = offsetof(struct smb2_negotiate_req, Dialects); if (smb2_neg_size > smb2_buf_len) { @@ -1169,7 +1894,7 @@ int smb2_handle_negotiate(struct ksmbd_work *work) KSMBD_DEFAULT_GFP); if (!conn->preauth_info) { rc = -ENOMEM; - rsp->hdr.Status = STATUS_INVALID_PARAMETER; + rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; goto err_out; } @@ -1184,6 +1909,16 @@ int smb2_handle_negotiate(struct ksmbd_work *work) conn->preauth_info = NULL; goto err_out; } + if (!conn->cipher_type) + conn->rdma_transform_ids &= + ~BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION); + ksmbd_debug(RDMA, + "RDMA transform negotiation: transport=%s context=%s encryption=%s cipher=0x%04x\n", + conn->transport->ops->rdma_read ? "rdma" : "tcp", + conn->rdma_transform_negotiated ? "present" : "absent", + conn->rdma_transform_ids & + BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION) ? "enabled" : "disabled", + le16_to_cpu(conn->cipher_type)); rc = init_smb3_11_server(conn); if (rc < 0) { @@ -1252,11 +1987,9 @@ int smb2_handle_negotiate(struct ksmbd_work *work) rsp->SecurityMode = SMB2_NEGOTIATE_SIGNING_ENABLED_LE; conn->use_spnego = true; - if ((server_conf.signing == KSMBD_CONFIG_OPT_AUTO || - server_conf.signing == KSMBD_CONFIG_OPT_DISABLED) && - req->SecurityMode & SMB2_NEGOTIATE_SIGNING_REQUIRED_LE) + if (req->SecurityMode & SMB2_NEGOTIATE_SIGNING_REQUIRED_LE) conn->sign = true; - else if (server_conf.signing == KSMBD_CONFIG_OPT_MANDATORY) { + if (server_conf.signing == KSMBD_CONFIG_OPT_MANDATORY) { server_conf.enforced_signing = true; rsp->SecurityMode |= SMB2_NEGOTIATE_SIGNING_REQUIRED_LE; conn->sign = true; @@ -1266,8 +1999,7 @@ int smb2_handle_negotiate(struct ksmbd_work *work) ksmbd_conn_set_need_setup(conn); err_out: - ksmbd_conn_unlock(conn); - if (rc) + if (rc && rsp->hdr.Status == STATUS_SUCCESS) rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; if (!rc) @@ -1459,9 +2191,11 @@ static int ntlm_authenticate(struct ksmbd_work *work, { struct ksmbd_conn *conn = work->conn; struct ksmbd_session *sess = work->sess; - struct channel *chann = NULL, *old; 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; ksmbd_debug(SMB, "authenticate phase\n"); @@ -1486,11 +2220,6 @@ static int ntlm_authenticate(struct ksmbd_work *work, return -EPERM; } - /* Check for previous session */ - prev_id = le64_to_cpu(req->PreviousSessionId); - if (prev_id && prev_id != sess->id) - destroy_previous_session(conn, user, prev_id); - if (sess->state == SMB2_SESSION_VALID) { /* * Reuse session if anonymous try to connect @@ -1503,7 +2232,7 @@ static int ntlm_authenticate(struct ksmbd_work *work, if (!ksmbd_compare_user(sess->user, user)) { ksmbd_free_user(user); - return -EPERM; + return -EKEYREJECTED; } ksmbd_free_user(user); } else { @@ -1520,14 +2249,20 @@ static int ntlm_authenticate(struct ksmbd_work *work, sz = conn->mechTokenLen; else sz = le16_to_cpu(req->SecurityBufferLength); - rc = ksmbd_decode_ntlmssp_auth_blob(authblob, sz, conn, sess); + rc = ksmbd_decode_ntlmssp_auth_blob(authblob, sz, conn, sess, + auth_key); if (rc) { set_user_flag(sess->user, KSMBD_USER_FLAG_BAD_PASSWORD); ksmbd_debug(SMB, "authentication failed\n"); - return -EPERM; + rc = -EPERM; + goto out; } } + prev_id = le64_to_cpu(req->PreviousSessionId); + if (prev_id && prev_id != sess->id) + destroy_previous_session(conn, sess->user, prev_id); + /* * If session state is SMB2_SESSION_VALID, We can assume * that it is reauthentication. And the user/password @@ -1559,37 +2294,30 @@ static int ntlm_authenticate(struct ksmbd_work *work, binding_session: if (conn->dialect >= SMB30_PROT_ID) { - chann = lookup_chann_list(sess, conn); - if (!chann) { - chann = kmalloc_obj(struct channel, KSMBD_DEFAULT_GFP); - if (!chann) - return -ENOMEM; - - chann->conn = conn; - down_write(&sess->chann_lock); - old = xa_store(&sess->ksmbd_chann_list, (long)conn, chann, - KSMBD_DEFAULT_GFP); - up_write(&sess->chann_lock); - if (xa_is_err(old)) { - kfree(chann); - return xa_err(old); - } - } + rc = register_session_channel(sess, conn, auth_key); + if (rc) + goto out; } if (conn->ops->generate_signingkey) { rc = conn->ops->generate_signingkey(sess, conn); if (rc) { ksmbd_debug(SMB, "SMB3 signing key generation failed\n"); - return -EINVAL; + rc = -EINVAL; + goto out; } } if (!ksmbd_conn_lookup_dialect(conn)) { pr_err("fail to verify the dialect\n"); - return -ENOENT; + rc = -ENOENT; + goto out; } - return 0; + rc = 0; +out: + if (binding) + memzero_explicit(channel_key, sizeof(channel_key)); + return rc; } #ifdef CONFIG_SMB_SERVER_KERBEROS5 @@ -1600,8 +2328,12 @@ static int krb5_authenticate(struct ksmbd_work *work, struct ksmbd_conn *conn = work->conn; struct ksmbd_session *sess = work->sess; char *in_blob, *out_blob; - struct channel *chann = NULL, *old; + char channel_key[CIFS_KEY_SIZE] = {}; + 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; @@ -1610,14 +2342,16 @@ static int krb5_authenticate(struct ksmbd_work *work, in_len = le16_to_cpu(req->SecurityBufferLength); out_blob = (char *)&rsp->hdr.ProtocolId + le16_to_cpu(rsp->SecurityBufferOffset); - out_len = work->response_sz - + out_len = work->response_sz - work->next_smb2_rsp_hdr_off - (le16_to_cpu(rsp->SecurityBufferOffset) + 4); retval = ksmbd_krb5_authenticate(sess, in_blob, in_len, - out_blob, &out_len); + out_blob, &out_len, auth_key); if (retval) { ksmbd_debug(SMB, "krb5 authentication failed\n"); - return -EINVAL; + if (retval != -EKEYREJECTED) + retval = -EPERM; + goto out; } /* Check previous session */ @@ -1632,12 +2366,21 @@ static int krb5_authenticate(struct ksmbd_work *work, * that it is reauthentication. And the user/password * has been verified, so return it here. */ - if (sess->state == SMB2_SESSION_VALID) { + if (sess->state == SMB2_SESSION_VALID && !work->session_setup_reauth) { if (conn->binding) goto binding_session; return 0; } + /* + * Reauthentication verifies the new Kerberos credentials but keeps + * the established SMB session keys. + */ + if (work->session_setup_reauth) { + retval = 0; + goto out; + } + if ((rsp->SessionFlags != SMB2_SESSION_FLAG_IS_GUEST_LE && (conn->sign || server_conf.enforced_signing)) || (req->SecurityMode & SMB2_NEGOTIATE_SIGNING_REQUIRED)) @@ -1654,37 +2397,31 @@ static int krb5_authenticate(struct ksmbd_work *work, binding_session: if (conn->dialect >= SMB30_PROT_ID) { - chann = lookup_chann_list(sess, conn); - if (!chann) { - chann = kmalloc_obj(struct channel, KSMBD_DEFAULT_GFP); - if (!chann) - return -ENOMEM; - - chann->conn = conn; - down_write(&sess->chann_lock); - old = xa_store(&sess->ksmbd_chann_list, (long)conn, - chann, KSMBD_DEFAULT_GFP); - up_write(&sess->chann_lock); - if (xa_is_err(old)) { - kfree(chann); - return xa_err(old); - } - } + retval = register_session_channel(sess, conn, auth_key); + if (retval) + goto out; } if (conn->ops->generate_signingkey) { retval = conn->ops->generate_signingkey(sess, conn); if (retval) { ksmbd_debug(SMB, "SMB3 signing key generation failed\n"); - return -EINVAL; + retval = -EINVAL; + goto out; } } if (!ksmbd_conn_lookup_dialect(conn)) { pr_err("fail to verify the dialect\n"); - return -ENOENT; + retval = -ENOENT; + goto out; } - return 0; + retval = 0; +out: + memzero_explicit(reauth_key, sizeof(reauth_key)); + if (binding) + memzero_explicit(channel_key, sizeof(channel_key)); + return retval; } #else static int krb5_authenticate(struct ksmbd_work *work, @@ -1700,7 +2437,7 @@ int smb2_sess_setup(struct ksmbd_work *work) struct ksmbd_conn *conn = work->conn; struct smb2_sess_setup_req *req; struct smb2_sess_setup_rsp *rsp; - struct ksmbd_session *sess; + struct ksmbd_session *sess = NULL; struct negotiate_message *negblob; unsigned int negblob_len, negblob_off; int rc = 0; @@ -1748,12 +2485,27 @@ int smb2_sess_setup(struct ksmbd_work *work) goto out_err; } + if (conn->dialect == SMB311_PROT_ID) { + struct channel *chann; + unsigned long index; + + down_read(&sess->chann_lock); + xa_for_each(&sess->ksmbd_chann_list, index, chann) { + if (conn->cipher_type != chann->conn->cipher_type) + rc = -EINVAL; + break; + } + up_read(&sess->chann_lock); + if (rc) + goto out_err; + } + if (!(req->hdr.Flags & SMB2_FLAGS_SIGNED)) { rc = -EINVAL; goto out_err; } - if (strncmp(conn->ClientGUID, sess->ClientGUID, + if (memcmp(conn->ClientGUID, sess->ClientGUID, SMB2_CLIENT_GUID_SIZE)) { rc = -ENOENT; goto out_err; @@ -1790,20 +2542,48 @@ int smb2_sess_setup(struct ksmbd_work *work) } else if ((conn->dialect < SMB30_PROT_ID || server_conf.flags & KSMBD_GLOBAL_FLAG_SMB3_MULTICHANNEL) && (req->Flags & SMB2_SESSION_REQ_FLAG_BINDING)) { - sess = NULL; + sess = ksmbd_session_lookup_slowpath(le64_to_cpu(req->hdr.SessionId)); + if (sess) { + int sign_ret; + + work->sess = sess; + if (sess->dialect >= SMB30_PROT_ID) + sign_ret = smb3_check_sign_req(work); + else + sign_ret = smb2_check_sign_req(work); + if (sess->state != SMB2_SESSION_VALID || + !(req->hdr.Flags & SMB2_FLAGS_SIGNED) || + !sign_ret) { + ksmbd_user_session_put(sess); + work->sess = NULL; + sess = NULL; + } + } rc = -EACCES; goto out_err; } else { sess = ksmbd_session_lookup(conn, le64_to_cpu(req->hdr.SessionId)); if (!sess) { + sess = ksmbd_session_lookup_slowpath(le64_to_cpu(req->hdr.SessionId)); + if (sess && !lookup_chann_list(sess, conn)) { + ksmbd_user_session_put(sess); + sess = NULL; + } + } + if (!sess) { rc = -ENOENT; goto out_err; } if (sess->state == SMB2_SESSION_EXPIRED) { - rc = -EFAULT; - goto out_err; + if (sess->kerberos_expiry && + ktime_get_real_seconds() >= sess->kerberos_expiry) { + work->session_setup_reauth = true; + } else { + rc = -EFAULT; + goto out_err; + } } if (ksmbd_conn_need_reconnect(conn)) { @@ -1813,6 +2593,9 @@ int smb2_sess_setup(struct ksmbd_work *work) goto out_err; } + if (work->session_setup_reauth) + WRITE_ONCE(sess->state, SMB2_SESSION_IN_PROGRESS); + conn->binding = false; } work->sess = sess; @@ -1847,10 +2630,8 @@ 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); - if (rc) { - rc = -EINVAL; + if (rc) goto out_err; - } if (!ksmbd_conn_need_reconnect(conn)) { ksmbd_conn_set_good(conn); @@ -1879,7 +2660,7 @@ int smb2_sess_setup(struct ksmbd_work *work) ksmbd_preauth_session_lookup(conn, sess->id); if (preauth_sess) { list_del(&preauth_sess->preauth_entry); - kfree(preauth_sess); + kfree_sensitive(preauth_sess); } } } else { @@ -1906,19 +2687,45 @@ out_err: rsp->hdr.Status = STATUS_REQUEST_NOT_ACCEPTED; else if (rc == -EFAULT) rsp->hdr.Status = STATUS_NETWORK_SESSION_EXPIRED; - else if (rc == -ENOMEM) + else if (rc == -ENOMEM || rc == -ENOSPC) rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; else if (rc == -EOPNOTSUPP) rsp->hdr.Status = STATUS_NOT_SUPPORTED; + else if (rc == -EKEYREJECTED) + rsp->hdr.Status = STATUS_ACCESS_DENIED; else if (rc) rsp->hdr.Status = STATUS_LOGON_FAILURE; + if ((rsp->hdr.Status == STATUS_USER_SESSION_DELETED || + (rsp->hdr.Status == STATUS_INVALID_PARAMETER && + (req->Flags & SMB2_SESSION_REQ_FLAG_BINDING))) && + (req->hdr.Flags & SMB2_FLAGS_SIGNED)) + rsp->hdr.Flags |= SMB2_FLAGS_SIGNED; - if (conn->use_spnego && conn->mechToken) { + if (conn->mechToken) { kfree(conn->mechToken); conn->mechToken = NULL; } if (rc < 0) { + bool setup_in_progress = sess && + READ_ONCE(sess->state) == SMB2_SESSION_IN_PROGRESS && + !(req->Flags & SMB2_SESSION_REQ_FLAG_BINDING); + + /* Authentication errors must not leave the new session published. */ + if (setup_in_progress) + ksmbd_session_unregister(conn, sess); + + if (sess && conn->dialect == SMB311_PROT_ID && + (req->Flags & SMB2_SESSION_REQ_FLAG_BINDING)) { + struct preauth_session *preauth_sess; + + preauth_sess = ksmbd_preauth_session_lookup(conn, sess->id); + if (preauth_sess) { + list_del(&preauth_sess->preauth_entry); + kfree_sensitive(preauth_sess); + } + } + /* * SecurityBufferOffset should be set to zero * in session setup error response. @@ -1937,10 +2744,26 @@ out_err: if (sess->user && sess->user->flags & KSMBD_USER_FLAG_DELAY_SESSION) try_delay = true; - sess->last_active = jiffies; - sess->state = SMB2_SESSION_EXPIRED; - ksmbd_user_session_put(sess); - work->sess = NULL; + /* + * For binding requests, session belongs to another + * connection. Do not expire it. + */ + if (!(req->Flags & SMB2_SESSION_REQ_FLAG_BINDING) && + !setup_in_progress) { + sess->last_active = jiffies; + sess->kerberos_expiry = 0; + sess->state = SMB2_SESSION_EXPIRED; + } + /* + * Keep the binding session reference until the response is + * signed and sent. Error responses for a signed binding + * request are signed with the existing session signing key. + */ + if (!(req->Flags & SMB2_SESSION_REQ_FLAG_BINDING) || + work->sess != sess) { + ksmbd_user_session_put(sess); + work->sess = NULL; + } if (try_delay) { ksmbd_conn_set_need_reconnect(conn); ssleep(5); @@ -1948,6 +2771,7 @@ out_err: } } smb2_set_err_rsp(work); + conn->binding = false; } else { unsigned int iov_len; @@ -1979,6 +2803,7 @@ int smb2_tree_connect(struct ksmbd_work *work) struct ksmbd_session *sess = work->sess; char *treename = NULL, *name = NULL; struct ksmbd_tree_conn_status status; + struct ksmbd_tree_connect *tree_conn = NULL; struct ksmbd_share_config *share = NULL; int rc = -EINVAL; @@ -2005,12 +2830,23 @@ int smb2_tree_connect(struct ksmbd_work *work) name, treename); status = ksmbd_tree_conn_connect(work, name); - if (status.ret == KSMBD_TREE_CONN_STATUS_OK) + if (status.ret == KSMBD_TREE_CONN_STATUS_OK) { + tree_conn = status.tree_conn; rsp->hdr.Id.SyncId.TreeId = cpu_to_le32(status.tree_conn->id); - else + share = status.tree_conn->share_conf; + + /* A share that requires encryption needs a negotiated SMB3 cipher. */ + if (test_share_config_flag(share, KSMBD_SHARE_FLAG_ENCRYPT_DATA) && + !smb3_encryption_negotiated(conn)) { + ksmbd_tree_conn_disconnect(sess, status.tree_conn); + status.tree_conn = NULL; + share = NULL; + status.ret = KSMBD_TREE_CONN_STATUS_ERROR; + goto out_err1; + } + } else goto out_err1; - share = status.tree_conn->share_conf; if (test_share_config_flag(share, KSMBD_SHARE_FLAG_PIPE)) { ksmbd_debug(SMB, "IPC share path request\n"); rsp->ShareType = SMB2_SHARE_TYPE_PIPE; @@ -2039,23 +2875,46 @@ int smb2_tree_connect(struct ksmbd_work *work) status.tree_conn->posix_extensions = true; down_write(&sess->tree_conns_lock); - status.tree_conn->t_state = TREE_CONNECTED; + if (status.tree_conn->t_state == TREE_DISCONNECTED) { + status.ret = KSMBD_TREE_CONN_STATUS_ERROR; + share = NULL; + } else { + status.tree_conn->t_state = TREE_CONNECTED; + } up_write(&sess->tree_conns_lock); + if (status.ret != KSMBD_TREE_CONN_STATUS_OK) + goto out_err1; rsp->StructureSize = cpu_to_le16(16); out_err1: - if (server_conf.flags & KSMBD_GLOBAL_FLAG_DURABLE_HANDLE && share && - test_share_config_flag(share, - KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY)) - rsp->Capabilities = SMB2_SHARE_CAP_CONTINUOUS_AVAILABILITY; - else - rsp->Capabilities = 0; + /* + * A configured CA share is not continuously available until persistent + * open recovery, ownership fencing, and failover are implemented. + */ + rsp->Capabilities = 0; rsp->Reserved = 0; /* default manual caching */ rsp->ShareFlags = SMB2_SHAREFLAG_MANUAL_CACHING; + /* Tell the client that READ requests may request compressed responses. */ + if (conn->dialect == SMB311_PROT_ID && + conn->compress_algorithm != SMB3_COMPRESS_NONE) + rsp->ShareFlags |= cpu_to_le32(SMB2_SHAREFLAG_COMPRESS_DATA); + if (share && test_share_config_flag(share, + KSMBD_SHARE_FLAG_HIDE_UNREADABLE)) + rsp->ShareFlags |= + cpu_to_le32(SMB2_SHAREFLAG_ACCESS_BASED_DIRECTORY_ENUM); + if (share && test_share_config_flag(share, + KSMBD_SHARE_FLAG_ENCRYPT_DATA)) + rsp->ShareFlags |= + cpu_to_le32(SMB2_SHAREFLAG_ENCRYPT_DATA); rc = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_tree_connect_rsp)); - if (rc) + if (rc) { + if (status.ret == KSMBD_TREE_CONN_STATUS_OK) { + ksmbd_tree_conn_disconnect(sess, status.tree_conn); + status.tree_conn = NULL; + } status.ret = KSMBD_TREE_CONN_STATUS_NOMEM; + } if (!IS_ERR(treename)) kfree(treename); @@ -2091,6 +2950,9 @@ out_err1: if (status.ret != KSMBD_TREE_CONN_STATUS_OK) smb2_set_err_rsp(work); + if (tree_conn) + ksmbd_tree_connect_put(tree_conn); + return rc; } @@ -2194,17 +3056,6 @@ int smb2_tree_disconnect(struct ksmbd_work *work) ksmbd_close_tree_conn_fds(work); - down_write(&sess->tree_conns_lock); - if (tcon->t_state == TREE_DISCONNECTED) { - up_write(&sess->tree_conns_lock); - rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED; - err = -ENOENT; - goto err_out; - } - - tcon->t_state = TREE_DISCONNECTED; - up_write(&sess->tree_conns_lock); - err = ksmbd_tree_conn_disconnect(sess, tcon); if (err) { rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED; @@ -2239,7 +3090,6 @@ int smb2_session_logoff(struct ksmbd_work *work) struct ksmbd_session *sess = work->sess; struct smb2_logoff_req *req; struct smb2_logoff_rsp *rsp; - u64 sess_id; int err; WORK_BUFFERS(work, req, rsp); @@ -2253,25 +3103,52 @@ int smb2_session_logoff(struct ksmbd_work *work) smb2_set_err_rsp(work); return -ENOENT; } - sess_id = le64_to_cpu(req->hdr.SessionId); - ksmbd_all_conn_set_status(sess_id, KSMBD_SESS_NEED_RECONNECT); + + down_write(&sess->chann_lock); + if (sess->tearing_down) { + up_write(&sess->chann_lock); + ksmbd_conn_unlock(conn); + rsp->hdr.Status = STATUS_USER_SESSION_DELETED; + smb2_set_err_rsp(work); + return -ENOENT; + } + sess->tearing_down = true; + up_write(&sess->chann_lock); + + ksmbd_all_conn_set_status(sess, KSMBD_SESS_NEED_RECONNECT); ksmbd_conn_unlock(conn); + err = ksmbd_conn_wait_idle_sess(conn, sess); + if (err) { + down_write(&sess->chann_lock); + sess->tearing_down = false; + up_write(&sess->chann_lock); + ksmbd_all_conn_set_status(sess, KSMBD_SESS_GOOD); + rsp->hdr.Status = STATUS_UNEXPECTED_IO_ERROR; + smb2_set_err_rsp(work); + return err; + } + ksmbd_close_session_fds(work); - ksmbd_conn_wait_idle(conn); if (ksmbd_tree_conn_session_logoff(sess)) { ksmbd_debug(SMB, "Invalid tid %d\n", req->hdr.Id.SyncId.TreeId); rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED; smb2_set_err_rsp(work); - return -ENOENT; + err = -ENOENT; + } else { + err = 0; } down_write(&conn->session_lock); + sess->kerberos_expiry = 0; sess->state = SMB2_SESSION_EXPIRED; up_write(&conn->session_lock); - ksmbd_all_conn_set_status(sess_id, KSMBD_SESS_NEED_SETUP); + ksmbd_all_conn_set_status(sess, KSMBD_SESS_NEED_SETUP); + + if (err) + return err; rsp->StructureSize = cpu_to_le16(4); err = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_logoff_rsp)); @@ -2309,7 +3186,16 @@ static noinline int create_smb2_pipe(struct ksmbd_work *work) id = ksmbd_session_rpc_open(work->sess, name); if (id < 0) { - pr_err("Unable to open RPC pipe: %d\n", id); + /* + * mdssvc (Spotlight) is a routine, expected probe from macOS + * that we deliberately don't support -- it's disabled at the + * __rpc_method() level (mgmt/user_session.c), but this + * generic failure log would otherwise still fire on every + * single probe regardless. + */ + if (!(id == -ENOENT && (!strcmp(name, "\\mdssvc") || + !strcmp(name, "mdssvc")))) + pr_err("Unable to open RPC pipe: %d\n", id); err = id; goto out; } @@ -2344,6 +3230,9 @@ out: case -EINVAL: rsp->hdr.Status = STATUS_INVALID_PARAMETER; break; + case -ENOENT: + rsp->hdr.Status = STATUS_OBJECT_NAME_NOT_FOUND; + break; case -ENOSPC: case -ENOMEM: rsp->hdr.Status = STATUS_NO_MEMORY; @@ -2360,6 +3249,22 @@ out: return err; } +static bool smb2_is_private_ea(const char *name, size_t name_len) +{ + if (name_len == SD_PREFIX_LEN && + !strncasecmp(name, SD_PREFIX, SD_PREFIX_LEN)) + return true; + if (name_len == DOS_ATTRIBUTE_PREFIX_LEN && + !strncasecmp(name, DOS_ATTRIBUTE_PREFIX, + DOS_ATTRIBUTE_PREFIX_LEN)) + return true; + if (name_len >= STREAM_PREFIX_LEN && + !strncasecmp(name, STREAM_PREFIX, STREAM_PREFIX_LEN)) + return true; + + return false; +} + /** * smb2_set_ea() - handler for setting extended attributes using set * info command @@ -2401,6 +3306,10 @@ static int smb2_set_ea(struct smb2_ea_info *eabuf, unsigned int buf_len, rc = -EINVAL; break; } + if (smb2_is_private_ea(eabuf->name, eabuf->EaNameLength)) { + rc = -EACCES; + break; + } memcpy(attr_name, XATTR_USER_PREFIX, XATTR_USER_PREFIX_LEN); memcpy(&attr_name[XATTR_USER_PREFIX_LEN], eabuf->name, @@ -2494,6 +3403,30 @@ static noinline int smb2_set_stream_name_xattr(const struct path *path, return 0; if (fp->cdoption == FILE_OPEN_LE) { + if (!strcmp(stream_name, "AFP_AfpInfo") && + test_share_config_flag(fp->tcon->share_conf, + KSMBD_SHARE_FLAG_TIME_MACHINE)) { + /* + * Synthesize an empty AFP_AfpInfo xattr on first access. + * type=0/creator=0 tells macOS to use the file extension + * for icon and type detection. + * + * Scoped to TIME_MACHINE shares, matching the rest of + * the AAPL series -- conn->is_aapl alone isn't a safe + * gate here, since the pre-existing narrow UniqueId=0 + * path can also set it on ordinary, non-Time-Machine + * shares whenever a Mac client happens to negotiate + * AAPL there too. + */ + static const u8 afpinfo_empty[60] = { + 0x00, 0x05, 0x16, 0x07, /* magic 0x00051607 BE */ + 0x00, 0x02, 0x00, 0x00, /* version 0x00020000 BE */ + }; + rc = ksmbd_vfs_setxattr(idmap, path, xattr_stream_name, + (void *)afpinfo_empty, + sizeof(afpinfo_empty), 0, false); + return rc < 0 ? rc : 0; + } ksmbd_debug(SMB, "XATTR stream name lookup failed: %d\n", rc); return -EBADF; } @@ -2504,6 +3437,22 @@ static noinline int smb2_set_stream_name_xattr(const struct path *path, return 0; } +/* + * fp->stream.size is the byte length of the mangled xattr *name* + * (used as attr_name_len when looking the xattr up), not the size of + * the xattr's value. Reporting it as EndOfFile/AllocationSize for a + * stream handle is wrong -- query the xattr's actual value length + * instead. + */ +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); + return slen < 0 ? 0 : (loff_t)slen; +} + static int smb2_remove_smb_xattrs(const struct path *path) { struct mnt_idmap *idmap = mnt_idmap(path->mnt); @@ -2578,25 +3527,57 @@ static void smb2_new_xattrs(struct ksmbd_tree_connect *tcon, const struct path * ksmbd_debug(SMB, "failed to store file attribute into xattr\n"); } +static bool smb2_parent_compressed(struct ksmbd_tree_connect *tcon, + const struct path *path) +{ + struct dentry *parent = dget_parent(path->dentry); + struct file_kattr fa = { .flags_valid = true }; + struct xattr_dos_attrib da; + bool compressed = false; + int rc; + + rc = vfs_fileattr_get(parent, &fa); + if (!rc && fa.flags & FS_COMPR_FL) { + compressed = true; + goto out; + } + + rc = ksmbd_vfs_get_dos_attrib_xattr(mnt_idmap(path->mnt), parent, &da); + if (rc > 0 && da.attr & FILE_ATTRIBUTE_COMPRESSED) + compressed = true; + +out: + dput(parent); + return compressed; +} + static void smb2_update_xattrs(struct ksmbd_tree_connect *tcon, const struct path *path, struct ksmbd_file *fp) { - struct xattr_dos_attrib da; + struct xattr_dos_attrib da = {}; + bool store_dos_attrs = test_share_config_flag(tcon->share_conf, + KSMBD_SHARE_FLAG_STORE_DOS_ATTRS); int rc; fp->f_ci->m_fattr &= ~(FILE_ATTRIBUTE_HIDDEN_LE | FILE_ATTRIBUTE_SYSTEM_LE); /* get FileAttributes from XATTR_NAME_DOS_ATTRIBUTE */ - if (!test_share_config_flag(tcon->share_conf, - KSMBD_SHARE_FLAG_STORE_DOS_ATTRS)) - return; - rc = ksmbd_vfs_get_dos_attrib_xattr(mnt_idmap(path->mnt), path->dentry, &da); if (rc > 0) { - fp->f_ci->m_fattr = cpu_to_le32(da.attr); - fp->create_time = da.create_time; - fp->itime = da.itime; + if (store_dos_attrs) { + fp->f_ci->m_fattr = cpu_to_le32(da.attr); + fp->create_time = da.create_time; + fp->itime = da.itime; + } else { + fp->f_ci->m_fattr &= + ~(FILE_ATTRIBUTE_COMPRESSED_LE | + FILE_ATTRIBUTE_SPARSE_FILE_LE); + fp->f_ci->m_fattr |= + cpu_to_le32(da.attr & + (FILE_ATTRIBUTE_COMPRESSED | + FILE_ATTRIBUTE_SPARSE_FILE)); + } } } @@ -2698,10 +3679,89 @@ struct durable_info { unsigned short int type; bool persistent; bool reconnected; + bool replay; + bool replay_consumed; + bool app_instance_id; + bool app_instance_version_valid; unsigned int timeout; char *CreateGuid; + char AppInstanceId[SMB2_CREATE_GUID_SIZE]; + u64 app_instance_version_high; + u64 app_instance_version_low; }; +static int smb2_check_durable_replay(struct ksmbd_work *work, + struct ksmbd_file *fp, + struct lease_ctx_info *lc, + bool persistent) +{ + struct oplock_info *opinfo; + int ret = 0; + + if (!fp->is_durable && !fp->is_persistent) + return -EACCES; + + if (ksmbd_vfs_compare_durable_owner(fp, work->sess->user) == false) + return -EACCES; + + if (fp->is_persistent && !persistent) + return -EINVAL; + + opinfo = opinfo_get(fp); + 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, + SMB2_LEASE_KEY_SIZE)) { + ret = -EACCES; + goto out; + } + } else { + if (lc) { + ret = -EACCES; + goto out; + } + + if (fp->is_durable && opinfo->level != SMB2_OPLOCK_LEVEL_BATCH) + ret = -EACCES; + } +out: + opinfo_put(opinfo); + return ret; +} + +static bool smb2_durable_replay_consumed(struct ksmbd_file *fp) +{ + bool consumed; + + spin_lock(&fp->f_lock); + consumed = fp->durable_replay_consumed; + spin_unlock(&fp->f_lock); + + return consumed; +} + +static void smb2_mark_durable_replay_consumed(struct ksmbd_file *fp) +{ + spin_lock(&fp->f_lock); + fp->durable_replay_consumed = true; + spin_unlock(&fp->f_lock); +} + +static bool smb2_durable_replay_differs(struct ksmbd_file *fp, + struct smb2_create_req *req) +{ + return fp->cdoption != req->CreateDisposition || + fp->create_file_attributes != req->FileAttributes; +} + static int parse_durable_handle_context(struct ksmbd_work *work, struct smb2_create_req *req, struct lease_ctx_info *lc, @@ -2729,6 +3789,7 @@ static int parse_durable_handle_context(struct ksmbd_work *work, case DURABLE_RECONN_V2: { struct create_durable_handle_reconnect_v2 *recon_v2; + u32 flags; if (dh_info->type == DURABLE_RECONN || dh_info->type == DURABLE_REQ_V2) { @@ -2736,14 +3797,19 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } - if (le16_to_cpu(context->DataOffset) + - le32_to_cpu(context->DataLength) < - sizeof(struct create_durable_handle_reconnect_v2)) { + if (le32_to_cpu(context->DataLength) < + sizeof(recon_v2->dcontext)) { err = -EINVAL; goto out; } recon_v2 = (struct create_durable_handle_reconnect_v2 *)context; + flags = le32_to_cpu(recon_v2->dcontext.Flags); + if (flags & ~SMB2_DHANDLE_FLAG_PERSISTENT) { + err = -EINVAL; + goto out; + } + dh_info->persistent = flags & SMB2_DHANDLE_FLAG_PERSISTENT; persistent_id = recon_v2->dcontext.Fid.PersistentFileId; dh_info->fp = ksmbd_lookup_durable_fd(persistent_id); if (!dh_info->fp) { @@ -2752,6 +3818,15 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } + /* A zero VolatileFileId means that the client did not specify it. */ + if (recon_v2->dcontext.Fid.VolatileFileId && + dh_info->fp->durable_volatile_id != + recon_v2->dcontext.Fid.VolatileFileId) { + err = -EBADF; + ksmbd_put_durable_fd(dh_info->fp); + goto out; + } + if (memcmp(dh_info->fp->create_guid, recon_v2->dcontext.CreateGuid, SMB2_CREATE_GUID_SIZE)) { err = -EBADF; @@ -2759,6 +3834,13 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } + /* A persistent reconnect must match the original open type. */ + if (dh_info->fp->is_persistent != dh_info->persistent) { + err = dh_info->persistent ? -EINVAL : -EBADF; + ksmbd_put_durable_fd(dh_info->fp); + goto out; + } + dh_info->type = dh_idx; dh_info->reconnected = true; ksmbd_debug(SMB, @@ -2776,9 +3858,8 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } - if (le16_to_cpu(context->DataOffset) + - le32_to_cpu(context->DataLength) < - sizeof(create_durable_reconn_t)) { + if (le32_to_cpu(context->DataLength) < + sizeof(recon->Data)) { err = -EINVAL; goto out; } @@ -2792,6 +3873,15 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } + /* A zero VolatileFileId means that the client did not specify it. */ + if (recon->Data.Fid.VolatileFileId && + dh_info->fp->durable_volatile_id != + recon->Data.Fid.VolatileFileId) { + err = -EBADF; + ksmbd_put_durable_fd(dh_info->fp); + goto out; + } + dh_info->type = dh_idx; dh_info->reconnected = true; ksmbd_debug(SMB, "reconnect Persistent-id from reconnect = %llu\n", @@ -2808,16 +3898,24 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } - if (le16_to_cpu(context->DataOffset) + - le32_to_cpu(context->DataLength) < - sizeof(struct create_durable_req_v2)) { + if (le32_to_cpu(context->DataLength) < + sizeof(durable_v2_blob->dcontext)) { err = -EINVAL; goto out; } durable_v2_blob = (struct create_durable_req_v2 *)context; + if (le32_to_cpu(durable_v2_blob->dcontext.Flags) & + ~SMB2_DHANDLE_FLAG_PERSISTENT) { + err = -EINVAL; + goto out; + } ksmbd_debug(SMB, "Request for durable v2 open\n"); + dh_info->CreateGuid = durable_v2_blob->dcontext.CreateGuid; + dh_info->persistent = + le32_to_cpu(durable_v2_blob->dcontext.Flags) & + SMB2_DHANDLE_FLAG_PERSISTENT; dh_info->fp = ksmbd_lookup_fd_cguid(durable_v2_blob->dcontext.CreateGuid); if (dh_info->fp) { if (!memcmp(conn->ClientGUID, dh_info->fp->client_guid, @@ -2828,18 +3926,81 @@ static int parse_durable_handle_context(struct ksmbd_work *work, goto out; } - dh_info->fp->conn = conn; - dh_info->reconnected = true; + if (dh_info->fp->f_state == FP_NEW) { + /* Original CREATE is still pending. */ + ksmbd_put_durable_fd(dh_info->fp); + err = -EAGAIN; + 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; + } + ksmbd_put_durable_fd(dh_info->fp); + err = -EACCES; + goto out; + } + + if (dh_info->fp->conn && + 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; + } + + 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)) + smb2_mark_durable_replay_consumed( + dh_info->fp); + dh_info->replay = true; + } else { + dh_info->reconnected = true; + } + dh_info->type = dh_idx; goto out; } + ksmbd_put_durable_fd(dh_info->fp); + dh_info->fp = NULL; } if ((lc && (lc->req_state & SMB2_LEASE_HANDLE_CACHING_LE)) || req_op_level == SMB2_OPLOCK_LEVEL_BATCH) { - dh_info->CreateGuid = - durable_v2_blob->dcontext.CreateGuid; - dh_info->persistent = - le32_to_cpu(durable_v2_blob->dcontext.Flags); dh_info->timeout = le32_to_cpu(durable_v2_blob->dcontext.Timeout); dh_info->type = dh_idx; @@ -2867,6 +4028,93 @@ out: return err; } +static int parse_app_instance_id(struct smb2_create_req *req, + struct durable_info *dh_info) +{ + struct create_context *context; + char *data; + + context = smb2_find_context_vals(req, SMB2_CREATE_APP_INSTANCE_ID, + SMB2_CREATE_GUID_SIZE); + if (IS_ERR(context)) + return PTR_ERR(context); + if (!context) + return 0; + + if (le32_to_cpu(context->DataLength) < 20) + return -EINVAL; + + data = (char *)context + le16_to_cpu(context->DataOffset); + if (data[0] != 20 || data[1]) + return -EINVAL; + + memcpy(dh_info->AppInstanceId, data + 4, SMB2_CREATE_GUID_SIZE); + dh_info->app_instance_id = true; + return 0; +} + +static int parse_app_instance_version(struct smb2_create_req *req, + struct durable_info *dh_info) +{ + struct create_context *context; + char *data; + + context = smb2_find_context_vals(req, SMB2_CREATE_APP_INSTANCE_VERSION, + SMB2_CREATE_GUID_SIZE); + if (IS_ERR(context)) + return PTR_ERR(context); + if (!context) + return 0; + + if (le32_to_cpu(context->DataLength) < 24) + return -EINVAL; + + data = (char *)context + le16_to_cpu(context->DataOffset); + if (get_unaligned_le16(data) != 24 || + get_unaligned_le16(data + 2) != 0) + return -EINVAL; + + dh_info->app_instance_version_high = get_unaligned_le64(data + 8); + dh_info->app_instance_version_low = get_unaligned_le64(data + 16); + dh_info->app_instance_version_valid = true; + return 0; +} + +static int smb2_handle_app_instance_id(struct smb2_create_rsp *rsp, + struct durable_info *dh_info) +{ + struct ksmbd_file *old_fp; + bool reject = false; + + if (!dh_info->app_instance_id) + return 0; + + old_fp = ksmbd_lookup_fd_app_instance_id(dh_info->AppInstanceId); + if (!old_fp) + return 0; + + if (dh_info->app_instance_version_valid) { + if (old_fp->app_instance_version_valid && + (dh_info->app_instance_version_high < + old_fp->app_instance_version_high || + (dh_info->app_instance_version_high == + old_fp->app_instance_version_high && + dh_info->app_instance_version_low <= + old_fp->app_instance_version_low))) + reject = true; + } else if (old_fp->app_instance_version_valid) { + reject = true; + } + + ksmbd_put_durable_fd(old_fp); + if (reject) { + rsp->hdr.Status = STATUS_FILE_FORCED_CLOSED; + return -EIO; + } + + return ksmbd_close_fd_app_instance_id(dh_info->AppInstanceId); +} + /** * smb2_open() - handler for smb file open request * @work: smb work containing request buffer @@ -2896,15 +4144,19 @@ int smb2_open(struct ksmbd_work *work) int rc = 0; int contxt_cnt = 0, query_disk_id = 0; bool maximal_access_ctxt = false, posix_ctxt = false; + bool aapl_ctxt = false; + bool durable_rsp = true; + __u64 aapl_req_bitmap = 0, aapl_client_caps = 0; int s_type = 0; int next_off = 0; char *name = NULL; char *stream_name = NULL; bool file_present = false, created = false, already_permitted = false; int share_ret, need_truncate = 0; - u64 time; + u64 time, alloc_size = 0; umode_t posix_mode = 0; __le32 daccess, maximal_access = 0; + u32 dos_attr; int iov_len = 0; ksmbd_debug(SMB, "Received smb2 create request\n"); @@ -2990,46 +4242,96 @@ int smb2_open(struct ksmbd_work *work) req_op_level = req->RequestedOplockLevel; + if (req->CreateContextsOffset) { + rc = parse_app_instance_id(req, &dh_info); + if (rc) + goto err_out2; + rc = parse_app_instance_version(req, &dh_info); + if (rc) + goto err_out2; + } + if (server_conf.flags & KSMBD_GLOBAL_FLAG_DURABLE_HANDLE && req->CreateContextsOffset) { lc = parse_lease_state(req); + if (IS_ERR(lc)) { + rc = PTR_ERR(lc); + lc = NULL; + goto err_out2; + } + if (lc && lc->version == 2 && conn->dialect < SMB30_PROT_ID) { + kfree(lc); + lc = NULL; + if (req_op_level == SMB2_OPLOCK_LEVEL_LEASE) + req_op_level = SMB2_OPLOCK_LEVEL_NONE; + } rc = parse_durable_handle_context(work, req, lc, &dh_info); if (rc) { ksmbd_debug(SMB, "error parsing durable handle context\n"); goto err_out2; } - if (dh_info.reconnected == true) { - rc = smb2_check_durable_oplock(conn, share, dh_info.fp, lc, name); - if (rc) { - ksmbd_put_durable_fd(dh_info.fp); + if (dh_info.replay == true) { + fp = dh_info.fp; + if (ksmbd_override_fsids(work)) { + rc = -ENOMEM; goto err_out2; } + file_info = FILE_OPENED; + rc = ksmbd_vfs_getattr(&fp->filp->f_path, &stat); + if (rc) + goto err_out2; + + goto reconnected_fp; + } + + if (dh_info.reconnected == true) { + rc = smb2_check_durable_oplock(conn, share, dh_info.fp, + lc, sess->user, name); + if (rc) + goto err_out2; + rc = ksmbd_reopen_durable_fd(work, dh_info.fp); - if (rc) { - ksmbd_put_durable_fd(dh_info.fp); + if (rc) goto err_out2; - } + + fp = dh_info.fp; if (ksmbd_override_fsids(work)) { rc = -ENOMEM; - ksmbd_put_durable_fd(dh_info.fp); goto err_out2; } - fp = dh_info.fp; file_info = FILE_OPENED; rc = ksmbd_vfs_getattr(&fp->filp->f_path, &stat); - ksmbd_put_durable_fd(fp); if (rc) goto err_out2; goto reconnected_fp; } - } else if (req_op_level == SMB2_OPLOCK_LEVEL_LEASE) + + } else if (req_op_level == SMB2_OPLOCK_LEVEL_LEASE) { lc = parse_lease_state(req); + if (IS_ERR(lc)) { + rc = PTR_ERR(lc); + lc = NULL; + goto err_out2; + } + if (lc && lc->version == 2 && conn->dialect < SMB30_PROT_ID) { + kfree(lc); + lc = NULL; + req_op_level = SMB2_OPLOCK_LEVEL_NONE; + } + } + + 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", @@ -3047,7 +4349,7 @@ int smb2_open(struct ksmbd_work *work) } else { if (req->CreateOptions & FILE_SEQUENTIAL_ONLY_LE && req->CreateOptions & FILE_RANDOM_ACCESS_LE) - req->CreateOptions = ~(FILE_SEQUENTIAL_ONLY_LE); + req->CreateOptions &= ~FILE_SEQUENTIAL_ONLY_LE; if (req->CreateOptions & (FILE_OPEN_BY_FILE_ID_LE | CREATE_TREE_CONNECTION | @@ -3060,8 +4362,6 @@ int smb2_open(struct ksmbd_work *work) if (req->CreateOptions & FILE_NON_DIRECTORY_FILE_LE) { rc = -EINVAL; goto err_out2; - } else if (req->CreateOptions & FILE_NO_COMPRESSION_LE) { - req->CreateOptions = ~(FILE_NO_COMPRESSION_LE); } } } @@ -3081,7 +4381,15 @@ int smb2_open(struct ksmbd_work *work) goto err_out2; } - if (req->FileAttributes && !(req->FileAttributes & FILE_ATTRIBUTE_MASK_LE)) { + if (req->DesiredAccess == FILE_SYNCHRONIZE_LE && + req->CreateDisposition == FILE_OPEN_IF_LE && + !req->FileAttributes) { + rc = -EACCES; + goto err_out2; + } + + if (req->FileAttributes && + (req->FileAttributes & ~cpu_to_le32(SMB2_CREATE_FILE_ATTRIBUTE_MASK))) { pr_err("Invalid file attribute : 0x%x\n", le32_to_cpu(req->FileAttributes)); rc = -EINVAL; @@ -3139,10 +4447,29 @@ int smb2_open(struct ksmbd_work *work) rc = ksmbd_vfs_kern_path(work, name, LOOKUP_NO_SYMLINKS, &path, 1); + + /* + * 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 + * delete-on-close open for the same name arrives instead, the + * disconnected handle must give way: close it so its delete-on-close + * removes the file, then re-resolve so this open can create a fresh one. + */ + if (!rc && (req->CreateOptions & FILE_DELETE_ON_CLOSE_LE) && + (req->CreateDisposition == FILE_OVERWRITE_IF_LE || + req->CreateDisposition == FILE_OPEN_IF_LE) && + ksmbd_close_disconnected_durable_delete_on_close(path.dentry)) { + path_put(&path); + rc = ksmbd_vfs_kern_path(work, name, LOOKUP_NO_SYMLINKS, + &path, 1); + } + if (!rc) { file_present = true; if (req->CreateOptions & FILE_DELETE_ON_CLOSE_LE) { + struct xattr_dos_attrib da; + /* * If file exists with under flags, return access * denied error. @@ -3156,6 +4483,16 @@ int smb2_open(struct ksmbd_work *work) if (!test_tree_conn_flag(tcon, KSMBD_TREE_CONN_FLAG_WRITABLE)) { ksmbd_debug(SMB, "User does not have write permission\n"); + rc = -EACCES; + goto err_out; + } + + if (test_share_config_flag(tcon->share_conf, + KSMBD_SHARE_FLAG_STORE_DOS_ATTRS) && + ksmbd_vfs_get_dos_attrib_xattr(mnt_idmap(path.mnt), + path.dentry, &da) > 0 && + da.attr & FILE_ATTRIBUTE_READONLY) { + rsp->hdr.Status = STATUS_CANNOT_DELETE; rc = -EACCES; goto err_out; } @@ -3173,7 +4510,21 @@ int smb2_open(struct ksmbd_work *work) rc = 0; } - if (stream_name) { + if (!file_present && req->CreateOptions & FILE_DELETE_ON_CLOSE_LE && + req->FileAttributes & FILE_ATTRIBUTE_READONLY_LE) { + rsp->hdr.Status = STATUS_CANNOT_DELETE; + rc = -EACCES; + goto err_out; + } + + /* + * An explicit ::$DATA suffix names the unnamed data stream and is + * canonicalized to a NULL stream name (base file), but the request + * still has to be validated against the data-stream type, e.g. opening + * <dir>::$DATA with FILE_DIRECTORY_FILE must fail with + * STATUS_NOT_A_DIRECTORY. + */ + if (stream_name || s_type == DATA_STREAM) { if (req->CreateOptions & FILE_DIRECTORY_FILE_LE) { if (s_type == DATA_STREAM) { rc = -EIO; @@ -3181,7 +4532,7 @@ int smb2_open(struct ksmbd_work *work) } } else { if (file_present && S_ISDIR(d_inode(path.dentry)->i_mode) && - s_type == DATA_STREAM) { + !stream_name && s_type == DATA_STREAM) { rc = -EIO; rsp->hdr.Status = STATUS_FILE_IS_A_DIRECTORY; } @@ -3225,9 +4576,29 @@ int smb2_open(struct ksmbd_work *work) if (file_present && !(req->CreateOptions & FILE_DELETE_ON_CLOSE_LE)) { rc = smb_check_perm_dacl(conn, &path, &daccess, - sess->user->uid); + req->DesiredAccess, + sess->user->uid, false); if (rc) goto err_out; + + if (maximal_access_ctxt) { + maximal_access = FILE_MAXIMAL_ACCESS_LE; + rc = smb_check_perm_dacl(conn, &path, &maximal_access, + 0, sess->user->uid, false); + if (rc) + goto err_out; + + /* + * smb_check_perm_dacl() returns success without + * touching *pdaccess when the object has no stored + * NT ACL, leaving maximal_access as the + * FILE_MAXIMAL_ACCESS_LE request sentinel instead of + * a real access mask. + */ + if (maximal_access == FILE_MAXIMAL_ACCESS_LE) + ksmbd_vfs_query_maximal_access(idmap, path.dentry, + &maximal_access); + } } if (daccess & FILE_MAXIMAL_ACCESS_LE) { @@ -3355,7 +4726,34 @@ int smb2_open(struct ksmbd_work *work) goto err_out; } + /* + * Publish the client and create GUID before an oplock/lease break can + * make this CREATE pending. A replay of that in-flight CREATE must find + * this FP_NEW handle and fail with STATUS_FILE_NOT_AVAILABLE instead of + * waiting on the same break again. + */ + memcpy(fp->client_guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE); + if (dh_info.app_instance_id) { + memcpy(fp->app_instance_id, dh_info.AppInstanceId, + SMB2_CREATE_GUID_SIZE); + fp->has_app_instance_id = true; + } + if (dh_info.app_instance_version_valid) { + fp->app_instance_version_high = + dh_info.app_instance_version_high; + fp->app_instance_version_low = dh_info.app_instance_version_low; + fp->app_instance_version_valid = true; + } + if (dh_info.CreateGuid) { + memcpy(fp->create_guid, dh_info.CreateGuid, SMB2_CREATE_GUID_SIZE); + fp->durable_replay_consumed = dh_info.replay_consumed; + rc = ksmbd_vfs_set_durable_owner(fp, sess->user); + if (rc) + goto err_out; + } + fp->cdoption = req->CreateDisposition; + fp->create_file_attributes = req->FileAttributes; fp->daccess = daccess; fp->saccess = req->ShareAccess; fp->coption = req->CreateOptions; @@ -3371,14 +4769,16 @@ int smb2_open(struct ksmbd_work *work) if (posix_acl_rc) ksmbd_debug(SMB, "inherit posix acl failed : %d\n", posix_acl_rc); - if (test_share_config_flag(work->tcon->share_conf, - KSMBD_SHARE_FLAG_ACL_XATTR)) { - rc = smb_inherit_dacl(conn, &path, sess->user->uid, - sess->user->gid); - } + rc = smb2_create_sd_buffer(work, req, &path); + if (rc && rc != -ENOENT) + goto err_out; - if (rc) { - rc = smb2_create_sd_buffer(work, req, &path); + if (rc == -ENOENT) { + if (test_share_config_flag(work->tcon->share_conf, + KSMBD_SHARE_FLAG_ACL_XATTR)) { + rc = smb_inherit_dacl(conn, &path, sess->user->uid, + sess->user->gid); + } if (rc) { if (posix_acl_rc) ksmbd_vfs_set_init_posix_acl(idmap, @@ -3388,20 +4788,24 @@ int smb2_open(struct ksmbd_work *work) KSMBD_SHARE_FLAG_ACL_XATTR)) { struct smb_fattr fattr; struct smb_ntsd *pntsd; - int pntsd_size, ace_num = 0; + int pntsd_size; + size_t scratch_len; ksmbd_acls_fattr(&fattr, idmap, inode); - if (fattr.cf_acls) - ace_num = fattr.cf_acls->a_count; - if (fattr.cf_dacls) - ace_num += fattr.cf_dacls->a_count; - - pntsd = kmalloc(sizeof(struct smb_ntsd) + - sizeof(struct smb_sid) * 3 + - sizeof(struct smb_acl) + - sizeof(struct smb_ace) * ace_num * 2, - KSMBD_DEFAULT_GFP); + scratch_len = smb_acl_sec_desc_scratch_len(&fattr, + NULL, 0, + OWNER_SECINFO | GROUP_SECINFO | + DACL_SECINFO); + if (!scratch_len || scratch_len == SIZE_MAX) { + rc = -EFBIG; + posix_acl_release(fattr.cf_acls); + posix_acl_release(fattr.cf_dacls); + goto err_out; + } + + pntsd = kvzalloc(scratch_len, KSMBD_DEFAULT_GFP); if (!pntsd) { + rc = -ENOMEM; posix_acl_release(fattr.cf_acls); posix_acl_release(fattr.cf_dacls); goto err_out; @@ -3416,7 +4820,7 @@ int smb2_open(struct ksmbd_work *work) posix_acl_release(fattr.cf_acls); posix_acl_release(fattr.cf_dacls); if (rc) { - kfree(pntsd); + kvfree(pntsd); goto err_out; } @@ -3426,7 +4830,7 @@ int smb2_open(struct ksmbd_work *work) pntsd, pntsd_size, false); - kfree(pntsd); + kvfree(pntsd); if (rc) pr_err("failed to store ntacl in xattr : %d\n", rc); @@ -3465,6 +4869,12 @@ int smb2_open(struct ksmbd_work *work) goto err_out; } + if (!stream_name && daccess & FILE_DELETE_LE && + ksmbd_has_stream_without_delete_share(fp)) { + rc = -EPERM; + goto err_out; + } + if (file_present || created) path_put(&path); @@ -3483,6 +4893,9 @@ int smb2_open(struct ksmbd_work *work) goto err_out1; } } else { + if (created && !lc) + smb_send_parent_lease_break_noti(fp, NULL); + if (req_op_level == SMB2_OPLOCK_LEVEL_LEASE && lc) { if (S_ISDIR(file_inode(filp)->i_mode)) { lc->req_state &= ~SMB2_LEASE_WRITE_CACHING_LE; @@ -3500,7 +4913,7 @@ int smb2_open(struct ksmbd_work *work) ksmbd_debug(SMB, "lease req for(%s) req oplock state 0x%x, lease state 0x%x\n", name, req_op_level, lc->req_state); - rc = find_same_lease_key(sess, fp->f_ci, lc); + rc = find_same_lease_key(conn, fp->f_ci, lc); if (rc) goto err_out1; } else if (open_flags == O_RDONLY && @@ -3511,13 +4924,16 @@ int smb2_open(struct ksmbd_work *work) rc = smb_grant_oplock(work, req_op_level, fp->persistent_id, fp, le32_to_cpu(req->hdr.Id.SyncId.TreeId), - lc, share_ret); + lc, share_ret, + smb3_hdr_replay(&req->hdr)); if (rc < 0) goto err_out1; } - if (req->CreateOptions & FILE_DELETE_ON_CLOSE_LE) + if (req->CreateOptions & FILE_DELETE_ON_CLOSE_LE) { + smb_break_all_levII_oplock_for_delete(work, fp); ksmbd_fd_set_delete_on_close(fp, file_info); + } if (need_truncate) { rc = smb2_create_truncate(&fp->filp->f_path); @@ -3534,7 +4950,6 @@ int smb2_open(struct ksmbd_work *work) rc = PTR_ERR(az_req); goto err_out1; } else if (az_req) { - loff_t alloc_size; int err; if (le16_to_cpu(az_req->ccontext.DataOffset) + @@ -3544,16 +4959,26 @@ int smb2_open(struct ksmbd_work *work) goto err_out1; } alloc_size = le64_to_cpu(az_req->AllocationSize); + fp->allocation_size_set = true; ksmbd_debug(SMB, "request smb2 create allocate size : %llu\n", alloc_size); - smb_break_all_levII_oplock(work, fp, 1); - err = vfs_fallocate(fp->filp, FALLOC_FL_KEEP_SIZE, 0, - alloc_size); - if (err < 0) - ksmbd_debug(SMB, - "vfs_fallocate is failed : %d\n", - err); + /* + * fp->filp is the base file's data fork for a stream + * handle (streams are xattr-backed on the same + * underlying file) -- fallocate has no meaning for a + * stream and would otherwise pre-allocate storage on + * the base file's data instead. + */ + if (!ksmbd_stream_fd(fp)) { + smb_break_all_levII_oplock(work, fp, 1); + err = vfs_fallocate(fp->filp, FALLOC_FL_KEEP_SIZE, 0, + alloc_size); + if (err < 0) + ksmbd_debug(SMB, + "vfs_fallocate is failed : %d\n", + err); + } } context = smb2_find_context_vals(req, SMB2_CREATE_QUERY_ON_DISK_ID, 4); @@ -3565,7 +4990,32 @@ int smb2_open(struct ksmbd_work *work) query_disk_id = 1; } - if (conn->is_aapl == false) { + if (test_share_config_flag(share, KSMBD_SHARE_FLAG_TIME_MACHINE)) { + context = smb2_find_context_vals(req, SMB2_CREATE_AAPL, 4); + if (IS_ERR(context)) { + rc = PTR_ERR(context); + goto err_out1; + } else if (context) { + struct aapl_server_query_req *aapl_req; + + if (le32_to_cpu(context->DataLength) < + sizeof(struct aapl_server_query_req)) { + rc = -EINVAL; + goto err_out1; + } + + aapl_req = (struct aapl_server_query_req *) + ((char *)context + + le16_to_cpu(context->DataOffset)); + if (le32_to_cpu(aapl_req->cmd) == + SMB2_CRTCTX_AAPL_SERVER_QUERY) { + conn->is_aapl = true; + aapl_ctxt = true; + aapl_req_bitmap = le64_to_cpu(aapl_req->req_bitmap); + aapl_client_caps = le64_to_cpu(aapl_req->client_caps); + } + } + } else if (conn->is_aapl == false) { context = smb2_find_context_vals(req, SMB2_CREATE_AAPL, 4); if (IS_ERR(context)) { rc = PTR_ERR(context); @@ -3583,55 +5033,124 @@ int smb2_open(struct ksmbd_work *work) fp->create_time = ksmbd_UnixTimeToNT(stat.btime); else fp->create_time = ksmbd_UnixTimeToNT(stat.ctime); - if (req->FileAttributes || fp->f_ci->m_fattr == 0) + fp->change_time = ksmbd_UnixTimeToNT(stat.ctime); + fp->allocation_size = S_ISDIR(stat.mode) ? 0 : + (alloc_size ?: stat.blocks << 9); + if (created || fp->f_ci->m_fattr == 0) fp->f_ci->m_fattr = cpu_to_le32(smb2_get_dos_mode(&stat, le32_to_cpu(req->FileAttributes))); if (!created) smb2_update_xattrs(tcon, &path, fp); - else + if (need_truncate && req->FileAttributes) { + dos_attr = le32_to_cpu(req->FileAttributes); + fp->f_ci->m_fattr = + cpu_to_le32(smb2_get_dos_mode(&stat, dos_attr)); smb2_new_xattrs(tcon, &path, fp); + } - memcpy(fp->client_guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE); + ksmbd_vfs_update_compressed_fattr(path.dentry, &fp->f_ci->m_fattr); + + if (created) { + if (fp->coption & FILE_NO_COMPRESSION_LE) { + rc = ksmbd_vfs_set_compression_create(work, fp, + COMPRESSION_FORMAT_NONE); + if (rc) + fp->f_ci->m_fattr &= ~FILE_ATTRIBUTE_COMPRESSED_LE; + rc = 0; + } else if (smb2_parent_compressed(tcon, &path)) { + rc = ksmbd_vfs_set_compression_create(work, fp, + COMPRESSION_FORMAT_LZNT1); + if (rc) + fp->f_ci->m_fattr |= FILE_ATTRIBUTE_COMPRESSED_LE; + rc = 0; + } + } + + if (created) + smb2_new_xattrs(tcon, &path, fp); + + fp->create_action = cpu_to_le32(file_info); if (dh_info.type == DURABLE_REQ_V2 || dh_info.type == DURABLE_REQ) { if (dh_info.type == DURABLE_REQ_V2 && dh_info.persistent && test_share_config_flag(work->tcon->share_conf, - KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY)) + KSMBD_SHARE_FLAG_CONTINUOUS_AVAILABILITY) && + (conn->vals->req_capabilities & + SMB2_GLOBAL_CAP_PERSISTENT_HANDLES)) { + /* MS-SMB2 3.3.5.9.10: a persistent open is durable too. */ + fp->is_durable = true; fp->is_persistent = true; - else + } else { fp->is_durable = true; - + } if (dh_info.type == DURABLE_REQ_V2) { - memcpy(fp->create_guid, dh_info.CreateGuid, - SMB2_CREATE_GUID_SIZE); + if (dh_info.app_instance_id) + memcpy(fp->app_instance_id, + dh_info.AppInstanceId, + SMB2_CREATE_GUID_SIZE); if (dh_info.timeout) fp->durable_timeout = min_t(unsigned int, dh_info.timeout, DURABLE_HANDLE_MAX_TIMEOUT); else - fp->durable_timeout = 60; + fp->durable_timeout = 60000; } } -reconnected_fp: + /* + * conn->is_aapl detection above (this function's create-context + * parsing) is skipped on the reconnect path below, since a + * reconnect always arrives on a fresh connection -- if the client + * cares, it sends its own AAPL context on this same CREATE, which + * this function's normal (non-reconnect) parsing already handles. + */ + reconnected_fp: + if (dh_info.replay) + file_info = le32_to_cpu(fp->create_action); rsp->StructureSize = cpu_to_le16(89); - rcu_read_lock(); - opinfo = rcu_dereference(fp->f_opinfo); + opinfo = opinfo_get(fp); rsp->OplockLevel = opinfo != NULL ? opinfo->level : 0; - rcu_read_unlock(); + /* + * A durable CREATE replay does not modify the existing open. When + * replayed without an oplock, however, its response reflects that + * request and cannot include a new durable-handle response context. + */ + if (dh_info.replay && !lc && + req_op_level == SMB2_OPLOCK_LEVEL_NONE) { + rsp->OplockLevel = SMB2_OPLOCK_LEVEL_NONE; + durable_rsp = false; + } rsp->Flags = 0; rsp->CreateAction = cpu_to_le32(file_info); rsp->CreationTime = cpu_to_le64(fp->create_time); time = ksmbd_UnixTimeToNT(stat.atime); rsp->LastAccessTime = cpu_to_le64(time); time = ksmbd_UnixTimeToNT(stat.mtime); + fp->open_mtime = time; rsp->LastWriteTime = cpu_to_le64(time); - time = ksmbd_UnixTimeToNT(stat.ctime); - rsp->ChangeTime = cpu_to_le64(time); - rsp->AllocationSize = S_ISDIR(stat.mode) ? 0 : - cpu_to_le64(stat.blocks << 9); - rsp->EndofFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); + rsp->ChangeTime = cpu_to_le64(fp->change_time); + /* + * The cached allocation size hides filesystem rounding for the + * requested allocation, but it can go stale when the file grows past + * it via writes (e.g. across a durable reconnect). Refresh it once the + * file exceeds the cached value, rounding the end of file up to the + * volume allocation unit (the filesystem block size, matching the + * SectorsPerAllocationUnit/BytesPerSector ksmbd advertises) rather than + * using the raw on-disk block count, which can include filesystem + * preallocation and metadata rounding. + */ + if (ksmbd_stream_fd(fp)) { + loff_t seof = ksmbd_stream_eof(fp); + + rsp->AllocationSize = cpu_to_le64((u64)seof); + rsp->EndofFile = cpu_to_le64((u64)seof); + } else { + if (!S_ISDIR(stat.mode) && stat.size > fp->allocation_size) + fp->allocation_size = round_up(stat.size, stat.blksize); + rsp->AllocationSize = cpu_to_le64(fp->allocation_size); + rsp->EndofFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); + } rsp->FileAttributes = fp->f_ci->m_fattr; rsp->Reserved2 = 0; @@ -3660,6 +5179,7 @@ reconnected_fp: next_ptr = &lease_ccontext->Next; next_off = conn->vals->create_lease_size; } + opinfo_put(opinfo); if (maximal_access_ctxt) { struct create_context *mxac_ccontext; @@ -3701,7 +5221,8 @@ reconnected_fp: next_off = conn->vals->create_disk_id_size; } - if (dh_info.type == DURABLE_REQ || dh_info.type == DURABLE_REQ_V2) { + if (durable_rsp && + (dh_info.type == DURABLE_REQ || dh_info.type == DURABLE_REQ_V2)) { struct create_context *durable_ccontext; durable_ccontext = (struct create_context *)(rsp->Buffer + @@ -3725,10 +5246,16 @@ reconnected_fp: if (next_ptr) *next_ptr = cpu_to_le32(next_off); next_ptr = &durable_ccontext->Next; - next_off = conn->vals->create_durable_size; + next_off = dh_info.type == DURABLE_REQ ? + conn->vals->create_durable_size : + conn->vals->create_durable_v2_size; } if (posix_ctxt) { + struct create_context *posix_ccontext; + + posix_ccontext = (struct create_context *)(rsp->Buffer + + le32_to_cpu(rsp->CreateContextsLength)); contxt_cnt++; create_posix_rsp_buf(rsp->Buffer + le32_to_cpu(rsp->CreateContextsLength), @@ -3738,6 +5265,40 @@ reconnected_fp: iov_len += conn->vals->create_posix_size; if (next_ptr) *next_ptr = cpu_to_le32(next_off); + next_ptr = &posix_ccontext->Next; + next_off = conn->vals->create_posix_size; + } + + /* + * AAPL create context response: see smb2pdu.h for the capability + * rationale. Scoped to TIME_MACHINE shares only. + */ + if (aapl_ctxt) { + if (aapl_client_caps & SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR) + conn->aapl_readdir_attr = true; + /* + * V2 extends the same inline-FinderInfo mechanism (see + * smb2pdu.h), so a V2-requesting client also gets + * aapl_readdir_attr treatment -- the reply just advertises + * the V2 bit instead of the V1 one (create_aapl_rsp_buf). + */ + if (aapl_client_caps & SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2) { + conn->aapl_readdir_attr = true; + conn->aapl_readdir_attr_v2 = true; + } + + contxt_cnt++; + create_aapl_rsp_buf(rsp->Buffer + + le32_to_cpu(rsp->CreateContextsLength), + SMB2_CRTCTX_AAPL_FULL_SYNC, + aapl_req_bitmap, + conn->aapl_readdir_attr_v2); + le32_add_cpu(&rsp->CreateContextsLength, + conn->vals->create_aapl_size); + iov_len += conn->vals->create_aapl_size; + if (next_ptr) + *next_ptr = cpu_to_le32(next_off); + /* AAPL is last; next_ptr need not be updated */ } if (contxt_cnt > 0) { @@ -3754,16 +5315,26 @@ err_out1: err_out2: if (!rc) { - ksmbd_update_fstate(&work->sess->file_table, fp, FP_INITED); - rc = ksmbd_iov_pin_rsp(work, (void *)rsp, iov_len); + if (!dh_info.replay) + rc = ksmbd_update_fstate(&work->sess->file_table, fp, + FP_INITED); + if (!rc) + rc = smb2_set_request_open(work, fp, &req->hdr, false, false); + if (!rc) + rc = ksmbd_iov_pin_rsp(work, (void *)rsp, iov_len); } if (rc) { if (rc == -EINVAL) rsp->hdr.Status = STATUS_INVALID_PARAMETER; else if (rc == -EOPNOTSUPP) rsp->hdr.Status = STATUS_NOT_SUPPORTED; - else if (rc == -EACCES || rc == -ESTALE || rc == -EXDEV) - rsp->hdr.Status = STATUS_ACCESS_DENIED; + else if ((rc == -EACCES || rc == -ESTALE || rc == -EXDEV) && + !rsp->hdr.Status) { + if (req->DesiredAccess & FILE_ACCESS_SYSTEM_SECURITY_LE) + rsp->hdr.Status = STATUS_PRIVILEGE_NOT_HELD; + else + rsp->hdr.Status = STATUS_ACCESS_DENIED; + } else if (rc == -ENOENT) rsp->hdr.Status = STATUS_OBJECT_NAME_INVALID; else if (rc == -EPERM) @@ -3780,15 +5351,36 @@ err_out2: rsp->hdr.Status = STATUS_OBJECT_NAME_COLLISION; else if (rc == -EMFILE) rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; + else if (rc == -EINPROGRESS) + rsp->hdr.Status = STATUS_FILE_NOT_AVAILABLE; + else if (rc == -EAGAIN) + rsp->hdr.Status = STATUS_FILE_NOT_AVAILABLE; if (!rsp->hdr.Status) rsp->hdr.Status = STATUS_UNEXPECTED_IO_ERROR; - if (fp) + if (fp && !dh_info.replay) ksmbd_fd_put(work, fp); smb2_set_err_rsp(work); ksmbd_debug(SMB, "Error response: %x\n", rsp->hdr.Status); } + if (dh_info.replay) + ksmbd_put_durable_fd(dh_info.fp); + + if (dh_info.reconnected) { + /* + * If reconnect succeeded, fp was republished in the + * session file table. On a later error, ksmbd_fd_put() + * above drops the session reference; drop the durable + * lookup reference through the same session-aware path so + * final close removes the volatile id before freeing fp. + */ + if (rc && fp == dh_info.fp) + ksmbd_fd_put(work, dh_info.fp); + else + ksmbd_put_durable_fd(dh_info.fp); + } + kfree(name); kfree(lc); @@ -3930,7 +5522,13 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, goto free_conv_name; } - struct_sz = readdir_info_level_struct_sz(info_level) + conv_len; + struct_sz = readdir_info_level_struct_sz(info_level); + if (struct_sz == -EOPNOTSUPP) { + rc = -EINVAL; + goto free_conv_name; + } + + struct_sz += conv_len; next_entry_offset = ALIGN(struct_sz, KSMBD_DIR_INFO_ALIGNMENT); d_info->last_entry_off_align = next_entry_offset - struct_sz; @@ -4028,17 +5626,83 @@ static int smb2_populate_readdir_entry(struct ksmbd_conn *conn, int info_level, fibdinfo = (struct file_id_both_directory_info *)kstat; fibdinfo->FileNameLength = cpu_to_le32(conv_len); - fibdinfo->EaSize = - smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); - if (fibdinfo->EaSize) - fibdinfo->ExtFileAttributes = FILE_ATTRIBUTE_REPARSE_POINT_LE; if (conn->is_aapl) fibdinfo->UniqueId = 0; else fibdinfo->UniqueId = cpu_to_le64(ksmbd_kstat->kstat->ino); fibdinfo->ShortNameLength = 0; fibdinfo->Reserved = 0; - fibdinfo->Reserved2 = cpu_to_le16(0); + if (conn->aapl_readdir_attr) { + /* + * READDIR_ATTR wire format, confirmed against reference server's + * reference implementation marshalling (reference implementation behavior): + * EaSize = max_access (expanded specific + * rights, simplified to "grant all") + * ShortNameLength = 24 (fixed; not 0, despite the spec) + * ShortName[0..7] = resource fork size (uint64 LE, 0 = no rfork) + * ShortName[8..23] = compressed FinderInfo (type+creator+flags+ + * ext_flags+date_added, 16 bytes LE; all + * zeros means type=0/creator=0, i.e. use + * the file extension for icon lookup) + * Reserved2 = Unix mode bits (uint16 LE) + * Reparse-point tag is indicated via ExtFileAttributes, not EaSize. + * + * V2 (conn->aapl_readdir_attr_v2): ShortNameLength+Reserved + * are read as a single flags field instead of being ignored + * -- see smb2pdu.h for the wire-format confirmation and + * AAPL_READDIR_ATTR_V2_NO_XATTR's meaning. + */ + __le32 reparse_tag = + smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); + + if (reparse_tag) + fibdinfo->ExtFileAttributes = FILE_ATTRIBUTE_REPARSE_POINT_LE; + /* + * FILE_GENERIC_ALL_LE (0x10000000) is the raw + * "generic all" meta-bit -- valid only in a + * client's requested access mask, for the server + * to expand. It has none of the specific FILE_* + * rights bits set (FILE_LIST_DIRECTORY, FILE_TRAVERSE, + * etc.), so reporting it here as max_access would make + * macOS's bit-by-bit access checks fail on every + * entry -> permanent "no entry" badges in Finder. + * Report the actual expanded rights instead, same + * as smb_map_generic_desired_access() does when + * translating a client's GENERIC_ALL request. + */ + fibdinfo->EaSize = cpu_to_le32(GENERIC_ALL_FLAGS); + /* + * The spec says ShortNameLength should be 0 when + * there's no short name; 24 here instead matches + * reference implementation marshalling (reference + * behavior) for server-to-server wire parity. + * V2 repurposes it as a flags field that is + * interpreted; V1 doesn't. Either value is safe + * here, so keep 24 for parity. + */ + if (conn->aapl_readdir_attr_v2) { + /* + * V2 repurposes this field as flags (see comment + * above) -- 24 is a V1-only convention that real + * macOS clients ignore outright, so don't reuse it + * here as a base value for a field V2 clients + * actually interpret. + */ + fibdinfo->ShortNameLength = 0; + if (!ksmbd_kstat->has_ads_stream) + fibdinfo->ShortNameLength = AAPL_READDIR_ATTR_V2_NO_XATTR; + } else { + fibdinfo->ShortNameLength = 24; + } + memset(fibdinfo->ShortName, 0, sizeof(fibdinfo->ShortName)); + fibdinfo->Reserved2 = cpu_to_le16(ksmbd_kstat->kstat->mode & 0xffff); + } else { + fibdinfo->EaSize = + smb2_get_reparse_tag_special_file(ksmbd_kstat->kstat->mode); + if (fibdinfo->EaSize) + fibdinfo->ExtFileAttributes = FILE_ATTRIBUTE_REPARSE_POINT_LE; + fibdinfo->Reserved2 = cpu_to_le16(0); + } if (d_info->hide_dot_file && d_info->name[0] == '.') fibdinfo->ExtFileAttributes |= FILE_ATTRIBUTE_HIDDEN_LE; memcpy(fibdinfo->FileName, conv_name, conv_len); @@ -4141,6 +5805,7 @@ static int process_query_dir_entries(struct smb2_query_dir_private *priv) for (i = 0; i < priv->d_info->num_entry; i++) { struct dentry *dent; + struct path path; if (dentry_name(priv->d_info, priv->info_level)) return -EINVAL; @@ -4163,6 +5828,23 @@ static int process_query_dir_entries(struct smb2_query_dir_private *priv) continue; } + if (test_share_config_flag(priv->work->tcon->share_conf, + KSMBD_SHARE_FLAG_HIDE_UNREADABLE)) { + __le32 daccess = FILE_READ_DATA_LE | FILE_READ_EA_LE | + FILE_READ_ATTRIBUTES_LE; + + path.mnt = priv->dir_fp->filp->f_path.mnt; + path.dentry = dent; + rc = smb_check_perm_dacl(priv->work->conn, &path, + &daccess, daccess, + priv->work->sess->user->uid, + true); + if (rc) { + dput(dent); + continue; + } + } + ksmbd_kstat.kstat = &kstat; if (priv->info_level != FILE_NAMES_INFORMATION) { rc = ksmbd_vfs_fill_dentry_attrs(priv->work, @@ -4380,11 +6062,28 @@ int smb2_query_dir(struct ksmbd_work *work) unsigned char srch_flag; int buffer_sz; struct smb2_query_dir_private query_dir_private = {NULL, }; + unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; ksmbd_debug(SMB, "Received smb2 query directory request\n"); WORK_BUFFERS(work, req, rsp); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + + if (work->next_smb2_rcv_hdr_off && + !has_file_id(req->VolatileFileId)) { + ksmbd_debug(SMB, "Compound request set FID = %llu\n", + work->compound_fid); + id = work->compound_fid; + pid = work->compound_pfid; + } + + if (!has_file_id(id)) { + id = req->VolatileFileId; + pid = req->PersistentFileId; + } + if (ksmbd_override_fsids(work)) { rsp->hdr.Status = STATUS_NO_MEMORY; smb2_set_err_rsp(work); @@ -4397,7 +6096,7 @@ int smb2_query_dir(struct ksmbd_work *work) goto err_out2; } - dir_fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId); + dir_fp = ksmbd_lookup_fd_slow(work, id, pid); if (!dir_fp) { rc = -EBADF; goto err_out2; @@ -4430,6 +6129,8 @@ int smb2_query_dir(struct ksmbd_work *work) ksmbd_debug(SMB, "Search pattern is %s\n", srch_ptr); } + mutex_lock(&dir_fp->readdir_lock); + if (srch_flag & SMB2_REOPEN || srch_flag & SMB2_RESTART_SCANS) { ksmbd_debug(SMB, "Restart directory scan\n"); generic_file_llseek(dir_fp->filp, 0, SEEK_SET); @@ -4439,8 +6140,9 @@ int smb2_query_dir(struct ksmbd_work *work) d_info.wptr = (char *)rsp->Buffer; d_info.rptr = (char *)rsp->Buffer; d_info.out_buf_len = - smb2_calc_max_out_buf_len(work, 8, - le32_to_cpu(req->OutputBufferLength)); + smb2_calc_max_out_buf_len(work, + offsetof(struct smb2_query_directory_rsp, Buffer), + le32_to_cpu(req->OutputBufferLength)); if (d_info.out_buf_len < 0) { rc = -EINVAL; goto err_out; @@ -4533,6 +6235,7 @@ no_buf_len: goto err_out; } + mutex_unlock(&dir_fp->readdir_lock); kfree(srch_ptr); ksmbd_fd_put(work, dir_fp); ksmbd_revert_fsids(work); @@ -4540,6 +6243,7 @@ no_buf_len: err_out: pr_err("error while processing smb2 query dir rc = %d\n", rc); + mutex_unlock(&dir_fp->readdir_lock); kfree(srch_ptr); err_out2: @@ -4569,26 +6273,31 @@ err_out2: /** * buffer_check_err() - helper function to check buffer errors * @reqOutputBufferLength: max buffer length expected in command response + * @fixed_len: minimum fixed response length * @rsp: query info response buffer contains output buffer length - * @rsp_org: base response buffer pointer in case of chained response * * Return: 0 on success, otherwise error */ static int buffer_check_err(int reqOutputBufferLength, - struct smb2_query_info_rsp *rsp, - void *rsp_org) + unsigned int fixed_len, + struct smb2_query_info_rsp *rsp) { - if (reqOutputBufferLength < le32_to_cpu(rsp->OutputBufferLength)) { + unsigned int output_len = le32_to_cpu(rsp->OutputBufferLength); + + if (reqOutputBufferLength < fixed_len) { pr_err("Invalid Buffer Size Requested\n"); rsp->hdr.Status = STATUS_INFO_LENGTH_MISMATCH; - *(__be32 *)rsp_org = cpu_to_be32(sizeof(struct smb2_hdr)); return -EINVAL; } + + if (reqOutputBufferLength < output_len) { + rsp->hdr.Status = STATUS_BUFFER_OVERFLOW; + rsp->OutputBufferLength = cpu_to_le32(reqOutputBufferLength); + } return 0; } -static void get_standard_info_pipe(struct smb2_query_info_rsp *rsp, - void *rsp_org) +static void get_standard_info_pipe(struct smb2_query_info_rsp *rsp) { struct smb2_file_standard_info *sinfo; @@ -4603,8 +6312,7 @@ static void get_standard_info_pipe(struct smb2_query_info_rsp *rsp, cpu_to_le32(sizeof(struct smb2_file_standard_info)); } -static void get_internal_info_pipe(struct smb2_query_info_rsp *rsp, u64 num, - void *rsp_org) +static void get_internal_info_pipe(struct smb2_query_info_rsp *rsp, u64 num) { struct smb2_file_internal_info *file_info; @@ -4618,8 +6326,7 @@ static void get_internal_info_pipe(struct smb2_query_info_rsp *rsp, u64 num, static int smb2_get_info_file_pipe(struct ksmbd_session *sess, struct smb2_query_info_req *req, - struct smb2_query_info_rsp *rsp, - void *rsp_org) + struct smb2_query_info_rsp *rsp) { u64 id; int rc; @@ -4644,14 +6351,16 @@ static int smb2_get_info_file_pipe(struct ksmbd_session *sess, switch (req->FileInfoClass) { case FILE_STANDARD_INFORMATION: - get_standard_info_pipe(rsp, rsp_org); + get_standard_info_pipe(rsp); rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength), - rsp, rsp_org); + le32_to_cpu(rsp->OutputBufferLength), + rsp); break; case FILE_INTERNAL_INFORMATION: - get_internal_info_pipe(rsp, id, rsp_org); + get_internal_info_pipe(rsp, id); rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength), - rsp, rsp_org); + le32_to_cpu(rsp->OutputBufferLength), + rsp); break; default: ksmbd_debug(SMB, "smb2_info_file_pipe for %u not supported\n", @@ -4698,6 +6407,11 @@ static int smb2_get_ea(struct ksmbd_work *work, struct ksmbd_file *fp, ea_req = (struct smb2_ea_info_req *)((char *)req + le16_to_cpu(req->InputBufferOffset)); + + if (le32_to_cpu(req->InputBufferLength) < + offsetof(struct smb2_ea_info_req, name) + + ea_req->EaNameLength) + return -EINVAL; } else { /* need to send all EAs, if no specific EA is requested*/ if (le32_to_cpu(req->Flags) & SL_RETURN_SINGLE_ENTRY) @@ -4707,8 +6421,9 @@ static int smb2_get_ea(struct ksmbd_work *work, struct ksmbd_file *fp, } buf_free_len = - smb2_calc_max_out_buf_len(work, 8, - le32_to_cpu(req->OutputBufferLength)); + smb2_calc_max_out_buf_len(work, + offsetof(struct smb2_query_info_rsp, Buffer), + le32_to_cpu(req->OutputBufferLength)); if (buf_free_len < 0) return -EINVAL; @@ -4742,17 +6457,13 @@ static int smb2_get_ea(struct ksmbd_work *work, struct ksmbd_file *fp, if (strncmp(name, XATTR_USER_PREFIX, XATTR_USER_PREFIX_LEN)) continue; - if (!strncmp(&name[XATTR_USER_PREFIX_LEN], STREAM_PREFIX, - STREAM_PREFIX_LEN)) - continue; - if (req->InputBufferLength && strncmp(&name[XATTR_USER_PREFIX_LEN], ea_req->name, ea_req->EaNameLength)) continue; - if (!strncmp(&name[XATTR_USER_PREFIX_LEN], - DOS_ATTRIBUTE_PREFIX, DOS_ATTRIBUTE_PREFIX_LEN)) + if (smb2_is_private_ea(&name[XATTR_USER_PREFIX_LEN], + name_len - XATTR_USER_PREFIX_LEN)) continue; if (!strncmp(name, XATTR_USER_PREFIX, XATTR_USER_PREFIX_LEN)) @@ -4797,6 +6508,8 @@ static int smb2_get_ea(struct ksmbd_work *work, struct ksmbd_file *fp, /* align next xattr entry at 4 byte bundary */ alignment_bytes = ((next_offset + 3) & ~3) - next_offset; if (alignment_bytes) { + if (buf_free_len < alignment_bytes) + break; memset(ptr, '\0', alignment_bytes); ptr += alignment_bytes; next_offset += alignment_bytes; @@ -4839,7 +6552,7 @@ static void get_file_access_info(struct smb2_query_info_rsp *rsp, static int get_file_basic_info(struct smb2_query_info_rsp *rsp, struct ksmbd_file *fp, void *rsp_org) { - struct smb2_file_basic_info *basic_info; + struct file_basic_info *basic_info; struct kstat stat; u64 time; int ret; @@ -4855,21 +6568,44 @@ static int get_file_basic_info(struct smb2_query_info_rsp *rsp, if (ret) return ret; - basic_info = (struct smb2_file_basic_info *)rsp->Buffer; + basic_info = (struct file_basic_info *)rsp->Buffer; basic_info->CreationTime = cpu_to_le64(fp->create_time); time = ksmbd_UnixTimeToNT(stat.atime); basic_info->LastAccessTime = cpu_to_le64(time); time = ksmbd_UnixTimeToNT(stat.mtime); basic_info->LastWriteTime = cpu_to_le64(time); - time = ksmbd_UnixTimeToNT(stat.ctime); - basic_info->ChangeTime = cpu_to_le64(time); + basic_info->ChangeTime = cpu_to_le64(fp->change_time); basic_info->Attributes = fp->f_ci->m_fattr; - basic_info->Pad1 = 0; + basic_info->Pad = 0; rsp->OutputBufferLength = - cpu_to_le32(sizeof(struct smb2_file_basic_info)); + cpu_to_le32(sizeof(struct file_basic_info)); return 0; } +static int get_file_allocation_stat(struct ksmbd_file *fp, struct kstat *stat) +{ + int ret; + + /* + * Buffered writes can leave delayed allocation in a state where two + * consecutive queries report different block counts even when the + * second write only overwrites the first one. Complete writeback before + * reporting the filesystem allocation for an ordinary open. + */ + if (!fp->allocation_size_set) { + ret = file_write_and_wait(fp->filp); + if (ret) + return ret; + } + + ret = vfs_getattr(&fp->filp->f_path, stat, STATX_BASIC_STATS, + AT_STATX_SYNC_AS_STAT); + if (!ret && !fp->allocation_size_set) + fp->allocation_size = S_ISDIR(stat->mode) ? 0 : stat->blocks << 9; + + return ret; +} + static int get_file_standard_info(struct smb2_query_info_rsp *rsp, struct ksmbd_file *fp, void *rsp_org) { @@ -4878,8 +6614,7 @@ static int get_file_standard_info(struct smb2_query_info_rsp *rsp, struct kstat stat; int ret; - ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, - AT_STATX_SYNC_AS_STAT); + ret = get_file_allocation_stat(fp, &stat); if (ret) return ret; @@ -4887,11 +6622,13 @@ static int get_file_standard_info(struct smb2_query_info_rsp *rsp, delete_pending = ksmbd_inode_pending_delete(fp); if (ksmbd_stream_fd(fp) == false) { - sinfo->AllocationSize = cpu_to_le64(stat.blocks << 9); + sinfo->AllocationSize = cpu_to_le64(fp->allocation_size); sinfo->EndOfFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); } else { - sinfo->AllocationSize = cpu_to_le64(fp->stream.size); - sinfo->EndOfFile = cpu_to_le64(fp->stream.size); + loff_t seof = ksmbd_stream_eof(fp); + + sinfo->AllocationSize = cpu_to_le64((u64)seof); + sinfo->EndOfFile = cpu_to_le64((u64)seof); } sinfo->NumberOfLinks = cpu_to_le32(get_nlink(&stat) - delete_pending); sinfo->DeletePending = delete_pending; @@ -4925,7 +6662,7 @@ static int get_file_all_info(struct ksmbd_work *work, int conv_len; char *filename; u64 time; - int ret; + int ret, buf_free_len, filename_len; if (!(fp->daccess & FILE_READ_ATTRIBUTES_LE)) { ksmbd_debug(SMB, "no right to read the attributes : 0x%x\n", @@ -4937,8 +6674,16 @@ static int get_file_all_info(struct ksmbd_work *work, if (IS_ERR(filename)) return PTR_ERR(filename); - ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, - AT_STATX_SYNC_AS_STAT); + filename_len = strlen(filename); + buf_free_len = smb2_resp_buf_len(work, + offsetof(struct smb2_query_info_rsp, Buffer) + + offsetof(struct smb2_file_all_info, FileName)); + if (buf_free_len < (filename_len + 1) * 2) { + kfree(filename); + return -EINVAL; + } + + ret = get_file_allocation_stat(fp, &stat); if (ret) { kfree(filename); return ret; @@ -4953,17 +6698,17 @@ static int get_file_all_info(struct ksmbd_work *work, file_info->LastAccessTime = cpu_to_le64(time); time = ksmbd_UnixTimeToNT(stat.mtime); file_info->LastWriteTime = cpu_to_le64(time); - time = ksmbd_UnixTimeToNT(stat.ctime); - file_info->ChangeTime = cpu_to_le64(time); + file_info->ChangeTime = cpu_to_le64(fp->change_time); file_info->Attributes = fp->f_ci->m_fattr; file_info->Pad1 = 0; if (ksmbd_stream_fd(fp) == false) { - file_info->AllocationSize = - cpu_to_le64(stat.blocks << 9); + file_info->AllocationSize = cpu_to_le64(fp->allocation_size); file_info->EndOfFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); } else { - file_info->AllocationSize = cpu_to_le64(fp->stream.size); - file_info->EndOfFile = cpu_to_le64(fp->stream.size); + loff_t seof = ksmbd_stream_eof(fp); + + file_info->AllocationSize = cpu_to_le64((u64)seof); + file_info->EndOfFile = cpu_to_le64((u64)seof); } file_info->NumberOfLinks = cpu_to_le32(get_nlink(&stat) - delete_pending); @@ -4980,7 +6725,8 @@ static int get_file_all_info(struct ksmbd_work *work, file_info->Mode = fp->coption; file_info->AlignmentRequirement = 0; conv_len = smbConvertToUTF16((__le16 *)file_info->FileName, filename, - PATH_MAX, conn->local_nls, 0); + min(filename_len, PATH_MAX), + conn->local_nls, 0); conv_len *= 2; file_info->FileNameLength = cpu_to_le32(conv_len); rsp->OutputBufferLength = @@ -5010,6 +6756,88 @@ static void get_file_alternate_info(struct ksmbd_work *work, cpu_to_le32(struct_size(file_info, FileName, conv_len)); } +static char *smb2_get_normalized_stream_name(struct ksmbd_file *fp) +{ + char *name, *stream_name = NULL, *xattr_list = NULL; + ssize_t xattr_list_len; + + if (!ksmbd_stream_fd(fp)) + return NULL; + + xattr_list_len = ksmbd_vfs_listxattr(fp->filp->f_path.dentry, + &xattr_list); + if (xattr_list_len <= 0) + goto out; + + for (name = xattr_list; name - xattr_list < xattr_list_len; + name += strlen(name) + 1) { + char *type; + + if (strlen(name) + 1 != fp->stream.size || + strncasecmp(name, fp->stream.name, fp->stream.size - 1)) + continue; + + name += XATTR_NAME_STREAM_LEN; + type = strrchr(name, ':'); + if (type) + stream_name = kstrndup(name, type - name, + KSMBD_DEFAULT_GFP); + break; + } +out: + kvfree(xattr_list); + return stream_name; +} + +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; + char *filename, *normalized, *stream_name; + int buf_free_len, conv_len, filename_len; + + if (work->conn->dialect < SMB311_PROT_ID) { + rsp->hdr.Status = STATUS_NOT_SUPPORTED; + return -EOPNOTSUPP; + } + + 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)); + + stream_name = smb2_get_normalized_stream_name(fp); + normalized = kasprintf(KSMBD_DEFAULT_GFP, "%s%s%s", filename, + stream_name ? ":" : "", + stream_name ? stream_name : ""); + kfree(stream_name); + kfree(filename); + if (!normalized) + return -ENOMEM; + + filename_len = strlen(normalized); + buf_free_len = smb2_resp_buf_len(work, sizeof(*rsp) + + sizeof(*file_info)); + if (buf_free_len < 0 || + (size_t)buf_free_len < (filename_len + 1) * sizeof(__le16)) { + kfree(normalized); + return -EINVAL; + } + + file_info = (struct smb2_file_alt_name_info *)rsp->Buffer; + conv_len = smbConvertToUTF16((__le16 *)file_info->FileName, + normalized, filename_len, + work->conn->local_nls, 0); + kfree(normalized); + conv_len *= 2; + file_info->FileNameLength = cpu_to_le32(conv_len); + rsp->OutputBufferLength = cpu_to_le32(sizeof(*file_info) + conv_len); + return 0; +} + static int get_file_stream_info(struct ksmbd_work *work, struct smb2_query_info_rsp *rsp, struct ksmbd_file *fp, @@ -5021,9 +6849,10 @@ static int get_file_stream_info(struct ksmbd_work *work, struct kstat stat; const struct path *path = &fp->filp->f_path; ssize_t xattr_list_len; + ssize_t slen; + loff_t ssize; int nbytes = 0, streamlen, stream_name_len, next, idx = 0; int buf_free_len; - struct smb2_query_info_req *req = ksmbd_req_buf_next(work); int ret; ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, @@ -5033,9 +6862,8 @@ static int get_file_stream_info(struct ksmbd_work *work, file_info = (struct smb2_file_stream_info *)rsp->Buffer; - buf_free_len = - smb2_calc_max_out_buf_len(work, 8, - le32_to_cpu(req->OutputBufferLength)); + buf_free_len = smb2_resp_buf_len(work, + offsetof(struct smb2_query_info_rsp, Buffer)); if (buf_free_len < 0) goto out; @@ -5084,8 +6912,20 @@ static int get_file_stream_info(struct ksmbd_work *work, streamlen *= 2; kfree(stream_buf); file_info->StreamNameLength = cpu_to_le32(streamlen); - file_info->StreamSize = cpu_to_le64(stream_name_len); - file_info->StreamAllocationSize = cpu_to_le64(stream_name_len); + /* + * 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); + ssize = slen < 0 ? 0 : (loff_t)slen; + file_info->StreamSize = cpu_to_le64(ssize); + file_info->StreamAllocationSize = cpu_to_le64(ssize); nbytes += next; buf_free_len -= next; @@ -5149,8 +6989,7 @@ static int get_file_network_open_info(struct smb2_query_info_rsp *rsp, return -EACCES; } - ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, - AT_STATX_SYNC_AS_STAT); + ret = get_file_allocation_stat(fp, &stat); if (ret) return ret; @@ -5161,15 +7000,16 @@ static int get_file_network_open_info(struct smb2_query_info_rsp *rsp, file_info->LastAccessTime = cpu_to_le64(time); time = ksmbd_UnixTimeToNT(stat.mtime); file_info->LastWriteTime = cpu_to_le64(time); - time = ksmbd_UnixTimeToNT(stat.ctime); - file_info->ChangeTime = cpu_to_le64(time); + file_info->ChangeTime = cpu_to_le64(fp->change_time); file_info->Attributes = fp->f_ci->m_fattr; if (ksmbd_stream_fd(fp) == false) { - file_info->AllocationSize = cpu_to_le64(stat.blocks << 9); + file_info->AllocationSize = cpu_to_le64(fp->allocation_size); file_info->EndOfFile = S_ISDIR(stat.mode) ? 0 : cpu_to_le64(stat.size); } else { - file_info->AllocationSize = cpu_to_le64(fp->stream.size); - file_info->EndOfFile = cpu_to_le64(fp->stream.size); + loff_t seof = ksmbd_stream_eof(fp); + + file_info->AllocationSize = cpu_to_le64((u64)seof); + file_info->EndOfFile = cpu_to_le64((u64)seof); } file_info->Reserved = cpu_to_le32(0); rsp->OutputBufferLength = @@ -5218,6 +7058,7 @@ static int get_file_compression_info(struct smb2_query_info_rsp *rsp, { struct smb2_file_comp_info *file_info; struct kstat stat; + u16 fmt; int ret; ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, @@ -5225,9 +7066,13 @@ static int get_file_compression_info(struct smb2_query_info_rsp *rsp, if (ret) return ret; + ret = ksmbd_vfs_get_compression(fp, &fmt); + if (ret) + return ret; + file_info = (struct smb2_file_comp_info *)rsp->Buffer; - file_info->CompressedFileSize = cpu_to_le64(stat.blocks << 9); - file_info->CompressionFormat = COMPRESSION_FORMAT_NONE; + file_info->CompressedFileSize = cpu_to_le64(min_t(u64, stat.blocks << 9, stat.size)); + file_info->CompressionFormat = cpu_to_le16(fmt); file_info->CompressionUnitShift = 0; file_info->ChunkShift = 0; file_info->ClusterShift = 0; @@ -5271,8 +7116,13 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp, int out_buf_len = sizeof(struct smb311_posix_qinfo) + 32; int ret; - ret = vfs_getattr(&fp->filp->f_path, &stat, STATX_BASIC_STATS, - AT_STATX_SYNC_AS_STAT); + if (!(fp->daccess & FILE_READ_ATTRIBUTES_LE)) { + pr_err("no right to read the attributes : 0x%x\n", + fp->daccess); + return -EACCES; + } + + ret = get_file_allocation_stat(fp, &stat); if (ret) return ret; @@ -5282,16 +7132,17 @@ static int find_file_posix_info(struct smb2_query_info_rsp *rsp, file_info->LastAccessTime = cpu_to_le64(time); time = ksmbd_UnixTimeToNT(stat.mtime); file_info->LastWriteTime = cpu_to_le64(time); - time = ksmbd_UnixTimeToNT(stat.ctime); - file_info->ChangeTime = cpu_to_le64(time); + file_info->ChangeTime = cpu_to_le64(fp->change_time); file_info->DosAttributes = fp->f_ci->m_fattr; file_info->Inode = cpu_to_le64(stat.ino); if (ksmbd_stream_fd(fp) == false) { file_info->EndOfFile = cpu_to_le64(stat.size); - file_info->AllocationSize = cpu_to_le64(stat.blocks << 9); + file_info->AllocationSize = cpu_to_le64(fp->allocation_size); } else { - file_info->EndOfFile = cpu_to_le64(fp->stream.size); - file_info->AllocationSize = cpu_to_le64(fp->stream.size); + loff_t seof = ksmbd_stream_eof(fp); + + file_info->EndOfFile = cpu_to_le64((u64)seof); + 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); @@ -5339,13 +7190,14 @@ 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 id = KSMBD_NO_FID, pid = KSMBD_NO_FID; if (test_share_config_flag(work->tcon->share_conf, KSMBD_SHARE_FLAG_PIPE)) { /* smb2 info file called for pipe */ - return smb2_get_info_file_pipe(work->sess, req, rsp, - work->response_buf); + rc = smb2_get_info_file_pipe(work->sess, req, rsp); + goto iov_pin_out; } if (work->next_smb2_rcv_hdr_off) { @@ -5392,6 +7244,9 @@ static int smb2_get_info_file(struct ksmbd_work *work, case FILE_ALTERNATE_NAME_INFORMATION: get_file_alternate_info(work, rsp, fp, work->response_buf); break; + case FILE_NORMALIZED_NAME_INFORMATION: + rc = get_file_normalized_name_info(work, rsp, fp); + break; case FILE_STREAM_INFORMATION: rc = get_file_stream_info(work, rsp, fp, work->response_buf); @@ -5441,10 +7296,33 @@ static int smb2_get_info_file(struct ksmbd_work *work, fileinfoclass); rc = -EOPNOTSUPP; } - if (!rc) + if (!rc) { + 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; + break; + case FILE_NORMALIZED_NAME_INFORMATION: + fixed_len = FILE_NORMALIZED_NAME_INFORMATION_SIZE; + break; + case FILE_STREAM_INFORMATION: + fixed_len = FILE_STREAM_INFORMATION_SIZE; + break; + } rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength), - rsp, work->response_buf); + fixed_len, + rsp); + } ksmbd_fd_put(work, fp); + +iov_pin_out: + if (!rc) + rc = ksmbd_iov_pin_rsp(work, (void *)rsp, + offsetof(struct smb2_query_info_rsp, Buffer) + + le32_to_cpu(rsp->OutputBufferLength)); return rc; } @@ -5452,18 +7330,19 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, struct smb2_query_info_req *req, struct smb2_query_info_rsp *rsp) { - struct ksmbd_session *sess = work->sess; struct ksmbd_conn *conn = work->conn; struct ksmbd_share_config *share = work->tcon->share_conf; int fsinfoclass = 0; struct kstatfs stfs; struct path path; int rc = 0, len; + unsigned int fixed_len = 0; if (!share->path) return -EIO; - rc = kern_path(share->path, LOOKUP_NO_SYMLINKS, &path); + scoped_with_init_fs() + rc = kern_path(share->path, LOOKUP_NO_SYMLINKS, &path); if (rc) { pr_err("cannot create vfs path\n"); return -EIO; @@ -5493,21 +7372,41 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->DeviceCharacteristics |= cpu_to_le32(FILE_READ_ONLY_DEVICE); rsp->OutputBufferLength = cpu_to_le32(8); + fixed_len = 8; break; } case FS_ATTRIBUTE_INFORMATION: { FILE_SYSTEM_ATTRIBUTE_INFO *info; + struct file_kattr fa = {}; size_t sz; + u32 attrs; + int err; info = (FILE_SYSTEM_ATTRIBUTE_INFO *)rsp->Buffer; - info->Attributes = cpu_to_le32(FILE_SUPPORTS_OBJECT_IDS | - FILE_PERSISTENT_ACLS | - FILE_UNICODE_ON_DISK | - FILE_CASE_PRESERVED_NAMES | - FILE_CASE_SENSITIVE_SEARCH | - FILE_SUPPORTS_BLOCK_REFCOUNTING); + attrs = FILE_SUPPORTS_OBJECT_IDS | + FILE_PERSISTENT_ACLS | + FILE_UNICODE_ON_DISK | + FILE_FILE_COMPRESSION | + FILE_SUPPORTS_SPARSE_FILES | + FILE_SUPPORTS_BLOCK_REFCOUNTING; + + err = vfs_fileattr_get(path.dentry, &fa); + /* + * -EINVAL, -EOPNOTSUPP: ntfs-3g and other FUSE + * filesystems that lack FS_IOC_FSGETXATTR support. + */ + if (err && err != -ENOIOCTLCMD && err != -ENOTTY && + err != -EINVAL && err != -EOPNOTSUPP) { + path_put(&path); + return err; + } + if (!(fa.fsx_xflags & FS_XFLAG_CASEFOLD)) + attrs |= FILE_CASE_SENSITIVE_SEARCH; + if (!(fa.fsx_xflags & FS_XFLAG_CASENONPRESERVING)) + attrs |= FILE_CASE_PRESERVED_NAMES; + info->Attributes = cpu_to_le32(attrs); info->Attributes |= cpu_to_le32(server_conf.share_fake_fscaps); if (test_share_config_flag(work->tcon->share_conf, @@ -5528,6 +7427,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->FileSystemNameLen = cpu_to_le32(len); sz = sizeof(FILE_SYSTEM_ATTRIBUTE_INFO) + len; rsp->OutputBufferLength = cpu_to_le32(sz); + fixed_len = 16; break; } case FS_VOLUME_INFORMATION: @@ -5545,15 +7445,17 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, serial_crc = crc32_le(serial_crc, ksmbd_netbios_name(), strlen(ksmbd_netbios_name())); /* Taking dummy value of serial number*/ - info->SerialNumber = cpu_to_le32(serial_crc); + info->VolumeSerialNumber = cpu_to_le32(serial_crc); len = smbConvertToUTF16((__le16 *)info->VolumeLabel, share->name, PATH_MAX, conn->local_nls, 0); len = len * 2; - info->VolumeLabelSize = cpu_to_le32(len); + info->VolumeLabelLength = cpu_to_le32(len); info->Reserved = 0; + info->SupportsObjects = 0; sz = sizeof(struct filesystem_vol_info) + len; rsp->OutputBufferLength = cpu_to_le32(sz); + fixed_len = 24; break; } case FS_SIZE_INFORMATION: @@ -5566,6 +7468,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->SectorsPerAllocationUnit = cpu_to_le32(1); info->BytesPerSector = cpu_to_le32(stfs.f_bsize); rsp->OutputBufferLength = cpu_to_le32(24); + fixed_len = 24; break; } case FS_FULL_SIZE_INFORMATION: @@ -5581,6 +7484,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->SectorsPerAllocationUnit = cpu_to_le32(1); info->BytesPerSector = cpu_to_le32(stfs.f_bsize); rsp->OutputBufferLength = cpu_to_le32(32); + fixed_len = 32; break; } case FS_OBJECT_ID_INFORMATION: @@ -5588,11 +7492,13 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, struct object_id_info *info; info = (struct object_id_info *)(rsp->Buffer); + memset(info, 0, sizeof(*info)); - if (!user_guest(sess->user)) - memcpy(info->objid, user_passkey(sess->user), 16); + if (path.mnt->mnt_sb->s_uuid_len == 16) + memcpy(info->objid, path.mnt->mnt_sb->s_uuid.b, + path.mnt->mnt_sb->s_uuid_len); else - memset(info->objid, 0, 16); + memcpy(info->objid, &stfs.f_fsid, sizeof(stfs.f_fsid)); info->extended_info.magic = cpu_to_le32(EXTENDED_INFO_MAGIC); info->extended_info.version = cpu_to_le32(1); @@ -5600,6 +7506,7 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->extended_info.rel_date = 0; memcpy(info->extended_info.version_string, "1.1.0", strlen("1.1.0")); rsp->OutputBufferLength = cpu_to_le32(64); + fixed_len = 64; break; } case FS_SECTOR_SIZE_INFORMATION: @@ -5617,10 +7524,12 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->FSEffPhysicalBytesPerSectorForAtomicity = cpu_to_le32(sector_size); info->Flags = cpu_to_le32(SSINFO_FLAGS_ALIGNED_DEVICE | - SSINFO_FLAGS_PARTITION_ALIGNED_ON_DEVICE); + SSINFO_FLAGS_PARTITION_ALIGNED_ON_DEVICE | + SSINFO_FLAGS_TRIM_ENABLED); info->ByteOffsetForSectorAlignment = 0; info->ByteOffsetForPartitionAlignment = 0; rsp->OutputBufferLength = cpu_to_le32(28); + fixed_len = 28; break; } case FS_CONTROL_INFORMATION: @@ -5639,8 +7548,10 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->FreeSpaceStopFiltering = 0; info->DefaultQuotaThreshold = cpu_to_le64(SMB2_NO_FID); info->DefaultQuotaLimit = cpu_to_le64(SMB2_NO_FID); + info->FileSystemControlFlags = 0; info->Padding = 0; rsp->OutputBufferLength = cpu_to_le32(48); + fixed_len = 48; break; } case FS_POSIX_INFORMATION: @@ -5660,7 +7571,11 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, info->UserBlocksAvail = cpu_to_le64(stfs.f_bavail); info->TotalFileNodes = cpu_to_le64(stfs.f_files); info->FreeFileNodes = cpu_to_le64(stfs.f_ffree); + info->FileSysIdentifier = + cpu_to_le64((u64)(u32)stfs.f_fsid.val[1] << 32 | + (u32)stfs.f_fsid.val[0]); rsp->OutputBufferLength = cpu_to_le32(56); + fixed_len = 56; } break; } @@ -5669,8 +7584,14 @@ static int smb2_get_info_filesystem(struct ksmbd_work *work, return -EOPNOTSUPP; } rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength), - rsp, work->response_buf); + fixed_len, + rsp); path_put(&path); + + if (!rc) + rc = ksmbd_iov_pin_rsp(work, (void *)rsp, + offsetof(struct smb2_query_info_rsp, Buffer) + + le32_to_cpu(rsp->OutputBufferLength)); return rc; } @@ -5680,13 +7601,14 @@ static int smb2_get_info_sec(struct ksmbd_work *work, { struct ksmbd_file *fp; struct mnt_idmap *idmap; - struct smb_ntsd *pntsd = (struct smb_ntsd *)rsp->Buffer, *ppntsd = NULL; + struct smb_ntsd *pntsd = NULL, *ppntsd = NULL; struct smb_fattr fattr = {{0}}; struct inode *inode; __u32 secdesclen = 0; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; int addition_info = le32_to_cpu(req->AdditionalInformation); - int rc = 0, ppntsd_size = 0; + int rc = 0, ppntsd_size = 0, max_len; + size_t scratch_len = 0; if (addition_info & ~(OWNER_SECINFO | GROUP_SECINFO | DACL_SECINFO | PROTECTED_DACL_SECINFO | @@ -5694,17 +7616,8 @@ static int smb2_get_info_sec(struct ksmbd_work *work, ksmbd_debug(SMB, "Unsupported addition info: 0x%x)\n", addition_info); - pntsd->revision = cpu_to_le16(1); - pntsd->type = cpu_to_le16(SELF_RELATIVE | DACL_PROTECTED); - pntsd->osidoffset = 0; - pntsd->gsidoffset = 0; - pntsd->sacloffset = 0; - pntsd->dacloffset = 0; - - secdesclen = sizeof(struct smb_ntsd); - rsp->OutputBufferLength = cpu_to_le32(secdesclen); - - return 0; + rsp->hdr.Status = STATUS_NOT_SUPPORTED; + return -EINVAL; } if (work->next_smb2_rcv_hdr_off) { @@ -5725,6 +7638,18 @@ static int smb2_get_info_sec(struct ksmbd_work *work, if (!fp) return -ENOENT; + if (addition_info & (OWNER_SECINFO | GROUP_SECINFO | DACL_SECINFO) && + !(fp->daccess & FILE_READ_CONTROL_LE)) { + ksmbd_fd_put(work, fp); + return -EACCES; + } + + if (le32_to_cpu(req->OutputBufferLength) < sizeof(struct smb_ntsd)) { + rsp->hdr.Status = STATUS_BUFFER_TOO_SMALL; + ksmbd_fd_put(work, fp); + return -ENOSPC; + } + idmap = file_mnt_idmap(fp->filp); inode = file_inode(fp->filp); ksmbd_acls_fattr(&fattr, idmap, inode); @@ -5736,18 +7661,58 @@ static int smb2_get_info_sec(struct ksmbd_work *work, &ppntsd); /* Check if sd buffer size exceeds response buffer size */ - if (smb2_resp_buf_len(work, 8) > ppntsd_size) - rc = build_sec_desc(idmap, pntsd, ppntsd, ppntsd_size, - addition_info, &secdesclen, &fattr); + max_len = smb2_calc_max_out_buf_len(work, + offsetof(struct smb2_query_info_rsp, Buffer), + le32_to_cpu(req->OutputBufferLength)); + if (max_len < 0) { + rc = -EINVAL; + goto release_acl; + } + + scratch_len = smb_acl_sec_desc_scratch_len(&fattr, ppntsd, + ppntsd_size, addition_info); + if (!scratch_len || scratch_len == SIZE_MAX) { + rc = -EFBIG; + goto release_acl; + } + + pntsd = kvzalloc(scratch_len, KSMBD_DEFAULT_GFP); + if (!pntsd) { + rc = -ENOMEM; + goto release_acl; + } + + rc = build_sec_desc(idmap, pntsd, ppntsd, ppntsd_size, + addition_info, &secdesclen, &fattr); + +release_acl: posix_acl_release(fattr.cf_acls); posix_acl_release(fattr.cf_dacls); kfree(ppntsd); ksmbd_fd_put(work, fp); + + if (!rc && ALIGN(secdesclen, 8) > scratch_len) + rc = -EFBIG; if (rc) - return rc; + goto err_out; rsp->OutputBufferLength = cpu_to_le32(secdesclen); - return 0; + rc = buffer_check_err(le32_to_cpu(req->OutputBufferLength), + le32_to_cpu(rsp->OutputBufferLength), + rsp); + if (rc) + goto err_out; + + rc = ksmbd_iov_pin_rsp_read(work, (void *)rsp, + offsetof(struct smb2_query_info_rsp, Buffer), + pntsd, secdesclen); +err_out: + if (rc) { + rsp->OutputBufferLength = 0; + kvfree(pntsd); + } + + return rc; } /** @@ -5766,11 +7731,17 @@ int smb2_query_info(struct ksmbd_work *work) WORK_BUFFERS(work, req, rsp); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + if (ksmbd_override_fsids(work)) { rc = -ENOMEM; goto err_out; } + rsp->StructureSize = cpu_to_le16(9); + rsp->OutputBufferOffset = cpu_to_le16(72); + switch (req->InfoType) { case SMB2_O_INFO_FILE: ksmbd_debug(SMB, "GOT SMB2_O_INFO_FILE\n"); @@ -5791,14 +7762,6 @@ int smb2_query_info(struct ksmbd_work *work) } ksmbd_revert_fsids(work); - if (!rc) { - rsp->StructureSize = cpu_to_le16(9); - rsp->OutputBufferOffset = cpu_to_le16(72); - rc = ksmbd_iov_pin_rsp(work, (void *)rsp, - offsetof(struct smb2_query_info_rsp, Buffer) + - le32_to_cpu(rsp->OutputBufferLength)); - } - err_out: if (rc < 0) { if (rc == -EACCES) @@ -5809,7 +7772,9 @@ err_out: rsp->hdr.Status = STATUS_UNEXPECTED_IO_ERROR; else if (rc == -ENOMEM) rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; - else if (rc == -EOPNOTSUPP || rsp->hdr.Status == 0) + else if (rc == -EINVAL && rsp->hdr.Status == 0) + rsp->hdr.Status = STATUS_INVALID_PARAMETER; + else if (rsp->hdr.Status == 0) rsp->hdr.Status = STATUS_INVALID_INFO_CLASS; smb2_set_err_rsp(work); @@ -5864,7 +7829,6 @@ int smb2_close(struct ksmbd_work *work) u64 sess_id; struct smb2_close_req *req; struct smb2_close_rsp *rsp; - struct ksmbd_conn *conn = work->conn; struct ksmbd_file *fp; u64 time; int err = 0; @@ -5873,6 +7837,9 @@ int smb2_close(struct ksmbd_work *work) WORK_BUFFERS(work, req, rsp); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + if (test_share_config_flag(work->tcon->share_conf, KSMBD_SHARE_FLAG_PIPE)) { ksmbd_debug(SMB, "IPC pipe close request\n"); @@ -5884,7 +7851,7 @@ int smb2_close(struct ksmbd_work *work) sess_id = work->compound_sid; work->compound_sid = 0; - if (check_session_id(conn, sess_id)) { + if (work->sess && work->sess->id == sess_id) { work->compound_sid = sess_id; } else { rsp->hdr.Status = STATUS_USER_SESSION_DELETED; @@ -5939,17 +7906,18 @@ int smb2_close(struct ksmbd_work *work) } rsp->Flags = SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB; - rsp->AllocationSize = S_ISDIR(stat.mode) ? 0 : - cpu_to_le64(stat.blocks << 9); + rsp->AllocationSize = cpu_to_le64(fp->allocation_size); rsp->EndOfFile = cpu_to_le64(stat.size); rsp->Attributes = fp->f_ci->m_fattr; rsp->CreationTime = cpu_to_le64(fp->create_time); time = ksmbd_UnixTimeToNT(stat.atime); rsp->LastAccessTime = cpu_to_le64(time); time = ksmbd_UnixTimeToNT(stat.mtime); + if (time > fp->open_mtime && + time - fp->open_mtime < KSMBD_WRITE_TIME_RESOLUTION) + time = fp->open_mtime; rsp->LastWriteTime = cpu_to_le64(time); - time = ksmbd_UnixTimeToNT(stat.ctime); - rsp->ChangeTime = cpu_to_le64(time); + rsp->ChangeTime = cpu_to_le64(fp->change_time); ksmbd_fd_put(work, fp); } else { rsp->Flags = 0; @@ -6045,9 +8013,8 @@ static int smb2_rename(struct ksmbd_work *work, pr_err("failed to store stream name in xattr: %d\n", rc); rc = -EINVAL; - goto out; } - + kfree(xattr_stream_name); goto out; } @@ -6061,9 +8028,12 @@ static int smb2_rename(struct ksmbd_work *work, if (!file_info->ReplaceIfExists) flags = RENAME_NOREPLACE; - rc = ksmbd_vfs_rename(work, &fp->filp->f_path, new_name, flags); - if (!rc) - smb_break_all_levII_oplock(work, fp, 0); + rc = ksmbd_vfs_check_rename_share(work, &fp->filp->f_path); + if (rc) + goto out; + + smb_break_all_levII_oplock_rename(work, fp); + rc = ksmbd_vfs_rename(work, fp, new_name, flags); out: kfree(new_name); return rc; @@ -6137,13 +8107,15 @@ out: } static int set_file_basic_info(struct ksmbd_file *fp, - struct smb2_file_basic_info *file_info, + struct file_basic_info *file_info, struct ksmbd_share_config *share) { struct iattr attrs; struct file *filp; struct inode *inode; struct mnt_idmap *idmap; + __le32 attrs_mask = FILE_ATTRIBUTE_DIRECTORY_LE | + FILE_ATTRIBUTE_COMPRESSED_LE; int rc = 0; if (!(fp->daccess & FILE_WRITE_ATTRIBUTES_LE)) @@ -6162,9 +8134,11 @@ static int set_file_basic_info(struct ksmbd_file *fp, attrs.ia_valid |= (ATTR_ATIME | ATTR_ATIME_SET); } - if (file_info->ChangeTime) + if (file_info->ChangeTime) { + fp->change_time = le64_to_cpu(file_info->ChangeTime); inode_set_ctime_to_ts(inode, ksmbd_NTtimeToUnix(file_info->ChangeTime)); + } if (file_info->LastWriteTime) { attrs.ia_mtime = ksmbd_NTtimeToUnix(file_info->LastWriteTime); @@ -6179,8 +8153,9 @@ static int set_file_basic_info(struct ksmbd_file *fp, } if (!(S_ISDIR(inode->i_mode) && file_info->Attributes == FILE_ATTRIBUTE_NORMAL_LE)) - fp->f_ci->m_fattr = file_info->Attributes | - (fp->f_ci->m_fattr & FILE_ATTRIBUTE_DIRECTORY_LE); + fp->f_ci->m_fattr = + (file_info->Attributes & ~FILE_ATTRIBUTE_COMPRESSED_LE) | + (fp->f_ci->m_fattr & attrs_mask); } if (test_share_config_flag(share, KSMBD_SHARE_FLAG_STORE_DOS_ATTRS) && @@ -6227,6 +8202,7 @@ static int set_file_allocation_info(struct ksmbd_work *work, */ loff_t alloc_blks; + u64 alloc_size; struct inode *inode; struct kstat stat; int rc; @@ -6242,7 +8218,19 @@ static int set_file_allocation_info(struct ksmbd_work *work, if (rc) return rc; - alloc_blks = (le64_to_cpu(file_alloc_info->AllocationSize) + 511) >> 9; + /* + * 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. + */ + alloc_size = le64_to_cpu(file_alloc_info->AllocationSize); + if (alloc_size > MAX_LFS_FILESIZE - 511) + return -EINVAL; + + alloc_blks = (alloc_size + 511) >> 9; inode = file_inode(fp->filp); if (alloc_blks > stat.blocks) { @@ -6272,6 +8260,9 @@ static int set_file_allocation_info(struct ksmbd_work *work, if (size < alloc_blks * 512) i_size_write(inode, size); } + + fp->allocation_size = le64_to_cpu(file_alloc_info->AllocationSize); + fp->allocation_size_set = true; return 0; } @@ -6328,7 +8319,8 @@ static int set_rename_info(struct ksmbd_work *work, struct ksmbd_file *fp, return smb2_rename(work, fp, rename_info, work->conn->local_nls); } -static int set_file_disposition_info(struct ksmbd_file *fp, +static int set_file_disposition_info(struct ksmbd_work *work, + struct ksmbd_file *fp, struct smb2_file_disposition_info *file_info) { struct inode *inode; @@ -6338,14 +8330,21 @@ static int set_file_disposition_info(struct ksmbd_file *fp, return -EACCES; } + if (fp->f_ci->m_fattr & FILE_ATTRIBUTE_READONLY_LE) + return -EACCES; + inode = file_inode(fp->filp); if (file_info->DeletePending) { - if (S_ISDIR(inode->i_mode) && + 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; - ksmbd_set_inode_pending_delete(fp); + smb_break_all_levII_oplock_for_delete(work, fp); + ksmbd_fd_set_delete_pending(fp); } else { - ksmbd_clear_inode_pending_delete(fp); + ksmbd_fd_clear_delete_pending(fp); } return 0; } @@ -6362,7 +8361,7 @@ static int set_file_position_info(struct ksmbd_file *fp, sector_size = inode->i_sb->s_blocksize; if (current_byte_offset < 0 || - (fp->coption == FILE_NO_INTERMEDIATE_BUFFERING_LE && + (fp->coption & FILE_NO_INTERMEDIATE_BUFFERING_LE && current_byte_offset & (sector_size - 1))) { pr_err("CurrentByteOffset is not valid : %llu\n", current_byte_offset); @@ -6419,10 +8418,10 @@ static int smb2_set_info_file(struct ksmbd_work *work, struct ksmbd_file *fp, switch (req->FileInfoClass) { case FILE_BASIC_INFORMATION: { - if (buf_len < sizeof(struct smb2_file_basic_info)) + if (buf_len < sizeof(struct file_basic_info)) return -EMSGSIZE; - return set_file_basic_info(fp, (struct smb2_file_basic_info *)buffer, share); + return set_file_basic_info(fp, (struct file_basic_info *)buffer, share); } case FILE_ALLOCATION_INFORMATION: { @@ -6451,11 +8450,18 @@ static int smb2_set_info_file(struct ksmbd_work *work, struct ksmbd_file *fp, } case FILE_LINK_INFORMATION: { + struct smb2_file_link_info *file_info; + if (buf_len < sizeof(struct smb2_file_link_info)) return -EMSGSIZE; - return smb2_create_link(work, work->tcon->share_conf, - (struct smb2_file_link_info *)buffer, + file_info = (struct smb2_file_link_info *)buffer; + if (file_info->ReplaceIfExists && !(fp->daccess & FILE_DELETE_LE)) { + pr_err("no right to delete : 0x%x\n", fp->daccess); + return -EACCES; + } + + return smb2_create_link(work, work->tcon->share_conf, file_info, buf_len, fp->filp, work->conn->local_nls); } @@ -6464,7 +8470,7 @@ static int smb2_set_info_file(struct ksmbd_work *work, struct ksmbd_file *fp, if (buf_len < sizeof(struct smb2_file_disposition_info)) return -EMSGSIZE; - return set_file_disposition_info(fp, + return set_file_disposition_info(work, fp, (struct smb2_file_disposition_info *)buffer); } case FILE_FULL_EA_INFORMATION: @@ -6508,6 +8514,9 @@ static int smb2_set_info_sec(struct ksmbd_file *fp, int addition_info, fp->saccess |= FILE_SHARE_DELETE_LE; + if (!(fp->daccess & (FILE_WRITE_DAC_LE | FILE_WRITE_OWNER_LE))) + return -EACCES; + return set_info_sec(fp->conn, fp->tcon, &fp->filp->f_path, pntsd, buf_len, false, true); } @@ -6520,10 +8529,12 @@ static int smb2_set_info_sec(struct ksmbd_file *fp, int addition_info, */ int smb2_set_info(struct ksmbd_work *work) { + const struct cred *saved_cred; struct smb2_set_info_req *req; struct smb2_set_info_rsp *rsp; struct ksmbd_file *fp = NULL; int rc = 0; + bool chseq_err = false; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; ksmbd_debug(SMB, "Received smb2 set info request\n"); @@ -6531,6 +8542,8 @@ int smb2_set_info(struct ksmbd_work *work) if (work->next_smb2_rcv_hdr_off) { req = ksmbd_req_buf_next(work); rsp = ksmbd_resp_buf_next(work); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; if (!has_file_id(req->VolatileFileId)) { ksmbd_debug(SMB, "Compound request set FID = %llu\n", work->compound_fid); @@ -6561,6 +8574,14 @@ int smb2_set_info(struct ksmbd_work *work) goto err_out; } + rc = smb2_set_request_open(work, fp, &req->hdr, true, false); + if (rc) { + rsp->hdr.Status = STATUS_FILE_NOT_AVAILABLE; + chseq_err = true; + goto err_out; + } + + saved_cred = override_creds(fp->filp->f_cred); switch (req->InfoType) { case SMB2_O_INFO_FILE: ksmbd_debug(SMB, "GOT SMB2_O_INFO_FILE\n"); @@ -6568,19 +8589,15 @@ int smb2_set_info(struct ksmbd_work *work) break; case SMB2_O_INFO_SECURITY: ksmbd_debug(SMB, "GOT SMB2_O_INFO_SECURITY\n"); - if (ksmbd_override_fsids(work)) { - rc = -ENOMEM; - goto err_out; - } rc = smb2_set_info_sec(fp, le32_to_cpu(req->AdditionalInformation), (char *)req + le16_to_cpu(req->BufferOffset), le32_to_cpu(req->BufferLength)); - ksmbd_revert_fsids(work); break; default: rc = -EOPNOTSUPP; } + revert_creds(saved_cred); if (rc < 0) goto err_out; @@ -6594,19 +8611,27 @@ int smb2_set_info(struct ksmbd_work *work) return 0; err_out: - if (rc == -EACCES || rc == -EPERM || rc == -EXDEV) - rsp->hdr.Status = STATUS_ACCESS_DENIED; + if (rc == -EACCES || rc == -EPERM || rc == -EXDEV) { + if (fp && req->InfoType == SMB2_O_INFO_FILE && + req->FileInfoClass == FILE_DISPOSITION_INFORMATION && + fp->f_ci->m_fattr & FILE_ATTRIBUTE_READONLY_LE) + rsp->hdr.Status = STATUS_CANNOT_DELETE; + else + rsp->hdr.Status = STATUS_ACCESS_DENIED; + } else if (rc == -EINVAL) rsp->hdr.Status = STATUS_INVALID_PARAMETER; else if (rc == -EMSGSIZE) rsp->hdr.Status = STATUS_INFO_LENGTH_MISMATCH; + else if (rc == -ENOSPC || rc == -EFBIG) + rsp->hdr.Status = STATUS_DISK_FULL; else if (rc == -ESHARE) rsp->hdr.Status = STATUS_SHARING_VIOLATION; else if (rc == -ENOENT) rsp->hdr.Status = STATUS_OBJECT_NAME_INVALID; else if (rc == -EBUSY || rc == -ENOTEMPTY) rsp->hdr.Status = STATUS_DIRECTORY_NOT_EMPTY; - else if (rc == -EAGAIN) + else if (rc == -EAGAIN && !chseq_err) rsp->hdr.Status = STATUS_FILE_LOCK_CONFLICT; else if (rc == -EBADF || rc == -ESTALE) rsp->hdr.Status = STATUS_INVALID_HANDLE; @@ -6648,13 +8673,18 @@ static noinline int smb2_read_pipe(struct ksmbd_work *work) } aux_payload_buf = - kvmalloc(rpc_resp->payload_sz, KSMBD_DEFAULT_GFP); + kvmalloc(ALIGN(rpc_resp->payload_sz, 8), + KSMBD_DEFAULT_GFP); if (!aux_payload_buf) { err = -ENOMEM; goto out; } memcpy(aux_payload_buf, rpc_resp->payload, rpc_resp->payload_sz); + if (rpc_resp->payload_sz & 7) + memset(aux_payload_buf + rpc_resp->payload_sz, 0, + ALIGN(rpc_resp->payload_sz, 8) - + rpc_resp->payload_sz); nbytes = rpc_resp->payload_sz; err = ksmbd_iov_pin_rsp_read(work, (void *)rsp, @@ -6687,18 +8717,31 @@ out: return err; } -static int smb2_set_remote_key_for_rdma(struct ksmbd_work *work, - struct smbdirect_buffer_descriptor_v1 *desc, - __le32 Channel, - __le16 ChannelInfoLength) +/** + * smb2_set_rdma_key() - validate descriptors and save invalidation state + * @work: request work item + * @desc: first RDMA buffer descriptor + * @Channel: nested RDMA channel type + * @channel_info_len: descriptor array length + * + * Return: 0 on success, otherwise -EINVAL + */ +static int smb2_set_rdma_key(struct ksmbd_work *work, + struct smbdirect_buffer_descriptor_v1 *desc, + __le32 Channel, __le16 channel_info_len) { unsigned int i, ch_count; + if (Channel != SMB2_CHANNEL_RDMA_V1 && + Channel != SMB2_CHANNEL_RDMA_V1_INVALIDATE) + return -EINVAL; if (work->conn->dialect == SMB30_PROT_ID && Channel != SMB2_CHANNEL_RDMA_V1) return -EINVAL; + if (le16_to_cpu(channel_info_len) % sizeof(*desc)) + return -EINVAL; - ch_count = le16_to_cpu(ChannelInfoLength) / sizeof(*desc); + ch_count = le16_to_cpu(channel_info_len) / sizeof(*desc); if (ksmbd_debug_types & KSMBD_DEBUG_RDMA) { for (i = 0; i < ch_count; i++) { pr_info("RDMA r/w request %#x: token %#x, length %#x\n", @@ -6717,9 +8760,240 @@ static int smb2_set_remote_key_for_rdma(struct ksmbd_work *work, return 0; } -static ssize_t smb2_read_rdma_channel(struct ksmbd_work *work, - struct smb2_read_req *req, void *data_buf, - size_t length) +/** + * smb2_prep_rdma_read() - transform an RDMA READ payload + * @work: request work item + * @req: READ request controlling encryption or signing + * @rsp: READ response receiving transform metadata + * @data: data that will be transferred through RDMA + * @datalen: data length + * + * Encrypt the payload in place and encode the detached crypto metadata in + * the response buffer. + * + * Return: metadata length, zero when no transform applies, or negative errno + */ +static int smb2_prep_rdma_read(struct ksmbd_work *work, + struct smb2_read_req *req, + struct smb2_read_rsp *rsp, + void *data, unsigned int datalen) +{ + struct ksmbd_conn *conn = work->conn; + struct smb2_rdma_transform *transform; + struct smb2_rdma_crypto_transform *crypto; + u8 *nonce; + unsigned int nonce_len = 0, transform_len; + u16 transform_type; + int err; + + if (!work->encrypted || + !(conn->rdma_transform_ids & BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION))) + return 0; + + transform_type = SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION; + nonce_len = (conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || + conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) ? + SMB3_AES_GCM_NONCE : SMB3_AES_CCM_NONCE; + + transform = (struct smb2_rdma_transform *)rsp->Buffer; + crypto = (struct smb2_rdma_crypto_transform *)(transform + 1); + memset(transform, 0, sizeof(*transform) + sizeof(*crypto) + + SMB2_SIGNATURE_SIZE + nonce_len); + transform->Channel = SMB2_CHANNEL_NONE; + transform->TransformCount = cpu_to_le16(1); + + crypto->TransformType = cpu_to_le16(transform_type); + crypto->SignatureLength = cpu_to_le16(SMB2_SIGNATURE_SIZE); + crypto->NonceLength = cpu_to_le16(nonce_len); + nonce = crypto->Signature + SMB2_SIGNATURE_SIZE; + + get_random_bytes(nonce, nonce_len); + err = ksmbd_crypt_rdma(conn, + work->sess->smb3encryptionkey, + data, datalen, nonce, nonce_len, + crypto->Signature, + SMB2_SIGNATURE_SIZE, true); + if (err) { + pr_err("RDMA READ encryption failed: session=%llu payload=%u rc=%d\n", + work->sess->id, datalen, err); + return err; + } + + transform_len = sizeof(*transform) + sizeof(*crypto) + + SMB2_SIGNATURE_SIZE + nonce_len; + rsp->Flags = SMB2_READFLAG_RESPONSE_RDMA_TRANSFORM; + rsp->DataLength = cpu_to_le32(transform_len); + ksmbd_debug(RDMA, + "RDMA READ encryption prepared: session=%llu cipher=0x%04x payload=%u transform=%u nonce=%u tag=%u\n", + work->sess->id, le16_to_cpu(conn->cipher_type), datalen, + transform_len, nonce_len, SMB2_SIGNATURE_SIZE); + return transform_len; +} + +struct smb2_rdma_write_transform { + struct smbdirect_buffer_descriptor_v1 *desc; + struct smb2_rdma_crypto_transform *crypto; + u8 *nonce; + unsigned int desc_len; + unsigned int nonce_len; + unsigned int signature_len; + u16 type; + __le32 channel; +}; + +/** + * smb2_current_req_len() - return the current compound request element size + * @work: request work item + * @hdr: current SMB2 header + * + * Return: current request element length measured from the SMB2 header + */ +static unsigned int smb2_current_req_len(struct ksmbd_work *work, + struct smb2_hdr *hdr) +{ + if (hdr->NextCommand) + return le32_to_cpu(hdr->NextCommand); + return get_rfc1002_len(work->request_buf) - + work->next_smb2_rcv_hdr_off; +} + +/** + * check_rdma_desc() - validate an RDMA descriptor array + * @desc: descriptor array + * @desc_len: descriptor array length + * @required_len: minimum aggregate buffer length + * + * Return: 0 when the descriptors cover the transfer, otherwise -EINVAL + */ +static int check_rdma_desc(struct smbdirect_buffer_descriptor_v1 *desc, + unsigned int desc_len, + unsigned int required_len) +{ + unsigned int i, count; + u64 described_len = 0; + + if (!desc_len || desc_len % sizeof(*desc)) + return -EINVAL; + count = desc_len / sizeof(*desc); + if (!le32_to_cpu(desc[0].length)) + return -EINVAL; + for (i = 0; i < count; i++) + described_len += le32_to_cpu(desc[i].length); + return described_len < required_len ? -EINVAL : 0; +} + +/** + * smb2_parse_rdma_write_transform() - validate RDMA WRITE transform metadata + * @work: request work item + * @req: WRITE request containing the transform + * @info: parsed transform information + * + * Validate transform counts, crypto fields, descriptor alignment and bounds, + * negotiated algorithms, and the nested RDMA channel. + * + * Return: 0 on success, otherwise a negative errno + */ +static int smb2_parse_rdma_write_transform(struct ksmbd_work *work, + struct smb2_write_req *req, + struct smb2_rdma_write_transform *info) +{ + struct smb2_rdma_transform *transform; + struct smb2_rdma_crypto_transform *crypto; + unsigned int req_len = smb2_current_req_len(work, &req->hdr); + unsigned int offset = le16_to_cpu(req->WriteChannelInfoOffset); + unsigned int length = le16_to_cpu(req->WriteChannelInfoLength); + unsigned int desc_offset, desc_len, crypto_len, expected_desc_offset; + int err; + + if (!work->conn->rdma_transform_ids || + offset < offsetof(struct smb2_write_req, Buffer) || + length < sizeof(*transform) || offset > req_len || + length > req_len - offset) + return -EINVAL; + + transform = (struct smb2_rdma_transform *)((char *)req + offset); + if (le16_to_cpu(transform->TransformCount) != 1 || + (transform->Channel != SMB2_CHANNEL_RDMA_V1 && + transform->Channel != SMB2_CHANNEL_RDMA_V1_INVALIDATE)) + return -EINVAL; + + desc_offset = le16_to_cpu(transform->RdmaDescriptorOffset); + desc_len = le16_to_cpu(transform->RdmaDescriptorLength); + if (!desc_len || desc_len % sizeof(*info->desc) || + desc_offset < sizeof(*transform) || desc_offset > length || + desc_len > length - desc_offset) + return -EINVAL; + + crypto = (struct smb2_rdma_crypto_transform *)(transform + 1); + if (length - sizeof(*transform) < sizeof(*crypto)) + return -EINVAL; + info->type = le16_to_cpu(crypto->TransformType); + info->signature_len = le16_to_cpu(crypto->SignatureLength); + info->nonce_len = le16_to_cpu(crypto->NonceLength); + if (!info->signature_len) + return info->type == SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION ? + -EBADMSG : -EINVAL; + if (info->signature_len > SMB2_SIGNATURE_SIZE) + return info->type == SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION ? + -EBADMSG : -EINVAL; + if (info->signature_len > length - sizeof(*transform) - sizeof(*crypto) || + info->nonce_len > length - sizeof(*transform) - sizeof(*crypto) - + info->signature_len) + return -EINVAL; + + crypto_len = sizeof(*crypto) + info->signature_len + info->nonce_len; + expected_desc_offset = ALIGN(sizeof(*transform) + crypto_len, 8); + if (desc_offset != expected_desc_offset) + return -EINVAL; + + if (info->type == SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION) { + unsigned int expected_nonce_len; + + if (!(work->conn->rdma_transform_ids & + BIT(SMB2_RDMA_TRANSFORM_ENCRYPTION)) || !work->encrypted) + return -EINVAL; + expected_nonce_len = + (work->conn->cipher_type == SMB2_ENCRYPTION_AES128_GCM || + work->conn->cipher_type == SMB2_ENCRYPTION_AES256_GCM) ? + SMB3_AES_GCM_NONCE : SMB3_AES_CCM_NONCE; + if (info->nonce_len != expected_nonce_len) + return -EBADMSG; + } else { + return -EINVAL; + } + + info->desc = (struct smbdirect_buffer_descriptor_v1 *) + ((char *)transform + desc_offset); + info->desc_len = desc_len; + info->crypto = crypto; + info->nonce = crypto->Signature + info->signature_len; + info->channel = transform->Channel; + err = check_rdma_desc(info->desc, info->desc_len, + le32_to_cpu(req->RemainingBytes)); + if (err) + return err; + + ksmbd_debug(RDMA, + "RDMA WRITE encryption metadata: session=%llu cipher=0x%04x payload=%u channel=0x%x descriptors=%zu nonce=%u tag=%u\n", + work->sess->id, le16_to_cpu(work->conn->cipher_type), + le32_to_cpu(req->RemainingBytes), le32_to_cpu(info->channel), + info->desc_len / sizeof(*info->desc), info->nonce_len, + info->signature_len); + return 0; +} + +/** + * smb2_read_rdma() - transfer READ data to client RDMA buffers + * @work: request work item + * @req: READ request containing client descriptors + * @data_buf: data to transfer + * @length: data length + * + * Return: transferred length on success, otherwise a negative errno + */ +static ssize_t smb2_read_rdma(struct ksmbd_work *work, + struct smb2_read_req *req, void *data_buf, + size_t length) { int err; @@ -6749,7 +9023,8 @@ int smb2_read(struct ksmbd_work *work) size_t length, mincount; ssize_t nbytes = 0, remain_bytes = 0; int err = 0; - bool is_rdma_channel = false; + int rdma_transform_len = 0; + bool is_rdma_channel = false, async_interim = false; unsigned int max_read_size = conn->vals->max_read_size; unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; void *aux_payload_buf; @@ -6765,6 +9040,8 @@ int smb2_read(struct ksmbd_work *work) if (work->next_smb2_rcv_hdr_off) { req = ksmbd_req_buf_next(work); rsp = ksmbd_resp_buf_next(work); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; if (!has_file_id(req->VolatileFileId)) { ksmbd_debug(SMB, "Compound request set FID = %llu\n", work->compound_fid); @@ -6781,6 +9058,12 @@ int smb2_read(struct ksmbd_work *work) pid = req->PersistentFileId; } + if (req->Channel != SMB2_CHANNEL_NONE && + req->Channel != SMB2_CHANNEL_RDMA_V1 && + req->Channel != SMB2_CHANNEL_RDMA_V1_INVALIDATE) { + err = -EINVAL; + goto out; + } if (req->Channel == SMB2_CHANNEL_RDMA_V1_INVALIDATE || req->Channel == SMB2_CHANNEL_RDMA_V1) { is_rdma_channel = true; @@ -6793,16 +9076,24 @@ int smb2_read(struct ksmbd_work *work) if (is_rdma_channel == true) { unsigned int ch_offset = le16_to_cpu(req->ReadChannelInfoOffset); + unsigned int ch_len = le16_to_cpu(req->ReadChannelInfoLength); + unsigned int req_len = smb2_current_req_len(work, &req->hdr); + struct smbdirect_buffer_descriptor_v1 *desc; - if (ch_offset < offsetof(struct smb2_read_req, Buffer)) { + if (!le32_to_cpu(req->Length) || + ch_offset < offsetof(struct smb2_read_req, Buffer) || + ch_offset > req_len || ch_len > req_len - ch_offset) { err = -EINVAL; goto out; } - err = smb2_set_remote_key_for_rdma(work, - (struct smbdirect_buffer_descriptor_v1 *) - ((char *)req + ch_offset), - req->Channel, - req->ReadChannelInfoLength); + desc = (struct smbdirect_buffer_descriptor_v1 *) + ((char *)req + ch_offset); + err = check_rdma_desc(desc, ch_len, le32_to_cpu(req->Length)); + if (err) + goto out; + err = smb2_set_rdma_key(work, desc, + req->Channel, + req->ReadChannelInfoLength); if (err) goto out; } @@ -6813,12 +9104,24 @@ int smb2_read(struct ksmbd_work *work) goto out; } + err = smb2_set_request_open(work, fp, &req->hdr, true, true); + if (err) + goto out; + if (!(fp->daccess & (FILE_READ_DATA_LE | FILE_READ_ATTRIBUTES_LE))) { pr_err("Not permitted to read : 0x%x\n", fp->daccess); err = -EACCES; goto out; } + if (work->next_smb2_rcv_hdr_off && !req->hdr.NextCommand) { + err = setup_async_work(work, NULL, NULL); + if (err) + goto out; + smb2_send_interim_resp(work, STATUS_PENDING); + async_interim = true; + } + offset = le64_to_cpu(req->Offset); if (offset < 0) { err = -EINVAL; @@ -6837,7 +9140,7 @@ int smb2_read(struct ksmbd_work *work) ksmbd_debug(SMB, "filename %pD, offset %lld, len %zu\n", fp->filp, offset, length); - aux_payload_buf = kvzalloc(ALIGN(length, 8), KSMBD_DEFAULT_GFP); + aux_payload_buf = kvmalloc(ALIGN(length, 8), KSMBD_DEFAULT_GFP); if (!aux_payload_buf) { err = -ENOMEM; goto out; @@ -6850,10 +9153,21 @@ int smb2_read(struct ksmbd_work *work) goto out; } + /* + * ksmbd_vfs_read() fills only nbytes; the [nbytes, ALIGN(nbytes, 8)) + * tail of the un-zeroed buffer is transmitted as compound-response + * alignment padding, leaking uninitialized kernel memory to the + * client. Zero just that tail. + */ + if (nbytes & 7) + memset(aux_payload_buf + nbytes, 0, ALIGN(nbytes, 8) - nbytes); + if ((nbytes == 0 && length != 0) || nbytes < mincount) { kvfree(aux_payload_buf); rsp->hdr.Status = STATUS_END_OF_FILE; smb2_set_err_rsp(work); + if (async_interim) + release_async_work(work); ksmbd_fd_put(work, fp); return -ENODATA; } @@ -6862,10 +9176,28 @@ int smb2_read(struct ksmbd_work *work) nbytes, offset, mincount); if (is_rdma_channel == true) { + rdma_transform_len = smb2_prep_rdma_read(work, req, + rsp, + aux_payload_buf, + nbytes); + if (rdma_transform_len < 0) { + kvfree(aux_payload_buf); + err = rdma_transform_len; + goto out; + } /* write data to the client using rdma channel */ - remain_bytes = smb2_read_rdma_channel(work, req, - aux_payload_buf, - nbytes); + remain_bytes = smb2_read_rdma(work, req, + aux_payload_buf, + nbytes); + if (remain_bytes < 0) + pr_err("RDMA READ transfer failed: session=%llu payload=%zu transform=%d rc=%zd\n", + work->sess ? work->sess->id : 0, nbytes, + rdma_transform_len, remain_bytes); + else + ksmbd_debug(RDMA, + "RDMA READ transfer completed: session=%llu payload=%zu transform=%d\n", + work->sess ? work->sess->id : 0, nbytes, + rdma_transform_len); kvfree(aux_payload_buf); aux_payload_buf = NULL; nbytes = 0; @@ -6878,20 +9210,35 @@ int smb2_read(struct ksmbd_work *work) rsp->StructureSize = cpu_to_le16(17); rsp->DataOffset = 80; rsp->Reserved = 0; - rsp->DataLength = cpu_to_le32(nbytes); + rsp->DataLength = cpu_to_le32(rdma_transform_len ?: nbytes); rsp->DataRemaining = cpu_to_le32(remain_bytes); - rsp->Flags = 0; + rsp->Flags = rdma_transform_len ? + SMB2_READFLAG_RESPONSE_RDMA_TRANSFORM : 0; err = ksmbd_iov_pin_rsp_read(work, (void *)rsp, - offsetof(struct smb2_read_rsp, Buffer), + offsetof(struct smb2_read_rsp, Buffer) + + rdma_transform_len, aux_payload_buf, nbytes); if (err) { kvfree(aux_payload_buf); goto out; } + if (async_interim) + release_async_work(work); + /* + * RDMA responses are transferred through channel buffers and encrypted + * responses use the encryption transform, so only normal SMB transport + * responses are candidates for compression. + */ + if (!is_rdma_channel && nbytes && + (req->Flags & SMB2_READFLAG_REQUEST_COMPRESSED) && + conn->compress_algorithm != SMB3_COMPRESS_NONE) + work->compress_response = true; ksmbd_fd_put(work, fp); return 0; out: + if (async_interim) + release_async_work(work); if (err) { if (err == -EISDIR) rsp->hdr.Status = STATUS_INVALID_DEVICE_REQUEST; @@ -6981,10 +9328,28 @@ out: return err; } -static ssize_t smb2_write_rdma_channel(struct ksmbd_work *work, - struct smb2_write_req *req, - struct ksmbd_file *fp, - loff_t offset, size_t length, bool sync) +/** + * smb2_write_rdma() - receive and store an RDMA WRITE payload + * @work: request work item + * @desc: client RDMA buffer descriptors + * @desc_len: descriptor array length + * @transform: parsed transform, or NULL for an untransformed transfer + * @fp: target open file + * @offset: target file offset + * @length: transfer length + * @sync: request synchronous storage completion + * + * Receive the payload, authenticate or decrypt it when required, and write it + * to the target file. + * + * Return: written byte count on success, otherwise a negative errno + */ +static ssize_t smb2_write_rdma(struct ksmbd_work *work, + struct smbdirect_buffer_descriptor_v1 *desc, + unsigned int desc_len, + struct smb2_rdma_write_transform *transform, + struct ksmbd_file *fp, loff_t offset, + size_t length, bool sync) { char *data_buf; int ret; @@ -6994,19 +9359,42 @@ static ssize_t smb2_write_rdma_channel(struct ksmbd_work *work, if (!data_buf) return -ENOMEM; - ret = ksmbd_conn_rdma_read(work->conn, data_buf, length, - (struct smbdirect_buffer_descriptor_v1 *) - ((char *)req + le16_to_cpu(req->WriteChannelInfoOffset)), - le16_to_cpu(req->WriteChannelInfoLength)); + ret = ksmbd_conn_rdma_read(work->conn, data_buf, length, desc, + desc_len); if (ret < 0) { + if (transform) + pr_err("RDMA WRITE encrypted transfer failed: session=%llu payload=%zu rdma_read_rc=%d\n", + work->sess->id, length, ret); kvfree(data_buf); return ret; } + if (transform && + transform->type == SMB2_RDMA_TRANSFORM_TYPE_ENCRYPTION) { + ret = ksmbd_crypt_rdma(work->conn, + work->sess->smb3decryptionkey, + data_buf, length, transform->nonce, + transform->nonce_len, + transform->crypto->Signature, + transform->signature_len, false); + if (ret) { + pr_err("RDMA WRITE decryption failed: session=%llu payload=%zu rc=%d\n", + work->sess->id, length, ret); + kvfree(data_buf); + return ret == -ENOMEM ? ret : -EBADMSG; + } + } ret = ksmbd_vfs_write(work, fp, data_buf, length, &offset, sync, &nbytes); kvfree(data_buf); - if (ret < 0) + if (ret < 0) { + if (transform) + pr_err("RDMA WRITE encrypted file write failed: session=%llu payload=%zu rc=%d\n", + work->sess->id, length, ret); return ret; + } + ksmbd_debug(RDMA, + "RDMA WRITE transfer completed: session=%llu payload=%zu transformed=%u written=%zd\n", + work->sess ? work->sess->id : 0, length, !!transform, nbytes); return nbytes; } @@ -7021,31 +9409,69 @@ int smb2_write(struct ksmbd_work *work) { struct smb2_write_req *req; struct smb2_write_rsp *rsp; + struct smb2_rdma_write_transform rdma_transform = {}; + struct smb2_rdma_write_transform *rdma_info = NULL; + struct smbdirect_buffer_descriptor_v1 *rdma_desc = NULL; + unsigned int rdma_desc_len = 0; struct ksmbd_file *fp = NULL; loff_t offset; size_t length; ssize_t nbytes; char *data_buf; bool writethrough = false, is_rdma_channel = false; + bool async_interim = false; + bool chseq_err = false; int err = 0; unsigned int max_write_size = work->conn->vals->max_write_size; + unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; ksmbd_debug(SMB, "Received smb2 write request\n"); WORK_BUFFERS(work, req, rsp); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + + if (work->next_smb2_rcv_hdr_off && + !has_file_id(req->VolatileFileId)) { + ksmbd_debug(SMB, "Compound request set FID = %llu\n", + work->compound_fid); + id = work->compound_fid; + pid = work->compound_pfid; + } + + if (!has_file_id(id)) { + id = req->VolatileFileId; + pid = req->PersistentFileId; + } + if (test_share_config_flag(work->tcon->share_conf, KSMBD_SHARE_FLAG_PIPE)) { ksmbd_debug(SMB, "IPC pipe write request\n"); return smb2_write_pipe(work); } offset = le64_to_cpu(req->Offset); - if (offset < 0) - return -EINVAL; + if (offset < 0) { + err = -EINVAL; + goto out; + } length = le32_to_cpu(req->Length); + if (req->Channel != SMB2_CHANNEL_NONE && + req->Channel != SMB2_CHANNEL_RDMA_V1 && + req->Channel != SMB2_CHANNEL_RDMA_V1_INVALIDATE && + req->Channel != SMB2_CHANNEL_RDMA_TRANSFORM) { + err = -EINVAL; + goto out; + } + if (req->Channel == SMB2_CHANNEL_RDMA_TRANSFORM && + work->conn->dialect != SMB311_PROT_ID) { + err = -EINVAL; + goto out; + } if (req->Channel == SMB2_CHANNEL_RDMA_V1 || - req->Channel == SMB2_CHANNEL_RDMA_V1_INVALIDATE) { + req->Channel == SMB2_CHANNEL_RDMA_V1_INVALIDATE || + req->Channel == SMB2_CHANNEL_RDMA_TRANSFORM) { is_rdma_channel = true; max_write_size = get_smbd_max_read_write_size(work->conn->transport); if (max_write_size == 0) { @@ -7055,19 +9481,55 @@ int smb2_write(struct ksmbd_work *work) length = le32_to_cpu(req->RemainingBytes); } + if (length) { + u64 end = (u64)offset + length; + + if (end > SMB2_MAX_FILE_SIZE) { + err = -EINVAL; + goto out; + } + if (end == SMB2_MAX_FILE_SIZE) { + err = -EFBIG; + goto out; + } + } + if (is_rdma_channel == true) { unsigned int ch_offset = le16_to_cpu(req->WriteChannelInfoOffset); + unsigned int ch_len = le16_to_cpu(req->WriteChannelInfoLength); + unsigned int req_len = smb2_current_req_len(work, &req->hdr); - if (req->Length != 0 || req->DataOffset != 0 || - ch_offset < offsetof(struct smb2_write_req, Buffer)) { + if (!length || req->Length != 0 || req->DataOffset != 0 || + ch_offset < offsetof(struct smb2_write_req, Buffer) || + ch_offset > req_len || ch_len > req_len - ch_offset) { err = -EINVAL; goto out; } - err = smb2_set_remote_key_for_rdma(work, - (struct smbdirect_buffer_descriptor_v1 *) - ((char *)req + ch_offset), - req->Channel, - req->WriteChannelInfoLength); + if (req->Channel == SMB2_CHANNEL_RDMA_TRANSFORM) { + err = smb2_parse_rdma_write_transform(work, req, + &rdma_transform); + if (err) { + pr_err("RDMA WRITE encryption metadata rejected: session=%llu rc=%d\n", + work->sess ? work->sess->id : 0, err); + goto out; + } + rdma_desc = rdma_transform.desc; + rdma_desc_len = rdma_transform.desc_len; + rdma_info = &rdma_transform; + err = smb2_set_rdma_key(work, rdma_desc, + rdma_transform.channel, + cpu_to_le16(rdma_desc_len)); + } else { + rdma_desc = (struct smbdirect_buffer_descriptor_v1 *) + ((char *)req + ch_offset); + rdma_desc_len = ch_len; + err = check_rdma_desc(rdma_desc, rdma_desc_len, length); + if (err) + goto out; + err = smb2_set_rdma_key(work, rdma_desc, + req->Channel, + req->WriteChannelInfoLength); + } if (err) goto out; } @@ -7078,18 +9540,33 @@ int smb2_write(struct ksmbd_work *work) goto out; } - fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId); + fp = ksmbd_lookup_fd_slow(work, id, pid); if (!fp) { err = -ENOENT; goto out; } + err = smb2_set_request_open(work, fp, &req->hdr, true, false); + if (err) { + rsp->hdr.Status = STATUS_FILE_NOT_AVAILABLE; + chseq_err = true; + goto out; + } + if (!(fp->daccess & (FILE_WRITE_DATA_LE | FILE_READ_ATTRIBUTES_LE))) { pr_err("Not permitted to write : 0x%x\n", fp->daccess); err = -EACCES; goto out; } + if (work->next_smb2_rcv_hdr_off && !req->hdr.NextCommand) { + err = setup_async_work(work, NULL, NULL); + if (err) + goto out; + smb2_send_interim_resp(work, STATUS_PENDING); + async_interim = true; + } + if (length > max_write_size) { ksmbd_debug(SMB, "limiting write size to max size(%u)\n", max_write_size); @@ -7121,8 +9598,9 @@ int smb2_write(struct ksmbd_work *work) /* read data from the client using rdma channel, and * write the data. */ - nbytes = smb2_write_rdma_channel(work, req, fp, offset, length, - writethrough); + nbytes = smb2_write_rdma(work, rdma_desc, rdma_desc_len, + rdma_info, fp, offset, length, + writethrough); if (nbytes < 0) { err = (int)nbytes; goto out; @@ -7138,11 +9616,16 @@ int smb2_write(struct ksmbd_work *work) err = ksmbd_iov_pin_rsp(work, rsp, offsetof(struct smb2_write_rsp, Buffer)); if (err) goto out; + if (async_interim) + release_async_work(work); ksmbd_fd_put(work, fp); return 0; out: - if (err == -EAGAIN) + if (async_interim) + release_async_work(work); + + if (err == -EAGAIN && !chseq_err) rsp->hdr.Status = STATUS_FILE_LOCK_CONFLICT; else if (err == -ENOSPC || err == -EFBIG) rsp->hdr.Status = STATUS_DISK_FULL; @@ -7154,7 +9637,11 @@ out: rsp->hdr.Status = STATUS_SHARING_VIOLATION; else if (err == -EINVAL) rsp->hdr.Status = STATUS_INVALID_PARAMETER; - else + else if (err == -EBADMSG) + rsp->hdr.Status = STATUS_AUTH_TAG_MISMATCH; + else if (err == -EKEYREJECTED) + rsp->hdr.Status = STATUS_INVALID_SIGNATURE; + else if (rsp->hdr.Status == 0) rsp->hdr.Status = STATUS_INVALID_HANDLE; smb2_set_err_rsp(work); @@ -7172,13 +9659,30 @@ int smb2_flush(struct ksmbd_work *work) { struct smb2_flush_req *req; struct smb2_flush_rsp *rsp; + u64 id = KSMBD_NO_FID, pid = KSMBD_NO_FID; int err; WORK_BUFFERS(work, req, rsp); ksmbd_debug(SMB, "Received smb2 flush request(fid : %llu)\n", req->VolatileFileId); - err = ksmbd_vfs_fsync(work, req->VolatileFileId, req->PersistentFileId); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + + if (work->next_smb2_rcv_hdr_off && + !has_file_id(req->VolatileFileId)) { + ksmbd_debug(SMB, "Compound request set FID = %llu\n", + work->compound_fid); + id = work->compound_fid; + pid = work->compound_pfid; + } + + if (!has_file_id(id)) { + id = req->VolatileFileId; + pid = req->PersistentFileId; + } + + err = ksmbd_vfs_fsync(work, id, pid); if (err) goto out; @@ -7204,13 +9708,15 @@ 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) hdr = ksmbd_resp_buf_next(work); ksmbd_debug(SMB, "smb2 cancel called on mid %llu, async flags 0x%x\n", - hdr->MessageId, hdr->Flags); + le64_to_cpu(hdr->MessageId), + le32_to_cpu(hdr->Flags)); if (hdr->Flags & SMB2_FLAGS_ASYNC_COMMAND) { command_list = &conn->async_requests; @@ -7224,16 +9730,39 @@ int smb2_cancel(struct ksmbd_work *work) le64_to_cpu(hdr->Id.AsyncId)) continue; + /* + * Only an ACTIVE deferred work may have its cancel_fn + * fired. A CANCELLED or CLOSED work already took the + * smb2_lock() non-ACTIVE early-exit that frees the + * file_lock and skips release_async_work(), so it is + * still on conn->async_requests with a live cancel_fn + * pointing at the freed file_lock. + */ + if (cmpxchg(&iter->state, KSMBD_WORK_ACTIVE, + KSMBD_WORK_CANCELLED) != KSMBD_WORK_ACTIVE) + break; + ksmbd_debug(SMB, "smb2 with AsyncId %llu cancelled command = 0x%x\n", le64_to_cpu(hdr->Id.AsyncId), le16_to_cpu(chdr->Command)); - iter->state = KSMBD_WORK_CANCELLED; - if (iter->cancel_fn) + if (iter->cancel_fn == smb2_notify_cancel_fn) + cancelled_notify = + smb2_notify_cancel_claim(iter->cancel_argv); + else 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; @@ -7245,11 +9774,16 @@ int smb2_cancel(struct ksmbd_work *work) iter == work) continue; + if (cmpxchg(&iter->state, KSMBD_WORK_ACTIVE, + KSMBD_WORK_CANCELLED) != KSMBD_WORK_ACTIVE) + break; + ksmbd_debug(SMB, "smb2 with mid %llu cancelled command = 0x%x\n", le64_to_cpu(hdr->MessageId), le16_to_cpu(chdr->Command)); - iter->state = KSMBD_WORK_CANCELLED; + if (iter->cancel_fn) + iter->cancel_fn(iter->cancel_argv); break; } spin_unlock(&conn->request_lock); @@ -7322,7 +9856,7 @@ static int smb2_set_flock_flags(struct file_lock *flock, int flags) } static struct ksmbd_lock *smb2_lock_init(struct file_lock *flock, - unsigned int cmd, int flags, + unsigned int cmd, int flags, bool zero_len, struct list_head *lock_list) { struct ksmbd_lock *lock; @@ -7336,8 +9870,7 @@ static struct ksmbd_lock *smb2_lock_init(struct file_lock *flock, lock->start = flock->fl_start; lock->end = flock->fl_end; lock->flags = flags; - if (lock->start == lock->end) - lock->zero_len = 1; + lock->zero_len = zero_len; INIT_LIST_HEAD(&lock->clist); INIT_LIST_HEAD(&lock->flist); INIT_LIST_HEAD(&lock->llist); @@ -7354,6 +9887,19 @@ static void smb2_remove_blocked_lock(void **argv) locks_wake_up(flock); } +static void smb2_free_lock(struct file_lock *flock) +{ + ksmbd_vfs_posix_lock_unblock(flock); + locks_free_lock(flock); +} + +static void smb2_free_blocked_lock(struct file_lock *flock) +{ + ksmbd_vfs_posix_lock_unblock(flock); + locks_wake_up(flock); + locks_free_lock(flock); +} + static inline bool lock_defer_pending(struct file_lock *fl) { /* check pending lock waiters */ @@ -7385,23 +9931,55 @@ int smb2_lock(struct ksmbd_work *work) LIST_HEAD(lock_list); LIST_HEAD(rollback_list); int prior_lock = 0, bkt; + unsigned int id = KSMBD_NO_FID, pid = KSMBD_NO_FID; + bool lock_replayed; WORK_BUFFERS(work, req, rsp); ksmbd_debug(SMB, "Received smb2 lock request\n"); - fp = ksmbd_lookup_fd_slow(work, req->VolatileFileId, req->PersistentFileId); + + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + + if (work->next_smb2_rcv_hdr_off && + !has_file_id(req->VolatileFileId)) { + ksmbd_debug(SMB, "Compound request set FID = %llu\n", + work->compound_fid); + id = work->compound_fid; + pid = work->compound_pfid; + } + + if (!has_file_id(id)) { + id = req->VolatileFileId; + pid = req->PersistentFileId; + } + + fp = ksmbd_lookup_fd_slow(work, id, pid); if (!fp) { ksmbd_debug(SMB, "Invalid file id for lock : %llu\n", req->VolatileFileId); err = -ENOENT; goto out2; } + err = smb2_set_request_open(work, fp, &req->hdr, false, false); + if (err) + goto out2; + + lock_replayed = smb2_verify_lock_sequence(work, fp, req); + if (lock_replayed) + goto lock_success; + filp = fp->filp; lock_count = le16_to_cpu(req->LockCount); lock_ele = req->locks; ksmbd_debug(SMB, "lock count is %d\n", lock_count); - if (!lock_count) { + /* + * Cap lock_count at 64. The MS-SMB2 spec defines Open.LockSequenceArray + * as exactly 64 entries so 64 is the intended ceiling. No real workload + * comes close to this in a single request. + */ + if (!lock_count || lock_count > 64) { err = -EINVAL; goto out2; } @@ -7417,32 +9995,18 @@ 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 > U64_MAX - lock_length) { + if (lock_start > OFFSET_MAX || + (lock_length && + lock_length - 1 > OFFSET_MAX - lock_start)) { pr_err("Invalid lock range requested\n"); rsp->hdr.Status = STATUS_INVALID_LOCK_RANGE; locks_free_lock(flock); goto out; } - if (lock_start > OFFSET_MAX) - flock->fl_start = OFFSET_MAX; - else - flock->fl_start = lock_start; - - lock_length = le64_to_cpu(lock_ele[i].Length); - if (lock_length > OFFSET_MAX - flock->fl_start) - lock_length = OFFSET_MAX - flock->fl_start; - - flock->fl_end = flock->fl_start + lock_length; - - if (flock->fl_end < flock->fl_start) { - ksmbd_debug(SMB, - "the end offset(%llx) is smaller than the start offset(%llx)\n", - flock->fl_end, flock->fl_start); - 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; /* Check conflict locks in one request */ list_for_each_entry(cmp_lock, &lock_list, llist) { @@ -7458,7 +10022,8 @@ int smb2_lock(struct ksmbd_work *work) } } - smb_lock = smb2_lock_init(flock, cmd, flags, &lock_list); + smb_lock = smb2_lock_init(flock, cmd, flags, !lock_length, + &lock_list); if (!smb_lock) { err = -EINVAL; locks_free_lock(flock); @@ -7467,6 +10032,13 @@ int smb2_lock(struct ksmbd_work *work) } list_for_each_entry_safe(smb_lock, tmp, &lock_list, llist) { + if (lock_count > 1 && + !(le32_to_cpu(lock_ele[0].Flags) & SMB2_LOCKFLAG_UNLOCK) && + !(smb_lock->flags & SMB2_LOCKFLAG_FAIL_IMMEDIATELY)) { + err = -EINVAL; + goto out; + } + if (smb_lock->cmd < 0) { err = -EINVAL; goto out; @@ -7507,12 +10079,14 @@ int smb2_lock(struct ksmbd_work *work) cmp_lock->end == smb_lock->end && !lock_defer_pending(cmp_lock->fl)) { nolock = 0; - list_del(&cmp_lock->flist); - list_del(&cmp_lock->clist); + 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); - locks_free_lock(cmp_lock->fl); + ksmbd_conn_put(conn); + smb2_free_lock(cmp_lock->fl); kfree(cmp_lock); goto out_check_cl; } @@ -7530,7 +10104,7 @@ int smb2_lock(struct ksmbd_work *work) /* 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) { + 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"); @@ -7539,17 +10113,15 @@ int smb2_lock(struct ksmbd_work *work) if (smb_lock->zero_len && !cmp_lock->zero_len && smb_lock->start > cmp_lock->start && - smb_lock->start < cmp_lock->end) { + 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->start && - cmp_lock->end > smb_lock->start) || - (cmp_lock->start < smb_lock->end && - cmp_lock->end >= smb_lock->end)) && + 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); @@ -7579,25 +10151,27 @@ retry: rc = vfs_lock_file(filp, smb_lock->cmd, flock, NULL); skip: if (smb_lock->flags & SMB2_LOCKFLAG_UNLOCK) { + locks_free_lock(flock); + kfree(smb_lock); if (!rc) { ksmbd_debug(SMB, "File unlocked\n"); } else if (rc == -ENOENT) { rsp->hdr.Status = STATUS_NOT_LOCKED; + err = rc; goto out; } - locks_free_lock(flock); - kfree(smb_lock); } else { if (rc == FILE_LOCK_DEFERRED) { void **argv; ksmbd_debug(SMB, "would have to wait for getting lock\n"); - list_add(&smb_lock->llist, &rollback_list); argv = kmalloc(sizeof(void *), KSMBD_DEFAULT_GFP); if (!argv) { err = -ENOMEM; + smb2_free_blocked_lock(flock); + kfree(smb_lock); goto out; } argv[0] = flock; @@ -7608,8 +10182,11 @@ skip: if (rc) { kfree(argv); err = -ENOMEM; + smb2_free_blocked_lock(flock); + kfree(smb_lock); goto out; } + 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); @@ -7622,39 +10199,38 @@ skip: list_del(&work->fp_entry); spin_unlock(&fp->f_lock); - if (work->state != KSMBD_WORK_ACTIVE) { - list_del(&smb_lock->llist); - locks_free_lock(flock); - - if (work->state == KSMBD_WORK_CANCELLED) { - rsp->hdr.Status = - STATUS_CANCELLED; - kfree(smb_lock); - smb2_send_interim_resp(work, - STATUS_CANCELLED); - work->send_no_response = 1; - goto out; - } + list_del(&smb_lock->llist); - rsp->hdr.Status = - STATUS_RANGE_NOT_LOCKED; + if (work->state == KSMBD_WORK_CANCELLED) { + rsp->hdr.Status = STATUS_CANCELLED; kfree(smb_lock); - goto out2; + smb2_send_interim_resp(work, + STATUS_CANCELLED); + release_async_work(work); + locks_free_lock(flock); + work->send_no_response = 1; + goto out; } - list_del(&smb_lock->llist); release_async_work(work); - goto retry; + + if (work->state == KSMBD_WORK_ACTIVE) + goto retry; + + locks_free_lock(flock); + + rsp->hdr.Status = + STATUS_RANGE_NOT_LOCKED; + kfree(smb_lock); + /* rollback_list may still hold earlier grants */ + goto out; } else if (!rc) { list_add(&smb_lock->llist, &rollback_list); - spin_lock(&work->conn->llist_lock); - 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_debug(SMB, "successful in taking lock\n"); } else { + locks_free_lock(flock); + kfree(smb_lock); + err = rc; goto out; } } @@ -7663,6 +10239,7 @@ skip: if (atomic_read(&fp->f_ci->op_count) > 1) smb_break_all_oplock(work, fp); +lock_success: rsp->StructureSize = cpu_to_le16(4); ksmbd_debug(SMB, "successful in taking lock\n"); rsp->hdr.Status = STATUS_SUCCESS; @@ -7671,6 +10248,23 @@ skip: if (err) 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); + } + + if (!lock_replayed) + smb2_update_lock_sequence(work, fp, req); + ksmbd_fd_put(work, fp); return 0; @@ -7685,23 +10279,22 @@ out: struct file_lock *rlock = NULL; rlock = smb_flock_init(filp); - rlock->c.flc_type = F_UNLCK; - rlock->fl_start = smb_lock->start; - rlock->fl_end = smb_lock->end; + if (rlock) { + rlock->c.flc_type = F_UNLCK; + rlock->fl_start = smb_lock->start; + rlock->fl_end = smb_lock->end; - rc = vfs_lock_file(filp, F_SETLK, rlock, NULL); - if (rc) - pr_err("rollback unlock fail : %d\n", rc); + 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"); + } list_del(&smb_lock->llist); - spin_lock(&work->conn->llist_lock); - if (!list_empty(&smb_lock->flist)) - list_del(&smb_lock->flist); - list_del(&smb_lock->clist); - spin_unlock(&work->conn->llist_lock); - - locks_free_lock(smb_lock->fl); - locks_free_lock(rlock); + smb2_free_lock(smb_lock->fl); + if (rlock) + locks_free_lock(rlock); kfree(smb_lock); } out2: @@ -7751,23 +10344,44 @@ static int fsctl_copychunk(struct ksmbd_work *work, cpu_to_le32(ksmbd_server_side_copy_max_total_size()); chunk_count = le32_to_cpu(ci_req->ChunkCount); - if (chunk_count == 0) + /* + * ChunkCount=0 is the standard SMB2 "query my copy limits" request + * (no data copied) -- but macOS Finder's Cmd+D duplicate sends + * FSCTL_SRV_COPYCHUNK with ChunkCount=0 meaning "copy the whole + * file", relying on the AAPL-negotiated server to do a full copy + * instead. Keep the standard no-op behavior for everyone else. + * + * Gate on the TIME_MACHINE share flag, not just conn->is_aapl: + * that flag alone has ambiguous provenance -- the pre-existing + * narrow UniqueId=0 path can also set it on ordinary, + * non-Time-Machine shares, and this series' stated design keeps + * every AAPL-driven behavior opt-in per share. + */ + if (chunk_count == 0 && + !(work->conn->is_aapl && + test_share_config_flag(work->tcon->share_conf, + KSMBD_SHARE_FLAG_TIME_MACHINE))) goto out; total_size_written = 0; + i = 0; - /* verify the SRV_COPYCHUNK_COPY packet */ - if (chunk_count > ksmbd_server_side_copy_max_chunk_count() || - input_count < struct_size(ci_req, Chunks, chunk_count)) { - rsp->hdr.Status = STATUS_INVALID_PARAMETER; - return -EINVAL; - } + if (chunk_count) { + /* verify the SRV_COPYCHUNK_COPY packet */ + if (chunk_count > ksmbd_server_side_copy_max_chunk_count() || + input_count < struct_size(ci_req, Chunks, chunk_count)) { + rsp->hdr.Status = STATUS_INVALID_PARAMETER; + return -EINVAL; + } - chunks = &ci_req->Chunks[0]; - for (i = 0; i < chunk_count; i++) { - if (le32_to_cpu(chunks[i].Length) == 0 || - le32_to_cpu(chunks[i].Length) > ksmbd_server_side_copy_max_chunk_size()) - break; - total_size_written += le32_to_cpu(chunks[i].Length); + chunks = &ci_req->Chunks[0]; + for (i = 0; i < chunk_count; i++) { + if (le32_to_cpu(chunks[i].Length) == 0 || + le32_to_cpu(chunks[i].Length) > ksmbd_server_side_copy_max_chunk_size()) + break; + total_size_written += le32_to_cpu(chunks[i].Length); + } + } else { + chunks = &ci_req->Chunks[0]; } if (i < chunk_count || @@ -7793,9 +10407,9 @@ static int fsctl_copychunk(struct ksmbd_work *work, /* * FILE_READ_DATA should only be included in - * the FSCTL_COPYCHUNK case + * the FSCTL_SRV_COPYCHUNK case */ - if (cnt_code == FSCTL_COPYCHUNK && + if (cnt_code == FSCTL_SRV_COPYCHUNK && !(dst_fp->daccess & (FILE_READ_DATA_LE | FILE_GENERIC_READ_LE))) { rsp->hdr.Status = STATUS_ACCESS_DENIED; goto out; @@ -7809,7 +10423,7 @@ static int fsctl_copychunk(struct ksmbd_work *work, if (ret < 0) { if (ret == -EACCES) rsp->hdr.Status = STATUS_ACCESS_DENIED; - if (ret == -EAGAIN) + else if (ret == -EAGAIN) rsp->hdr.Status = STATUS_FILE_LOCK_CONFLICT; else if (ret == -EBADF) rsp->hdr.Status = STATUS_INVALID_HANDLE; @@ -7982,7 +10596,7 @@ static int fsctl_validate_negotiate_info(struct ksmbd_conn *conn, goto err_out; } - if (strncmp(neg_req->Guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE)) { + if (memcmp(neg_req->Guid, conn->ClientGUID, SMB2_CLIENT_GUID_SIZE)) { ret = -EINVAL; goto err_out; } @@ -8015,8 +10629,6 @@ static int fsctl_query_allocated_ranges(struct ksmbd_work *work, u64 id, int ret = 0; *out_count = 0; - if (in_count == 0) - return -EINVAL; start = le64_to_cpu(qar_req->file_offset); length = le64_to_cpu(qar_req->length); @@ -8028,11 +10640,28 @@ static int fsctl_query_allocated_ranges(struct ksmbd_work *work, u64 id, if (!fp) return -ENOENT; - ret = ksmbd_vfs_fqar_lseek(fp, start, length, - qar_rsp, in_count, out_count); + if (!(fp->daccess & FILE_READ_DATA_LE)) { + ret = -EACCES; + goto out; + } + + if (!in_count) { + struct file_allocated_range_buffer range; + + ret = ksmbd_vfs_query_allocated_ranges(fp, start, length, + &range, 1, out_count); + if ((!ret || ret == -E2BIG) && *out_count) + ret = -ENOSPC; + *out_count = 0; + } else { + ret = ksmbd_vfs_query_allocated_ranges(fp, start, length, + qar_rsp, in_count, + out_count); + } if (ret && ret != -E2BIG) *out_count = 0; +out: ksmbd_fd_put(work, fp); return ret; } @@ -8090,31 +10719,62 @@ static inline int fsctl_set_sparse(struct ksmbd_work *work, u64 id, int ret = 0; __le32 old_fattr; + if (!test_tree_conn_flag(work->tcon, KSMBD_TREE_CONN_FLAG_WRITABLE)) { + ksmbd_debug(SMB, "User does not have write permission\n"); + return -EACCES; + } + fp = ksmbd_lookup_fd_fast(work, id); if (!fp) return -ENOENT; + + if (S_ISDIR(file_inode(fp->filp)->i_mode)) { + ret = -EINVAL; + goto out; + } + + if (!(fp->daccess & (FILE_WRITE_DATA_LE | FILE_APPEND_DATA_LE | + FILE_WRITE_ATTRIBUTES_LE))) { + ret = -EACCES; + goto out; + } + idmap = file_mnt_idmap(fp->filp); old_fattr = fp->f_ci->m_fattr; + if (!sparse->SetSparse && + (old_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE)) { + ret = ksmbd_vfs_zero_holes(fp); + if (ret) + goto out; + } + if (sparse->SetSparse) fp->f_ci->m_fattr |= FILE_ATTRIBUTE_SPARSE_FILE_LE; else fp->f_ci->m_fattr &= ~FILE_ATTRIBUTE_SPARSE_FILE_LE; - if (fp->f_ci->m_fattr != old_fattr && - test_share_config_flag(work->tcon->share_conf, - KSMBD_SHARE_FLAG_STORE_DOS_ATTRS)) { - struct xattr_dos_attrib da; + if (fp->f_ci->m_fattr != old_fattr) { + const struct cred *saved_cred; + struct xattr_dos_attrib da = {0}; ret = ksmbd_vfs_get_dos_attrib_xattr(idmap, fp->filp->f_path.dentry, &da); - if (ret <= 0) - goto out; + if (ret <= 0) { + da.version = 4; + da.itime = fp->itime; + da.create_time = fp->create_time; + da.flags = XATTR_DOSINFO_CREATE_TIME | + XATTR_DOSINFO_ITIME; + } da.attr = le32_to_cpu(fp->f_ci->m_fattr); + da.flags |= XATTR_DOSINFO_ATTRIB; + saved_cred = override_creds(fp->filp->f_cred); ret = ksmbd_vfs_set_dos_attrib_xattr(idmap, &fp->filp->f_path, &da, true); + revert_creds(saved_cred); if (ret) fp->f_ci->m_fattr = old_fattr; } @@ -8153,28 +10813,35 @@ int smb2_ioctl(struct ksmbd_work *work) struct smb2_ioctl_req *req; struct smb2_ioctl_rsp *rsp; unsigned int cnt_code, nbytes = 0, out_buf_len, in_buf_len; - u64 id = KSMBD_NO_FID; + u64 id = KSMBD_NO_FID, pid = KSMBD_NO_FID; struct ksmbd_conn *conn = work->conn; int ret = 0; char *buffer; + bool no_fileid_ioctl = false; + bool chseq_err = false; ksmbd_debug(SMB, "Received smb2 ioctl request\n"); if (work->next_smb2_rcv_hdr_off) { req = ksmbd_req_buf_next(work); rsp = ksmbd_resp_buf_next(work); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; if (!has_file_id(req->VolatileFileId)) { ksmbd_debug(SMB, "Compound request set FID = %llu\n", work->compound_fid); id = work->compound_fid; + pid = work->compound_pfid; } } else { req = smb_get_msg(work->request_buf); rsp = smb_get_msg(work->response_buf); } - if (!has_file_id(id)) + if (!has_file_id(id)) { id = req->VolatileFileId; + pid = req->PersistentFileId; + } if (req->Flags != cpu_to_le32(SMB2_0_IOCTL_IS_FSCTL)) { ret = -EOPNOTSUPP; @@ -8184,8 +10851,44 @@ int smb2_ioctl(struct ksmbd_work *work) buffer = (char *)req + le32_to_cpu(req->InputOffset); cnt_code = le32_to_cpu(req->CtlCode); - ret = smb2_calc_max_out_buf_len(work, 48, - le32_to_cpu(req->MaxOutputResponse)); + switch (cnt_code) { + 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: + case FSCTL_PIPE_TRANSCEIVE: + no_fileid_ioctl = true; + break; + default: + break; + } + + if (!no_fileid_ioctl && has_file_id(id)) { + struct ksmbd_file *fp; + + fp = ksmbd_lookup_fd_slow(work, id, pid); + if (!fp) { + if (cnt_code == FSCTL_DUPLICATE_EXTENTS_TO_FILE) { + rsp->hdr.Status = STATUS_FILE_CLOSED; + goto out2; + } + ret = -ENOENT; + goto out; + } + + ret = smb2_set_request_open(work, fp, &req->hdr, true, false); + ksmbd_fd_put(work, fp); + if (ret) { + rsp->hdr.Status = STATUS_FILE_NOT_AVAILABLE; + chseq_err = true; + goto out; + } + } + + ret = smb2_calc_max_out_buf_len(work, + offsetof(struct smb2_ioctl_rsp, Buffer), + le32_to_cpu(req->MaxOutputResponse)); if (ret < 0) { rsp->hdr.Status = STATUS_INVALID_PARAMETER; goto out; @@ -8194,15 +10897,110 @@ int smb2_ioctl(struct ksmbd_work *work) in_buf_len = le32_to_cpu(req->InputCount); switch (cnt_code) { + case FSCTL_SRV_ENUM_SNAPS: { + struct srv_snapshot_array *snap_rsp; + struct ksmbd_file *fp; + + if (out_buf_len < sizeof(*snap_rsp)) { + ret = -EINVAL; + goto out; + } + + fp = ksmbd_lookup_fd_fast(work, id); + if (!fp) { + ret = -ENOENT; + goto out; + } + ksmbd_fd_put(work, fp); + + snap_rsp = (struct srv_snapshot_array *)rsp->Buffer; + snap_rsp->NumberOfSnapShots = 0; + snap_rsp->NumberOfSnapShotsReturned = 0; + snap_rsp->SnapShotArraySize = cpu_to_le32(2); + snap_rsp->Reserved = 0; + nbytes = sizeof(*snap_rsp); + break; + } case FSCTL_DFS_GET_REFERRALS: case FSCTL_DFS_GET_REFERRALS_EX: /* Not support DFS yet */ ret = -EOPNOTSUPP; rsp->hdr.Status = STATUS_FS_DRIVER_REQUIRED; goto out2; + case FSCTL_GET_COMPRESSION: { + struct compress_ioctl *cmpr_rsp; + struct ksmbd_file *fp; + u16 fmt; + + if (out_buf_len < sizeof(struct compress_ioctl)) { + ret = -EINVAL; + goto out; + } + + fp = ksmbd_lookup_fd_fast(work, id); + if (!fp) { + ret = -ENOENT; + goto out; + } + + ret = ksmbd_vfs_get_compression(fp, &fmt); + ksmbd_fd_put(work, fp); + if (ret < 0) + goto out; + + cmpr_rsp = (struct compress_ioctl *)&rsp->Buffer[0]; + cmpr_rsp->CompressionState = cpu_to_le16(fmt); + nbytes = sizeof(struct compress_ioctl); + rsp->PersistentFileId = req->PersistentFileId; + rsp->VolatileFileId = req->VolatileFileId; + break; + } + case FSCTL_SET_COMPRESSION: { + struct compress_ioctl *cmpr_req; + struct ksmbd_file *fp; + + if (in_buf_len < sizeof(struct compress_ioctl)) { + ret = -EINVAL; + goto out; + } + + if (!test_tree_conn_flag(work->tcon, KSMBD_TREE_CONN_FLAG_WRITABLE)) { + ksmbd_debug(SMB, "User does not have write permission\n"); + ret = -EACCES; + goto out; + } + + cmpr_req = (struct compress_ioctl *)buffer; + fp = ksmbd_lookup_fd_fast(work, id); + if (!fp) { + ret = -ENOENT; + goto out; + } + + ret = ksmbd_vfs_set_compression(work, fp, le16_to_cpu(cmpr_req->CompressionState)); + ksmbd_fd_put(work, fp); + if (ret) + goto out; + break; + } case FSCTL_CREATE_OR_GET_OBJECT_ID: { struct file_object_buf_type1_ioctl_rsp *obj_buf; + struct ksmbd_file *fp; + + fp = ksmbd_lookup_fd_fast(work, id); + if (!fp) { + ret = -EBADF; + rsp->hdr.Status = STATUS_FILE_CLOSED; + goto out2; + } + + if (out_buf_len < sizeof(struct file_object_buf_type1_ioctl_rsp)) { + ksmbd_fd_put(work, fp); + ret = -EINVAL; + goto out; + } + ksmbd_fd_put(work, fp); nbytes = sizeof(struct file_object_buf_type1_ioctl_rsp); obj_buf = (struct file_object_buf_type1_ioctl_rsp *) @@ -8252,12 +11050,18 @@ int smb2_ioctl(struct ksmbd_work *work) rsp->VolatileFileId = SMB2_NO_FID; break; case FSCTL_QUERY_NETWORK_INTERFACE_INFO: + if (req->PersistentFileId != SMB2_NO_FID || + req->VolatileFileId != SMB2_NO_FID) { + ret = -EINVAL; + goto out; + } + ret = fsctl_query_iface_info_ioctl(conn, rsp, out_buf_len); if (ret < 0) goto out; nbytes = ret; break; - case FSCTL_REQUEST_RESUME_KEY: + case FSCTL_SRV_REQUEST_RESUME_KEY: if (out_buf_len < sizeof(struct resume_key_ioctl_rsp)) { ret = -EINVAL; goto out; @@ -8271,8 +11075,8 @@ int smb2_ioctl(struct ksmbd_work *work) rsp->VolatileFileId = req->VolatileFileId; nbytes = sizeof(struct resume_key_ioctl_rsp); break; - case FSCTL_COPYCHUNK: - case FSCTL_COPYCHUNK_WRITE: + case FSCTL_SRV_COPYCHUNK: + case FSCTL_SRV_COPYCHUNK_WRITE: if (!test_tree_conn_flag(work->tcon, KSMBD_TREE_CONN_FLAG_WRITABLE)) { ksmbd_debug(SMB, "User does not have write permission\n"); @@ -8280,7 +11084,7 @@ int smb2_ioctl(struct ksmbd_work *work) goto out; } - if (in_buf_len <= sizeof(struct copychunk_ioctl_req)) { + if (in_buf_len < offsetof(struct copychunk_ioctl_req, Chunks)) { ret = -EINVAL; goto out; } @@ -8302,15 +11106,21 @@ int smb2_ioctl(struct ksmbd_work *work) rsp); break; case FSCTL_SET_SPARSE: - if (in_buf_len < sizeof(struct file_sparse)) { + { + struct file_sparse sparse = {0}; + + if (in_buf_len && in_buf_len < sizeof(struct file_sparse)) { ret = -EINVAL; goto out; } - ret = fsctl_set_sparse(work, id, (struct file_sparse *)buffer); + *(u8 *)&sparse = 1; + ret = fsctl_set_sparse(work, id, in_buf_len ? + (struct file_sparse *)buffer : &sparse); if (ret < 0) goto out; break; + } case FSCTL_SET_ZERO_DATA: { struct file_zero_data_information *zero_data; @@ -8347,13 +11157,103 @@ int smb2_ioctl(struct ksmbd_work *work) goto out; } + if (!(fp->daccess & FILE_WRITE_DATA_LE)) { + ksmbd_fd_put(work, fp); + ret = -EACCES; + goto out; + } + ret = ksmbd_vfs_zero_data(work, fp, off, len); ksmbd_fd_put(work, fp); - if (ret < 0) + if (ret == -EAGAIN) { + rsp->hdr.Status = STATUS_FILE_LOCK_CONFLICT; + ret = 0; + goto out; + } else if (ret < 0) { goto out; + } } break; } + case FSCTL_FILE_LEVEL_TRIM: + { + struct file_level_trim *trim_req; + struct file_level_trim_output *trim_rsp; + struct ksmbd_file *fp; + u32 i, num_ranges; + + if (!test_tree_conn_flag(work->tcon, KSMBD_TREE_CONN_FLAG_WRITABLE)) { + ksmbd_debug(SMB, + "User does not have write permission\n"); + ret = -EACCES; + goto out; + } + + if (in_buf_len < offsetof(struct file_level_trim, Ranges)) { + ret = -EINVAL; + goto out; + } + + if (out_buf_len < sizeof(struct file_level_trim_output)) { + ret = -EINVAL; + goto out; + } + + trim_req = (struct file_level_trim *)buffer; + num_ranges = le32_to_cpu(trim_req->NumRanges); + if (num_ranges > + (in_buf_len - offsetof(struct file_level_trim, Ranges)) / + sizeof(struct file_level_trim_range)) { + ret = -EINVAL; + goto out; + } + + fp = ksmbd_lookup_fd_fast(work, id); + if (!fp) { + ret = -ENOENT; + goto out; + } + + if (!(fp->daccess & FILE_WRITE_DATA_LE)) { + ksmbd_fd_put(work, fp); + ret = -EACCES; + goto out; + } + + trim_rsp = (struct file_level_trim_output *)&rsp->Buffer[0]; + trim_rsp->NumRangesProcessed = 0; + for (i = 0; i < num_ranges; i++) { + loff_t off = le64_to_cpu(trim_req->Ranges[i].Offset); + loff_t len = le64_to_cpu(trim_req->Ranges[i].Length); + + if (off < 0 || len < 0) { + ret = -EINVAL; + break; + } + + if (!len) { + trim_rsp->NumRangesProcessed = + cpu_to_le32(i + 1); + continue; + } + + ret = ksmbd_vfs_trim_data(work, fp, off, len); + if (ret) + break; + trim_rsp->NumRangesProcessed = cpu_to_le32(i + 1); + } + ksmbd_fd_put(work, fp); + if (ret == -EAGAIN) { + rsp->hdr.Status = STATUS_FILE_LOCK_CONFLICT; + ret = 0; + goto out; + } else if (ret < 0) { + goto out; + } + + nbytes = sizeof(struct file_level_trim_output); + break; + } case FSCTL_QUERY_ALLOCATED_RANGES: if (in_buf_len < sizeof(struct file_allocated_range_buffer)) { ret = -EINVAL; @@ -8379,6 +11279,11 @@ int smb2_ioctl(struct ksmbd_work *work) struct reparse_data_buffer *reparse_ptr; struct ksmbd_file *fp; + if (out_buf_len < sizeof(struct reparse_data_buffer)) { + ret = -EINVAL; + goto out; + } + reparse_ptr = (struct reparse_data_buffer *)&rsp->Buffer[0]; fp = ksmbd_lookup_fd_fast(work, id); if (!fp) { @@ -8411,35 +11316,64 @@ int smb2_ioctl(struct ksmbd_work *work) dup_ext->PersistentFileHandle); if (!fp_in) { pr_err("not found file handle in duplicate extent to file\n"); - ret = -ENOENT; - goto out; + ret = -EBADF; + rsp->hdr.Status = STATUS_INVALID_HANDLE; + goto out2; } fp_out = ksmbd_lookup_fd_fast(work, id); if (!fp_out) { pr_err("not found fp\n"); - ret = -ENOENT; + ret = -EBADF; + rsp->hdr.Status = STATUS_FILE_CLOSED; + ksmbd_fd_put(work, fp_in); + goto out2; + } + + if (!test_tree_conn_flag(work->tcon, + KSMBD_TREE_CONN_FLAG_WRITABLE)) { + ret = -EACCES; + goto dup_ext_out; + } + + if (!(fp_out->daccess & FILE_WRITE_DATA_LE)) { + ret = -EACCES; + goto dup_ext_out; + } + if (!(fp_in->daccess & FILE_READ_DATA_LE)) { + ret = -EACCES; goto dup_ext_out; } src_off = le64_to_cpu(dup_ext->SourceFileOffset); dst_off = le64_to_cpu(dup_ext->TargetFileOffset); length = le64_to_cpu(dup_ext->ByteCount); - /* - * XXX: It is not clear if FSCTL_DUPLICATE_EXTENTS_TO_FILE - * should fall back to vfs_copy_file_range(). This could be - * beneficial when re-exporting nfs/smb mount, but note that - * this can result in partial copy that returns an error status. - * If/when FSCTL_DUPLICATE_EXTENTS_TO_FILE_EX is implemented, - * fall back to vfs_copy_file_range(), should be avoided when - * the flag DUPLICATE_EXTENTS_DATA_EX_SOURCE_ATOMIC is set. - */ - cloned = vfs_clone_file_range(fp_in->filp, src_off, - fp_out->filp, dst_off, length, 0); - if (cloned == -EXDEV || cloned == -EOPNOTSUPP) { + if (src_off < 0 || dst_off < 0 || length < 0 || + src_off + length < src_off || dst_off + length < dst_off) { + ret = -EINVAL; + goto dup_ext_out; + } + if (src_off + length > i_size_read(file_inode(fp_in->filp))) { + ret = -EOPNOTSUPP; + goto dup_ext_out; + } + if (dst_off + length > i_size_read(file_inode(fp_out->filp))) + goto dup_ext_out; + if ((fp_in->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE) && + !(fp_out->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE)) { ret = -EOPNOTSUPP; goto dup_ext_out; - } else if (cloned != length) { + } + if (file_inode(fp_in->filp) == file_inode(fp_out->filp) && + dst_off + length > src_off && + dst_off < src_off + length) { + ret = -EOPNOTSUPP; + goto dup_ext_out; + } + + cloned = vfs_clone_file_range(fp_in->filp, src_off, + fp_out->filp, dst_off, length, 0); + if (cloned != length) { cloned = vfs_copy_file_range(fp_in->filp, src_off, fp_out->filp, dst_off, length, 0); @@ -8487,7 +11421,7 @@ out: rsp->hdr.Status = STATUS_NOT_SUPPORTED; else if (ret == -ENOSPC) rsp->hdr.Status = STATUS_BUFFER_TOO_SMALL; - else if (ret < 0 || rsp->hdr.Status == 0) + else if (!chseq_err && (ret < 0 || rsp->hdr.Status == 0)) rsp->hdr.Status = STATUS_INVALID_PARAMETER; out2: @@ -8507,11 +11441,10 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) struct smb2_oplock_break *rsp; struct ksmbd_file *fp; struct oplock_info *opinfo = NULL; - __le32 err = 0; - int ret = 0; + __le32 status = STATUS_SUCCESS; + int ret; u64 volatile_id, persistent_id; char req_oplevel = 0, rsp_oplevel = 0; - unsigned int oplock_change_type; WORK_BUFFERS(work, req, rsp); @@ -8528,6 +11461,14 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) return; } + ret = smb2_set_request_open(work, fp, &req->hdr, false, false); + if (ret) { + rsp->hdr.Status = STATUS_FILE_CLOSED; + smb2_set_err_rsp(work); + ksmbd_fd_put(work, fp); + return; + } + opinfo = opinfo_get(fp); if (!opinfo) { pr_err("unexpected null oplock_info\n"); @@ -8537,71 +11478,74 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) return; } - if (opinfo->level == SMB2_OPLOCK_LEVEL_NONE) { - rsp->hdr.Status = STATUS_INVALID_OPLOCK_PROTOCOL; + if (opinfo->op_state != OPLOCK_ACK_WAIT) { + ksmbd_debug(SMB, "unexpected oplock state 0x%x\n", + opinfo->op_state); + if (smb3_hdr_replay(&req->hdr) && + opinfo->op_state == OPLOCK_STATE_NONE) { + rsp->StructureSize = cpu_to_le16(24); + rsp->OplockLevel = opinfo->level; + rsp->Reserved = 0; + rsp->Reserved2 = 0; + rsp->VolatileFid = volatile_id; + rsp->PersistentFid = persistent_id; + ret = ksmbd_iov_pin_rsp(work, rsp, + sizeof(struct smb2_oplock_break)); + if (ret) + ksmbd_debug(SMB, + "failed to pin replayed oplock break response: %d\n", + ret); + goto out_no_state_change; + } + if (opinfo->level == SMB2_OPLOCK_LEVEL_NONE) + status = STATUS_INVALID_OPLOCK_PROTOCOL; + else + status = STATUS_INVALID_DEVICE_STATE; goto err_out; } - if (opinfo->op_state == OPLOCK_STATE_NONE) { - ksmbd_debug(SMB, "unexpected oplock state 0x%x\n", opinfo->op_state); - rsp->hdr.Status = STATUS_UNSUCCESSFUL; + if (req_oplevel == SMB2_OPLOCK_LEVEL_LEASE) { + opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + status = STATUS_INVALID_PARAMETER; goto err_out; } - if ((opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE || - opinfo->level == SMB2_OPLOCK_LEVEL_BATCH) && - (req_oplevel != SMB2_OPLOCK_LEVEL_II && - req_oplevel != SMB2_OPLOCK_LEVEL_NONE)) { - err = STATUS_INVALID_OPLOCK_PROTOCOL; - oplock_change_type = OPLOCK_WRITE_TO_NONE; - } else if (opinfo->level == SMB2_OPLOCK_LEVEL_II && - req_oplevel != SMB2_OPLOCK_LEVEL_NONE) { - err = STATUS_INVALID_OPLOCK_PROTOCOL; - oplock_change_type = OPLOCK_READ_TO_NONE; - } else if (req_oplevel == SMB2_OPLOCK_LEVEL_II || - req_oplevel == SMB2_OPLOCK_LEVEL_NONE) { - err = STATUS_INVALID_DEVICE_STATE; - if ((opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE || - opinfo->level == SMB2_OPLOCK_LEVEL_BATCH) && - req_oplevel == SMB2_OPLOCK_LEVEL_II) { - oplock_change_type = OPLOCK_WRITE_TO_READ; - } else if ((opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE || - opinfo->level == SMB2_OPLOCK_LEVEL_BATCH) && - req_oplevel == SMB2_OPLOCK_LEVEL_NONE) { - oplock_change_type = OPLOCK_WRITE_TO_NONE; - } else if (opinfo->level == SMB2_OPLOCK_LEVEL_II && - req_oplevel == SMB2_OPLOCK_LEVEL_NONE) { - oplock_change_type = OPLOCK_READ_TO_NONE; - } else { - oplock_change_type = 0; - } - } else { - oplock_change_type = 0; + if (opinfo->level == SMB2_OPLOCK_LEVEL_NONE) { + status = STATUS_INVALID_OPLOCK_PROTOCOL; + goto err_out; } - switch (oplock_change_type) { - case OPLOCK_WRITE_TO_READ: - ret = opinfo_write_to_read(opinfo); - rsp_oplevel = SMB2_OPLOCK_LEVEL_II; - break; - case OPLOCK_WRITE_TO_NONE: - ret = opinfo_write_to_none(opinfo); - rsp_oplevel = SMB2_OPLOCK_LEVEL_NONE; - break; - case OPLOCK_READ_TO_NONE: - ret = opinfo_read_to_none(opinfo); - rsp_oplevel = SMB2_OPLOCK_LEVEL_NONE; - break; - default: - pr_err("unknown oplock change 0x%x -> 0x%x\n", - opinfo->level, rsp_oplevel); + if (opinfo->level == SMB2_OPLOCK_LEVEL_EXCLUSIVE && + req_oplevel != SMB2_OPLOCK_LEVEL_II && + req_oplevel != SMB2_OPLOCK_LEVEL_NONE) { + opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + status = STATUS_INVALID_OPLOCK_PROTOCOL; + goto err_out; } - if (ret < 0) { - rsp->hdr.Status = err; + if (opinfo->level == SMB2_OPLOCK_LEVEL_BATCH && + req_oplevel != SMB2_OPLOCK_LEVEL_II && + req_oplevel != SMB2_OPLOCK_LEVEL_NONE && + req_oplevel != SMB2_OPLOCK_LEVEL_EXCLUSIVE) { + opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + status = STATUS_INVALID_OPLOCK_PROTOCOL; + goto err_out; + } + + if (opinfo->level == SMB2_OPLOCK_LEVEL_II && + req_oplevel != SMB2_OPLOCK_LEVEL_NONE) { + opinfo->level = SMB2_OPLOCK_LEVEL_NONE; + status = STATUS_INVALID_OPLOCK_PROTOCOL; goto err_out; } + if (req_oplevel == SMB2_OPLOCK_LEVEL_EXCLUSIVE) + rsp_oplevel = SMB2_OPLOCK_LEVEL_NONE; + else + rsp_oplevel = req_oplevel; + + opinfo->level = rsp_oplevel; + rsp->StructureSize = cpu_to_le16(24); rsp->OplockLevel = rsp_oplevel; rsp->Reserved = 0; @@ -8609,27 +11553,37 @@ static void smb20_oplock_break_ack(struct ksmbd_work *work) rsp->VolatileFid = volatile_id; rsp->PersistentFid = persistent_id; ret = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_oplock_break)); - if (ret) { + if (ret) + ksmbd_debug(SMB, "failed to pin oplock break response: %d\n", + ret); + goto out; + err_out: - smb2_set_err_rsp(work); - } + rsp->hdr.Status = status; + smb2_set_err_rsp(work); - opinfo->op_state = OPLOCK_STATE_NONE; +out: + spin_lock(&opinfo->state_lock); + if (opinfo->op_state != OPLOCK_CLOSING) + opinfo->op_state = OPLOCK_STATE_NONE; + spin_unlock(&opinfo->state_lock); wake_up_interruptible_all(&opinfo->oplock_q); +out_no_state_change: opinfo_put(opinfo); ksmbd_fd_put(work, fp); } -static int check_lease_state(struct lease *lease, __le32 req_state) +static bool smb2_lease_state_valid(__le32 state) { - if ((lease->new_state == - (SMB2_LEASE_READ_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE)) && - !(req_state & SMB2_LEASE_WRITE_CACHING_LE)) { - lease->new_state = req_state; - return 0; - } + return !(state & ~(SMB2_LEASE_READ_CACHING_LE | + SMB2_LEASE_HANDLE_CACHING_LE | + SMB2_LEASE_WRITE_CACHING_LE)); +} - if (lease->new_state == req_state) +static int check_lease_state(struct lease *lease, __le32 req_state) +{ + if (smb2_lease_state_valid(req_state) && + !(req_state & ~lease->new_state)) return 0; return 1; @@ -8647,9 +11601,7 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) struct smb2_lease_ack *req; struct smb2_lease_ack *rsp; struct oplock_info *opinfo; - __le32 err = 0; int ret = 0; - unsigned int lease_change_type; __le32 lease_state; struct lease *lease; @@ -8669,84 +11621,31 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) 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)) + goto replay_rsp; rsp->hdr.Status = STATUS_UNSUCCESSFUL; goto err_out; } - if (check_lease_state(lease, req->LeaseState)) { - rsp->hdr.Status = STATUS_REQUEST_NOT_ACCEPTED; - ksmbd_debug(OPLOCK, - "req lease state: 0x%x, expected state: 0x%x\n", - req->LeaseState, lease->new_state); - goto err_out; - } - if (!atomic_read(&opinfo->breaking_cnt)) { + if (smb3_hdr_replay(&req->hdr)) + goto replay_rsp; rsp->hdr.Status = STATUS_UNSUCCESSFUL; goto err_out; } - /* check for bad lease state */ - if (req->LeaseState & - (~(SMB2_LEASE_READ_CACHING_LE | SMB2_LEASE_HANDLE_CACHING_LE))) { - err = STATUS_INVALID_OPLOCK_PROTOCOL; - if (lease->state & SMB2_LEASE_WRITE_CACHING_LE) - lease_change_type = OPLOCK_WRITE_TO_NONE; - else - lease_change_type = OPLOCK_READ_TO_NONE; - ksmbd_debug(OPLOCK, "handle bad lease state 0x%x -> 0x%x\n", - le32_to_cpu(lease->state), - le32_to_cpu(req->LeaseState)); - } else if (lease->state == SMB2_LEASE_READ_CACHING_LE && - req->LeaseState != SMB2_LEASE_NONE_LE) { - err = STATUS_INVALID_OPLOCK_PROTOCOL; - lease_change_type = OPLOCK_READ_TO_NONE; - ksmbd_debug(OPLOCK, "handle bad lease state 0x%x -> 0x%x\n", - le32_to_cpu(lease->state), - le32_to_cpu(req->LeaseState)); - } else { - /* valid lease state changes */ - err = STATUS_INVALID_DEVICE_STATE; - if (req->LeaseState == SMB2_LEASE_NONE_LE) { - if (lease->state & SMB2_LEASE_WRITE_CACHING_LE) - lease_change_type = OPLOCK_WRITE_TO_NONE; - else - lease_change_type = OPLOCK_READ_TO_NONE; - } else if (req->LeaseState & SMB2_LEASE_READ_CACHING_LE) { - if (lease->state & SMB2_LEASE_WRITE_CACHING_LE) - lease_change_type = OPLOCK_WRITE_TO_READ; - else - lease_change_type = OPLOCK_READ_HANDLE_TO_READ; - } else { - lease_change_type = 0; - } - } - - switch (lease_change_type) { - case OPLOCK_WRITE_TO_READ: - ret = opinfo_write_to_read(opinfo); - break; - case OPLOCK_READ_HANDLE_TO_READ: - ret = opinfo_read_handle_to_read(opinfo); - break; - case OPLOCK_WRITE_TO_NONE: - ret = opinfo_write_to_none(opinfo); - break; - case OPLOCK_READ_TO_NONE: - ret = opinfo_read_to_none(opinfo); - break; - default: - ksmbd_debug(OPLOCK, "unknown lease change 0x%x -> 0x%x\n", - le32_to_cpu(lease->state), - le32_to_cpu(req->LeaseState)); - } - - if (ret < 0) { - rsp->hdr.Status = err; + if (check_lease_state(lease, req->LeaseState)) { + rsp->hdr.Status = STATUS_REQUEST_NOT_ACCEPTED; + ksmbd_debug(OPLOCK, + "req lease state: 0x%x, expected state: 0x%x\n", + req->LeaseState, lease->new_state); goto err_out; } - lease_state = lease->state; + 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; @@ -8755,16 +11654,36 @@ static void smb21_lease_break_ack(struct ksmbd_work *work) rsp->LeaseState = lease_state; rsp->LeaseDuration = 0; ret = ksmbd_iov_pin_rsp(work, rsp, sizeof(struct smb2_lease_ack)); - if (ret) { -err_out: - smb2_set_err_rsp(work); - } + if (ret) + goto err_out; - opinfo->op_state = OPLOCK_STATE_NONE; + spin_lock(&opinfo->state_lock); + if (opinfo->op_state != OPLOCK_CLOSING) + opinfo->op_state = OPLOCK_STATE_NONE; + spin_unlock(&opinfo->state_lock); wake_up_interruptible_all(&opinfo->oplock_q); - atomic_dec(&opinfo->breaking_cnt); + atomic_dec_if_positive(&opinfo->breaking_cnt); wake_up_interruptible_all(&opinfo->oplock_brk); opinfo_put(opinfo); + return; + +replay_rsp: + 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; + opinfo_put(opinfo); + return; + +err_out: + smb2_set_err_rsp(work); + opinfo_put(opinfo); + return; } /** @@ -8800,6 +11719,139 @@ 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; +}; + +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) +{ + 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; + + 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); +} + /** * smb2_notify() - handler for smb2 notify request * @work: smb work containing notify command buffer @@ -8810,20 +11862,177 @@ 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; ksmbd_debug(SMB, "Received smb2 notify\n"); WORK_BUFFERS(work, req, rsp); + if (smb2_compound_has_failed(work, &rsp->hdr)) + return -EACCES; + if (work->next_smb2_rcv_hdr_off && req->hdr.NextCommand) { rsp->hdr.Status = STATUS_INTERNAL_ERROR; smb2_set_err_rsp(work); return -EIO; } - smb2_set_err_rsp(work); - rsp->hdr.Status = STATUS_NOT_IMPLEMENTED; - return -EOPNOTSUPP; + /* + * 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; + } + + /* + * 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; + } + + 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); + 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; + } + memcpy(smb_get_msg(in_work->request_buf), req, + __SMB2_HEADER_STRUCTURE_SIZE); + + if (setup_async_work(work, NULL, NULL)) { + ksmbd_free_work_struct(in_work); + ksmbd_fd_put(work, fp); + rsp->hdr.Status = STATUS_INSUFFICIENT_RESOURCES; + smb2_set_err_rsp(work); + return 0; + } + + 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; + + /* + * 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. + */ + in_work->async_id = work->async_id; + work->async_id = 0; + release_async_work(work); + + /* + * 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; + } + + spin_lock(&fp->f_lock); + list_add_tail(&in_work->notify_entry, &fp->notify_pendings); + spin_unlock(&fp->f_lock); + + ksmbd_fd_put(work, fp); + work->send_no_response = 1; + return 0; } /** @@ -8838,9 +12047,7 @@ bool smb2_is_sign_req(struct ksmbd_work *work, unsigned int command) struct smb2_hdr *rcv_hdr2 = smb_get_msg(work->request_buf); if ((rcv_hdr2->Flags & SMB2_FLAGS_SIGNED) && - command != SMB2_NEGOTIATE_HE && - command != SMB2_SESSION_SETUP_HE && - command != SMB2_OPLOCK_BREAK_HE) + command != SMB2_NEGOTIATE_HE) return true; return false; @@ -8890,6 +12097,39 @@ int smb2_check_sign_req(struct ksmbd_work *work) } /** + * smb2_get_sign_rsp_iov() - get the iovecs used to sign a response + * @work: work that has the response iovecs + * @hdr: SMB2 header of the response + * @n_vec: set to the number of iovecs to sign + * + * Response data may be in another buffer. In this case, the response uses + * more than one iovec. Find the iovec that starts with @hdr. Sign this + * iovec and all iovecs after it. + * + * Return: The first iovec to sign. + */ +static struct kvec *smb2_get_sign_rsp_iov(struct ksmbd_work *work, + struct smb2_hdr *hdr, int *n_vec) +{ + int i; + + /* + * iov[0] has the RFC1002 message length. It is not part of the SMB2 + * message, so do not sign it. + */ + for (i = 1; i <= work->iov_idx; i++) { + if (work->iov[i].iov_base == hdr) { + *n_vec = work->iov_idx - i + 1; + return &work->iov[i]; + } + } + + WARN_ON_ONCE(1); + *n_vec = 1; + return &work->iov[work->iov_idx]; +} + +/** * smb2_set_sign_rsp() - handler for rsp packet sign processing * @work: smb work containing notify command buffer * @@ -8899,18 +12139,13 @@ void smb2_set_sign_rsp(struct ksmbd_work *work) struct smb2_hdr *hdr; char signature[SMB2_HMACSHA256_SIZE]; struct kvec *iov; - int n_vec = 1; + int n_vec; hdr = ksmbd_resp_buf_curr(work); hdr->Flags |= SMB2_FLAGS_SIGNED; memset(hdr->Signature, 0, SMB2_SIGNATURE_SIZE); - if (hdr->Command == SMB2_READ) { - iov = &work->iov[work->iov_idx - 1]; - n_vec++; - } else { - iov = &work->iov[work->iov_idx]; - } + iov = smb2_get_sign_rsp_iov(work, hdr, &n_vec); ksmbd_sign_smb2_pdu(work->conn, work->sess->sess_key, iov, n_vec, signature); @@ -8951,9 +12186,13 @@ int smb3_check_sign_req(struct ksmbd_work *work) } else { chann = lookup_chann_list(work->sess, conn); if (!chann) { - return 0; + if (le16_to_cpu(hdr->Command) != SMB2_SESSION_SETUP_HE || + !(hdr->Flags & SMB2_FLAGS_SIGNED)) + return 0; + signing_key = work->sess->smb3signingkey; + } else { + signing_key = chann->smb3signingkey; } - signing_key = chann->smb3signingkey; } if (!signing_key) { @@ -8966,8 +12205,7 @@ int smb3_check_sign_req(struct ksmbd_work *work) iov[0].iov_base = (char *)&hdr->ProtocolId; iov[0].iov_len = len; - if (ksmbd_sign_smb3_pdu(conn, signing_key, iov, 1, signature)) - return 0; + ksmbd_sign_smb3_pdu(conn, signing_key, iov, 1, signature); if (crypto_memneq(signature, signature_req, SMB2_SIGNATURE_SIZE)) { pr_err("bad smb2 signature\n"); @@ -8989,13 +12227,14 @@ void smb3_set_sign_rsp(struct ksmbd_work *work) struct channel *chann; char signature[SMB2_CMACAES_SIZE]; struct kvec *iov; - int n_vec = 1; + u16 command = conn->ops->get_cmd_val(work); + int n_vec; char *signing_key; hdr = ksmbd_resp_buf_curr(work); - if (conn->binding == false && - le16_to_cpu(hdr->Command) == SMB2_SESSION_SETUP_HE) { + if (command == SMB2_SESSION_SETUP_HE && + (!conn->binding || hdr->Status != STATUS_SUCCESS)) { signing_key = work->sess->smb3signingkey; } else { chann = lookup_chann_list(work->sess, work->conn); @@ -9011,16 +12250,10 @@ void smb3_set_sign_rsp(struct ksmbd_work *work) hdr->Flags |= SMB2_FLAGS_SIGNED; memset(hdr->Signature, 0, SMB2_SIGNATURE_SIZE); - if (hdr->Command == SMB2_READ) { - iov = &work->iov[work->iov_idx - 1]; - n_vec++; - } else { - iov = &work->iov[work->iov_idx]; - } + iov = smb2_get_sign_rsp_iov(work, hdr, &n_vec); - if (!ksmbd_sign_smb3_pdu(conn, signing_key, iov, n_vec, - signature)) - memcpy(hdr->Signature, signature, SMB2_SIGNATURE_SIZE); + ksmbd_sign_smb3_pdu(conn, signing_key, iov, n_vec, signature); + memcpy(hdr->Signature, signature, SMB2_SIGNATURE_SIZE); } /** @@ -9039,28 +12272,30 @@ void smb3_preauth_hash_rsp(struct ksmbd_work *work) WORK_BUFFERS(work, req, rsp); - if (le16_to_cpu(req->Command) == SMB2_NEGOTIATE_HE && - conn->preauth_info) - ksmbd_gen_preauth_integrity_hash(conn, work->response_buf, - conn->preauth_info->Preauth_HashValue); + if (le16_to_cpu(req->Command) == SMB2_NEGOTIATE_HE) { + ksmbd_conn_lock(conn); + if (conn->preauth_info) + ksmbd_gen_preauth_integrity_hash(conn, work->response_buf, + conn->preauth_info->Preauth_HashValue); + ksmbd_conn_unlock(conn); + } if (le16_to_cpu(rsp->Command) == SMB2_SESSION_SETUP_HE && sess) { - __u8 *hash_value; + ksmbd_conn_lock(conn); if (conn->binding) { struct preauth_session *preauth_sess; preauth_sess = ksmbd_preauth_session_lookup(conn, sess->id); - if (!preauth_sess) - return; - hash_value = preauth_sess->Preauth_HashValue; - } else { - hash_value = sess->Preauth_HashValue; - if (!hash_value) - return; + if (preauth_sess) + ksmbd_gen_preauth_integrity_hash(conn, + work->response_buf, + preauth_sess->Preauth_HashValue); + } else if (sess->Preauth_HashValue) { + ksmbd_gen_preauth_integrity_hash(conn, work->response_buf, + sess->Preauth_HashValue); } - ksmbd_gen_preauth_integrity_hash(conn, work->response_buf, - hash_value); + ksmbd_conn_unlock(conn); } } @@ -9113,33 +12348,33 @@ bool smb3_is_transform_hdr(void *buf) int smb3_decrypt_req(struct ksmbd_work *work) { - struct ksmbd_session *sess; char *buf = work->request_buf; unsigned int pdu_length = get_rfc1002_len(buf); struct kvec iov[2]; - int buf_data_size = pdu_length - sizeof(struct smb2_transform_hdr); + unsigned int buf_data_size; struct smb2_transform_hdr *tr_hdr = smb_get_msg(buf); + unsigned int original_msg_size; int rc = 0; - if (pdu_length < sizeof(struct smb2_transform_hdr) || - buf_data_size < sizeof(struct smb2_hdr)) { + if (pdu_length < sizeof(struct smb2_transform_hdr)) { pr_err("Transform message is too small (%u)\n", pdu_length); return -ECONNABORTED; } - if (buf_data_size < le32_to_cpu(tr_hdr->OriginalMessageSize)) { - pr_err("Transform message is broken\n"); + 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) || + original_msg_size < sizeof(struct smb2_compression_hdr)) { + pr_err("Transform message is too small (%u)\n", + pdu_length); return -ECONNABORTED; } - sess = ksmbd_session_lookup_all(work->conn, le64_to_cpu(tr_hdr->SessionId)); - if (!sess) { - pr_err("invalid session id(%llx) in transform header\n", - le64_to_cpu(tr_hdr->SessionId)); + if (buf_data_size < original_msg_size) { + pr_err("Transform message is broken\n"); return -ECONNABORTED; } - ksmbd_user_session_put(sess); iov[0].iov_base = buf; iov[0].iov_len = sizeof(struct smb2_transform_hdr) + 4; @@ -9149,8 +12384,9 @@ int smb3_decrypt_req(struct ksmbd_work *work) if (rc) return rc; - memmove(buf + 4, iov[1].iov_base, buf_data_size); - *(__be32 *)buf = cpu_to_be32(buf_data_size); + /* Drop the AEAD authentication tag from the inner RFC1002 frame. */ + memmove(buf + 4, iov[1].iov_base, original_msg_size); + *(__be32 *)buf = cpu_to_be32(original_msg_size); return rc; } diff --git a/fs/smb/server/smb2pdu.h b/fs/smb/server/smb2pdu.h index 257c6d26df26..ca8e27f7b712 100644 --- a/fs/smb/server/smb2pdu.h +++ b/fs/smb/server/smb2pdu.h @@ -23,7 +23,7 @@ #define MAX_SMB2_HDR_SIZE 0x78 /* 4 len + 64 hdr + (2*24 wct) + 2 bct + 2 pad */ #define SMB21_DEFAULT_IOSIZE (1024 * 1024) -#define SMB3_DEFAULT_TRANS_SIZE (1024 * 1024) +#define SMB3_DEFAULT_TRANS_SIZE (4 * 1024 * 1024) #define SMB3_MIN_IOSIZE (64 * 1024) #define SMB3_MAX_IOSIZE (8 * 1024 * 1024) #define SMB3_MAX_MSGSIZE (4 * 4096) @@ -61,11 +61,83 @@ struct preauth_integrity_info { #define SMB2_SESSION_IN_PROGRESS BIT(0) #define SMB2_SESSION_VALID BIT(1) -#define SMB2_SESSION_TIMEOUT (10 * HZ) - /* Apple Defined Contexts */ #define SMB2_CREATE_AAPL "AAPL" +/* + * AAPL SMB2 extension -- kAAPL_SERVER_QUERY create context. + * + * Command code and bitmap values are the existing + * SMB2_CRTCTX_AAPL_* constants in fs/smb/common/smb2pdu.h. + * + * Omitting the model string when reply_bitmap includes + * SMB2_CRTCTX_AAPL_MODEL_INFO causes smbfs.kext to enter a broken + * disconnect path requiring a reboot. + * + * Layout: ccontext(16) + Name[4] + Pad[4] + cmd(4) + reserved(4) + + * reply_bitmap(8) + server_caps(8) + vol_caps(8) + * When MODEL_INFO requested, appended: pad2(4) + model_bytes(4) + UTF-16LE + */ +#define SMB2_CREATE_AAPL_LEN 4 + +/* + * Server capability flags (server_caps field) -- SMB2_CRTCTX_AAPL_UNIX_BASED: + * prevents macOS Windows-compat mode (question-mark icons). + * SMB2_CRTCTX_AAPL_SUPPORTS_OSX_COPYFILE: enables server-side file copy via + * FSCTL_SRV_COPYCHUNK. SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR: inline + * FinderInfo per FIND entry, set when client also advertises the bit; + * format: EaSize=max_access, ShortName[0..7]=rfork_size, + * ShortName[8..23]=FinderInfo(16B), Reserved2=unix_mode. + */ +#define AAPL_SERVER_CAPS_KSMBD (SMB2_CRTCTX_AAPL_UNIX_BASED | \ + SMB2_CRTCTX_AAPL_SUPPORTS_OSX_COPYFILE | \ + SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR) + +/* + * READDIR_ATTR_V2 (SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR_V2, see + * fs/smb/common/smb2pdu.h) extends the same inline-FinderInfo mechanism + * above with a flags field, confirmed byte-identical to V1 otherwise + * against AAPL's actual public client behavior. When a client's own + * client_caps requests V2, the server advertises V2 instead of V1 in + * its own server_caps reply; V1 and V2 are mutually exclusive on the + * wire, not both set together. The wire format's ShortNameLength+Reserved + * (ignored in V1) become a single flags field in V2 -- + * AAPL_READDIR_ATTR_V2_NO_XATTR is the only flag bit currently defined, + * signaling the item has no xattrs/streams so the client can skip a + * separate query. + */ +#define AAPL_READDIR_ATTR_V2_NO_XATTR 0x01 + +/* Model string: up to 31 ASCII chars */ +#define AAPL_MODEL_MAX_CHARS 31 +#define AAPL_MODEL_UTF16_BYTES (AAPL_MODEL_MAX_CHARS * 2) + +/* + * Max AAPL response: header(24) + base data(32) + pad2(4) + model_bytes(4) + * + model(62), 8-byte aligned: ALIGN(126, 8) = 128 bytes. + */ +#define AAPL_RSP_MAX_SIZE 128 + +/* AAPL server query request (client->server) */ +struct aapl_server_query_req { + __le32 cmd; + __le32 reserved; + __le64 req_bitmap; + __le64 client_caps; +} __packed; + +struct create_aapl_rsp { + struct create_context_hdr ccontext; + __u8 Name[4]; + __u8 Pad[4]; + __le32 cmd; + __le32 reserved; + __le64 reply_bitmap; + __le64 server_caps; + __le64 vol_caps; + /* when MODEL_INFO requested: __le32 pad2; __le32 model_bytes; __le16 model[] */ +} __packed; + #define DURABLE_HANDLE_MAX_TIMEOUT 300000 struct create_alloc_size_req { @@ -83,7 +155,10 @@ struct create_durable_rsp { } Data; } __packed; -/* equivalent of the contents of SMB3.1.1 POSIX open context response */ +/* + * See POSIX-SMB2 2.2.14.2.16 + * Link: https://gitlab.com/samba-team/smb3-posix-spec/-/blob/master/smb3_posix_extensions.md + */ struct create_posix_rsp { struct create_context_hdr ccontext; __u8 Name[16]; @@ -137,6 +212,7 @@ struct file_sparse { #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 @@ -183,15 +259,6 @@ struct smb2_file_alignment_info { __le32 AlignmentRequirement; } __packed; -struct smb2_file_basic_info { /* data block encoding of response to level 18 */ - __le64 CreationTime; /* Beginning of FILE_BASIC_INFO equivalent */ - __le64 LastAccessTime; - __le64 LastWriteTime; - __le64 ChangeTime; - __le32 Attributes; - __u32 Pad1; /* End of FILE_BASIC_INFO_INFO equivalent */ -} __packed; - struct smb2_file_alt_name_info { __le32 FileNameLength; char FileName[]; @@ -205,6 +272,13 @@ struct smb2_file_stream_info { char StreamName[]; } __packed; +struct srv_snapshot_array { + __le32 NumberOfSnapShots; + __le32 NumberOfSnapShotsReturned; + __le32 SnapShotArraySize; + __le32 Reserved; +} __packed; + struct smb2_file_standard_info { __le64 AllocationSize; __le64 EndOfFile; @@ -218,10 +292,6 @@ struct smb2_file_ea_info { __le32 EASize; } __packed; -struct smb2_file_alloc_info { - __le64 AllocationSize; -} __packed; - struct smb2_file_disposition_info { __u8 DeletePending; } __packed; @@ -236,9 +306,6 @@ struct smb2_file_mode_info { __le32 Mode; } __packed; -#define COMPRESSION_FORMAT_NONE 0x0000 -#define COMPRESSION_FORMAT_LZNT1 0x0002 - struct smb2_file_comp_info { __le64 CompressedFileSize; __le16 CompressionFormat; @@ -360,6 +427,7 @@ bool smb3_encryption_negotiated(struct ksmbd_conn *conn); /* smb2 misc functions */ int ksmbd_smb2_check_message(struct ksmbd_work *work); +void smb2_complete_request_open(struct ksmbd_work *work); /* smb2 command handlers */ int smb2_handle_negotiate(struct ksmbd_work *work); diff --git a/fs/smb/server/smb_common.c b/fs/smb/server/smb_common.c index 741aabdfcef5..4c2da65510bc 100644 --- a/fs/smb/server/smb_common.c +++ b/fs/smb/server/smb_common.c @@ -102,9 +102,6 @@ static const struct { int version; const char *string; } version_strings[] = { -#ifdef CONFIG_SMB_INSECURE_SERVER - {SMB1_PROT, SMB1_VERSION_STRING}, -#endif {SMB2_PROT, SMB20_VERSION_STRING}, {SMB21_PROT, SMB21_VERSION_STRING}, {SMB30_PROT, SMB30_VERSION_STRING}, @@ -167,7 +164,22 @@ int ksmbd_verify_smb_message(struct ksmbd_work *work) hdr = smb_get_msg(work->request_buf); if (*(__le32 *)hdr->Protocol == SMB1_PROTO_NUMBER && hdr->Command == SMB_COM_NEGOTIATE) { - work->conn->outstanding_credits++; + struct ksmbd_conn *conn = work->conn; + + conn->outstanding_credits++; + /* + * A legacy SMB1 multi-protocol negotiate occupies sequence + * number 0 but does not pass through + * ksmbd_smb2_check_message(). Consume it here so that, after + * the connection is upgraded to SMB2, the command sequence + * window can advance instead of staying pinned at 0. + */ + spin_lock(&conn->credits_lock); + if (conn->seq_low == 0) { + __clear_bit(0, conn->seq_bitmap); + conn->seq_low = 1; + } + spin_unlock(&conn->credits_lock); return 0; } @@ -188,11 +200,6 @@ bool ksmbd_smb_request(struct ksmbd_conn *conn) return false; proto = (__le32 *)smb_get_msg(conn->request_buf); - if (*proto == SMB2_COMPRESSION_TRANSFORM_ID) { - pr_err_ratelimited("smb2 compression not support yet"); - return false; - } - if (*proto != SMB1_PROTO_NUMBER && *proto != SMB2_PROTO_NUMBER && *proto != SMB2_TRANSFORM_PROTO_NUM) @@ -616,23 +623,46 @@ int ksmbd_smb_negotiate_common(struct ksmbd_work *work, unsigned int command) struct ksmbd_conn *conn = work->conn; int ret; - conn->dialect = - ksmbd_negotiate_smb_dialect(work->request_buf); - ksmbd_debug(SMB, "conn->dialect 0x%x\n", conn->dialect); - if (command == SMB2_NEGOTIATE_HE) { + /* + * An SMB2 NEGOTIATE is valid for a new connection, or after an + * SMB1 multi-protocol negotiate has selected SMB2. Do not allow + * a second SMB2 NEGOTIATE to replace connection-wide state + * while a session setup is pending. KSMBD_SESS_NEED_RECONNECT + * is a transient session state and does not restart transport + * negotiation. + */ + ksmbd_conn_lock(conn); + if (!ksmbd_conn_new(conn) && + !ksmbd_conn_need_negotiate(conn)) { + work->send_no_response = 1; + ksmbd_conn_set_exiting(conn); + ksmbd_conn_unlock(conn); + return 0; + } + + conn->dialect = + ksmbd_negotiate_smb_dialect(work->request_buf); + ksmbd_debug(SMB, "conn->dialect 0x%x\n", conn->dialect); ret = smb2_handle_negotiate(work); + ksmbd_conn_unlock(conn); return ret; } if (command == SMB_COM_NEGOTIATE) { + ksmbd_conn_lock(conn); + conn->dialect = + ksmbd_negotiate_smb_dialect(work->request_buf); + ksmbd_debug(SMB, "conn->dialect 0x%x\n", conn->dialect); if (__smb2_negotiate(conn)) { init_smb3_11_server(conn); - init_smb2_neg_rsp(work); + ret = init_smb2_neg_rsp(work); ksmbd_debug(SMB, "Upgrade to SMB2 negotiation\n"); - return 0; + } else { + ret = smb_handle_negotiate(work); } - return smb_handle_negotiate(work); + ksmbd_conn_unlock(conn); + return ret; } pr_err("Unknown SMB negotiation command: %u\n", command); diff --git a/fs/smb/server/smb_common.h b/fs/smb/server/smb_common.h index ca7e3610d074..7b9c5cfcb63b 100644 --- a/fs/smb/server/smb_common.h +++ b/fs/smb/server/smb_common.h @@ -90,14 +90,6 @@ struct smb_negotiate_rsp { __le16 ByteCount; } __packed; -struct filesystem_vol_info { - __le64 VolumeCreationTime; - __le32 SerialNumber; - __le32 VolumeLabelSize; - __le16 Reserved; - __le16 VolumeLabel[]; -} __packed; - #define EXTENDED_INFO_MAGIC 0x43667364 /* Cfsd */ #define STRING_LENGTH 28 @@ -143,7 +135,7 @@ struct file_id_both_directory_info { struct smb_version_ops { u16 (*get_cmd_val)(struct ksmbd_work *swork); - void (*inc_reqs)(unsigned int cmd); + void (*inc_reqs)(unsigned int cmd, __le32 status); int (*init_rsp_hdr)(struct ksmbd_work *swork); void (*set_rsp_status)(struct ksmbd_work *swork, __le32 err); int (*allocate_rsp_buf)(struct ksmbd_work *work); diff --git a/fs/smb/server/smbacl.c b/fs/smb/server/smbacl.c index 49c2abb29bf5..1fad6ccf3a72 100644 --- a/fs/smb/server/smbacl.c +++ b/fs/smb/server/smbacl.c @@ -27,6 +27,9 @@ static const struct smb_sid creator_owner = { /* security id for everyone/world system group */ static const struct smb_sid creator_group = { 1, 1, {0, 0, 0, 0, 0, 3}, {cpu_to_le32(1)} }; +/* security id for owner rights */ +static const struct smb_sid sid_owner_rights = { + 1, 1, {0, 0, 0, 0, 0, 3}, {cpu_to_le32(4)} }; /* security id for everyone/world system group */ static const struct smb_sid sid_everyone = { @@ -258,6 +261,7 @@ static int sid_to_id(struct mnt_idmap *idmap, struct smb_sid *psid, uint sidtype, struct smb_fattr *fattr) { + const struct smb_sid *sid_prefix; int rc = -EINVAL; /* @@ -279,6 +283,12 @@ static int sid_to_id(struct mnt_idmap *idmap, kuid_t uid; uid_t id; + /* Only the server domain RID has a local uid representation. */ + sid_prefix = &server_conf.domain_sid; + if (psid->num_subauth != sid_prefix->num_subauth + 1 || + compare_sids(psid, sid_prefix)) + return -EINVAL; + id = le32_to_cpu(psid->sub_auth[psid->num_subauth - 1]); uid = KUIDT_INIT(id); uid = from_vfsuid(idmap, &init_user_ns, VFSUIDT_INIT(uid)); @@ -290,6 +300,12 @@ static int sid_to_id(struct mnt_idmap *idmap, kgid_t gid; gid_t id; + /* Local gids are represented by S-1-22-2-<gid>. */ + sid_prefix = &sid_unix_groups; + if (psid->num_subauth != sid_prefix->num_subauth + 1 || + compare_sids(psid, sid_prefix)) + return -EINVAL; + id = le32_to_cpu(psid->sub_auth[psid->num_subauth - 1]); gid = KGIDT_INIT(id); gid = from_vfsgid(idmap, &init_user_ns, VFSGIDT_INIT(gid)); @@ -367,13 +383,14 @@ void free_acl_state(struct posix_acl_state *state) kfree(state->groups); } -static void parse_dacl(struct mnt_idmap *idmap, - struct smb_acl *pdacl, char *end_of_acl, - struct smb_sid *pownersid, struct smb_sid *pgrpsid, - struct smb_fattr *fattr) +static int parse_dacl(struct mnt_idmap *idmap, + struct smb_acl *pdacl, char *end_of_acl, + struct smb_sid *pownersid, struct smb_sid *pgrpsid, + struct smb_fattr *fattr) { int i, ret; u16 num_aces = 0; + u16 dacl_size; unsigned int acl_size; char *acl_base; struct smb_ace **ppace; @@ -383,13 +400,13 @@ static void parse_dacl(struct mnt_idmap *idmap, bool owner_found = false, group_found = false, others_found = false; if (!pdacl) - return; + return 0; /* validate that we do not go past end of acl */ if (end_of_acl < (char *)pdacl + sizeof(struct smb_acl) || end_of_acl < (char *)pdacl + le16_to_cpu(pdacl->size)) { pr_err("ACL too small to parse DACL\n"); - return; + return -EINVAL; } ksmbd_debug(SMB, "DACL revision %d size %d num aces %d\n", @@ -401,27 +418,31 @@ static void parse_dacl(struct mnt_idmap *idmap, num_aces = le16_to_cpu(pdacl->num_aces); if (num_aces <= 0) - return; + return 0; + + dacl_size = le16_to_cpu(pdacl->size); + if (dacl_size < sizeof(struct smb_acl)) + return -EINVAL; - if (num_aces > (le16_to_cpu(pdacl->size) - sizeof(struct smb_acl)) / + if (num_aces > (dacl_size - sizeof(struct smb_acl)) / (offsetof(struct smb_ace, sid) + offsetof(struct smb_sid, sub_auth) + sizeof(__le16))) - return; + return -EINVAL; ret = init_acl_state(&acl_state, num_aces); if (ret) - return; + return ret; ret = init_acl_state(&default_acl_state, num_aces); if (ret) { free_acl_state(&acl_state); - return; + return ret; } ppace = kmalloc_objs(struct smb_ace *, num_aces, KSMBD_DEFAULT_GFP); if (!ppace) { free_acl_state(&default_acl_state); free_acl_state(&acl_state); - return; + return -ENOMEM; } /* @@ -430,8 +451,10 @@ static void parse_dacl(struct mnt_idmap *idmap, * user/group/other have no permissions */ for (i = 0; i < num_aces; ++i) { - if (end_of_acl - acl_base < acl_size) - break; + if (end_of_acl - acl_base < acl_size) { + ret = -EINVAL; + goto out; + } ppace[i] = (struct smb_ace *)(acl_base + acl_size); acl_base = (char *)ppace[i]; @@ -444,14 +467,17 @@ static void parse_dacl(struct mnt_idmap *idmap, (end_of_acl - acl_base < acl_size + sizeof(__le32) * ppace[i]->sid.num_subauth) || (le16_to_cpu(ppace[i]->size) < - acl_size + sizeof(__le32) * ppace[i]->sid.num_subauth)) - break; + acl_size + sizeof(__le32) * ppace[i]->sid.num_subauth)) { + ret = -EINVAL; + goto out; + } acl_size = le16_to_cpu(ppace[i]->size); ppace[i]->access_req = smb_map_generic_desired_access(ppace[i]->access_req); - if (!(compare_sids(&ppace[i]->sid, &sid_unix_NFS_mode))) { + if (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]); break; @@ -502,8 +528,8 @@ static void parse_dacl(struct mnt_idmap *idmap, temp_fattr.cf_uid = INVALID_UID; ret = sid_to_id(idmap, &ppace[i]->sid, SIDOWNER, &temp_fattr); if (ret || uid_eq(temp_fattr.cf_uid, INVALID_UID)) { - pr_err("%s: Error %d mapping Owner SID to uid\n", - __func__, ret); + pr_err_ratelimited("%s: Error %d mapping Owner SID to uid\n", + __func__, ret); continue; } @@ -519,7 +545,6 @@ static void parse_dacl(struct mnt_idmap *idmap, ((acl_mode & 0700) >> 6) | 0004; } } - kfree(ppace); if (owner_found) { /* The owner must be set to at least read-only. */ @@ -562,10 +587,12 @@ static void parse_dacl(struct mnt_idmap *idmap, fattr->cf_acls = posix_acl_alloc(acl_state.users->n + acl_state.groups->n + 4, KSMBD_DEFAULT_GFP); - if (fattr->cf_acls) { - cf_pace = fattr->cf_acls->a_entries; - posix_state_to_acl(&acl_state, cf_pace); + if (!fattr->cf_acls) { + ret = -ENOMEM; + goto out; } + cf_pace = fattr->cf_acls->a_entries; + posix_state_to_acl(&acl_state, cf_pace); } } @@ -576,25 +603,33 @@ static void parse_dacl(struct mnt_idmap *idmap, fattr->cf_dacls = posix_acl_alloc(default_acl_state.users->n + default_acl_state.groups->n + 4, KSMBD_DEFAULT_GFP); - if (fattr->cf_dacls) { - cf_pdace = fattr->cf_dacls->a_entries; - posix_state_to_acl(&default_acl_state, cf_pdace); + if (!fattr->cf_dacls) { + ret = -ENOMEM; + goto out; } + cf_pdace = fattr->cf_dacls->a_entries; + posix_state_to_acl(&default_acl_state, cf_pdace); } } + ret = 0; +out: + kfree(ppace); free_acl_state(&acl_state); free_acl_state(&default_acl_state); + return ret; } static void set_posix_acl_entries_dacl(struct mnt_idmap *idmap, struct smb_ace *pndace, struct smb_fattr *fattr, u16 *num_aces, - u16 *size, u32 nt_aces_num) + u16 *size, u16 existing_nt_aces, + bool had_nt_aces) { struct posix_acl_entry *pace; struct smb_sid *sid; struct smb_ace *ntace; int i, j; + u16 ace_sz; if (!fattr->cf_acls) goto posix_default_acl; @@ -620,14 +655,14 @@ static void set_posix_acl_entries_dacl(struct mnt_idmap *idmap, gid = posix_acl_gid_translate(idmap, pace); id_to_sid(gid, SIDUNIX_GROUP, sid); - } else if (pace->e_tag == ACL_OTHER && !nt_aces_num) { + } else if (pace->e_tag == ACL_OTHER && !had_nt_aces) { smb_copy_sid(sid, &sid_everyone); } else { kfree(sid); continue; } ntace = pndace; - for (j = 0; j < nt_aces_num; j++) { + for (j = 0; j < existing_nt_aces; j++) { if (ntace->sid.sub_auth[ntace->sid.num_subauth - 1] == sid->sub_auth[sid->num_subauth - 1]) goto pass_same_sid; @@ -639,8 +674,13 @@ static void set_posix_acl_entries_dacl(struct mnt_idmap *idmap, flags = 0x03; ntace = (struct smb_ace *)((char *)pndace + *size); - *size += fill_ace_for_sid(ntace, sid, ACCESS_ALLOWED, flags, + ace_sz = fill_ace_for_sid(ntace, sid, ACCESS_ALLOWED, flags, pace->e_perm, 0777); + if (check_add_overflow(*size, ace_sz, size)) { + *size -= ace_sz; + kfree(sid); + break; + } (*num_aces)++; if (pace->e_tag == ACL_USER) ntace->access_req |= @@ -649,8 +689,13 @@ static void set_posix_acl_entries_dacl(struct mnt_idmap *idmap, if (S_ISDIR(fattr->cf_mode) && (pace->e_tag == ACL_USER || pace->e_tag == ACL_GROUP)) { ntace = (struct smb_ace *)((char *)pndace + *size); - *size += fill_ace_for_sid(ntace, sid, ACCESS_ALLOWED, + ace_sz = fill_ace_for_sid(ntace, sid, ACCESS_ALLOWED, 0x03, pace->e_perm, 0777); + if (check_add_overflow(*size, ace_sz, size)) { + *size -= ace_sz; + kfree(sid); + break; + } (*num_aces)++; if (pace->e_tag == ACL_USER) ntace->access_req |= @@ -661,7 +706,7 @@ pass_same_sid: kfree(sid); } - if (nt_aces_num) + if (had_nt_aces) return; posix_default_acl: @@ -690,8 +735,13 @@ posix_default_acl: } ntace = (struct smb_ace *)((char *)pndace + *size); - *size += fill_ace_for_sid(ntace, sid, ACCESS_ALLOWED, 0x0b, + ace_sz = fill_ace_for_sid(ntace, sid, ACCESS_ALLOWED, 0x0b, pace->e_perm, 0777); + if (check_add_overflow(*size, ace_sz, size)) { + *size -= ace_sz; + kfree(sid); + break; + } (*num_aces)++; if (pace->e_tag == ACL_USER) ntace->access_req |= @@ -710,6 +760,7 @@ static void set_ntacl_dacl(struct mnt_idmap *idmap, { struct smb_ace *ntace, *pndace; u16 nt_num_aces = le16_to_cpu(nt_dacl->num_aces), num_aces = 0; + u16 copied_nt_aces; unsigned short size = 0; int i; @@ -719,23 +770,41 @@ static void set_ntacl_dacl(struct mnt_idmap *idmap, for (i = 0; i < nt_num_aces; i++) { unsigned short nt_ace_size; - if (offsetof(struct smb_ace, access_req) > aces_size) + if (aces_size < offsetof(struct smb_ace, sid) + + CIFS_SID_BASE_SIZE) break; nt_ace_size = le16_to_cpu(ntace->size); - if (nt_ace_size > aces_size) + if (nt_ace_size > aces_size || + nt_ace_size < offsetof(struct smb_ace, sid) + + CIFS_SID_BASE_SIZE) break; + if (ntace->sid.num_subauth == 0 || + ntace->sid.num_subauth > SID_MAX_SUB_AUTHORITIES || + nt_ace_size < offsetof(struct smb_ace, sid) + + CIFS_SID_BASE_SIZE + + sizeof(__le32) * + ntace->sid.num_subauth) + goto next_ace; + memcpy((char *)pndace + size, ntace, nt_ace_size); - size += nt_ace_size; + if (check_add_overflow(size, nt_ace_size, &size)) { + size -= nt_ace_size; + break; + } + num_aces++; + +next_ace: aces_size -= nt_ace_size; ntace = (struct smb_ace *)((char *)ntace + nt_ace_size); - num_aces++; } } + copied_nt_aces = num_aces; set_posix_acl_entries_dacl(idmap, pndace, fattr, - &num_aces, &size, nt_num_aces); + &num_aces, &size, copied_nt_aces, + nt_num_aces != 0); pndacl->num_aces = cpu_to_le16(num_aces); pndacl->size = cpu_to_le16(le16_to_cpu(pndacl->size) + size); } @@ -753,7 +822,7 @@ static void set_mode_dacl(struct mnt_idmap *idmap, if (fattr->cf_acls) { set_posix_acl_entries_dacl(idmap, pndace, fattr, - &num_aces, &size, num_aces); + &num_aces, &size, num_aces, false); goto out; } @@ -874,9 +943,9 @@ int parse_sec_desc(struct mnt_idmap *idmap, struct smb_ntsd *pntsd, rc = sid_to_id(idmap, owner_sid_ptr, SIDOWNER, fattr); if (rc) { - pr_err("%s: Error %d mapping Owner SID to uid\n", - __func__, rc); + ksmbd_debug(SMB, "Owner SID has no Unix uid mapping\n"); owner_sid_ptr = NULL; + rc = 0; } } @@ -892,9 +961,9 @@ int parse_sec_desc(struct mnt_idmap *idmap, struct smb_ntsd *pntsd, } rc = sid_to_id(idmap, group_sid_ptr, SIDUNIX_GROUP, fattr); if (rc) { - pr_err("%s: Error %d mapping Group SID to gid\n", - __func__, rc); + ksmbd_debug(SMB, "Group SID has no Unix gid mapping\n"); group_sid_ptr = NULL; + rc = 0; } } @@ -908,13 +977,58 @@ int parse_sec_desc(struct mnt_idmap *idmap, struct smb_ntsd *pntsd, if (dacloffset < sizeof(struct smb_ntsd)) return -EINVAL; - parse_dacl(idmap, dacl_ptr, end_of_acl, - owner_sid_ptr, group_sid_ptr, fattr); + rc = parse_dacl(idmap, dacl_ptr, end_of_acl, + owner_sid_ptr, group_sid_ptr, fattr); + if (rc) + return rc; } return 0; } +size_t smb_acl_sec_desc_scratch_len(struct smb_fattr *fattr, + struct smb_ntsd *ppntsd, int ppntsd_size, int addition_info) +{ + size_t len = sizeof(struct smb_ntsd); + size_t tmp; + + if (addition_info & OWNER_SECINFO) + len += sizeof(struct smb_sid); + if (addition_info & GROUP_SECINFO) + len += sizeof(struct smb_sid); + if (!(addition_info & DACL_SECINFO)) + return len; + + len += sizeof(struct smb_acl); + if (ppntsd && ppntsd_size > 0) { + unsigned int dacl_offset = le32_to_cpu(ppntsd->dacloffset); + + if (dacl_offset < ppntsd_size && + check_add_overflow(len, ppntsd_size - dacl_offset, &len)) + return 0; + } + + if (fattr->cf_acls) { + if (check_mul_overflow((size_t)fattr->cf_acls->a_count, + 2 * sizeof(struct smb_ace), &tmp) || + check_add_overflow(len, tmp, &len)) + return 0; + } else { + /* default/minimum DACL */ + if (check_add_overflow(len, 5 * sizeof(struct smb_ace), &len)) + return 0; + } + + if (fattr->cf_dacls) { + if (check_mul_overflow((size_t)fattr->cf_dacls->a_count, + sizeof(struct smb_ace), &tmp) || + check_add_overflow(len, tmp, &len)) + return 0; + } + + return len; +} + /* Convert permission bits from mode to equivalent CIFS ACL */ int build_sec_desc(struct mnt_idmap *idmap, struct smb_ntsd *pntsd, struct smb_ntsd *ppntsd, @@ -1016,7 +1130,60 @@ static void smb_set_ace(struct smb_ace *ace, const struct smb_sid *sid, u8 type, ace->flags = flags; ace->access_req = access_req; smb_copy_sid(&ace->sid, sid); - ace->size = cpu_to_le16(1 + 1 + 2 + 4 + 1 + 1 + 6 + (sid->num_subauth * 4)); + ace->size = cpu_to_le16(1 + 1 + 2 + 4 + 1 + 1 + 6 + + (ace->sid.num_subauth * 4)); +} + +static int smb_append_inherited_ace(struct smb_ace **ace, int *nt_size, + u16 *ace_cnt, const struct smb_sid *sid, + u8 type, u8 flags, __le32 access_req) +{ + int ace_size; + + smb_set_ace(*ace, sid, type, flags, access_req); + ace_size = le16_to_cpu((*ace)->size); + /* pdacl->size is __le16 and includes struct smb_acl. */ + if (check_add_overflow(*nt_size, ace_size, nt_size) || + *nt_size > U16_MAX - (int)sizeof(struct smb_acl)) + return -EINVAL; + + (*ace_cnt)++; + *ace = (struct smb_ace *)((char *)*ace + ace_size); + return 0; +} + +static int smb_validate_ntsd_sid(struct smb_ntsd *pntsd, size_t pntsd_size, + unsigned int sid_offset, struct smb_sid **sid, + size_t *sid_size) +{ + size_t sid_end; + + *sid = NULL; + *sid_size = 0; + + if (!sid_offset) + return 0; + + if (sid_offset < sizeof(struct smb_ntsd) || + check_add_overflow(sid_offset, (size_t)CIFS_SID_BASE_SIZE, + &sid_end) || + sid_end > pntsd_size) + return -EINVAL; + + *sid = (struct smb_sid *)((char *)pntsd + sid_offset); + if ((*sid)->num_subauth > SID_MAX_SUB_AUTHORITIES) + return -EINVAL; + + if (check_add_overflow((size_t)CIFS_SID_BASE_SIZE, + sizeof(__le32) * (size_t)(*sid)->num_subauth, + &sid_end)) + return -EINVAL; + + if (sid_offset > pntsd_size || sid_end > pntsd_size - sid_offset) + return -EINVAL; + + *sid_size = sid_end; + return 0; } int smb_inherit_dacl(struct ksmbd_conn *conn, @@ -1031,28 +1198,28 @@ int smb_inherit_dacl(struct ksmbd_conn *conn, struct dentry *parent = path->dentry->d_parent; struct mnt_idmap *idmap = mnt_idmap(path->mnt); int inherited_flags = 0, flags = 0, i, nt_size = 0, pdacl_size; - int rc = 0, pntsd_type, pntsd_size, acl_len, aces_size; + int rc = 0, pntsd_type, ppntsd_size, acl_len, aces_size; unsigned int dacloffset; size_t dacl_struct_end; u16 num_aces, ace_cnt = 0; char *aces_base; bool is_dir = S_ISDIR(d_inode(path->dentry)->i_mode); - pntsd_size = ksmbd_vfs_get_sd_xattr(conn, idmap, + ppntsd_size = ksmbd_vfs_get_sd_xattr(conn, idmap, parent, &parent_pntsd); - if (pntsd_size <= 0) + if (ppntsd_size <= 0) return -ENOENT; dacloffset = le32_to_cpu(parent_pntsd->dacloffset); if (!dacloffset || check_add_overflow(dacloffset, sizeof(struct smb_acl), &dacl_struct_end) || - dacl_struct_end > (size_t)pntsd_size) { + dacl_struct_end > (size_t)ppntsd_size) { rc = -EINVAL; goto free_parent_pntsd; } parent_pdacl = (struct smb_acl *)((char *)parent_pntsd + dacloffset); - acl_len = pntsd_size - dacloffset; + acl_len = ppntsd_size - dacloffset; num_aces = le16_to_cpu(parent_pdacl->num_aces); pntsd_type = le16_to_cpu(parent_pntsd->type); pdacl_size = le16_to_cpu(parent_pdacl->size); @@ -1062,8 +1229,24 @@ int smb_inherit_dacl(struct ksmbd_conn *conn, goto free_parent_pntsd; } - aces_base = kmalloc(sizeof(struct smb_ace) * num_aces * 2, - KSMBD_DEFAULT_GFP); + aces_size = pdacl_size - sizeof(struct smb_acl); + + /* + * Validate num_aces against the DACL payload before allocating. + * Each ACE must be at least as large as its fixed-size header + * (up to the SID base), so num_aces cannot exceed the payload + * divided by the minimum ACE size. This mirrors the existing + * check in parse_dacl(). + */ + if (num_aces > aces_size / (offsetof(struct smb_ace, sid) + + offsetof(struct smb_sid, sub_auth) + + sizeof(__le16))) { + rc = -EINVAL; + goto free_parent_pntsd; + } + + aces_base = kmalloc_array(num_aces * 2, sizeof(struct smb_ace), + KSMBD_DEFAULT_GFP); if (!aces_base) { rc = -ENOMEM; goto free_parent_pntsd; @@ -1072,7 +1255,6 @@ int smb_inherit_dacl(struct ksmbd_conn *conn, aces = (struct smb_ace *)aces_base; parent_aces = (struct smb_ace *)((char *)parent_pdacl + sizeof(struct smb_acl)); - aces_size = acl_len - sizeof(struct smb_acl); if (pntsd_type & DACL_AUTO_INHERITED) inherited_flags = INHERITED_ACE; @@ -1080,11 +1262,20 @@ int smb_inherit_dacl(struct ksmbd_conn *conn, for (i = 0; i < num_aces; i++) { int pace_size; - if (offsetof(struct smb_ace, access_req) > aces_size) + if (aces_size < offsetof(struct smb_ace, sid) + + CIFS_SID_BASE_SIZE) break; pace_size = le16_to_cpu(parent_aces->size); - if (pace_size > aces_size) + if (pace_size > aces_size || + pace_size < offsetof(struct smb_ace, sid) + + CIFS_SID_BASE_SIZE) + break; + + if (parent_aces->sid.num_subauth > SID_MAX_SUB_AUTHORITIES || + pace_size < offsetof(struct smb_ace, sid) + + CIFS_SID_BASE_SIZE + + sizeof(__le32) * parent_aces->sid.num_subauth) break; aces_size -= pace_size; @@ -1116,22 +1307,24 @@ int smb_inherit_dacl(struct ksmbd_conn *conn, } if (is_dir && creator && flags & CONTAINER_INHERIT_ACE) { - smb_set_ace(aces, psid, parent_aces->type, inherited_flags, - parent_aces->access_req); - nt_size += le16_to_cpu(aces->size); - ace_cnt++; - aces = (struct smb_ace *)((char *)aces + le16_to_cpu(aces->size)); + rc = smb_append_inherited_ace(&aces, &nt_size, &ace_cnt, + psid, parent_aces->type, + inherited_flags, + parent_aces->access_req); + if (rc) + goto free_aces_base; flags |= INHERIT_ONLY_ACE; psid = creator; } else if (is_dir && !(parent_aces->flags & NO_PROPAGATE_INHERIT_ACE)) { psid = &parent_aces->sid; } - smb_set_ace(aces, psid, parent_aces->type, flags | inherited_flags, - parent_aces->access_req); - nt_size += le16_to_cpu(aces->size); - aces = (struct smb_ace *)((char *)aces + le16_to_cpu(aces->size)); - ace_cnt++; + rc = smb_append_inherited_ace(&aces, &nt_size, &ace_cnt, psid, + parent_aces->type, + flags | inherited_flags, + parent_aces->access_req); + if (rc) + goto free_aces_base; pass: parent_aces = (struct smb_ace *)((char *)parent_aces + pace_size); } @@ -1140,22 +1333,33 @@ pass: struct smb_ntsd *pntsd; struct smb_acl *pdacl; struct smb_sid *powner_sid = NULL, *pgroup_sid = NULL; - int powner_sid_size = 0, pgroup_sid_size = 0, pntsd_size; - int pntsd_alloc_size; + size_t powner_sid_size = 0, pgroup_sid_size = 0, pntsd_size; + size_t pntsd_alloc_size; - if (parent_pntsd->osidoffset) { - powner_sid = (struct smb_sid *)((char *)parent_pntsd + - le32_to_cpu(parent_pntsd->osidoffset)); - powner_sid_size = 1 + 1 + 6 + (powner_sid->num_subauth * 4); - } - if (parent_pntsd->gsidoffset) { - pgroup_sid = (struct smb_sid *)((char *)parent_pntsd + - le32_to_cpu(parent_pntsd->gsidoffset)); - pgroup_sid_size = 1 + 1 + 6 + (pgroup_sid->num_subauth * 4); - } + rc = smb_validate_ntsd_sid(parent_pntsd, ppntsd_size, + le32_to_cpu(parent_pntsd->osidoffset), + &powner_sid, &powner_sid_size); + if (rc) + goto free_aces_base; + rc = smb_validate_ntsd_sid(parent_pntsd, ppntsd_size, + le32_to_cpu(parent_pntsd->gsidoffset), + &pgroup_sid, &pgroup_sid_size); + if (rc) + goto free_aces_base; - pntsd_alloc_size = sizeof(struct smb_ntsd) + powner_sid_size + - pgroup_sid_size + sizeof(struct smb_acl) + nt_size; + if (check_add_overflow(sizeof(struct smb_ntsd), + (size_t)powner_sid_size, + &pntsd_alloc_size) || + check_add_overflow(pntsd_alloc_size, + (size_t)pgroup_sid_size, + &pntsd_alloc_size) || + check_add_overflow(pntsd_alloc_size, sizeof(struct smb_acl), + &pntsd_alloc_size) || + check_add_overflow(pntsd_alloc_size, (size_t)nt_size, + &pntsd_alloc_size)) { + rc = -EINVAL; + goto free_aces_base; + } pntsd = kzalloc(pntsd_alloc_size, KSMBD_DEFAULT_GFP); if (!pntsd) { @@ -1244,7 +1448,8 @@ bool smb_inherit_flags(int flags, bool is_dir) } int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, - __le32 *pdaccess, int uid) + __le32 *pdaccess, __le32 raw_daccess, int uid, + bool strict) { struct mnt_idmap *idmap = mnt_idmap(path->mnt); struct smb_ntsd *pntsd = NULL; @@ -1254,14 +1459,17 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, unsigned int dacl_offset; size_t dacl_struct_end; struct smb_sid sid; - int granted = le32_to_cpu(*pdaccess & ~FILE_MAXIMAL_ACCESS_LE); + int requested = le32_to_cpu(*pdaccess & ~FILE_MAXIMAL_ACCESS_LE); + int granted = requested; struct smb_ace *ace; int i, found = 0; - unsigned int access_bits = 0; + unsigned int access_bits = 0, denied = 0; struct smb_ace *others_ace = NULL; struct posix_acl_entry *pa_entry; unsigned int sid_type = SIDOWNER; unsigned short ace_size; + bool is_owner, owner_rights = false; + vfsuid_t vfsuid; ksmbd_debug(SMB, "check permission using windows acl\n"); pntsd_size = ksmbd_vfs_get_sd_xattr(conn, idmap, @@ -1291,62 +1499,103 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, goto err_out; } - if (*pdaccess & FILE_MAXIMAL_ACCESS_LE) { - granted = READ_CONTROL | WRITE_DAC | FILE_READ_ATTRIBUTES | - DELETE; + 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 (*pdaccess & FILE_MAXIMAL_ACCESS_LE) { ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl)); - aces_size = acl_size - sizeof(struct smb_acl); + aces_size = pdacl_size - sizeof(struct smb_acl); for (i = 0; i < le16_to_cpu(pdacl->num_aces); i++) { - if (offsetof(struct smb_ace, access_req) > aces_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) + if (ace_size > aces_size || + ace_size < offsetof(struct smb_ace, sid) + + CIFS_SID_BASE_SIZE) break; aces_size -= ace_size; - granted |= le32_to_cpu(ace->access_req); + + 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 (!compare_sids(&sid_owner_rights, &ace->sid)) { + owner_rights = true; + if (!is_owner) + goto next_ace; + } + + if (ace->flags & INHERIT_ONLY_ACE || + (compare_sids(&sid, &ace->sid) && + compare_sids(&sid_unix_NFS_mode, &ace->sid) && + compare_sids(&sid_everyone, &ace->sid) && + compare_sids(&sid_authusers, &ace->sid) && + compare_sids(&sid_owner_rights, &ace->sid))) + goto next_ace; + + switch (ace->type) { + case ACCESS_ALLOWED_ACE_TYPE: + access_bits |= le32_to_cpu(ace->access_req); + break; + case ACCESS_DENIED_ACE_TYPE: + case ACCESS_DENIED_CALLBACK_ACE_TYPE: + denied |= ~access_bits & + le32_to_cpu(ace->access_req); + break; + } +next_ace: ace = (struct smb_ace *)((char *)ace + le16_to_cpu(ace->size)); } + if (is_owner && !owner_rights) + access_bits |= READ_CONTROL | WRITE_DAC | + FILE_READ_ATTRIBUTES | DELETE; + access_bits &= ~denied; + if ((raw_daccess & FILE_GENERIC_EXECUTE_LE) && + S_ISREG(d_inode(path->dentry)->i_mode) && + (access_bits & GENERIC_READ_FLAGS) == GENERIC_READ_FLAGS) + access_bits |= FILE_EXECUTE; + granted = requested | access_bits; } - if (!uid) - sid_type = SIDUNIX_USER; - id_to_sid(uid, sid_type, &sid); - ace = (struct smb_ace *)((char *)pdacl + sizeof(struct smb_acl)); - aces_size = acl_size - sizeof(struct smb_acl); + aces_size = pdacl_size - sizeof(struct smb_acl); for (i = 0; i < le16_to_cpu(pdacl->num_aces); i++) { - if (offsetof(struct smb_ace, access_req) > aces_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) + 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 (!compare_sids(&sid, &ace->sid) || !compare_sids(&sid_unix_NFS_mode, &ace->sid)) { found = 1; break; } - if (!compare_sids(&sid_everyone, &ace->sid)) + if (!compare_sids(&sid_everyone, &ace->sid) || + !compare_sids(&sid_authusers, &ace->sid)) others_ace = ace; ace = (struct smb_ace *)((char *)ace + le16_to_cpu(ace->size)); } - if (*pdaccess & FILE_MAXIMAL_ACCESS_LE && found) { - granted = READ_CONTROL | WRITE_DAC | FILE_READ_ATTRIBUTES | - DELETE; - - granted |= le32_to_cpu(ace->access_req); - - if (!pdacl->num_aces) - granted = GENERIC_ALL_FLAGS; - } - if (IS_ENABLED(CONFIG_FS_POSIX_ACL)) { posix_acls = get_inode_acl(d_inode(path->dentry), ACL_TYPE_ACCESS); - if (!IS_ERR_OR_NULL(posix_acls) && !found) { + if (!IS_ERR_OR_NULL(posix_acls) && !found && !others_ace) { unsigned int id = -1; pa_entry = posix_acls->a_entries; @@ -1384,19 +1633,27 @@ int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, } } - switch (ace->type) { - case ACCESS_ALLOWED_ACE_TYPE: - access_bits = le32_to_cpu(ace->access_req); - break; - case ACCESS_DENIED_ACE_TYPE: - case ACCESS_DENIED_CALLBACK_ACE_TYPE: - access_bits = le32_to_cpu(~ace->access_req); - break; + if (!(*pdaccess & FILE_MAXIMAL_ACCESS_LE)) { + switch (ace->type) { + case ACCESS_ALLOWED_ACE_TYPE: + access_bits = le32_to_cpu(ace->access_req); + break; + case ACCESS_DENIED_ACE_TYPE: + case ACCESS_DENIED_CALLBACK_ACE_TYPE: + access_bits = le32_to_cpu(~ace->access_req); + break; + } } check_access_bits: - if (granted & - ~(access_bits | FILE_READ_ATTRIBUTES | READ_CONTROL | WRITE_DAC | DELETE)) { + if (strict) { + access_bits &= granted; + } else { + access_bits |= FILE_READ_ATTRIBUTES | READ_CONTROL | + WRITE_DAC | DELETE; + } + + if (granted & ~access_bits) { ksmbd_debug(SMB, "Access denied with winACL, granted : %x, access_req : %x\n", granted, le32_to_cpu(ace->access_req)); rc = -EACCES; diff --git a/fs/smb/server/smbacl.h b/fs/smb/server/smbacl.h index 355adaee39b8..01810c16cc04 100644 --- a/fs/smb/server/smbacl.h +++ b/fs/smb/server/smbacl.h @@ -95,12 +95,15 @@ bool smb_inherit_flags(int flags, bool is_dir); int smb_inherit_dacl(struct ksmbd_conn *conn, const struct path *path, unsigned int uid, unsigned int gid); int smb_check_perm_dacl(struct ksmbd_conn *conn, const struct path *path, - __le32 *pdaccess, int uid); + __le32 *pdaccess, __le32 raw_daccess, int uid, + 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); 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, + struct smb_ntsd *ppntsd, int ppntsd_size, int addition_info); static inline uid_t posix_acl_uid_translate(struct mnt_idmap *idmap, struct posix_acl_entry *pace) diff --git a/fs/smb/server/smbfsctl.h b/fs/smb/server/smbfsctl.h deleted file mode 100644 index ecdf8f6e0df4..000000000000 --- a/fs/smb/server/smbfsctl.h +++ /dev/null @@ -1,91 +0,0 @@ -/* SPDX-License-Identifier: LGPL-2.1+ */ -/* - * fs/smb/server/smbfsctl.h: SMB, CIFS, SMB2 FSCTL definitions - * - * Copyright (c) International Business Machines Corp., 2002,2009 - * Author(s): Steve French (sfrench@us.ibm.com) - */ - -/* IOCTL information */ -/* - * List of ioctl/fsctl function codes that are or could be useful in the - * future to remote clients like cifs or SMB2 client. There is probably - * a slightly larger set of fsctls that NTFS local filesystem could handle, - * including the seven below that we do not have struct definitions for. - * Even with protocol definitions for most of these now available, we still - * need to do some experimentation to identify which are practical to do - * remotely. Some of the following, such as the encryption/compression ones - * could be invoked from tools via a specialized hook into the VFS rather - * than via the standard vfs entry points - */ - -#ifndef __KSMBD_SMBFSCTL_H -#define __KSMBD_SMBFSCTL_H - -#define FSCTL_DFS_GET_REFERRALS 0x00060194 -#define FSCTL_DFS_GET_REFERRALS_EX 0x000601B0 -#define FSCTL_REQUEST_OPLOCK_LEVEL_1 0x00090000 -#define FSCTL_REQUEST_OPLOCK_LEVEL_2 0x00090004 -#define FSCTL_REQUEST_BATCH_OPLOCK 0x00090008 -#define FSCTL_LOCK_VOLUME 0x00090018 -#define FSCTL_UNLOCK_VOLUME 0x0009001C -#define FSCTL_IS_PATHNAME_VALID 0x0009002C /* BB add struct */ -#define FSCTL_GET_COMPRESSION 0x0009003C /* BB add struct */ -#define FSCTL_SET_COMPRESSION 0x0009C040 /* BB add struct */ -#define FSCTL_QUERY_FAT_BPB 0x00090058 /* BB add struct */ -/* Verify the next FSCTL number, we had it as 0x00090090 before */ -#define FSCTL_FILESYSTEM_GET_STATS 0x00090060 /* BB add struct */ -#define FSCTL_GET_NTFS_VOLUME_DATA 0x00090064 /* BB add struct */ -#define FSCTL_GET_RETRIEVAL_POINTERS 0x00090073 /* BB add struct */ -#define FSCTL_IS_VOLUME_DIRTY 0x00090078 /* BB add struct */ -#define FSCTL_ALLOW_EXTENDED_DASD_IO 0x00090083 /* BB add struct */ -#define FSCTL_REQUEST_FILTER_OPLOCK 0x0009008C -#define FSCTL_FIND_FILES_BY_SID 0x0009008F /* BB add struct */ -#define FSCTL_SET_OBJECT_ID 0x00090098 /* BB add struct */ -#define FSCTL_GET_OBJECT_ID 0x0009009C /* BB add struct */ -#define FSCTL_DELETE_OBJECT_ID 0x000900A0 /* BB add struct */ -#define FSCTL_SET_REPARSE_POINT 0x000900A4 /* BB add struct */ -#define FSCTL_GET_REPARSE_POINT 0x000900A8 /* BB add struct */ -#define FSCTL_DELETE_REPARSE_POINT 0x000900AC /* BB add struct */ -#define FSCTL_SET_OBJECT_ID_EXTENDED 0x000900BC /* BB add struct */ -#define FSCTL_CREATE_OR_GET_OBJECT_ID 0x000900C0 /* BB add struct */ -#define FSCTL_SET_SPARSE 0x000900C4 /* BB add struct */ -#define FSCTL_SET_ZERO_DATA 0x000980C8 /* BB add struct */ -#define FSCTL_SET_ENCRYPTION 0x000900D7 /* BB add struct */ -#define FSCTL_ENCRYPTION_FSCTL_IO 0x000900DB /* BB add struct */ -#define FSCTL_WRITE_RAW_ENCRYPTED 0x000900DF /* BB add struct */ -#define FSCTL_READ_RAW_ENCRYPTED 0x000900E3 /* BB add struct */ -#define FSCTL_READ_FILE_USN_DATA 0x000900EB /* BB add struct */ -#define FSCTL_WRITE_USN_CLOSE_RECORD 0x000900EF /* BB add struct */ -#define FSCTL_SIS_COPYFILE 0x00090100 /* BB add struct */ -#define FSCTL_RECALL_FILE 0x00090117 /* BB add struct */ -#define FSCTL_QUERY_SPARING_INFO 0x00090138 /* BB add struct */ -#define FSCTL_SET_ZERO_ON_DEALLOC 0x00090194 /* BB add struct */ -#define FSCTL_SET_SHORT_NAME_BEHAVIOR 0x000901B4 /* BB add struct */ -#define FSCTL_QUERY_ALLOCATED_RANGES 0x000940CF /* BB add struct */ -#define FSCTL_SET_DEFECT_MANAGEMENT 0x00098134 /* BB add struct */ -#define FSCTL_DUPLICATE_EXTENTS_TO_FILE 0x00098344 -#define FSCTL_SIS_LINK_FILES 0x0009C104 -#define FSCTL_PIPE_PEEK 0x0011400C /* BB add struct */ -#define FSCTL_PIPE_TRANSCEIVE 0x0011C017 /* BB add struct */ -/* strange that the number for this op is not sequential with previous op */ -#define FSCTL_PIPE_WAIT 0x00110018 /* BB add struct */ -#define FSCTL_REQUEST_RESUME_KEY 0x00140078 -#define FSCTL_LMR_GET_LINK_TRACK_INF 0x001400E8 /* BB add struct */ -#define FSCTL_LMR_SET_LINK_TRACK_INF 0x001400EC /* BB add struct */ -#define FSCTL_VALIDATE_NEGOTIATE_INFO 0x00140204 -#define FSCTL_QUERY_NETWORK_INTERFACE_INFO 0x001401FC -#define FSCTL_COPYCHUNK 0x001440F2 -#define FSCTL_COPYCHUNK_WRITE 0x001480F2 - -#define IO_REPARSE_TAG_MOUNT_POINT 0xA0000003 -#define IO_REPARSE_TAG_HSM 0xC0000004 -#define IO_REPARSE_TAG_SIS 0x80000007 - -/* WSL reparse tags */ -#define IO_REPARSE_TAG_LX_SYMLINK_LE cpu_to_le32(0xA000001D) -#define IO_REPARSE_TAG_AF_UNIX_LE cpu_to_le32(0x80000023) -#define IO_REPARSE_TAG_LX_FIFO_LE cpu_to_le32(0x80000024) -#define IO_REPARSE_TAG_LX_CHR_LE cpu_to_le32(0x80000025) -#define IO_REPARSE_TAG_LX_BLK_LE cpu_to_le32(0x80000026) -#endif /* __KSMBD_SMBFSCTL_H */ diff --git a/fs/smb/server/stats.h b/fs/smb/server/stats.h index b60c30c69077..8b32b8b4e8be 100644 --- a/fs/smb/server/stats.h +++ b/fs/smb/server/stats.h @@ -9,12 +9,25 @@ #ifndef __KSMBD_STATS_H__ #define __KSMBD_STATS_H__ +#include "../common/smb2status.h" + #define KSMBD_COUNTER_MAX_REQS 19 enum { KSMBD_COUNTER_SESSIONS = 0, + KSMBD_COUNTER_SESSION_TIMEOUTS, KSMBD_COUNTER_TREE_CONNS, KSMBD_COUNTER_REQUESTS, + KSMBD_COUNTER_STATUS_SUCCESS, + KSMBD_COUNTER_STATUS_INFORMATIONAL, + KSMBD_COUNTER_STATUS_WARNING, + KSMBD_COUNTER_STATUS_ERROR, + KSMBD_COUNTER_ERROR_ACCESS_DENIED, + KSMBD_COUNTER_ERROR_NOT_FOUND, + KSMBD_COUNTER_ERROR_INVALID_PARAMETER, + KSMBD_COUNTER_ERROR_SHARING_VIOLATION, + KSMBD_COUNTER_ERROR_NOT_SUPPORTED, + KSMBD_COUNTER_ERROR_OTHER, KSMBD_COUNTER_READ_BYTES, KSMBD_COUNTER_WRITE_BYTES, KSMBD_COUNTER_FIRST_REQ, @@ -50,10 +63,49 @@ static inline void ksmbd_counter_sub(int type, s64 value) percpu_counter_sub(&ksmbd_counters.counters[type], value); } -static inline void ksmbd_counter_inc_reqs(unsigned int cmd) +static inline void ksmbd_counter_inc_reqs(unsigned int cmd, __le32 status) { - if (cmd < KSMBD_COUNTER_MAX_REQS) + unsigned int severity = le32_to_cpu(status) >> 30; + int type; + + switch (severity) { + case 0: + type = KSMBD_COUNTER_STATUS_SUCCESS; + break; + case 1: + type = KSMBD_COUNTER_STATUS_INFORMATIONAL; + break; + case 2: + type = KSMBD_COUNTER_STATUS_WARNING; + break; + default: + type = KSMBD_COUNTER_STATUS_ERROR; + break; + } + percpu_counter_inc(&ksmbd_counters.counters[type]); + + if (severity == 3) { + if (status == STATUS_ACCESS_DENIED) + type = KSMBD_COUNTER_ERROR_ACCESS_DENIED; + else if (status == STATUS_OBJECT_NAME_NOT_FOUND || + status == STATUS_NO_SUCH_FILE) + type = KSMBD_COUNTER_ERROR_NOT_FOUND; + else if (status == STATUS_INVALID_PARAMETER) + type = KSMBD_COUNTER_ERROR_INVALID_PARAMETER; + else if (status == STATUS_SHARING_VIOLATION) + type = KSMBD_COUNTER_ERROR_SHARING_VIOLATION; + else if (status == STATUS_NOT_SUPPORTED || + status == STATUS_NOT_IMPLEMENTED) + type = KSMBD_COUNTER_ERROR_NOT_SUPPORTED; + else + type = KSMBD_COUNTER_ERROR_OTHER; + percpu_counter_inc(&ksmbd_counters.counters[type]); + } + + if (cmd < KSMBD_COUNTER_MAX_REQS) { + percpu_counter_inc(&ksmbd_counters.counters[KSMBD_COUNTER_REQUESTS]); percpu_counter_inc(&ksmbd_counters.counters[KSMBD_COUNTER_FIRST_REQ + cmd]); + } } static inline s64 ksmbd_counter_sum(int type) @@ -66,7 +118,7 @@ static inline void ksmbd_counter_inc(int type) {} static inline void ksmbd_counter_dec(int type) {} static inline void ksmbd_counter_add(int type, s64 value) {} static inline void ksmbd_counter_sub(int type, s64 value) {} -static inline void ksmbd_counter_inc_reqs(unsigned int cmd) {} +static inline void ksmbd_counter_inc_reqs(unsigned int cmd, __le32 status) {} static inline s64 ksmbd_counter_sum(int type) { return 0; } #endif diff --git a/fs/smb/server/transport_ipc.c b/fs/smb/server/transport_ipc.c index 2dbabe2d8005..e550aa41ad2c 100644 --- a/fs/smb/server/transport_ipc.c +++ b/fs/smb/server/transport_ipc.c @@ -13,6 +13,7 @@ #include <net/genetlink.h> #include <linux/socket.h> #include <linux/workqueue.h> +#include <linux/overflow.h> #include "vfs_cache.h" #include "transport_ipc.h" @@ -55,7 +56,7 @@ static bool ksmbd_ipc_validate_version(struct genl_info *m) struct ksmbd_ipc_msg { unsigned int type; unsigned int sz; - unsigned char payload[]; + unsigned char payload[] __counted_by(sz); }; struct ipc_msg_table_entry { @@ -242,9 +243,8 @@ static void ipc_update_last_active(void) static struct ksmbd_ipc_msg *ipc_msg_alloc(size_t sz) { struct ksmbd_ipc_msg *msg; - size_t msg_sz = sz + sizeof(struct ksmbd_ipc_msg); - msg = kvzalloc(msg_sz, KSMBD_DEFAULT_GFP); + msg = kvzalloc_flex(*msg, payload, sz, KSMBD_DEFAULT_GFP); if (msg) msg->sz = sz; return msg; @@ -322,6 +322,15 @@ static int ipc_server_config_on_startup(struct ksmbd_startup_request *req) goto out; } server_conf.share_fake_fscaps = req->share_fake_fscaps; + + /* AAPL model string for Finder icon */ + if (req->aapl_model[0]) + strscpy(server_conf.aapl_model, req->aapl_model, + sizeof(server_conf.aapl_model)); + else + strscpy(server_conf.aapl_model, "Xserve", + sizeof(server_conf.aapl_model)); + ksmbd_init_domain(req->sub_auth); if (req->smb2_max_read) @@ -497,13 +506,21 @@ static int ipc_validate_msg(struct ipc_msg_table_entry *entry) { struct ksmbd_rpc_command *resp = entry->response; - msg_sz = sizeof(struct ksmbd_rpc_command) + resp->payload_sz; + if (entry->msg_sz < sizeof(struct ksmbd_rpc_command)) + return -EINVAL; + + if (check_add_overflow(sizeof(struct ksmbd_rpc_command), + resp->payload_sz, &msg_sz)) + return -EINVAL; break; } case KSMBD_EVENT_SPNEGO_AUTHEN_REQUEST: { struct ksmbd_spnego_authen_response *resp = entry->response; + if (entry->msg_sz < sizeof(struct ksmbd_spnego_authen_response)) + return -EINVAL; + msg_sz = sizeof(struct ksmbd_spnego_authen_response) + resp->session_key_len + resp->spnego_blob_len; break; @@ -512,20 +529,40 @@ static int ipc_validate_msg(struct ipc_msg_table_entry *entry) { struct ksmbd_share_config_response *resp = entry->response; - if (resp->payload_sz) { - if (resp->payload_sz < resp->veto_list_sz) - return -EINVAL; + if (entry->msg_sz < sizeof(struct ksmbd_share_config_response)) + return -EINVAL; - msg_sz = sizeof(struct ksmbd_share_config_response) + - resp->payload_sz; - } + if (strnlen(resp->share_name, sizeof(resp->share_name)) == + sizeof(resp->share_name)) + return -EINVAL; + + if (resp->veto_list_sz > resp->payload_sz) + return -EINVAL; + + if (resp->flags != KSMBD_SHARE_FLAG_INVALID && + !(resp->flags & KSMBD_SHARE_FLAG_PIPE) && + resp->payload_sz <= resp->veto_list_sz) + return -EINVAL; + + if (check_add_overflow(sizeof(struct ksmbd_share_config_response), + resp->payload_sz, &msg_sz)) + return -EINVAL; break; } case KSMBD_EVENT_LOGIN_REQUEST_EXT: { struct ksmbd_login_response_ext *resp = entry->response; + if (entry->msg_sz < sizeof(struct ksmbd_login_response_ext)) + return -EINVAL; + if (resp->ngroups) { + if (resp->ngroups < 0 || + resp->ngroups > NGROUPS_MAX) { + pr_err("ngroups(%d) from login response exceeds max groups(%d)\n", + resp->ngroups, NGROUPS_MAX); + return -EINVAL; + } msg_sz = sizeof(struct ksmbd_login_response_ext) + resp->ngroups * sizeof(gid_t); } @@ -649,7 +686,7 @@ ksmbd_ipc_spnego_authen_request(const char *spnego_blob, int blob_len) return NULL; msg = ipc_msg_alloc(sizeof(struct ksmbd_spnego_authen_request) + - blob_len + 1); + blob_len); if (!msg) return NULL; @@ -830,7 +867,7 @@ struct ksmbd_rpc_command *ksmbd_rpc_write(struct ksmbd_session *sess, int handle if (payload_sz > KSMBD_IPC_MAX_PAYLOAD) return NULL; - msg = ipc_msg_alloc(sizeof(struct ksmbd_rpc_command) + payload_sz + 1); + msg = ipc_msg_alloc(sizeof(struct ksmbd_rpc_command) + payload_sz); if (!msg) return NULL; @@ -889,7 +926,7 @@ struct ksmbd_rpc_command *ksmbd_rpc_ioctl(struct ksmbd_session *sess, int handle if (payload_sz > KSMBD_IPC_MAX_PAYLOAD) return NULL; - msg = ipc_msg_alloc(sizeof(struct ksmbd_rpc_command) + payload_sz + 1); + msg = ipc_msg_alloc(sizeof(struct ksmbd_rpc_command) + payload_sz); if (!msg) return NULL; diff --git a/fs/smb/server/transport_rdma.c b/fs/smb/server/transport_rdma.c index 188572491d53..ee28a4d1cc86 100644 --- a/fs/smb/server/transport_rdma.c +++ b/fs/smb/server/transport_rdma.c @@ -11,30 +11,18 @@ #include <linux/kthread.h> #include <linux/list.h> -#include <linux/mempool.h> -#include <linux/highmem.h> -#include <linux/scatterlist.h> #include <linux/string_choices.h> -#include <rdma/ib_verbs.h> -#include <rdma/rdma_cm.h> -#include <rdma/rw.h> - -#define __SMBDIRECT_SOCKET_DISCONNECT(__sc) smb_direct_disconnect_rdma_connection(__sc) #include "glob.h" #include "connection.h" #include "smb_common.h" #include "../common/smb2status.h" -#include "../common/smbdirect/smbdirect.h" -#include "../common/smbdirect/smbdirect_pdu.h" -#include "../common/smbdirect/smbdirect_socket.h" #include "transport_rdma.h" + #define SMB_DIRECT_PORT_IWARP 5445 #define SMB_DIRECT_PORT_INFINIBAND 445 -#define SMB_DIRECT_VERSION_LE cpu_to_le16(SMBDIRECT_V1) - /* SMB_DIRECT negotiation timeout (for the server) in seconds */ #define SMB_DIRECT_NEGOTIATE_TIMEOUT 5 @@ -50,11 +38,6 @@ */ #define SMB_DIRECT_CM_INITIATOR_DEPTH 8 -/* Maximum number of retries on data transfer operations */ -#define SMB_DIRECT_CM_RETRY 6 -/* No need to retry on Receiver Not Ready since SMB_DIRECT manages credits */ -#define SMB_DIRECT_CM_RNR_RETRY 0 - /* * User configurable initial values per SMB_DIRECT transport connection * as defined in [MS-SMBD] 3.1.1.1 @@ -93,357 +76,120 @@ static int smb_direct_max_receive_size = 1364; static int smb_direct_max_read_write_size = SMBD_DEFAULT_IOSIZE; -static LIST_HEAD(smb_direct_device_list); -static DEFINE_RWLOCK(smb_direct_device_lock); - -struct smb_direct_device { - struct ib_device *ib_dev; - struct list_head list; -}; +static bool smb_direct_enabled; static struct smb_direct_listener { int port; - struct rdma_cm_id *cm_id; -} smb_direct_ib_listener, smb_direct_iw_listener; -static struct workqueue_struct *smb_direct_wq; + struct task_struct *thread; + + struct smbdirect_socket *socket; +} smb_direct_ib_listener, smb_direct_iw_listener; struct smb_direct_transport { struct ksmbd_transport transport; - struct smbdirect_socket socket; + struct smbdirect_socket *socket; }; -#define KSMBD_TRANS(t) (&(t)->transport) -#define SMBD_TRANS(t) (container_of(t, \ - struct smb_direct_transport, transport)) - -static const struct ksmbd_transport_ops ksmbd_smb_direct_transport_ops; - -void init_smbd_max_io_size(unsigned int sz) -{ - sz = clamp_val(sz, SMBD_MIN_IOSIZE, SMBD_MAX_IOSIZE); - smb_direct_max_read_write_size = sz; -} - -unsigned int get_smbd_max_read_write_size(struct ksmbd_transport *kt) -{ - struct smb_direct_transport *t; - struct smbdirect_socket *sc; - struct smbdirect_socket_parameters *sp; - - if (kt->ops != &ksmbd_smb_direct_transport_ops) - return 0; - - t = SMBD_TRANS(kt); - sc = &t->socket; - sp = &sc->parameters; - - return sp->max_read_write_size; -} - -static inline int get_buf_page_count(void *buf, int size) -{ - return DIV_ROUND_UP((uintptr_t)buf + size, PAGE_SIZE) - - (uintptr_t)buf / PAGE_SIZE; -} - -static void smb_direct_destroy_pools(struct smbdirect_socket *sc); -static void smb_direct_post_recv_credits(struct work_struct *work); -static int smb_direct_post_send_data(struct smbdirect_socket *sc, - struct smbdirect_send_batch *send_ctx, - struct kvec *iov, int niov, - int remaining_data_length); - -static inline void -*smbdirect_recv_io_payload(struct smbdirect_recv_io *recvmsg) -{ - return (void *)recvmsg->packet; -} - -static struct -smbdirect_recv_io *get_free_recvmsg(struct smbdirect_socket *sc) -{ - struct smbdirect_recv_io *recvmsg = NULL; - unsigned long flags; - - spin_lock_irqsave(&sc->recv_io.free.lock, flags); - if (!list_empty(&sc->recv_io.free.list)) { - recvmsg = list_first_entry(&sc->recv_io.free.list, - struct smbdirect_recv_io, - list); - list_del(&recvmsg->list); - } - spin_unlock_irqrestore(&sc->recv_io.free.lock, flags); - return recvmsg; -} - -static void put_recvmsg(struct smbdirect_socket *sc, - struct smbdirect_recv_io *recvmsg) -{ - unsigned long flags; - - if (likely(recvmsg->sge.length != 0)) { - ib_dma_unmap_single(sc->ib.dev, - recvmsg->sge.addr, - recvmsg->sge.length, - DMA_FROM_DEVICE); - recvmsg->sge.length = 0; - } - - spin_lock_irqsave(&sc->recv_io.free.lock, flags); - list_add(&recvmsg->list, &sc->recv_io.free.list); - spin_unlock_irqrestore(&sc->recv_io.free.lock, flags); - - queue_work(sc->workqueue, &sc->recv_io.posted.refill_work); -} - -static void enqueue_reassembly(struct smbdirect_socket *sc, - struct smbdirect_recv_io *recvmsg, - int data_length) +static bool smb_direct_logging_needed(struct smbdirect_socket *sc, + void *private_ptr, + unsigned int lvl, + unsigned int cls) { - unsigned long flags; - - spin_lock_irqsave(&sc->recv_io.reassembly.lock, flags); - list_add_tail(&recvmsg->list, &sc->recv_io.reassembly.list); - sc->recv_io.reassembly.queue_length++; - /* - * Make sure reassembly_data_length is updated after list and - * reassembly_queue_length are updated. On the dequeue side - * reassembly_data_length is checked without a lock to determine - * if reassembly_queue_length and list is up to date - */ - virt_wmb(); - sc->recv_io.reassembly.data_length += data_length; - spin_unlock_irqrestore(&sc->recv_io.reassembly.lock, flags); -} + if (lvl <= SMBDIRECT_LOG_ERR) + return true; -static struct smbdirect_recv_io *get_first_reassembly(struct smbdirect_socket *sc) -{ - if (!list_empty(&sc->recv_io.reassembly.list)) - return list_first_entry(&sc->recv_io.reassembly.list, - struct smbdirect_recv_io, list); - else - return NULL; -} + if (lvl > SMBDIRECT_LOG_INFO) + return false; -static void smb_direct_disconnect_wake_up_all(struct smbdirect_socket *sc) -{ + switch (cls) { /* - * Wake up all waiters in all wait queues - * in order to notice the broken connection. - */ - wake_up_all(&sc->status_wait); - wake_up_all(&sc->send_io.bcredits.wait_queue); - wake_up_all(&sc->send_io.lcredits.wait_queue); - wake_up_all(&sc->send_io.credits.wait_queue); - wake_up_all(&sc->send_io.pending.zero_wait_queue); - wake_up_all(&sc->recv_io.reassembly.wait_queue); - wake_up_all(&sc->rw_io.credits.wait_queue); -} - -static void smb_direct_disconnect_rdma_work(struct work_struct *work) -{ - struct smbdirect_socket *sc = - container_of(work, struct smbdirect_socket, disconnect_work); - - if (sc->first_error == 0) - sc->first_error = -ECONNABORTED; - + * These were more or less also logged before + * the move to common code. + * + * SMBDIRECT_LOG_RDMA_MR was not used, but + * that's client only code and we should + * notice if it's used on the server... + */ + case SMBDIRECT_LOG_RDMA_EVENT: + case SMBDIRECT_LOG_RDMA_SEND: + case SMBDIRECT_LOG_RDMA_RECV: + case SMBDIRECT_LOG_WRITE: + case SMBDIRECT_LOG_READ: + case SMBDIRECT_LOG_NEGOTIATE: + case SMBDIRECT_LOG_OUTGOING: + case SMBDIRECT_LOG_RDMA_RW: + case SMBDIRECT_LOG_RDMA_MR: + return true; /* - * make sure this and other work is not queued again - * but here we don't block and avoid - * disable[_delayed]_work_sync() + * These were not logged before the move + * to common code. */ - disable_work(&sc->disconnect_work); - disable_work(&sc->connect.work); - disable_work(&sc->recv_io.posted.refill_work); - disable_delayed_work(&sc->idle.timer_work); - disable_work(&sc->idle.immediate_work); - - switch (sc->status) { - case SMBDIRECT_SOCKET_NEGOTIATE_NEEDED: - case SMBDIRECT_SOCKET_NEGOTIATE_RUNNING: - case SMBDIRECT_SOCKET_NEGOTIATE_FAILED: - case SMBDIRECT_SOCKET_CONNECTED: - case SMBDIRECT_SOCKET_ERROR: - sc->status = SMBDIRECT_SOCKET_DISCONNECTING; - rdma_disconnect(sc->rdma.cm_id); - break; - - case SMBDIRECT_SOCKET_CREATED: - case SMBDIRECT_SOCKET_RESOLVE_ADDR_NEEDED: - case SMBDIRECT_SOCKET_RESOLVE_ADDR_RUNNING: - case SMBDIRECT_SOCKET_RESOLVE_ADDR_FAILED: - case SMBDIRECT_SOCKET_RESOLVE_ROUTE_NEEDED: - case SMBDIRECT_SOCKET_RESOLVE_ROUTE_RUNNING: - case SMBDIRECT_SOCKET_RESOLVE_ROUTE_FAILED: - case SMBDIRECT_SOCKET_RDMA_CONNECT_NEEDED: - case SMBDIRECT_SOCKET_RDMA_CONNECT_RUNNING: - case SMBDIRECT_SOCKET_RDMA_CONNECT_FAILED: - /* - * rdma_accept() never reached - * RDMA_CM_EVENT_ESTABLISHED - */ - sc->status = SMBDIRECT_SOCKET_DISCONNECTED; - break; - - case SMBDIRECT_SOCKET_DISCONNECTING: - case SMBDIRECT_SOCKET_DISCONNECTED: - case SMBDIRECT_SOCKET_DESTROYED: - break; + case SMBDIRECT_LOG_KEEP_ALIVE: + case SMBDIRECT_LOG_INCOMING: + return false; } /* - * Wake up all waiters in all wait queues - * in order to notice the broken connection. + * Log all unknown messages */ - smb_direct_disconnect_wake_up_all(sc); + return true; } -static void -smb_direct_disconnect_rdma_connection(struct smbdirect_socket *sc) +static void smb_direct_logging_vaprintf(struct smbdirect_socket *sc, + const char *func, + unsigned int line, + void *private_ptr, + unsigned int lvl, + unsigned int cls, + struct va_format *vaf) { - if (sc->first_error == 0) - sc->first_error = -ECONNABORTED; - - /* - * make sure other work (than disconnect_work) is - * not queued again but here we don't block and avoid - * disable[_delayed]_work_sync() - */ - disable_work(&sc->connect.work); - disable_work(&sc->recv_io.posted.refill_work); - disable_work(&sc->idle.immediate_work); - disable_delayed_work(&sc->idle.timer_work); - - switch (sc->status) { - case SMBDIRECT_SOCKET_RESOLVE_ADDR_FAILED: - case SMBDIRECT_SOCKET_RESOLVE_ROUTE_FAILED: - case SMBDIRECT_SOCKET_RDMA_CONNECT_FAILED: - case SMBDIRECT_SOCKET_NEGOTIATE_FAILED: - case SMBDIRECT_SOCKET_ERROR: - case SMBDIRECT_SOCKET_DISCONNECTING: - case SMBDIRECT_SOCKET_DISCONNECTED: - case SMBDIRECT_SOCKET_DESTROYED: - /* - * Keep the current error status - */ - break; - - case SMBDIRECT_SOCKET_RESOLVE_ADDR_NEEDED: - case SMBDIRECT_SOCKET_RESOLVE_ADDR_RUNNING: - sc->status = SMBDIRECT_SOCKET_RESOLVE_ADDR_FAILED; - break; - - case SMBDIRECT_SOCKET_RESOLVE_ROUTE_NEEDED: - case SMBDIRECT_SOCKET_RESOLVE_ROUTE_RUNNING: - sc->status = SMBDIRECT_SOCKET_RESOLVE_ROUTE_FAILED; - break; - - case SMBDIRECT_SOCKET_RDMA_CONNECT_NEEDED: - case SMBDIRECT_SOCKET_RDMA_CONNECT_RUNNING: - sc->status = SMBDIRECT_SOCKET_RDMA_CONNECT_FAILED; - break; - - case SMBDIRECT_SOCKET_NEGOTIATE_NEEDED: - case SMBDIRECT_SOCKET_NEGOTIATE_RUNNING: - sc->status = SMBDIRECT_SOCKET_NEGOTIATE_FAILED; - break; - - case SMBDIRECT_SOCKET_CREATED: - sc->status = SMBDIRECT_SOCKET_DISCONNECTED; - break; - - case SMBDIRECT_SOCKET_CONNECTED: - sc->status = SMBDIRECT_SOCKET_ERROR; - break; - } - - /* - * Wake up all waiters in all wait queues - * in order to notice the broken connection. - */ - smb_direct_disconnect_wake_up_all(sc); - - queue_work(sc->workqueue, &sc->disconnect_work); + if (lvl <= SMBDIRECT_LOG_ERR) + pr_err("%pV", vaf); + else + ksmbd_debug(RDMA, "%pV", vaf); } -static void smb_direct_send_immediate_work(struct work_struct *work) -{ - struct smbdirect_socket *sc = - container_of(work, struct smbdirect_socket, idle.immediate_work); +#define KSMBD_TRANS(t) (&(t)->transport) +#define SMBD_TRANS(t) (container_of(t, \ + struct smb_direct_transport, transport)) - if (sc->status != SMBDIRECT_SOCKET_CONNECTED) - return; +static const struct ksmbd_transport_ops ksmbd_smb_direct_transport_ops; - smb_direct_post_send_data(sc, NULL, NULL, 0, 0); +void init_smbd_max_io_size(unsigned int sz) +{ + sz = clamp_val(sz, SMBD_MIN_IOSIZE, SMBD_MAX_IOSIZE); + smb_direct_max_read_write_size = sz; } -static void smb_direct_idle_connection_timer(struct work_struct *work) +unsigned int get_smbd_max_read_write_size(struct ksmbd_transport *kt) { - struct smbdirect_socket *sc = - container_of(work, struct smbdirect_socket, idle.timer_work.work); - struct smbdirect_socket_parameters *sp = &sc->parameters; + struct smb_direct_transport *t; + const struct smbdirect_socket_parameters *sp; - if (sc->idle.keepalive != SMBDIRECT_KEEPALIVE_NONE) { - smb_direct_disconnect_rdma_connection(sc); - return; - } + if (kt->ops != &ksmbd_smb_direct_transport_ops) + return 0; - if (sc->status != SMBDIRECT_SOCKET_CONNECTED) - return; + t = SMBD_TRANS(kt); + sp = smbdirect_socket_get_current_parameters(t->socket); - /* - * Now use the keepalive timeout (instead of keepalive interval) - * in order to wait for a response - */ - sc->idle.keepalive = SMBDIRECT_KEEPALIVE_PENDING; - mod_delayed_work(sc->workqueue, &sc->idle.timer_work, - msecs_to_jiffies(sp->keepalive_timeout_msec)); - queue_work(sc->workqueue, &sc->idle.immediate_work); + return sp->max_read_write_size; } -static struct smb_direct_transport *alloc_transport(struct rdma_cm_id *cm_id) +static struct smb_direct_transport *alloc_transport(struct smbdirect_socket *sc) { struct smb_direct_transport *t; - struct smbdirect_socket *sc; - struct smbdirect_socket_parameters *sp; struct ksmbd_conn *conn; t = kzalloc_obj(*t, KSMBD_DEFAULT_GFP); if (!t) return NULL; - sc = &t->socket; - smbdirect_socket_init(sc); - sp = &sc->parameters; - - sc->workqueue = smb_direct_wq; - - INIT_WORK(&sc->disconnect_work, smb_direct_disconnect_rdma_work); - - sp->negotiate_timeout_msec = SMB_DIRECT_NEGOTIATE_TIMEOUT * 1000; - sp->initiator_depth = SMB_DIRECT_CM_INITIATOR_DEPTH; - sp->responder_resources = 1; - sp->recv_credit_max = smb_direct_receive_credit_max; - sp->send_credit_target = smb_direct_send_credit_target; - sp->max_send_size = smb_direct_max_send_size; - sp->max_fragmented_recv_size = smb_direct_max_fragmented_recv_size; - sp->max_recv_size = smb_direct_max_receive_size; - sp->max_read_write_size = smb_direct_max_read_write_size; - sp->keepalive_interval_msec = SMB_DIRECT_KEEPALIVE_SEND_INTERVAL * 1000; - sp->keepalive_timeout_msec = SMB_DIRECT_KEEPALIVE_RECV_TIMEOUT * 1000; - - sc->rdma.cm_id = cm_id; - cm_id->context = sc; - - sc->ib.dev = sc->rdma.cm_id->device; - - INIT_DELAYED_WORK(&sc->idle.timer_work, smb_direct_idle_connection_timer); + t->socket = sc; conn = ksmbd_conn_alloc(); if (!conn) - goto err; + goto conn_alloc_failed; down_write(&conn_list_lock); hash_add(conn_list, &conn->hlist, 0); @@ -452,1480 +198,61 @@ static struct smb_direct_transport *alloc_transport(struct rdma_cm_id *cm_id) conn->transport = KSMBD_TRANS(t); KSMBD_TRANS(t)->conn = conn; KSMBD_TRANS(t)->ops = &ksmbd_smb_direct_transport_ops; + return t; -err: + +conn_alloc_failed: kfree(t); return NULL; } static void smb_direct_free_transport(struct ksmbd_transport *kt) { - kfree(SMBD_TRANS(kt)); + struct smb_direct_transport *t = SMBD_TRANS(kt); + + smbdirect_socket_release(t->socket); + kfree(t); } static void free_transport(struct smb_direct_transport *t) { - struct smbdirect_socket *sc = &t->socket; - struct smbdirect_recv_io *recvmsg; - - disable_work_sync(&sc->disconnect_work); - if (sc->status < SMBDIRECT_SOCKET_DISCONNECTING) - smb_direct_disconnect_rdma_work(&sc->disconnect_work); - if (sc->status < SMBDIRECT_SOCKET_DISCONNECTED) - wait_event(sc->status_wait, sc->status == SMBDIRECT_SOCKET_DISCONNECTED); - - /* - * Wake up all waiters in all wait queues - * in order to notice the broken connection. - * - * Most likely this was already called via - * smb_direct_disconnect_rdma_work(), but call it again... - */ - smb_direct_disconnect_wake_up_all(sc); - - disable_work_sync(&sc->connect.work); - disable_work_sync(&sc->recv_io.posted.refill_work); - disable_delayed_work_sync(&sc->idle.timer_work); - disable_work_sync(&sc->idle.immediate_work); - - if (sc->rdma.cm_id) - rdma_lock_handler(sc->rdma.cm_id); - - if (sc->ib.qp) { - ib_drain_qp(sc->ib.qp); - sc->ib.qp = NULL; - rdma_destroy_qp(sc->rdma.cm_id); - } - - ksmbd_debug(RDMA, "drain the reassembly queue\n"); - do { - unsigned long flags; - - spin_lock_irqsave(&sc->recv_io.reassembly.lock, flags); - recvmsg = get_first_reassembly(sc); - if (recvmsg) { - list_del(&recvmsg->list); - spin_unlock_irqrestore(&sc->recv_io.reassembly.lock, flags); - put_recvmsg(sc, recvmsg); - } else { - spin_unlock_irqrestore(&sc->recv_io.reassembly.lock, flags); - } - } while (recvmsg); - sc->recv_io.reassembly.data_length = 0; - - if (sc->ib.send_cq) - ib_free_cq(sc->ib.send_cq); - if (sc->ib.recv_cq) - ib_free_cq(sc->ib.recv_cq); - if (sc->ib.pd) - ib_dealloc_pd(sc->ib.pd); - if (sc->rdma.cm_id) { - rdma_unlock_handler(sc->rdma.cm_id); - rdma_destroy_id(sc->rdma.cm_id); - } - - smb_direct_destroy_pools(sc); + smbdirect_socket_shutdown(t->socket); ksmbd_conn_free(KSMBD_TRANS(t)->conn); } -static struct smbdirect_send_io -*smb_direct_alloc_sendmsg(struct smbdirect_socket *sc) -{ - struct smbdirect_send_io *msg; - - msg = mempool_alloc(sc->send_io.mem.pool, KSMBD_DEFAULT_GFP); - if (!msg) - return ERR_PTR(-ENOMEM); - msg->socket = sc; - INIT_LIST_HEAD(&msg->sibling_list); - msg->num_sge = 0; - return msg; -} - -static void smb_direct_free_sendmsg(struct smbdirect_socket *sc, - struct smbdirect_send_io *msg) -{ - int i; - - /* - * The list needs to be empty! - * The caller should take care of it. - */ - WARN_ON_ONCE(!list_empty(&msg->sibling_list)); - - if (msg->num_sge > 0) { - ib_dma_unmap_single(sc->ib.dev, - msg->sge[0].addr, msg->sge[0].length, - DMA_TO_DEVICE); - for (i = 1; i < msg->num_sge; i++) - ib_dma_unmap_page(sc->ib.dev, - msg->sge[i].addr, msg->sge[i].length, - DMA_TO_DEVICE); - } - mempool_free(msg, sc->send_io.mem.pool); -} - -static int smb_direct_check_recvmsg(struct smbdirect_recv_io *recvmsg) -{ - struct smbdirect_socket *sc = recvmsg->socket; - - switch (sc->recv_io.expected) { - case SMBDIRECT_EXPECT_DATA_TRANSFER: { - struct smbdirect_data_transfer *req = - (struct smbdirect_data_transfer *)recvmsg->packet; - struct smb2_hdr *hdr = (struct smb2_hdr *)(recvmsg->packet - + le32_to_cpu(req->data_offset)); - ksmbd_debug(RDMA, - "CreditGranted: %u, CreditRequested: %u, DataLength: %u, RemainingDataLength: %u, SMB: %x, Command: %u\n", - le16_to_cpu(req->credits_granted), - le16_to_cpu(req->credits_requested), - req->data_length, req->remaining_data_length, - hdr->ProtocolId, hdr->Command); - return 0; - } - case SMBDIRECT_EXPECT_NEGOTIATE_REQ: { - struct smbdirect_negotiate_req *req = - (struct smbdirect_negotiate_req *)recvmsg->packet; - ksmbd_debug(RDMA, - "MinVersion: %u, MaxVersion: %u, CreditRequested: %u, MaxSendSize: %u, MaxRecvSize: %u, MaxFragmentedSize: %u\n", - le16_to_cpu(req->min_version), - le16_to_cpu(req->max_version), - le16_to_cpu(req->credits_requested), - le32_to_cpu(req->preferred_send_size), - le32_to_cpu(req->max_receive_size), - le32_to_cpu(req->max_fragmented_size)); - if (le16_to_cpu(req->min_version) > 0x0100 || - le16_to_cpu(req->max_version) < 0x0100) - return -EOPNOTSUPP; - if (le16_to_cpu(req->credits_requested) <= 0 || - le32_to_cpu(req->max_receive_size) <= 128 || - le32_to_cpu(req->max_fragmented_size) <= - 128 * 1024) - return -ECONNABORTED; - - return 0; - } - case SMBDIRECT_EXPECT_NEGOTIATE_REP: - /* client only */ - break; - } - - /* This is an internal error */ - return -EINVAL; -} - -static void recv_done(struct ib_cq *cq, struct ib_wc *wc) -{ - struct smbdirect_recv_io *recvmsg; - struct smbdirect_socket *sc; - struct smbdirect_socket_parameters *sp; - - recvmsg = container_of(wc->wr_cqe, struct smbdirect_recv_io, cqe); - sc = recvmsg->socket; - sp = &sc->parameters; - - if (wc->status != IB_WC_SUCCESS || wc->opcode != IB_WC_RECV) { - put_recvmsg(sc, recvmsg); - if (wc->status != IB_WC_WR_FLUSH_ERR) { - pr_err("Recv error. status='%s (%d)' opcode=%d\n", - ib_wc_status_msg(wc->status), wc->status, - wc->opcode); - smb_direct_disconnect_rdma_connection(sc); - } - return; - } - - ksmbd_debug(RDMA, "Recv completed. status='%s (%d)', opcode=%d\n", - ib_wc_status_msg(wc->status), wc->status, - wc->opcode); - - ib_dma_sync_single_for_cpu(wc->qp->device, recvmsg->sge.addr, - recvmsg->sge.length, DMA_FROM_DEVICE); - - /* - * Reset timer to the keepalive interval in - * order to trigger our next keepalive message. - */ - sc->idle.keepalive = SMBDIRECT_KEEPALIVE_NONE; - mod_delayed_work(sc->workqueue, &sc->idle.timer_work, - msecs_to_jiffies(sp->keepalive_interval_msec)); - - switch (sc->recv_io.expected) { - case SMBDIRECT_EXPECT_NEGOTIATE_REQ: - /* see smb_direct_negotiate_recv_done */ - break; - case SMBDIRECT_EXPECT_DATA_TRANSFER: { - struct smbdirect_data_transfer *data_transfer = - (struct smbdirect_data_transfer *)recvmsg->packet; - u32 remaining_data_length, data_offset, data_length; - int current_recv_credits; - u16 old_recv_credit_target; - - if (wc->byte_len < - offsetof(struct smbdirect_data_transfer, padding)) { - put_recvmsg(sc, recvmsg); - smb_direct_disconnect_rdma_connection(sc); - return; - } - - remaining_data_length = le32_to_cpu(data_transfer->remaining_data_length); - data_length = le32_to_cpu(data_transfer->data_length); - data_offset = le32_to_cpu(data_transfer->data_offset); - if (wc->byte_len < data_offset || - wc->byte_len < (u64)data_offset + data_length) { - put_recvmsg(sc, recvmsg); - smb_direct_disconnect_rdma_connection(sc); - return; - } - if (remaining_data_length > sp->max_fragmented_recv_size || - data_length > sp->max_fragmented_recv_size || - (u64)remaining_data_length + (u64)data_length > - (u64)sp->max_fragmented_recv_size) { - put_recvmsg(sc, recvmsg); - smb_direct_disconnect_rdma_connection(sc); - return; - } - - if (data_length) { - if (sc->recv_io.reassembly.full_packet_received) - recvmsg->first_segment = true; - - if (le32_to_cpu(data_transfer->remaining_data_length)) - sc->recv_io.reassembly.full_packet_received = false; - else - sc->recv_io.reassembly.full_packet_received = true; - } - - atomic_dec(&sc->recv_io.posted.count); - current_recv_credits = atomic_dec_return(&sc->recv_io.credits.count); - - old_recv_credit_target = sc->recv_io.credits.target; - sc->recv_io.credits.target = - le16_to_cpu(data_transfer->credits_requested); - sc->recv_io.credits.target = - min_t(u16, sc->recv_io.credits.target, sp->recv_credit_max); - sc->recv_io.credits.target = - max_t(u16, sc->recv_io.credits.target, 1); - atomic_add(le16_to_cpu(data_transfer->credits_granted), - &sc->send_io.credits.count); - - if (le16_to_cpu(data_transfer->flags) & - SMBDIRECT_FLAG_RESPONSE_REQUESTED) - queue_work(sc->workqueue, &sc->idle.immediate_work); - - if (atomic_read(&sc->send_io.credits.count) > 0) - wake_up(&sc->send_io.credits.wait_queue); - - if (data_length) { - if (current_recv_credits <= (sc->recv_io.credits.target / 4) || - sc->recv_io.credits.target > old_recv_credit_target) - queue_work(sc->workqueue, &sc->recv_io.posted.refill_work); - - enqueue_reassembly(sc, recvmsg, (int)data_length); - wake_up(&sc->recv_io.reassembly.wait_queue); - } else - put_recvmsg(sc, recvmsg); - - return; - } - case SMBDIRECT_EXPECT_NEGOTIATE_REP: - /* client only */ - break; - } - - /* - * This is an internal error! - */ - WARN_ON_ONCE(sc->recv_io.expected != SMBDIRECT_EXPECT_DATA_TRANSFER); - put_recvmsg(sc, recvmsg); - smb_direct_disconnect_rdma_connection(sc); -} - -static void smb_direct_negotiate_recv_work(struct work_struct *work); - -static void smb_direct_negotiate_recv_done(struct ib_cq *cq, struct ib_wc *wc) -{ - struct smbdirect_recv_io *recv_io = - container_of(wc->wr_cqe, struct smbdirect_recv_io, cqe); - struct smbdirect_socket *sc = recv_io->socket; - unsigned long flags; - - /* - * reset the common recv_done for later reuse. - */ - recv_io->cqe.done = recv_done; - - if (wc->status != IB_WC_SUCCESS || wc->opcode != IB_WC_RECV) { - put_recvmsg(sc, recv_io); - if (wc->status != IB_WC_WR_FLUSH_ERR) { - pr_err("Negotiate Recv error. status='%s (%d)' opcode=%d\n", - ib_wc_status_msg(wc->status), wc->status, - wc->opcode); - smb_direct_disconnect_rdma_connection(sc); - } - return; - } - - ksmbd_debug(RDMA, "Negotiate Recv completed. status='%s (%d)', opcode=%d\n", - ib_wc_status_msg(wc->status), wc->status, - wc->opcode); - - ib_dma_sync_single_for_cpu(sc->ib.dev, - recv_io->sge.addr, - recv_io->sge.length, - DMA_FROM_DEVICE); - - /* - * This is an internal error! - */ - if (WARN_ON_ONCE(sc->recv_io.expected != SMBDIRECT_EXPECT_NEGOTIATE_REQ)) { - put_recvmsg(sc, recv_io); - smb_direct_disconnect_rdma_connection(sc); - return; - } - - /* - * Don't reset timer to the keepalive interval in - * this will be done in smb_direct_negotiate_recv_work. - */ - - /* - * Only remember the recv_io if it has enough bytes, - * this gives smb_direct_negotiate_recv_work enough - * information in order to disconnect if it was not - * valid. - */ - sc->recv_io.reassembly.full_packet_received = true; - if (wc->byte_len >= sizeof(struct smbdirect_negotiate_req)) - enqueue_reassembly(sc, recv_io, 0); - else - put_recvmsg(sc, recv_io); - - /* - * Some drivers (at least mlx5_ib and irdma in roce mode) - * might post a recv completion before RDMA_CM_EVENT_ESTABLISHED, - * we need to adjust our expectation in that case. - * - * So we defer further processing of the negotiation - * to smb_direct_negotiate_recv_work(). - * - * If we are already in SMBDIRECT_SOCKET_NEGOTIATE_NEEDED - * we queue the work directly otherwise - * smb_direct_cm_handler() will do it, when - * RDMA_CM_EVENT_ESTABLISHED arrived. - */ - spin_lock_irqsave(&sc->connect.lock, flags); - if (!sc->first_error) { - INIT_WORK(&sc->connect.work, smb_direct_negotiate_recv_work); - if (sc->status == SMBDIRECT_SOCKET_NEGOTIATE_NEEDED) - queue_work(sc->workqueue, &sc->connect.work); - } - spin_unlock_irqrestore(&sc->connect.lock, flags); -} - -static void smb_direct_negotiate_recv_work(struct work_struct *work) -{ - struct smbdirect_socket *sc = - container_of(work, struct smbdirect_socket, connect.work); - const struct smbdirect_socket_parameters *sp = &sc->parameters; - struct smbdirect_recv_io *recv_io; - - if (sc->first_error) - return; - - ksmbd_debug(RDMA, "Negotiate Recv Work running\n"); - - /* - * Reset timer to the keepalive interval in - * order to trigger our next keepalive message. - */ - sc->idle.keepalive = SMBDIRECT_KEEPALIVE_NONE; - mod_delayed_work(sc->workqueue, &sc->idle.timer_work, - msecs_to_jiffies(sp->keepalive_interval_msec)); - - /* - * If smb_direct_negotiate_recv_done() detected an - * invalid request we want to disconnect. - */ - recv_io = get_first_reassembly(sc); - if (!recv_io) { - smb_direct_disconnect_rdma_connection(sc); - return; - } - - if (SMBDIRECT_CHECK_STATUS_WARN(sc, SMBDIRECT_SOCKET_NEGOTIATE_NEEDED)) { - smb_direct_disconnect_rdma_connection(sc); - return; - } - sc->status = SMBDIRECT_SOCKET_NEGOTIATE_RUNNING; - wake_up(&sc->status_wait); -} - -static int smb_direct_post_recv(struct smbdirect_socket *sc, - struct smbdirect_recv_io *recvmsg) -{ - struct smbdirect_socket_parameters *sp = &sc->parameters; - struct ib_recv_wr wr; - int ret; - - recvmsg->sge.addr = ib_dma_map_single(sc->ib.dev, - recvmsg->packet, - sp->max_recv_size, - DMA_FROM_DEVICE); - ret = ib_dma_mapping_error(sc->ib.dev, recvmsg->sge.addr); - if (ret) - return ret; - recvmsg->sge.length = sp->max_recv_size; - recvmsg->sge.lkey = sc->ib.pd->local_dma_lkey; - - wr.wr_cqe = &recvmsg->cqe; - wr.next = NULL; - wr.sg_list = &recvmsg->sge; - wr.num_sge = 1; - - ret = ib_post_recv(sc->ib.qp, &wr, NULL); - if (ret) { - pr_err("Can't post recv: %d\n", ret); - ib_dma_unmap_single(sc->ib.dev, - recvmsg->sge.addr, recvmsg->sge.length, - DMA_FROM_DEVICE); - recvmsg->sge.length = 0; - smb_direct_disconnect_rdma_connection(sc); - return ret; - } - return ret; -} - static int smb_direct_read(struct ksmbd_transport *t, char *buf, unsigned int size, int unused) { - struct smbdirect_recv_io *recvmsg; - struct smbdirect_data_transfer *data_transfer; - int to_copy, to_read, data_read, offset; - u32 data_length, remaining_data_length, data_offset; - int rc; struct smb_direct_transport *st = SMBD_TRANS(t); - struct smbdirect_socket *sc = &st->socket; - -again: - if (sc->status != SMBDIRECT_SOCKET_CONNECTED) { - pr_err("disconnected\n"); - return -ENOTCONN; - } - - /* - * No need to hold the reassembly queue lock all the time as we are - * the only one reading from the front of the queue. The transport - * may add more entries to the back of the queue at the same time - */ - if (sc->recv_io.reassembly.data_length >= size) { - int queue_length; - int queue_removed = 0; - unsigned long flags; - - /* - * Need to make sure reassembly_data_length is read before - * reading reassembly_queue_length and calling - * get_first_reassembly. This call is lock free - * as we never read at the end of the queue which are being - * updated in SOFTIRQ as more data is received - */ - virt_rmb(); - queue_length = sc->recv_io.reassembly.queue_length; - data_read = 0; - to_read = size; - offset = sc->recv_io.reassembly.first_entry_offset; - while (data_read < size) { - recvmsg = get_first_reassembly(sc); - data_transfer = smbdirect_recv_io_payload(recvmsg); - data_length = le32_to_cpu(data_transfer->data_length); - remaining_data_length = - le32_to_cpu(data_transfer->remaining_data_length); - data_offset = le32_to_cpu(data_transfer->data_offset); - - /* - * The upper layer expects RFC1002 length at the - * beginning of the payload. Return it to indicate - * the total length of the packet. This minimize the - * change to upper layer packet processing logic. This - * will be eventually remove when an intermediate - * transport layer is added - */ - if (recvmsg->first_segment && size == 4) { - unsigned int rfc1002_len = - data_length + remaining_data_length; - *((__be32 *)buf) = cpu_to_be32(rfc1002_len); - data_read = 4; - recvmsg->first_segment = false; - ksmbd_debug(RDMA, - "returning rfc1002 length %d\n", - rfc1002_len); - goto read_rfc1002_done; - } - - to_copy = min_t(int, data_length - offset, to_read); - memcpy(buf + data_read, (char *)data_transfer + data_offset + offset, - to_copy); - - /* move on to the next buffer? */ - if (to_copy == data_length - offset) { - queue_length--; - /* - * No need to lock if we are not at the - * end of the queue - */ - if (queue_length) { - list_del(&recvmsg->list); - } else { - spin_lock_irqsave(&sc->recv_io.reassembly.lock, flags); - list_del(&recvmsg->list); - spin_unlock_irqrestore(&sc->recv_io.reassembly.lock, flags); - } - queue_removed++; - put_recvmsg(sc, recvmsg); - offset = 0; - } else { - offset += to_copy; - } - - to_read -= to_copy; - data_read += to_copy; - } - - spin_lock_irqsave(&sc->recv_io.reassembly.lock, flags); - sc->recv_io.reassembly.data_length -= data_read; - sc->recv_io.reassembly.queue_length -= queue_removed; - spin_unlock_irqrestore(&sc->recv_io.reassembly.lock, flags); - - sc->recv_io.reassembly.first_entry_offset = offset; - ksmbd_debug(RDMA, - "returning to thread data_read=%d reassembly_data_length=%d first_entry_offset=%d\n", - data_read, sc->recv_io.reassembly.data_length, - sc->recv_io.reassembly.first_entry_offset); -read_rfc1002_done: - return data_read; - } - - ksmbd_debug(RDMA, "wait_event on more data\n"); - rc = wait_event_interruptible(sc->recv_io.reassembly.wait_queue, - sc->recv_io.reassembly.data_length >= size || - sc->status != SMBDIRECT_SOCKET_CONNECTED); - if (rc) - return -EINTR; - - goto again; -} - -static void smb_direct_post_recv_credits(struct work_struct *work) -{ - struct smbdirect_socket *sc = - container_of(work, struct smbdirect_socket, recv_io.posted.refill_work); - struct smbdirect_recv_io *recvmsg; - int credits = 0; - int ret; - - if (atomic_read(&sc->recv_io.credits.count) < sc->recv_io.credits.target) { - while (true) { - recvmsg = get_free_recvmsg(sc); - if (!recvmsg) - break; - - recvmsg->first_segment = false; - - ret = smb_direct_post_recv(sc, recvmsg); - if (ret) { - pr_err("Can't post recv: %d\n", ret); - put_recvmsg(sc, recvmsg); - break; - } - credits++; - - atomic_inc(&sc->recv_io.posted.count); - } - } - - atomic_add(credits, &sc->recv_io.credits.available); - - /* - * If the last send credit is waiting for credits - * it can grant we need to wake it up - */ - if (credits && - atomic_read(&sc->send_io.bcredits.count) == 0 && - atomic_read(&sc->send_io.credits.count) == 0) - wake_up(&sc->send_io.credits.wait_queue); - - if (credits) - queue_work(sc->workqueue, &sc->idle.immediate_work); -} - -static void send_done(struct ib_cq *cq, struct ib_wc *wc) -{ - struct smbdirect_send_io *sendmsg, *sibling, *next; - struct smbdirect_socket *sc; - int lcredits = 0; - - sendmsg = container_of(wc->wr_cqe, struct smbdirect_send_io, cqe); - sc = sendmsg->socket; - - ksmbd_debug(RDMA, "Send completed. status='%s (%d)', opcode=%d\n", - ib_wc_status_msg(wc->status), wc->status, - wc->opcode); - - if (unlikely(!(sendmsg->wr.send_flags & IB_SEND_SIGNALED))) { - /* - * This happens when smbdirect_send_io is a sibling - * before the final message, it is signaled on - * error anyway, so we need to skip - * smbdirect_connection_free_send_io here, - * otherwise is will destroy the memory - * of the siblings too, which will cause - * use after free problems for the others - * triggered from ib_drain_qp(). - */ - if (wc->status != IB_WC_SUCCESS) - goto skip_free; - - /* - * This should not happen! - * But we better just close the - * connection... - */ - pr_err("unexpected send completion wc->status=%s (%d) wc->opcode=%d\n", - ib_wc_status_msg(wc->status), wc->status, wc->opcode); - smb_direct_disconnect_rdma_connection(sc); - return; - } - - /* - * Free possible siblings and then the main send_io - */ - list_for_each_entry_safe(sibling, next, &sendmsg->sibling_list, sibling_list) { - list_del_init(&sibling->sibling_list); - smb_direct_free_sendmsg(sc, sibling); - lcredits += 1; - } - /* Note this frees wc->wr_cqe, but not wc */ - smb_direct_free_sendmsg(sc, sendmsg); - lcredits += 1; - - if (wc->status != IB_WC_SUCCESS || wc->opcode != IB_WC_SEND) { -skip_free: - pr_err("Send error. status='%s (%d)', opcode=%d\n", - ib_wc_status_msg(wc->status), wc->status, - wc->opcode); - smb_direct_disconnect_rdma_connection(sc); - return; - } - - atomic_add(lcredits, &sc->send_io.lcredits.count); - wake_up(&sc->send_io.lcredits.wait_queue); - - if (atomic_dec_and_test(&sc->send_io.pending.count)) - wake_up(&sc->send_io.pending.zero_wait_queue); -} - -static int manage_credits_prior_sending(struct smbdirect_socket *sc) -{ - int missing; - int available; - int new_credits; - - if (atomic_read(&sc->recv_io.credits.count) >= sc->recv_io.credits.target) - return 0; - - missing = (int)sc->recv_io.credits.target - atomic_read(&sc->recv_io.credits.count); - available = atomic_xchg(&sc->recv_io.credits.available, 0); - new_credits = (u16)min3(U16_MAX, missing, available); - if (new_credits <= 0) { - /* - * If credits are available, but not granted - * we need to re-add them again. - */ - if (available) - atomic_add(available, &sc->recv_io.credits.available); - return 0; - } - - if (new_credits < available) { - /* - * Readd the remaining available again. - */ - available -= new_credits; - atomic_add(available, &sc->recv_io.credits.available); - } - - /* - * Remember we granted the credits - */ - atomic_add(new_credits, &sc->recv_io.credits.count); - return new_credits; -} - -static int manage_keep_alive_before_sending(struct smbdirect_socket *sc) -{ - struct smbdirect_socket_parameters *sp = &sc->parameters; - - if (sc->idle.keepalive == SMBDIRECT_KEEPALIVE_PENDING) { - sc->idle.keepalive = SMBDIRECT_KEEPALIVE_SENT; - /* - * Now use the keepalive timeout (instead of keepalive interval) - * in order to wait for a response - */ - mod_delayed_work(sc->workqueue, &sc->idle.timer_work, - msecs_to_jiffies(sp->keepalive_timeout_msec)); - return 1; - } - return 0; -} - -static int smb_direct_post_send(struct smbdirect_socket *sc, - struct ib_send_wr *wr) -{ + struct smbdirect_socket *sc = st->socket; + struct msghdr msg = { .msg_flags = 0, }; + struct kvec iov = { + .iov_base = buf, + .iov_len = size, + }; int ret; - atomic_inc(&sc->send_io.pending.count); - ret = ib_post_send(sc->ib.qp, wr, NULL); - if (ret) { - pr_err("failed to post send: %d\n", ret); - smb_direct_disconnect_rdma_connection(sc); - } - return ret; -} - -static void smb_direct_send_ctx_init(struct smbdirect_send_batch *send_ctx, - bool need_invalidate_rkey, - unsigned int remote_key) -{ - INIT_LIST_HEAD(&send_ctx->msg_list); - send_ctx->wr_cnt = 0; - send_ctx->need_invalidate_rkey = need_invalidate_rkey; - send_ctx->remote_key = remote_key; - send_ctx->credit = 0; -} - -static int smb_direct_flush_send_list(struct smbdirect_socket *sc, - struct smbdirect_send_batch *send_ctx, - bool is_last) -{ - struct smbdirect_send_io *first, *last; - int ret = 0; - - if (list_empty(&send_ctx->msg_list)) - goto release_credit; - - first = list_first_entry(&send_ctx->msg_list, - struct smbdirect_send_io, - sibling_list); - last = list_last_entry(&send_ctx->msg_list, - struct smbdirect_send_io, - sibling_list); - - if (send_ctx->need_invalidate_rkey) { - first->wr.opcode = IB_WR_SEND_WITH_INV; - first->wr.ex.invalidate_rkey = send_ctx->remote_key; - send_ctx->need_invalidate_rkey = false; - send_ctx->remote_key = 0; - } - - last->wr.send_flags = IB_SEND_SIGNALED; - last->wr.wr_cqe = &last->cqe; - - /* - * Remove last from send_ctx->msg_list - * and splice the rest of send_ctx->msg_list - * to last->sibling_list. - * - * send_ctx->msg_list is a valid empty list - * at the end. - */ - list_del_init(&last->sibling_list); - list_splice_tail_init(&send_ctx->msg_list, &last->sibling_list); - send_ctx->wr_cnt = 0; - - ret = smb_direct_post_send(sc, &first->wr); - if (ret) { - struct smbdirect_send_io *sibling, *next; - - list_for_each_entry_safe(sibling, next, &last->sibling_list, sibling_list) { - list_del_init(&sibling->sibling_list); - smb_direct_free_sendmsg(sc, sibling); - } - smb_direct_free_sendmsg(sc, last); - } - -release_credit: - if (is_last && !ret && send_ctx->credit) { - atomic_add(send_ctx->credit, &sc->send_io.bcredits.count); - send_ctx->credit = 0; - wake_up(&sc->send_io.bcredits.wait_queue); - } + iov_iter_kvec(&msg.msg_iter, ITER_DEST, &iov, 1, size); - return ret; -} - -static int wait_for_credits(struct smbdirect_socket *sc, - wait_queue_head_t *waitq, atomic_t *total_credits, - int needed) -{ - int ret; - - do { - if (atomic_sub_return(needed, total_credits) >= 0) - return 0; - - atomic_add(needed, total_credits); - ret = wait_event_interruptible(*waitq, - atomic_read(total_credits) >= needed || - sc->status != SMBDIRECT_SOCKET_CONNECTED); - - if (sc->status != SMBDIRECT_SOCKET_CONNECTED) - return -ENOTCONN; - else if (ret < 0) - return ret; - } while (true); -} - -static int wait_for_send_bcredit(struct smbdirect_socket *sc, - struct smbdirect_send_batch *send_ctx) -{ - int ret; - - if (send_ctx->credit) - return 0; - - ret = wait_for_credits(sc, - &sc->send_io.bcredits.wait_queue, - &sc->send_io.bcredits.count, - 1); - if (ret) - return ret; - - send_ctx->credit = 1; - return 0; -} - -static int wait_for_send_lcredit(struct smbdirect_socket *sc, - struct smbdirect_send_batch *send_ctx) -{ - if (send_ctx && (atomic_read(&sc->send_io.lcredits.count) <= 1)) { - int ret; - - ret = smb_direct_flush_send_list(sc, send_ctx, false); - if (ret) - return ret; - } - - return wait_for_credits(sc, - &sc->send_io.lcredits.wait_queue, - &sc->send_io.lcredits.count, - 1); -} - -static int wait_for_send_credits(struct smbdirect_socket *sc, - struct smbdirect_send_batch *send_ctx) -{ - int ret; - - if (send_ctx && - (send_ctx->wr_cnt >= 16 || atomic_read(&sc->send_io.credits.count) <= 1)) { - ret = smb_direct_flush_send_list(sc, send_ctx, false); - if (ret) - return ret; - } - - return wait_for_credits(sc, &sc->send_io.credits.wait_queue, &sc->send_io.credits.count, 1); -} - -static int wait_for_rw_credits(struct smbdirect_socket *sc, int credits) -{ - return wait_for_credits(sc, - &sc->rw_io.credits.wait_queue, - &sc->rw_io.credits.count, - credits); -} - -static int calc_rw_credits(struct smbdirect_socket *sc, - char *buf, unsigned int len) -{ - return DIV_ROUND_UP(get_buf_page_count(buf, len), - sc->rw_io.credits.num_pages); -} - -static int smb_direct_create_header(struct smbdirect_socket *sc, - int size, int remaining_data_length, - int new_credits, - struct smbdirect_send_io **sendmsg_out) -{ - struct smbdirect_socket_parameters *sp = &sc->parameters; - struct smbdirect_send_io *sendmsg; - struct smbdirect_data_transfer *packet; - int header_length; - int ret; - - sendmsg = smb_direct_alloc_sendmsg(sc); - if (IS_ERR(sendmsg)) - return PTR_ERR(sendmsg); - - /* Fill in the packet header */ - packet = (struct smbdirect_data_transfer *)sendmsg->packet; - packet->credits_requested = cpu_to_le16(sp->send_credit_target); - packet->credits_granted = cpu_to_le16(new_credits); - - packet->flags = 0; - if (manage_keep_alive_before_sending(sc)) - packet->flags |= cpu_to_le16(SMBDIRECT_FLAG_RESPONSE_REQUESTED); - - packet->reserved = 0; - if (!size) - packet->data_offset = 0; - else - packet->data_offset = cpu_to_le32(24); - packet->data_length = cpu_to_le32(size); - packet->remaining_data_length = cpu_to_le32(remaining_data_length); - packet->padding = 0; - - ksmbd_debug(RDMA, - "credits_requested=%d credits_granted=%d data_offset=%d data_length=%d remaining_data_length=%d\n", - le16_to_cpu(packet->credits_requested), - le16_to_cpu(packet->credits_granted), - le32_to_cpu(packet->data_offset), - le32_to_cpu(packet->data_length), - le32_to_cpu(packet->remaining_data_length)); - - /* Map the packet to DMA */ - header_length = sizeof(struct smbdirect_data_transfer); - /* If this is a packet without payload, don't send padding */ - if (!size) - header_length = - offsetof(struct smbdirect_data_transfer, padding); - - sendmsg->sge[0].addr = ib_dma_map_single(sc->ib.dev, - (void *)packet, - header_length, - DMA_TO_DEVICE); - ret = ib_dma_mapping_error(sc->ib.dev, sendmsg->sge[0].addr); - if (ret) { - smb_direct_free_sendmsg(sc, sendmsg); - return ret; - } - - sendmsg->num_sge = 1; - sendmsg->sge[0].length = header_length; - sendmsg->sge[0].lkey = sc->ib.pd->local_dma_lkey; - - *sendmsg_out = sendmsg; - return 0; -} - -static int get_sg_list(void *buf, int size, struct scatterlist *sg_list, int nentries) -{ - bool high = is_vmalloc_addr(buf); - struct page *page; - int offset, len; - int i = 0; - - if (size <= 0 || nentries < get_buf_page_count(buf, size)) - return -EINVAL; - - offset = offset_in_page(buf); - buf -= offset; - while (size > 0) { - len = min_t(int, PAGE_SIZE - offset, size); - if (high) - page = vmalloc_to_page(buf); - else - page = kmap_to_page(buf); - - if (!sg_list) - return -EINVAL; - sg_set_page(sg_list, page, len, offset); - sg_list = sg_next(sg_list); - - buf += PAGE_SIZE; - size -= len; - offset = 0; - i++; - } - return i; -} - -static int get_mapped_sg_list(struct ib_device *device, void *buf, int size, - struct scatterlist *sg_list, int nentries, - enum dma_data_direction dir, int *npages) -{ - *npages = get_sg_list(buf, size, sg_list, nentries); - if (*npages < 0) - return -EINVAL; - return ib_dma_map_sg(device, sg_list, *npages, dir); -} - -static int post_sendmsg(struct smbdirect_socket *sc, - struct smbdirect_send_batch *send_ctx, - struct smbdirect_send_io *msg) -{ - int i; - - for (i = 0; i < msg->num_sge; i++) - ib_dma_sync_single_for_device(sc->ib.dev, - msg->sge[i].addr, msg->sge[i].length, - DMA_TO_DEVICE); - - msg->cqe.done = send_done; - msg->wr.opcode = IB_WR_SEND; - msg->wr.sg_list = &msg->sge[0]; - msg->wr.num_sge = msg->num_sge; - msg->wr.next = NULL; - - if (send_ctx) { - msg->wr.wr_cqe = NULL; - msg->wr.send_flags = 0; - if (!list_empty(&send_ctx->msg_list)) { - struct smbdirect_send_io *last; - - last = list_last_entry(&send_ctx->msg_list, - struct smbdirect_send_io, - sibling_list); - last->wr.next = &msg->wr; - } - list_add_tail(&msg->sibling_list, &send_ctx->msg_list); - send_ctx->wr_cnt++; - return 0; - } - - msg->wr.wr_cqe = &msg->cqe; - msg->wr.send_flags = IB_SEND_SIGNALED; - return smb_direct_post_send(sc, &msg->wr); -} - -static int smb_direct_post_send_data(struct smbdirect_socket *sc, - struct smbdirect_send_batch *send_ctx, - struct kvec *iov, int niov, - int remaining_data_length) -{ - int i, j, ret; - struct smbdirect_send_io *msg; - int data_length; - struct scatterlist sg[SMBDIRECT_SEND_IO_MAX_SGE - 1]; - struct smbdirect_send_batch _send_ctx; - int new_credits; - - if (!send_ctx) { - smb_direct_send_ctx_init(&_send_ctx, false, 0); - send_ctx = &_send_ctx; - } - - ret = wait_for_send_bcredit(sc, send_ctx); - if (ret) - goto bcredit_failed; - - ret = wait_for_send_lcredit(sc, send_ctx); - if (ret) - goto lcredit_failed; - - ret = wait_for_send_credits(sc, send_ctx); - if (ret) - goto credit_failed; - - new_credits = manage_credits_prior_sending(sc); - if (new_credits == 0 && - atomic_read(&sc->send_io.credits.count) == 0 && - atomic_read(&sc->recv_io.credits.count) == 0) { - queue_work(sc->workqueue, &sc->recv_io.posted.refill_work); - ret = wait_event_interruptible(sc->send_io.credits.wait_queue, - atomic_read(&sc->send_io.credits.count) >= 1 || - atomic_read(&sc->recv_io.credits.available) >= 1 || - sc->status != SMBDIRECT_SOCKET_CONNECTED); - if (sc->status != SMBDIRECT_SOCKET_CONNECTED) - ret = -ENOTCONN; - if (ret < 0) - goto credit_failed; - - new_credits = manage_credits_prior_sending(sc); - } - - data_length = 0; - for (i = 0; i < niov; i++) - data_length += iov[i].iov_len; - - ret = smb_direct_create_header(sc, data_length, remaining_data_length, - new_credits, &msg); - if (ret) - goto header_failed; - - for (i = 0; i < niov; i++) { - struct ib_sge *sge; - int sg_cnt; - int npages; - - sg_init_table(sg, SMBDIRECT_SEND_IO_MAX_SGE - 1); - sg_cnt = get_mapped_sg_list(sc->ib.dev, - iov[i].iov_base, iov[i].iov_len, - sg, SMBDIRECT_SEND_IO_MAX_SGE - 1, - DMA_TO_DEVICE, &npages); - if (sg_cnt <= 0) { - pr_err("failed to map buffer\n"); - ret = -ENOMEM; - goto err; - } else if (sg_cnt + msg->num_sge > SMBDIRECT_SEND_IO_MAX_SGE) { - pr_err("buffer not fitted into sges\n"); - ret = -E2BIG; - ib_dma_unmap_sg(sc->ib.dev, sg, npages, - DMA_TO_DEVICE); - goto err; - } - - for (j = 0; j < sg_cnt; j++) { - sge = &msg->sge[msg->num_sge]; - sge->addr = sg_dma_address(&sg[j]); - sge->length = sg_dma_len(&sg[j]); - sge->lkey = sc->ib.pd->local_dma_lkey; - msg->num_sge++; - } - } - - ret = post_sendmsg(sc, send_ctx, msg); - if (ret) - goto err; - - if (send_ctx == &_send_ctx) { - ret = smb_direct_flush_send_list(sc, send_ctx, true); - if (ret) - goto err; - } - - return 0; -err: - smb_direct_free_sendmsg(sc, msg); -header_failed: - atomic_inc(&sc->send_io.credits.count); -credit_failed: - atomic_inc(&sc->send_io.lcredits.count); -lcredit_failed: - atomic_add(send_ctx->credit, &sc->send_io.bcredits.count); - send_ctx->credit = 0; -bcredit_failed: + ret = smbdirect_connection_recvmsg(sc, &msg, 0); + if (ret == -ERESTARTSYS) + ret = -EINTR; return ret; } static int smb_direct_writev(struct ksmbd_transport *t, - struct kvec *iov, int niovs, int buflen, - bool need_invalidate, unsigned int remote_key) + const struct ksmbd_transport_write *tx) { struct smb_direct_transport *st = SMBD_TRANS(t); - struct smbdirect_socket *sc = &st->socket; - struct smbdirect_socket_parameters *sp = &sc->parameters; - size_t remaining_data_length; - size_t iov_idx; - size_t iov_ofs; - size_t max_iov_size = sp->max_send_size - - sizeof(struct smbdirect_data_transfer); - int ret; - struct smbdirect_send_batch send_ctx; - int error = 0; - - if (sc->status != SMBDIRECT_SOCKET_CONNECTED) - return -ENOTCONN; - - //FIXME: skip RFC1002 header.. - if (WARN_ON_ONCE(niovs <= 1 || iov[0].iov_len != 4)) - return -EINVAL; - buflen -= 4; - iov_idx = 1; - iov_ofs = 0; - - remaining_data_length = buflen; - ksmbd_debug(RDMA, "Sending smb (RDMA): smb_len=%u\n", buflen); - - smb_direct_send_ctx_init(&send_ctx, need_invalidate, remote_key); - while (remaining_data_length) { - struct kvec vecs[SMBDIRECT_SEND_IO_MAX_SGE - 1]; /* minus smbdirect hdr */ - size_t possible_bytes = max_iov_size; - size_t possible_vecs; - size_t bytes = 0; - size_t nvecs = 0; - - /* - * For the last message remaining_data_length should be - * have been 0 already! - */ - if (WARN_ON_ONCE(iov_idx >= niovs)) { - error = -EINVAL; - goto done; - } - - /* - * We have 2 factors which limit the arguments we pass - * to smb_direct_post_send_data(): - * - * 1. The number of supported sges for the send, - * while one is reserved for the smbdirect header. - * And we currently need one SGE per page. - * 2. The number of negotiated payload bytes per send. - */ - possible_vecs = min_t(size_t, ARRAY_SIZE(vecs), niovs - iov_idx); - - while (iov_idx < niovs && possible_vecs && possible_bytes) { - struct kvec *v = &vecs[nvecs]; - int page_count; - - v->iov_base = ((u8 *)iov[iov_idx].iov_base) + iov_ofs; - v->iov_len = min_t(size_t, - iov[iov_idx].iov_len - iov_ofs, - possible_bytes); - page_count = get_buf_page_count(v->iov_base, v->iov_len); - if (page_count > possible_vecs) { - /* - * If the number of pages in the buffer - * is to much (because we currently require - * one SGE per page), we need to limit the - * length. - * - * We know possible_vecs is at least 1, - * so we always keep the first page. - * - * We need to calculate the number extra - * pages (epages) we can also keep. - * - * We calculate the number of bytes in the - * first page (fplen), this should never be - * larger than v->iov_len because page_count is - * at least 2, but adding a limitation feels - * better. - * - * Then we calculate the number of bytes (elen) - * we can keep for the extra pages. - */ - size_t epages = possible_vecs - 1; - size_t fpofs = offset_in_page(v->iov_base); - size_t fplen = min_t(size_t, PAGE_SIZE - fpofs, v->iov_len); - size_t elen = min_t(size_t, v->iov_len - fplen, epages*PAGE_SIZE); - - v->iov_len = fplen + elen; - page_count = get_buf_page_count(v->iov_base, v->iov_len); - if (WARN_ON_ONCE(page_count > possible_vecs)) { - /* - * Something went wrong in the above - * logic... - */ - error = -EINVAL; - goto done; - } - } - possible_vecs -= page_count; - nvecs += 1; - possible_bytes -= v->iov_len; - bytes += v->iov_len; - - iov_ofs += v->iov_len; - if (iov_ofs >= iov[iov_idx].iov_len) { - iov_idx += 1; - iov_ofs = 0; - } - } - - remaining_data_length -= bytes; - - ret = smb_direct_post_send_data(sc, &send_ctx, - vecs, nvecs, - remaining_data_length); - if (unlikely(ret)) { - error = ret; - goto done; - } - } - -done: - ret = smb_direct_flush_send_list(sc, &send_ctx, true); - if (unlikely(!ret && error)) - ret = error; - - /* - * As an optimization, we don't wait for individual I/O to finish - * before sending the next one. - * Send them all and wait for pending send count to get to 0 - * that means all the I/Os have been out and we are good to return - */ - - wait_event(sc->send_io.pending.zero_wait_queue, - atomic_read(&sc->send_io.pending.count) == 0 || - sc->status != SMBDIRECT_SOCKET_CONNECTED); - if (sc->status != SMBDIRECT_SOCKET_CONNECTED && ret == 0) - ret = -ENOTCONN; - - return ret; -} - -static void smb_direct_free_rdma_rw_msg(struct smb_direct_transport *t, - struct smbdirect_rw_io *msg, - enum dma_data_direction dir) -{ - struct smbdirect_socket *sc = &t->socket; - - rdma_rw_ctx_destroy(&msg->rdma_ctx, sc->ib.qp, sc->ib.qp->port, - msg->sgt.sgl, msg->sgt.nents, dir); - sg_free_table_chained(&msg->sgt, SG_CHUNK_SIZE); - kfree(msg); -} - -static void read_write_done(struct ib_cq *cq, struct ib_wc *wc, - enum dma_data_direction dir) -{ - struct smbdirect_rw_io *msg = - container_of(wc->wr_cqe, struct smbdirect_rw_io, cqe); - struct smbdirect_socket *sc = msg->socket; - - if (wc->status != IB_WC_SUCCESS) { - msg->error = -EIO; - pr_err("read/write error. opcode = %d, status = %s(%d)\n", - wc->opcode, ib_wc_status_msg(wc->status), wc->status); - if (wc->status != IB_WC_WR_FLUSH_ERR) - smb_direct_disconnect_rdma_connection(sc); - } - - complete(msg->completion); -} - -static void read_done(struct ib_cq *cq, struct ib_wc *wc) -{ - read_write_done(cq, wc, DMA_FROM_DEVICE); -} - -static void write_done(struct ib_cq *cq, struct ib_wc *wc) -{ - read_write_done(cq, wc, DMA_TO_DEVICE); -} - -static int smb_direct_rdma_xmit(struct smb_direct_transport *t, - void *buf, int buf_len, - struct smbdirect_buffer_descriptor_v1 *desc, - unsigned int desc_len, - bool is_read) -{ - struct smbdirect_socket *sc = &t->socket; - struct smbdirect_socket_parameters *sp = &sc->parameters; - struct smbdirect_rw_io *msg, *next_msg; - int i, ret; - DECLARE_COMPLETION_ONSTACK(completion); - struct ib_send_wr *first_wr; - LIST_HEAD(msg_list); - char *desc_buf; - int credits_needed; - unsigned int desc_buf_len, desc_num = 0; - - if (sc->status != SMBDIRECT_SOCKET_CONNECTED) - return -ENOTCONN; - - if (buf_len > sp->max_read_write_size) - return -EINVAL; - - /* calculate needed credits */ - credits_needed = 0; - desc_buf = buf; - for (i = 0; i < desc_len / sizeof(*desc); i++) { - if (!buf_len) - break; - - desc_buf_len = le32_to_cpu(desc[i].length); - if (!desc_buf_len) - return -EINVAL; + struct smbdirect_socket *sc = st->socket; + struct iov_iter iter; - if (desc_buf_len > buf_len) { - desc_buf_len = buf_len; - desc[i].length = cpu_to_le32(desc_buf_len); - buf_len = 0; - } + iov_iter_kvec(&iter, ITER_SOURCE, tx->iov, tx->iov_cnt, + tx->size); - credits_needed += calc_rw_credits(sc, desc_buf, desc_buf_len); - desc_buf += desc_buf_len; - buf_len -= desc_buf_len; - desc_num++; - } - - ksmbd_debug(RDMA, "RDMA %s, len %#x, needed credits %#x\n", - str_read_write(is_read), buf_len, credits_needed); - - ret = wait_for_rw_credits(sc, credits_needed); - if (ret < 0) - return ret; - - /* build rdma_rw_ctx for each descriptor */ - desc_buf = buf; - for (i = 0; i < desc_num; i++) { - msg = kzalloc_flex(*msg, sg_list, SG_CHUNK_SIZE, - KSMBD_DEFAULT_GFP); - if (!msg) { - ret = -ENOMEM; - goto out; - } - - desc_buf_len = le32_to_cpu(desc[i].length); - - msg->socket = sc; - msg->cqe.done = is_read ? read_done : write_done; - msg->completion = &completion; - - msg->sgt.sgl = &msg->sg_list[0]; - ret = sg_alloc_table_chained(&msg->sgt, - get_buf_page_count(desc_buf, desc_buf_len), - msg->sg_list, SG_CHUNK_SIZE); - if (ret) { - ret = -ENOMEM; - goto free_msg; - } - - ret = get_sg_list(desc_buf, desc_buf_len, - msg->sgt.sgl, msg->sgt.orig_nents); - if (ret < 0) - goto free_table; - - ret = rdma_rw_ctx_init(&msg->rdma_ctx, sc->ib.qp, sc->ib.qp->port, - msg->sgt.sgl, - get_buf_page_count(desc_buf, desc_buf_len), - 0, - le64_to_cpu(desc[i].offset), - le32_to_cpu(desc[i].token), - is_read ? DMA_FROM_DEVICE : DMA_TO_DEVICE); - if (ret < 0) { - pr_err("failed to init rdma_rw_ctx: %d\n", ret); - goto free_table; - } - - list_add_tail(&msg->list, &msg_list); - desc_buf += desc_buf_len; - } - - /* concatenate work requests of rdma_rw_ctxs */ - first_wr = NULL; - list_for_each_entry_reverse(msg, &msg_list, list) { - first_wr = rdma_rw_ctx_wrs(&msg->rdma_ctx, sc->ib.qp, sc->ib.qp->port, - &msg->cqe, first_wr); - } - - ret = ib_post_send(sc->ib.qp, first_wr, NULL); - if (ret) { - pr_err("failed to post send wr for RDMA R/W: %d\n", ret); - goto out; - } - - msg = list_last_entry(&msg_list, struct smbdirect_rw_io, list); - wait_for_completion(&completion); - ret = msg->error; -out: - list_for_each_entry_safe(msg, next_msg, &msg_list, list) { - list_del(&msg->list); - smb_direct_free_rdma_rw_msg(t, msg, - is_read ? DMA_FROM_DEVICE : DMA_TO_DEVICE); - } - atomic_add(credits_needed, &sc->rw_io.credits.count); - wake_up(&sc->rw_io.credits.wait_queue); - return ret; - -free_table: - sg_free_table_chained(&msg->sgt, SG_CHUNK_SIZE); -free_msg: - kfree(msg); - goto out; + return smbdirect_connection_send_iter(sc, &iter, 0, + tx->need_invalidate_rkey, + tx->remote_key); } static int smb_direct_rdma_write(struct ksmbd_transport *t, @@ -1933,8 +260,11 @@ static int smb_direct_rdma_write(struct ksmbd_transport *t, struct smbdirect_buffer_descriptor_v1 *desc, unsigned int desc_len) { - return smb_direct_rdma_xmit(SMBD_TRANS(t), buf, buflen, - desc, desc_len, false); + struct smb_direct_transport *st = SMBD_TRANS(t); + struct smbdirect_socket *sc = st->socket; + + return smbdirect_connection_rdma_xmit(sc, buf, buflen, + desc, desc_len, false); } static int smb_direct_rdma_read(struct ksmbd_transport *t, @@ -1942,16 +272,19 @@ static int smb_direct_rdma_read(struct ksmbd_transport *t, struct smbdirect_buffer_descriptor_v1 *desc, unsigned int desc_len) { - return smb_direct_rdma_xmit(SMBD_TRANS(t), buf, buflen, - desc, desc_len, true); + struct smb_direct_transport *st = SMBD_TRANS(t); + struct smbdirect_socket *sc = st->socket; + + return smbdirect_connection_rdma_xmit(sc, buf, buflen, + desc, desc_len, true); } static void smb_direct_disconnect(struct ksmbd_transport *t) { struct smb_direct_transport *st = SMBD_TRANS(t); - struct smbdirect_socket *sc = &st->socket; + struct smbdirect_socket *sc = st->socket; - ksmbd_debug(RDMA, "Disconnecting cm_id=%p\n", sc->rdma.cm_id); + ksmbd_debug(RDMA, "Disconnecting sc=%p\n", sc); free_transport(st); } @@ -1959,840 +292,103 @@ static void smb_direct_disconnect(struct ksmbd_transport *t) static void smb_direct_shutdown(struct ksmbd_transport *t) { struct smb_direct_transport *st = SMBD_TRANS(t); - struct smbdirect_socket *sc = &st->socket; - - ksmbd_debug(RDMA, "smb-direct shutdown cm_id=%p\n", sc->rdma.cm_id); - - smb_direct_disconnect_rdma_work(&sc->disconnect_work); -} - -static int smb_direct_cm_handler(struct rdma_cm_id *cm_id, - struct rdma_cm_event *event) -{ - struct smbdirect_socket *sc = cm_id->context; - unsigned long flags; - - ksmbd_debug(RDMA, "RDMA CM event. cm_id=%p event=%s (%d)\n", - cm_id, rdma_event_msg(event->event), event->event); - - switch (event->event) { - case RDMA_CM_EVENT_ESTABLISHED: { - /* - * Some drivers (at least mlx5_ib and irdma in roce mode) - * might post a recv completion before RDMA_CM_EVENT_ESTABLISHED, - * we need to adjust our expectation in that case. - * - * If smb_direct_negotiate_recv_done was called first - * it initialized sc->connect.work only for us to - * start, so that we turned into - * SMBDIRECT_SOCKET_NEGOTIATE_NEEDED, before - * smb_direct_negotiate_recv_work() runs. - * - * If smb_direct_negotiate_recv_done didn't happen - * yet. sc->connect.work is still be disabled and - * queue_work() is a no-op. - */ - if (SMBDIRECT_CHECK_STATUS_DISCONNECT(sc, SMBDIRECT_SOCKET_RDMA_CONNECT_RUNNING)) - break; - sc->status = SMBDIRECT_SOCKET_NEGOTIATE_NEEDED; - spin_lock_irqsave(&sc->connect.lock, flags); - if (!sc->first_error) - queue_work(sc->workqueue, &sc->connect.work); - spin_unlock_irqrestore(&sc->connect.lock, flags); - wake_up(&sc->status_wait); - break; - } - case RDMA_CM_EVENT_DEVICE_REMOVAL: - case RDMA_CM_EVENT_DISCONNECTED: { - sc->status = SMBDIRECT_SOCKET_DISCONNECTED; - smb_direct_disconnect_rdma_work(&sc->disconnect_work); - if (sc->ib.qp) - ib_drain_qp(sc->ib.qp); - break; - } - case RDMA_CM_EVENT_CONNECT_ERROR: { - sc->status = SMBDIRECT_SOCKET_DISCONNECTED; - smb_direct_disconnect_rdma_work(&sc->disconnect_work); - break; - } - default: - pr_err("Unexpected RDMA CM event. cm_id=%p, event=%s (%d)\n", - cm_id, rdma_event_msg(event->event), - event->event); - break; - } - return 0; -} - -static void smb_direct_qpair_handler(struct ib_event *event, void *context) -{ - struct smbdirect_socket *sc = context; - - ksmbd_debug(RDMA, "Received QP event. cm_id=%p, event=%s (%d)\n", - sc->rdma.cm_id, ib_event_msg(event->event), event->event); - - switch (event->event) { - case IB_EVENT_CQ_ERR: - case IB_EVENT_QP_FATAL: - smb_direct_disconnect_rdma_connection(sc); - break; - default: - break; - } -} - -static int smb_direct_send_negotiate_response(struct smbdirect_socket *sc, - int failed) -{ - struct smbdirect_socket_parameters *sp = &sc->parameters; - struct smbdirect_send_io *sendmsg; - struct smbdirect_negotiate_resp *resp; - int ret; - - sendmsg = smb_direct_alloc_sendmsg(sc); - if (IS_ERR(sendmsg)) - return -ENOMEM; - - resp = (struct smbdirect_negotiate_resp *)sendmsg->packet; - if (failed) { - memset(resp, 0, sizeof(*resp)); - resp->min_version = SMB_DIRECT_VERSION_LE; - resp->max_version = SMB_DIRECT_VERSION_LE; - resp->status = STATUS_NOT_SUPPORTED; - - sc->status = SMBDIRECT_SOCKET_NEGOTIATE_FAILED; - } else { - resp->status = STATUS_SUCCESS; - resp->min_version = SMB_DIRECT_VERSION_LE; - resp->max_version = SMB_DIRECT_VERSION_LE; - resp->negotiated_version = SMB_DIRECT_VERSION_LE; - resp->reserved = 0; - resp->credits_requested = - cpu_to_le16(sp->send_credit_target); - resp->credits_granted = cpu_to_le16(manage_credits_prior_sending(sc)); - resp->max_readwrite_size = cpu_to_le32(sp->max_read_write_size); - resp->preferred_send_size = cpu_to_le32(sp->max_send_size); - resp->max_receive_size = cpu_to_le32(sp->max_recv_size); - resp->max_fragmented_size = - cpu_to_le32(sp->max_fragmented_recv_size); - - atomic_set(&sc->send_io.bcredits.count, 1); - sc->recv_io.expected = SMBDIRECT_EXPECT_DATA_TRANSFER; - sc->status = SMBDIRECT_SOCKET_CONNECTED; - } - - sendmsg->sge[0].addr = ib_dma_map_single(sc->ib.dev, - (void *)resp, sizeof(*resp), - DMA_TO_DEVICE); - ret = ib_dma_mapping_error(sc->ib.dev, sendmsg->sge[0].addr); - if (ret) { - smb_direct_free_sendmsg(sc, sendmsg); - return ret; - } - - sendmsg->num_sge = 1; - sendmsg->sge[0].length = sizeof(*resp); - sendmsg->sge[0].lkey = sc->ib.pd->local_dma_lkey; - - ret = post_sendmsg(sc, NULL, sendmsg); - if (ret) { - smb_direct_free_sendmsg(sc, sendmsg); - return ret; - } - - wait_event(sc->send_io.pending.zero_wait_queue, - atomic_read(&sc->send_io.pending.count) == 0 || - sc->status != SMBDIRECT_SOCKET_CONNECTED); - if (sc->status != SMBDIRECT_SOCKET_CONNECTED) - return -ENOTCONN; - - return 0; -} - -static int smb_direct_accept_client(struct smbdirect_socket *sc) -{ - struct smbdirect_socket_parameters *sp = &sc->parameters; - struct rdma_conn_param conn_param; - __be32 ird_ord_hdr[2]; - int ret; - - /* - * smb_direct_handle_connect_request() - * already negotiated sp->initiator_depth - * and sp->responder_resources - */ - memset(&conn_param, 0, sizeof(conn_param)); - conn_param.initiator_depth = sp->initiator_depth; - conn_param.responder_resources = sp->responder_resources; - - if (sc->rdma.legacy_iwarp) { - ird_ord_hdr[0] = cpu_to_be32(conn_param.responder_resources); - ird_ord_hdr[1] = cpu_to_be32(conn_param.initiator_depth); - conn_param.private_data = ird_ord_hdr; - conn_param.private_data_len = sizeof(ird_ord_hdr); - } else { - conn_param.private_data = NULL; - conn_param.private_data_len = 0; - } - conn_param.retry_count = SMB_DIRECT_CM_RETRY; - conn_param.rnr_retry_count = SMB_DIRECT_CM_RNR_RETRY; - conn_param.flow_control = 0; + struct smbdirect_socket *sc = st->socket; - /* - * start with the negotiate timeout and SMBDIRECT_KEEPALIVE_PENDING - * so that the timer will cause a disconnect. - */ - sc->idle.keepalive = SMBDIRECT_KEEPALIVE_PENDING; - mod_delayed_work(sc->workqueue, &sc->idle.timer_work, - msecs_to_jiffies(sp->negotiate_timeout_msec)); + ksmbd_debug(RDMA, "smb-direct shutdown sc=%p\n", sc); - WARN_ON_ONCE(sc->status != SMBDIRECT_SOCKET_RDMA_CONNECT_NEEDED); - sc->status = SMBDIRECT_SOCKET_RDMA_CONNECT_RUNNING; - ret = rdma_accept(sc->rdma.cm_id, &conn_param); - if (ret) { - pr_err("error at rdma_accept: %d\n", ret); - return ret; - } - return 0; + smbdirect_socket_shutdown(sc); } -static int smb_direct_prepare_negotiation(struct smbdirect_socket *sc) +static int smb_direct_new_connection(struct smb_direct_listener *listener, + struct smbdirect_socket *client_sc) { - struct smbdirect_recv_io *recvmsg; - bool recv_posted = false; + struct smb_direct_transport *t; + struct task_struct *handler; int ret; - WARN_ON_ONCE(sc->status != SMBDIRECT_SOCKET_CREATED); - sc->status = SMBDIRECT_SOCKET_RDMA_CONNECT_NEEDED; - - sc->recv_io.expected = SMBDIRECT_EXPECT_NEGOTIATE_REQ; - - recvmsg = get_free_recvmsg(sc); - if (!recvmsg) + t = alloc_transport(client_sc); + if (!t) { + smbdirect_socket_release(client_sc); return -ENOMEM; - recvmsg->cqe.done = smb_direct_negotiate_recv_done; - - ret = smb_direct_post_recv(sc, recvmsg); - if (ret) { - pr_err("Can't post recv: %d\n", ret); - goto out_err; } - recv_posted = true; - ret = smb_direct_accept_client(sc); - if (ret) { - pr_err("Can't accept client\n"); + handler = kthread_run(ksmbd_conn_handler_loop, + KSMBD_TRANS(t)->conn, "ksmbd:r%u", + listener->port); + if (IS_ERR(handler)) { + ret = PTR_ERR(handler); + pr_err("Can't start thread\n"); goto out_err; } return 0; out_err: - /* - * If the recv was never posted, return it to the free list. - * If it was posted, leave it alone so disconnect teardown can - * drain the QP and complete it (flush) and the completion path - * will unmap it exactly once. - */ - if (!recv_posted) - put_recvmsg(sc, recvmsg); - return ret; -} - -static int smb_direct_init_params(struct smbdirect_socket *sc) -{ - struct smbdirect_socket_parameters *sp = &sc->parameters; - int max_send_sges; - unsigned int maxpages; - - /* need 3 more sge. because a SMB_DIRECT header, SMB2 header, - * SMB2 response could be mapped. - */ - max_send_sges = DIV_ROUND_UP(sp->max_send_size, PAGE_SIZE) + 3; - if (max_send_sges > SMBDIRECT_SEND_IO_MAX_SGE) { - pr_err("max_send_size %d is too large\n", sp->max_send_size); - return -EINVAL; - } - - atomic_set(&sc->send_io.lcredits.count, sp->send_credit_target); - - maxpages = DIV_ROUND_UP(sp->max_read_write_size, PAGE_SIZE); - sc->rw_io.credits.max = rdma_rw_mr_factor(sc->ib.dev, - sc->rdma.cm_id->port_num, - maxpages); - sc->rw_io.credits.num_pages = DIV_ROUND_UP(maxpages, sc->rw_io.credits.max); - /* add one extra in order to handle unaligned pages */ - sc->rw_io.credits.max += 1; - - sc->recv_io.credits.target = 1; - - atomic_set(&sc->rw_io.credits.count, sc->rw_io.credits.max); - - return 0; -} - -static void smb_direct_destroy_pools(struct smbdirect_socket *sc) -{ - struct smbdirect_recv_io *recvmsg; - - while ((recvmsg = get_free_recvmsg(sc))) - mempool_free(recvmsg, sc->recv_io.mem.pool); - - mempool_destroy(sc->recv_io.mem.pool); - sc->recv_io.mem.pool = NULL; - - kmem_cache_destroy(sc->recv_io.mem.cache); - sc->recv_io.mem.cache = NULL; - - mempool_destroy(sc->send_io.mem.pool); - sc->send_io.mem.pool = NULL; - - kmem_cache_destroy(sc->send_io.mem.cache); - sc->send_io.mem.cache = NULL; -} - -static int smb_direct_create_pools(struct smbdirect_socket *sc) -{ - struct smbdirect_socket_parameters *sp = &sc->parameters; - char name[80]; - int i; - struct smbdirect_recv_io *recvmsg; - - snprintf(name, sizeof(name), "smbdirect_send_io_pool_%p", sc); - sc->send_io.mem.cache = kmem_cache_create(name, - sizeof(struct smbdirect_send_io) + - sizeof(struct smbdirect_negotiate_resp), - 0, SLAB_HWCACHE_ALIGN, NULL); - if (!sc->send_io.mem.cache) - return -ENOMEM; - - sc->send_io.mem.pool = mempool_create(sp->send_credit_target, - mempool_alloc_slab, mempool_free_slab, - sc->send_io.mem.cache); - if (!sc->send_io.mem.pool) - goto err; - - snprintf(name, sizeof(name), "smbdirect_recv_io_pool_%p", sc); - sc->recv_io.mem.cache = kmem_cache_create(name, - sizeof(struct smbdirect_recv_io) + - sp->max_recv_size, - 0, SLAB_HWCACHE_ALIGN, NULL); - if (!sc->recv_io.mem.cache) - goto err; - - sc->recv_io.mem.pool = - mempool_create(sp->recv_credit_max, mempool_alloc_slab, - mempool_free_slab, sc->recv_io.mem.cache); - if (!sc->recv_io.mem.pool) - goto err; - - for (i = 0; i < sp->recv_credit_max; i++) { - recvmsg = mempool_alloc(sc->recv_io.mem.pool, KSMBD_DEFAULT_GFP); - if (!recvmsg) - goto err; - recvmsg->socket = sc; - recvmsg->sge.length = 0; - list_add(&recvmsg->list, &sc->recv_io.free.list); - } - - return 0; -err: - smb_direct_destroy_pools(sc); - return -ENOMEM; -} - -static u32 smb_direct_rdma_rw_send_wrs(struct ib_device *dev, const struct ib_qp_init_attr *attr) -{ - /* - * This could be split out of rdma_rw_init_qp() - * and be a helper function next to rdma_rw_mr_factor() - * - * We can't check unlikely(rdma_rw_force_mr) here, - * but that is most likely 0 anyway. - */ - u32 factor; - - WARN_ON_ONCE(attr->port_num == 0); - - /* - * Each context needs at least one RDMA READ or WRITE WR. - * - * For some hardware we might need more, eventually we should ask the - * HCA driver for a multiplier here. - */ - factor = 1; - - /* - * If the device needs MRs to perform RDMA READ or WRITE operations, - * we'll need two additional MRs for the registrations and the - * invalidation. - */ - if (rdma_protocol_iwarp(dev, attr->port_num) || dev->attrs.max_sgl_rd) - factor += 2; /* inv + reg */ - - return factor * attr->cap.max_rdma_ctxs; -} - -static int smb_direct_create_qpair(struct smbdirect_socket *sc) -{ - struct smbdirect_socket_parameters *sp = &sc->parameters; - int ret; - struct ib_qp_cap qp_cap; - struct ib_qp_init_attr qp_attr; - u32 max_send_wr; - u32 rdma_send_wr; - - /* - * Note that {rdma,ib}_create_qp() will call - * rdma_rw_init_qp() if cap->max_rdma_ctxs is not 0. - * It will adjust cap->max_send_wr to the required - * number of additional WRs for the RDMA RW operations. - * It will cap cap->max_send_wr to the device limit. - * - * +1 for ib_drain_qp - */ - qp_cap.max_send_wr = sp->send_credit_target + 1; - qp_cap.max_recv_wr = sp->recv_credit_max + 1; - qp_cap.max_send_sge = SMBDIRECT_SEND_IO_MAX_SGE; - qp_cap.max_recv_sge = SMBDIRECT_RECV_IO_MAX_SGE; - qp_cap.max_inline_data = 0; - qp_cap.max_rdma_ctxs = sc->rw_io.credits.max; - - /* - * Find out the number of max_send_wr - * after rdma_rw_init_qp() adjusted it. - * - * We only do it on a temporary variable, - * as rdma_create_qp() will trigger - * rdma_rw_init_qp() again. - */ - memset(&qp_attr, 0, sizeof(qp_attr)); - qp_attr.cap = qp_cap; - qp_attr.port_num = sc->rdma.cm_id->port_num; - rdma_send_wr = smb_direct_rdma_rw_send_wrs(sc->ib.dev, &qp_attr); - max_send_wr = qp_cap.max_send_wr + rdma_send_wr; - - if (qp_cap.max_send_wr > sc->ib.dev->attrs.max_cqe || - qp_cap.max_send_wr > sc->ib.dev->attrs.max_qp_wr) { - pr_err("Possible CQE overrun: max_send_wr %d\n", - qp_cap.max_send_wr); - pr_err("device %.*s reporting max_cqe %d max_qp_wr %d\n", - IB_DEVICE_NAME_MAX, - sc->ib.dev->name, - sc->ib.dev->attrs.max_cqe, - sc->ib.dev->attrs.max_qp_wr); - pr_err("consider lowering send_credit_target = %d\n", - sp->send_credit_target); - return -EINVAL; - } - - if (qp_cap.max_rdma_ctxs && - (max_send_wr >= sc->ib.dev->attrs.max_cqe || - max_send_wr >= sc->ib.dev->attrs.max_qp_wr)) { - pr_err("Possible CQE overrun: rdma_send_wr %d + max_send_wr %d = %d\n", - rdma_send_wr, qp_cap.max_send_wr, max_send_wr); - pr_err("device %.*s reporting max_cqe %d max_qp_wr %d\n", - IB_DEVICE_NAME_MAX, - sc->ib.dev->name, - sc->ib.dev->attrs.max_cqe, - sc->ib.dev->attrs.max_qp_wr); - pr_err("consider lowering send_credit_target = %d, max_rdma_ctxs = %d\n", - sp->send_credit_target, qp_cap.max_rdma_ctxs); - return -EINVAL; - } - - if (qp_cap.max_recv_wr > sc->ib.dev->attrs.max_cqe || - qp_cap.max_recv_wr > sc->ib.dev->attrs.max_qp_wr) { - pr_err("Possible CQE overrun: max_recv_wr %d\n", - qp_cap.max_recv_wr); - pr_err("device %.*s reporting max_cqe %d max_qp_wr %d\n", - IB_DEVICE_NAME_MAX, - sc->ib.dev->name, - sc->ib.dev->attrs.max_cqe, - sc->ib.dev->attrs.max_qp_wr); - pr_err("consider lowering receive_credit_max = %d\n", - sp->recv_credit_max); - return -EINVAL; - } - - if (qp_cap.max_send_sge > sc->ib.dev->attrs.max_send_sge || - qp_cap.max_recv_sge > sc->ib.dev->attrs.max_recv_sge) { - pr_err("device %.*s max_send_sge/max_recv_sge = %d/%d too small\n", - IB_DEVICE_NAME_MAX, - sc->ib.dev->name, - sc->ib.dev->attrs.max_send_sge, - sc->ib.dev->attrs.max_recv_sge); - return -EINVAL; - } - - sc->ib.pd = ib_alloc_pd(sc->ib.dev, 0); - if (IS_ERR(sc->ib.pd)) { - pr_err("Can't create RDMA PD\n"); - ret = PTR_ERR(sc->ib.pd); - sc->ib.pd = NULL; - return ret; - } - - sc->ib.send_cq = ib_alloc_cq_any(sc->ib.dev, sc, - max_send_wr, - IB_POLL_WORKQUEUE); - if (IS_ERR(sc->ib.send_cq)) { - pr_err("Can't create RDMA send CQ\n"); - ret = PTR_ERR(sc->ib.send_cq); - sc->ib.send_cq = NULL; - goto err; - } - - sc->ib.recv_cq = ib_alloc_cq_any(sc->ib.dev, sc, - qp_cap.max_recv_wr, - IB_POLL_WORKQUEUE); - if (IS_ERR(sc->ib.recv_cq)) { - pr_err("Can't create RDMA recv CQ\n"); - ret = PTR_ERR(sc->ib.recv_cq); - sc->ib.recv_cq = NULL; - goto err; - } - - /* - * We reset completely here! - * As the above use was just temporary - * to calc max_send_wr and rdma_send_wr. - * - * rdma_create_qp() will trigger rdma_rw_init_qp() - * again if max_rdma_ctxs is not 0. - */ - memset(&qp_attr, 0, sizeof(qp_attr)); - qp_attr.event_handler = smb_direct_qpair_handler; - qp_attr.qp_context = sc; - qp_attr.cap = qp_cap; - qp_attr.sq_sig_type = IB_SIGNAL_REQ_WR; - qp_attr.qp_type = IB_QPT_RC; - qp_attr.send_cq = sc->ib.send_cq; - qp_attr.recv_cq = sc->ib.recv_cq; - qp_attr.port_num = ~0; - - ret = rdma_create_qp(sc->rdma.cm_id, sc->ib.pd, &qp_attr); - if (ret) { - pr_err("Can't create RDMA QP: %d\n", ret); - goto err; - } - - sc->ib.qp = sc->rdma.cm_id->qp; - sc->rdma.cm_id->event_handler = smb_direct_cm_handler; - - return 0; -err: - if (sc->ib.qp) { - sc->ib.qp = NULL; - rdma_destroy_qp(sc->rdma.cm_id); - } - if (sc->ib.recv_cq) { - ib_destroy_cq(sc->ib.recv_cq); - sc->ib.recv_cq = NULL; - } - if (sc->ib.send_cq) { - ib_destroy_cq(sc->ib.send_cq); - sc->ib.send_cq = NULL; - } - if (sc->ib.pd) { - ib_dealloc_pd(sc->ib.pd); - sc->ib.pd = NULL; - } - return ret; -} - -static int smb_direct_prepare(struct ksmbd_transport *t) -{ - struct smb_direct_transport *st = SMBD_TRANS(t); - struct smbdirect_socket *sc = &st->socket; - struct smbdirect_socket_parameters *sp = &sc->parameters; - struct smbdirect_recv_io *recvmsg; - struct smbdirect_negotiate_req *req; - unsigned long flags; - int ret; - - /* - * We are waiting to pass the following states: - * - * SMBDIRECT_SOCKET_RDMA_CONNECT_NEEDED - * SMBDIRECT_SOCKET_RDMA_CONNECT_RUNNING - * SMBDIRECT_SOCKET_NEGOTIATE_NEEDED - * - * To finally get to SMBDIRECT_SOCKET_NEGOTIATE_RUNNING - * in order to continue below. - * - * Everything else is unexpected and an error. - */ - ksmbd_debug(RDMA, "Waiting for SMB_DIRECT negotiate request\n"); - ret = wait_event_interruptible_timeout(sc->status_wait, - sc->status != SMBDIRECT_SOCKET_RDMA_CONNECT_NEEDED && - sc->status != SMBDIRECT_SOCKET_RDMA_CONNECT_RUNNING && - sc->status != SMBDIRECT_SOCKET_NEGOTIATE_NEEDED, - msecs_to_jiffies(sp->negotiate_timeout_msec)); - if (ret <= 0 || sc->status != SMBDIRECT_SOCKET_NEGOTIATE_RUNNING) - return ret < 0 ? ret : -ETIMEDOUT; - - recvmsg = get_first_reassembly(sc); - if (!recvmsg) - return -ECONNABORTED; - - ret = smb_direct_check_recvmsg(recvmsg); - if (ret) - goto put; - - req = (struct smbdirect_negotiate_req *)recvmsg->packet; - sp->max_recv_size = min_t(u32, sp->max_recv_size, - le32_to_cpu(req->preferred_send_size)); - sp->max_send_size = min_t(u32, sp->max_send_size, - le32_to_cpu(req->max_receive_size)); - sp->max_fragmented_send_size = - le32_to_cpu(req->max_fragmented_size); - /* - * The maximum fragmented upper-layer payload receive size supported - * - * Assume max_payload_per_credit is - * smb_direct_receive_credit_max - 24 = 1340 - * - * The maximum number would be - * smb_direct_receive_credit_max * max_payload_per_credit - * - * 1340 * 255 = 341700 (0x536C4) - * - * The minimum value from the spec is 131072 (0x20000) - * - * For now we use the logic we used before: - * (1364 * 255) / 2 = 173910 (0x2A756) - * - * We need to adjust this here in case the peer - * lowered sp->max_recv_size. - * - * TODO: instead of adjusting max_fragmented_recv_size - * we should adjust the number of available buffers, - * but for now we keep the current logic. - */ - sp->max_fragmented_recv_size = - (sp->recv_credit_max * sp->max_recv_size) / 2; - sc->recv_io.credits.target = le16_to_cpu(req->credits_requested); - sc->recv_io.credits.target = min_t(u16, sc->recv_io.credits.target, sp->recv_credit_max); - sc->recv_io.credits.target = max_t(u16, sc->recv_io.credits.target, 1); - -put: - spin_lock_irqsave(&sc->recv_io.reassembly.lock, flags); - sc->recv_io.reassembly.queue_length--; - list_del(&recvmsg->list); - spin_unlock_irqrestore(&sc->recv_io.reassembly.lock, flags); - put_recvmsg(sc, recvmsg); - - if (ret == -ECONNABORTED) - return ret; - - if (ret) - goto respond; - - /* - * We negotiated with success, so we need to refill the recv queue. - * We do that with sc->idle.immediate_work still being disabled - * via smbdirect_socket_init(), so that queue_work(sc->workqueue, - * &sc->idle.immediate_work) in smb_direct_post_recv_credits() - * is a no-op. - * - * The message that grants the credits to the client is - * the negotiate response. - */ - INIT_WORK(&sc->recv_io.posted.refill_work, smb_direct_post_recv_credits); - smb_direct_post_recv_credits(&sc->recv_io.posted.refill_work); - if (unlikely(sc->first_error)) - return sc->first_error; - INIT_WORK(&sc->idle.immediate_work, smb_direct_send_immediate_work); - -respond: - ret = smb_direct_send_negotiate_response(sc, ret); - + free_transport(t); return ret; } -static int smb_direct_connect(struct smbdirect_socket *sc) +static int smb_direct_listener_kthread_fn(void *p) { - struct smbdirect_recv_io *recv_io; - int ret; + struct smb_direct_listener *listener = (struct smb_direct_listener *)p; + struct smbdirect_socket *client_sc = NULL; - ret = smb_direct_init_params(sc); - if (ret) { - pr_err("Can't configure RDMA parameters\n"); - return ret; - } + while (!kthread_should_stop()) { + struct proto_accept_arg arg = { .err = -EINVAL, }; + long timeo = MAX_SCHEDULE_TIMEOUT; - ret = smb_direct_create_pools(sc); - if (ret) { - pr_err("Can't init RDMA pool: %d\n", ret); - return ret; - } - - list_for_each_entry(recv_io, &sc->recv_io.free.list, list) - recv_io->cqe.done = recv_done; + if (!listener->socket) + break; + client_sc = smbdirect_socket_accept(listener->socket, timeo, &arg); + if (!client_sc && arg.err == -EINVAL) + break; + if (!client_sc) + continue; - ret = smb_direct_create_qpair(sc); - if (ret) { - pr_err("Can't accept RDMA client: %d\n", ret); - return ret; + ksmbd_debug(CONN, "connect success: accepted new connection\n"); + smb_direct_new_connection(listener, client_sc); } - ret = smb_direct_prepare_negotiation(sc); - if (ret) { - pr_err("Can't negotiate: %d\n", ret); - return ret; - } + ksmbd_debug(CONN, "releasing socket\n"); return 0; } -static bool rdma_frwr_is_supported(struct ib_device_attr *attrs) +static void smb_direct_listener_destroy(struct smb_direct_listener *listener) { - if (!(attrs->device_cap_flags & IB_DEVICE_MEM_MGT_EXTENSIONS)) - return false; - if (attrs->max_fast_reg_page_list_len == 0) - return false; - return true; -} - -static int smb_direct_handle_connect_request(struct rdma_cm_id *new_cm_id, - struct rdma_cm_event *event) -{ - struct smb_direct_listener *listener = new_cm_id->context; - struct smb_direct_transport *t; - struct smbdirect_socket *sc; - struct smbdirect_socket_parameters *sp; - struct task_struct *handler; - u8 peer_initiator_depth; - u8 peer_responder_resources; int ret; - if (!rdma_frwr_is_supported(&new_cm_id->device->attrs)) { - ksmbd_debug(RDMA, - "Fast Registration Work Requests is not supported. device capabilities=%llx\n", - new_cm_id->device->attrs.device_cap_flags); - return -EPROTONOSUPPORT; - } + if (listener->socket) + smbdirect_socket_shutdown(listener->socket); - t = alloc_transport(new_cm_id); - if (!t) - return -ENOMEM; - sc = &t->socket; - sp = &sc->parameters; - - peer_initiator_depth = event->param.conn.initiator_depth; - peer_responder_resources = event->param.conn.responder_resources; - if (rdma_protocol_iwarp(new_cm_id->device, new_cm_id->port_num) && - event->param.conn.private_data_len == 8) { - /* - * Legacy clients with only iWarp MPA v1 support - * need a private blob in order to negotiate - * the IRD/ORD values. - */ - const __be32 *ird_ord_hdr = event->param.conn.private_data; - u32 ird32 = be32_to_cpu(ird_ord_hdr[0]); - u32 ord32 = be32_to_cpu(ird_ord_hdr[1]); - - /* - * cifs.ko sends the legacy IRD/ORD negotiation - * event if iWarp MPA v2 was used. - * - * Here we check that the values match and only - * mark the client as legacy if they don't match. - */ - if ((u32)event->param.conn.initiator_depth != ird32 || - (u32)event->param.conn.responder_resources != ord32) { - /* - * There are broken clients (old cifs.ko) - * using little endian and also - * struct rdma_conn_param only uses u8 - * for initiator_depth and responder_resources, - * so we truncate the value to U8_MAX. - * - * smb_direct_accept_client() will then - * do the real negotiation in order to - * select the minimum between client and - * server. - */ - ird32 = min_t(u32, ird32, U8_MAX); - ord32 = min_t(u32, ord32, U8_MAX); - - sc->rdma.legacy_iwarp = true; - peer_initiator_depth = (u8)ird32; - peer_responder_resources = (u8)ord32; - } + if (listener->thread) { + ret = kthread_stop(listener->thread); + if (ret) + pr_err("failed to stop forker thread\n"); + listener->thread = NULL; } - /* - * First set what the we as server are able to support - */ - sp->initiator_depth = min_t(u8, sp->initiator_depth, - new_cm_id->device->attrs.max_qp_rd_atom); - - /* - * negotiate the value by using the minimum - * between client and server if the client provided - * non 0 values. - */ - if (peer_initiator_depth != 0) - sp->initiator_depth = min_t(u8, sp->initiator_depth, - peer_initiator_depth); - if (peer_responder_resources != 0) - sp->responder_resources = min_t(u8, sp->responder_resources, - peer_responder_resources); - - ret = smb_direct_connect(sc); - if (ret) - goto out_err; - - handler = kthread_run(ksmbd_conn_handler_loop, - KSMBD_TRANS(t)->conn, "ksmbd:r%u", - listener->port); - if (IS_ERR(handler)) { - ret = PTR_ERR(handler); - pr_err("Can't start thread\n"); - goto out_err; + if (listener->socket) { + smbdirect_socket_release(listener->socket); + listener->socket = NULL; } - return 0; -out_err: - free_transport(t); - return ret; -} - -static int smb_direct_listen_handler(struct rdma_cm_id *cm_id, - struct rdma_cm_event *event) -{ - switch (event->event) { - case RDMA_CM_EVENT_CONNECT_REQUEST: { - int ret = smb_direct_handle_connect_request(cm_id, event); - - if (ret) { - pr_err("Can't create transport: %d\n", ret); - return ret; - } - - ksmbd_debug(RDMA, "Received connection request. cm_id=%p\n", - cm_id); - break; - } - default: - pr_err("Unexpected listen event. cm_id=%p, event=%s (%d)\n", - cm_id, rdma_event_msg(event->event), event->event); - break; - } - return 0; + listener->port = 0; } static int smb_direct_listen(struct smb_direct_listener *listener, int port) { - int ret; - struct rdma_cm_id *cm_id; - u8 node_type = RDMA_NODE_UNSPECIFIED; + struct net *net = current->nsproxy->net_ns; + struct task_struct *kthread; struct sockaddr_in sin = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_ANY), .sin_port = htons(port), }; + struct smbdirect_socket_parameters init_params = {}; + struct smbdirect_socket_parameters *sp; + struct smbdirect_socket *sc; + u64 port_flags = 0; + int ret; switch (port) { case SMB_DIRECT_PORT_IWARP: @@ -2800,7 +396,7 @@ static int smb_direct_listen(struct smb_direct_listener *listener, * only allow iWarp devices * for port 5445. */ - node_type = RDMA_NODE_RNIC; + port_flags |= SMBDIRECT_FLAG_PORT_RANGE_ONLY_IW; break; case SMB_DIRECT_PORT_INFINIBAND: /* @@ -2809,119 +405,95 @@ static int smb_direct_listen(struct smb_direct_listener *listener, * * (Basically don't allow iWarp devices) */ - node_type = RDMA_NODE_IB_CA; + port_flags |= SMBDIRECT_FLAG_PORT_RANGE_ONLY_IB; break; default: pr_err("unsupported smbdirect port=%d!\n", port); return -ENODEV; } - cm_id = rdma_create_id(&init_net, smb_direct_listen_handler, - listener, RDMA_PS_TCP, IB_QPT_RC); - if (IS_ERR(cm_id)) { - pr_err("Can't create cm id: %ld\n", PTR_ERR(cm_id)); - return PTR_ERR(cm_id); + ret = smbdirect_socket_create_kern(net, &sc); + if (ret) { + pr_err("smbdirect_socket_create_kern() failed: %d %1pe\n", + ret, ERR_PTR(ret)); + return ret; } - ret = rdma_restrict_node_type(cm_id, node_type); + /* + * Create the initial parameters + */ + sp = &init_params; + sp->flags |= port_flags; + sp->negotiate_timeout_msec = SMB_DIRECT_NEGOTIATE_TIMEOUT * 1000; + sp->initiator_depth = SMB_DIRECT_CM_INITIATOR_DEPTH; + sp->responder_resources = 1; + sp->recv_credit_max = smb_direct_receive_credit_max; + sp->send_credit_target = smb_direct_send_credit_target; + sp->max_send_size = smb_direct_max_send_size; + sp->max_fragmented_recv_size = smb_direct_max_fragmented_recv_size; + sp->max_recv_size = smb_direct_max_receive_size; + sp->max_read_write_size = smb_direct_max_read_write_size; + sp->keepalive_interval_msec = SMB_DIRECT_KEEPALIVE_SEND_INTERVAL * 1000; + sp->keepalive_timeout_msec = SMB_DIRECT_KEEPALIVE_RECV_TIMEOUT * 1000; + + smbdirect_socket_set_logging(sc, NULL, + smb_direct_logging_needed, + smb_direct_logging_vaprintf); + ret = smbdirect_socket_set_initial_parameters(sc, sp); if (ret) { - pr_err("rdma_restrict_node_type(%u) failed %d\n", - node_type, ret); + pr_err("Failed smbdirect_socket_set_initial_parameters(): %d %1pe\n", + ret, ERR_PTR(ret)); + goto err; + } + ret = smbdirect_socket_set_kernel_settings(sc, IB_POLL_WORKQUEUE, KSMBD_DEFAULT_GFP); + if (ret) { + pr_err("Failed smbdirect_socket_set_kernel_settings(): %d %1pe\n", + ret, ERR_PTR(ret)); goto err; } - ret = rdma_bind_addr(cm_id, (struct sockaddr *)&sin); + ret = smbdirect_socket_bind(sc, (struct sockaddr *)&sin); if (ret) { - pr_err("Can't bind: %d\n", ret); + pr_err("smbdirect_socket_bind() failed: %d %1pe\n", + ret, ERR_PTR(ret)); goto err; } - ret = rdma_listen(cm_id, 10); + ret = smbdirect_socket_listen(sc, 10); if (ret) { - pr_err("Can't listen: %d\n", ret); + pr_err("Port[%d] smbdirect_socket_listen() failed: %d %1pe\n", + port, ret, ERR_PTR(ret)); goto err; } listener->port = port; - listener->cm_id = cm_id; + listener->socket = sc; + + kthread = kthread_run(smb_direct_listener_kthread_fn, + listener, + "ksmbd-smbdirect-listener-%u", port); + if (IS_ERR(kthread)) { + ret = PTR_ERR(kthread); + pr_err("Can't start ksmbd listen kthread: %d %1pe\n", + ret, ERR_PTR(ret)); + goto err; + } + listener->thread = kthread; return 0; err: - listener->port = 0; - listener->cm_id = NULL; - rdma_destroy_id(cm_id); + smb_direct_listener_destroy(listener); return ret; } -static int smb_direct_ib_client_add(struct ib_device *ib_dev) -{ - struct smb_direct_device *smb_dev; - - if (!rdma_frwr_is_supported(&ib_dev->attrs)) - return 0; - - smb_dev = kzalloc_obj(*smb_dev, KSMBD_DEFAULT_GFP); - if (!smb_dev) - return -ENOMEM; - smb_dev->ib_dev = ib_dev; - - write_lock(&smb_direct_device_lock); - list_add(&smb_dev->list, &smb_direct_device_list); - write_unlock(&smb_direct_device_lock); - - ksmbd_debug(RDMA, "ib device added: name %s\n", ib_dev->name); - return 0; -} - -static void smb_direct_ib_client_remove(struct ib_device *ib_dev, - void *client_data) -{ - struct smb_direct_device *smb_dev, *tmp; - - write_lock(&smb_direct_device_lock); - list_for_each_entry_safe(smb_dev, tmp, &smb_direct_device_list, list) { - if (smb_dev->ib_dev == ib_dev) { - list_del(&smb_dev->list); - kfree(smb_dev); - break; - } - } - write_unlock(&smb_direct_device_lock); -} - -static struct ib_client smb_direct_ib_client = { - .name = "ksmbd_smb_direct_ib", - .add = smb_direct_ib_client_add, - .remove = smb_direct_ib_client_remove, -}; - int ksmbd_rdma_init(void) { int ret; smb_direct_ib_listener = smb_direct_iw_listener = (struct smb_direct_listener) { - .cm_id = NULL, + .socket = NULL, }; - ret = ib_register_client(&smb_direct_ib_client); - if (ret) { - pr_err("failed to ib_register_client\n"); - return ret; - } - - /* When a client is running out of send credits, the credits are - * granted by the server's sending a packet using this queue. - * This avoids the situation that a clients cannot send packets - * for lack of credits - */ - smb_direct_wq = alloc_workqueue("ksmbd-smb_direct-wq", - WQ_HIGHPRI | WQ_MEM_RECLAIM | WQ_PERCPU, - 0); - if (!smb_direct_wq) { - ret = -ENOMEM; - goto err; - } - ret = smb_direct_listen(&smb_direct_ib_listener, SMB_DIRECT_PORT_INFINIBAND); if (ret) { @@ -2929,8 +501,8 @@ int ksmbd_rdma_init(void) goto err; } - ksmbd_debug(RDMA, "InfiniBand/RoCEv1/RoCEv2 RDMA listener. cm_id=%p\n", - smb_direct_ib_listener.cm_id); + ksmbd_debug(RDMA, "InfiniBand/RoCEv1/RoCEv2 RDMA listener. socket=%p\n", + smb_direct_ib_listener.socket); ret = smb_direct_listen(&smb_direct_iw_listener, SMB_DIRECT_PORT_IWARP); @@ -2939,107 +511,37 @@ int ksmbd_rdma_init(void) goto err; } - ksmbd_debug(RDMA, "iWarp RDMA listener. cm_id=%p\n", - smb_direct_iw_listener.cm_id); + ksmbd_debug(RDMA, "iWarp RDMA listener. socket=%p\n", + smb_direct_iw_listener.socket); + WRITE_ONCE(smb_direct_enabled, true); return 0; err: + WRITE_ONCE(smb_direct_enabled, false); ksmbd_rdma_stop_listening(); - ksmbd_rdma_destroy(); return ret; } void ksmbd_rdma_stop_listening(void) { - if (!smb_direct_ib_listener.cm_id && !smb_direct_iw_listener.cm_id) - return; - - ib_unregister_client(&smb_direct_ib_client); - - if (smb_direct_ib_listener.cm_id) - rdma_destroy_id(smb_direct_ib_listener.cm_id); - if (smb_direct_iw_listener.cm_id) - rdma_destroy_id(smb_direct_iw_listener.cm_id); - - smb_direct_ib_listener = smb_direct_iw_listener = (struct smb_direct_listener) { - .cm_id = NULL, - }; + WRITE_ONCE(smb_direct_enabled, false); + smb_direct_listener_destroy(&smb_direct_ib_listener); + smb_direct_listener_destroy(&smb_direct_iw_listener); } -void ksmbd_rdma_destroy(void) +bool ksmbd_rdma_enabled(void) { - if (smb_direct_wq) { - destroy_workqueue(smb_direct_wq); - smb_direct_wq = NULL; - } -} - -static bool ksmbd_find_rdma_capable_netdev(struct net_device *netdev) -{ - struct smb_direct_device *smb_dev; - int i; - bool rdma_capable = false; - - read_lock(&smb_direct_device_lock); - list_for_each_entry(smb_dev, &smb_direct_device_list, list) { - for (i = 0; i < smb_dev->ib_dev->phys_port_cnt; i++) { - struct net_device *ndev; - - ndev = ib_device_get_netdev(smb_dev->ib_dev, i + 1); - if (!ndev) - continue; - - if (ndev == netdev) { - dev_put(ndev); - rdma_capable = true; - goto out; - } - dev_put(ndev); - } - } -out: - read_unlock(&smb_direct_device_lock); - - if (rdma_capable == false) { - struct ib_device *ibdev; - - ibdev = ib_device_get_by_netdev(netdev, RDMA_DRIVER_UNKNOWN); - if (ibdev) { - rdma_capable = rdma_frwr_is_supported(&ibdev->attrs); - ib_device_put(ibdev); - } - } - - ksmbd_debug(RDMA, "netdev(%s) rdma capable : %s\n", - netdev->name, str_true_false(rdma_capable)); - - return rdma_capable; + return READ_ONCE(smb_direct_enabled); } bool ksmbd_rdma_capable_netdev(struct net_device *netdev) { - struct net_device *lower_dev; - struct list_head *iter; + u8 node_type = smbdirect_netdev_rdma_capable_node_type(netdev); - if (ksmbd_find_rdma_capable_netdev(netdev)) - return true; - - /* check if netdev is bridge or VLAN */ - if (netif_is_bridge_master(netdev) || - netdev->priv_flags & IFF_802_1Q_VLAN) - netdev_for_each_lower_dev(netdev, lower_dev, iter) - if (ksmbd_find_rdma_capable_netdev(lower_dev)) - return true; - - /* check if netdev is IPoIB safely without layer violation */ - if (netdev->type == ARPHRD_INFINIBAND) - return true; - - return false; + return node_type != RDMA_NODE_UNSPECIFIED; } static const struct ksmbd_transport_ops ksmbd_smb_direct_transport_ops = { - .prepare = smb_direct_prepare, .disconnect = smb_direct_disconnect, .shutdown = smb_direct_shutdown, .writev = smb_direct_writev, @@ -3048,3 +550,5 @@ static const struct ksmbd_transport_ops ksmbd_smb_direct_transport_ops = { .rdma_write = smb_direct_rdma_write, .free_transport = smb_direct_free_transport, }; + +MODULE_IMPORT_NS("SMBDIRECT"); diff --git a/fs/smb/server/transport_rdma.h b/fs/smb/server/transport_rdma.h index 3f93c6a9f7e4..23247713b5c3 100644 --- a/fs/smb/server/transport_rdma.h +++ b/fs/smb/server/transport_rdma.h @@ -14,17 +14,19 @@ #ifdef CONFIG_SMB_SERVER_SMBDIRECT int ksmbd_rdma_init(void); void ksmbd_rdma_stop_listening(void); -void ksmbd_rdma_destroy(void); +bool ksmbd_rdma_enabled(void); bool ksmbd_rdma_capable_netdev(struct net_device *netdev); void init_smbd_max_io_size(unsigned int sz); unsigned int get_smbd_max_read_write_size(struct ksmbd_transport *kt); #else static inline int ksmbd_rdma_init(void) { return 0; } static inline void ksmbd_rdma_stop_listening(void) { } -static inline void ksmbd_rdma_destroy(void) { } +static inline bool ksmbd_rdma_enabled(void) { return false; } static inline bool ksmbd_rdma_capable_netdev(struct net_device *netdev) { return false; } static inline void init_smbd_max_io_size(unsigned int sz) { } static inline unsigned int get_smbd_max_read_write_size(struct ksmbd_transport *kt) { return 0; } #endif +#include <linux/smbdirect.h> + #endif /* __KSMBD_TRANSPORT_RDMA_H__ */ diff --git a/fs/smb/server/transport_tcp.c b/fs/smb/server/transport_tcp.c index 7e29b06820e2..4968cfc1a572 100644 --- a/fs/smb/server/transport_tcp.c +++ b/fs/smb/server/transport_tcp.c @@ -39,6 +39,7 @@ struct tcp_transport { static const struct ksmbd_transport_ops ksmbd_tcp_transport_ops; 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); @@ -183,6 +184,8 @@ static int ksmbd_tcp_new_connection(struct socket *client_sk) t = alloc_transport(client_sk); if (!t) { sock_release(client_sk); + if (server_conf.max_connections) + atomic_dec(&active_num_conn); return -ENOMEM; } @@ -279,7 +282,7 @@ static int ksmbd_kthread_fn(void *p) skip_max_ip_conns_limit: if (server_conf.max_connections && - atomic_inc_return(&active_num_conn) >= server_conf.max_connections) { + atomic_inc_return(&active_num_conn) > server_conf.max_connections) { pr_info_ratelimited("Limit the maximum number of connections(%u)\n", atomic_read(&active_num_conn)); atomic_dec(&active_num_conn); @@ -290,6 +293,12 @@ skip_max_ip_conns_limit: ksmbd_debug(CONN, "connect success: accepted new connection\n"); client_sk->sk->sk_rcvtimeo = KSMBD_TCP_RECV_TIMEOUT; client_sk->sk->sk_sndtimeo = KSMBD_TCP_SEND_TIMEOUT; + /* + * Detect peers that disappear without sending a FIN or RST. + * Otherwise the connection handler can retry receive timeouts + * indefinitely and keep the connection in conn_list. + */ + sock_set_keepalive(client_sk->sk); ksmbd_tcp_new_connection(client_sk); } @@ -313,13 +322,20 @@ static int ksmbd_tcp_run_kthread(struct interface *iface) int rc; struct task_struct *kthread; - kthread = kthread_run(ksmbd_kthread_fn, (void *)iface, "ksmbd-%s", - iface->name); + kthread = kthread_create(ksmbd_kthread_fn, (void *)iface, "ksmbd-%s", + iface->name); if (IS_ERR(kthread)) { rc = PTR_ERR(kthread); return rc; } + + /* + * The listener can exit after its socket is shutdown, so keep the + * task_struct alive until the caller has stopped it. + */ + get_task_struct(kthread); iface->ksmbd_kthread = kthread; + wake_up_process(kthread); return 0; } @@ -415,14 +431,15 @@ 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 int ksmbd_tcp_writev(struct ksmbd_transport *t, struct kvec *iov, - int nvecs, int size, bool need_invalidate, - unsigned int remote_key) - +static int ksmbd_tcp_writev(struct ksmbd_transport *t, + const struct ksmbd_transport_write *tx) { - struct msghdr smb_msg = {.msg_flags = MSG_NOSIGNAL}; + struct msghdr smb_msg = { + .msg_flags = MSG_NOSIGNAL | tx->msg_flags, + }; - return kernel_sendmsg(TCP_TRANS(t)->sock, &smb_msg, iov, nvecs, size); + return kernel_sendmsg(TCP_TRANS(t)->sock, &smb_msg, tx->iov, + tx->iov_cnt, tx->size); } static void ksmbd_tcp_disconnect(struct ksmbd_transport *t) @@ -432,6 +449,11 @@ static void ksmbd_tcp_disconnect(struct ksmbd_transport *t) atomic_dec(&active_num_conn); } +static void ksmbd_tcp_shutdown(struct ksmbd_transport *t) +{ + kernel_sock_shutdown(TCP_TRANS(t)->sock, SHUT_RDWR); +} + static void tcp_destroy_socket(struct socket *ksmbd_socket) { int ret; @@ -509,6 +531,12 @@ static int create_socket(struct interface *iface) goto out_error; } + /* + * Accepted sockets inherit the listener's net reference. Keep TCP + * timers alive after a kernel socket is released. + */ + sk_net_refcnt_upgrade(ksmbd_socket->sk); + ret = kernel_listen(ksmbd_socket, KSMBD_SOCKET_BACKLOG); if (ret) { pr_err("Port listen() error: %d\n", ret); @@ -578,12 +606,7 @@ static int ksmbd_netdev_event(struct notifier_block *nb, unsigned long event, if (iface && iface->state == IFACE_STATE_CONFIGURED) { ksmbd_debug(CONN, "netdev-down event: netdev(%s) is going down\n", iface->name); - kernel_sock_shutdown(iface->ksmbd_socket, SHUT_RDWR); - tcp_stop_kthread(iface->ksmbd_kthread); - iface->ksmbd_kthread = NULL; - sock_release(iface->ksmbd_socket); - iface->ksmbd_socket = NULL; - + ksmbd_tcp_stop_listener(iface); iface->state = IFACE_STATE_DOWN; break; } @@ -611,11 +634,25 @@ static void tcp_stop_kthread(struct task_struct *kthread) if (!kthread) return; - ret = kthread_stop(kthread); + ret = kthread_stop_put(kthread); if (ret) pr_err("failed to stop forker thread\n"); } +static void ksmbd_tcp_stop_listener(struct interface *iface) +{ + if (iface->ksmbd_socket) + kernel_sock_shutdown(iface->ksmbd_socket, SHUT_RDWR); + + tcp_stop_kthread(iface->ksmbd_kthread); + iface->ksmbd_kthread = NULL; + + if (iface->ksmbd_socket) { + sock_release(iface->ksmbd_socket); + iface->ksmbd_socket = NULL; + } +} + void ksmbd_tcp_destroy(void) { struct interface *iface, *tmp; @@ -623,6 +660,7 @@ void ksmbd_tcp_destroy(void) unregister_netdevice_notifier(&ksmbd_netdev_notifier); list_for_each_entry_safe(iface, tmp, &iface_list, entry) { + ksmbd_tcp_stop_listener(iface); list_del(&iface->entry); kfree(iface->name); kfree(iface); @@ -678,5 +716,6 @@ static const struct ksmbd_transport_ops ksmbd_tcp_transport_ops = { .read = ksmbd_tcp_read, .writev = ksmbd_tcp_writev, .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 d08973b288e5..c2c9aaa5de1b 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -7,6 +7,7 @@ #include <crypto/sha2.h> #include <linux/kernel.h> #include <linux/fs.h> +#include <linux/fs_struct.h> #include <linux/filelock.h> #include <linux/uaccess.h> #include <linux/backing-dev.h> @@ -16,11 +17,11 @@ #include <linux/fsnotify.h> #include <linux/dcache.h> #include <linux/slab.h> +#include <linux/sizes.h> #include <linux/vmalloc.h> #include <linux/sched/xacct.h> #include <linux/crc32c.h> -#include <linux/namei.h> -#include <linux/splice.h> +#include <linux/fileattr.h> #include "glob.h" #include "oplock.h" @@ -56,7 +57,7 @@ static int ksmbd_vfs_path_lookup(struct ksmbd_share_config *share_conf, { struct qstr last; const struct path *root_share_path = &share_conf->vfs_path; - int err, type; + int err; struct dentry *d; if (pathname[0] == '\0') { @@ -67,17 +68,12 @@ static int ksmbd_vfs_path_lookup(struct ksmbd_share_config *share_conf, } CLASS(filename_kernel, filename)(pathname); - err = vfs_path_parent_lookup(filename, flags, - path, &last, &type, - root_share_path); + scoped_with_init_fs() + err = vfs_path_parent_lookup(filename, flags, path, &last, + root_share_path); if (err) return err; - if (unlikely(type != LAST_NORM)) { - path_put(path); - return -ENOENT; - } - if (for_remove) { err = mnt_want_write(path->mnt); if (err) { @@ -254,17 +250,20 @@ out: static int ksmbd_vfs_stream_read(struct ksmbd_file *fp, char *buf, loff_t *pos, size_t count) { + const struct cred *saved_cred; ssize_t v_len; char *stream_buf = NULL; ksmbd_debug(VFS, "read stream data pos : %llu, count : %zd\n", *pos, count); + saved_cred = override_creds(fp->filp->f_cred); v_len = ksmbd_vfs_getcasexattr(file_mnt_idmap(fp->filp), fp->filp->f_path.dentry, fp->stream.name, fp->stream.size, &stream_buf); + revert_creds(saved_cred); if ((int)v_len <= 0) return (int)v_len; @@ -300,9 +299,6 @@ static int check_lock_range(struct file *filp, loff_t start, loff_t end, struct file_lock_context *ctx = locks_inode_context(file_inode(filp)); int error = 0; - if (start == end) - return 0; - if (!ctx || list_empty_careful(&ctx->flc_posix)) return 0; @@ -348,7 +344,7 @@ int ksmbd_vfs_read(struct ksmbd_work *work, struct ksmbd_file *fp, size_t count, ssize_t nbytes = 0; struct inode *inode = file_inode(filp); - if (S_ISDIR(inode->i_mode)) + if (S_ISDIR(inode->i_mode) && !ksmbd_stream_fd(fp)) return -EISDIR; if (unlikely(count == 0)) @@ -388,6 +384,7 @@ int ksmbd_vfs_read(struct ksmbd_work *work, struct ksmbd_file *fp, size_t count, static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos, size_t count) { + const struct cred *saved_cred; char *stream_buf = NULL, *wbuf; struct mnt_idmap *idmap = file_mnt_idmap(fp->filp); size_t size; @@ -408,6 +405,7 @@ static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos, count = XATTR_SIZE_MAX - *pos; } + saved_cred = override_creds(fp->filp->f_cred); v_len = ksmbd_vfs_getcasexattr(idmap, fp->filp->f_path.dentry, fp->stream.name, @@ -416,14 +414,14 @@ static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos, if (v_len < 0) { pr_err("not found stream in xattr : %zd\n", v_len); err = v_len; - goto out; + goto out_revert; } if (v_len < size) { wbuf = kvzalloc(size, KSMBD_DEFAULT_GFP); if (!wbuf) { err = -ENOMEM; - goto out; + goto out_revert; } if (v_len > 0) @@ -441,6 +439,8 @@ static int ksmbd_vfs_stream_write(struct ksmbd_file *fp, char *buf, loff_t *pos, size, 0, true); +out_revert: + revert_creds(saved_cred); if (err < 0) goto out; else @@ -473,7 +473,8 @@ int ksmbd_vfs_write(struct ksmbd_work *work, struct ksmbd_file *fp, if (work->conn->connection_type) { if (!(fp->daccess & (FILE_WRITE_DATA_LE | FILE_APPEND_DATA_LE)) || - S_ISDIR(file_inode(fp->filp)->i_mode)) { + (S_ISDIR(file_inode(fp->filp)->i_mode) && + !ksmbd_stream_fd(fp))) { pr_err("no right to write(%pD)\n", fp->filp); err = -EACCES; goto out; @@ -622,7 +623,8 @@ int ksmbd_vfs_link(struct ksmbd_work *work, const char *oldname, if (ksmbd_override_fsids(work)) return -ENOMEM; - err = kern_path(oldname, LOOKUP_NO_SYMLINKS, &oldpath); + scoped_with_init_fs() + err = kern_path(oldname, LOOKUP_NO_SYMLINKS, &oldpath); if (err) { pr_err("cannot get linux path for %s, err = %d\n", oldname, err); @@ -659,16 +661,35 @@ out1: return err; } -int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, - char *newname, int flags) +int ksmbd_vfs_check_rename_share(struct ksmbd_work *work, + const struct path *old_path) +{ + struct ksmbd_file *parent_fp; + int err = 0; + + parent_fp = ksmbd_lookup_fd_inode(old_path->dentry->d_parent); + if (!parent_fp) + return 0; + + if ((parent_fp->daccess & FILE_DELETE_LE) || + (!parent_fp->attrib_only && + !(parent_fp->saccess & FILE_SHARE_DELETE_LE))) { + ksmbd_debug(VFS, "parent dir blocks delete sharing\n"); + err = -ESHARE; + } + ksmbd_fd_put(work, parent_fp); + return err; +} + +int ksmbd_vfs_rename(struct ksmbd_work *work, struct ksmbd_file *old_fp, + char *newname, int flags) { + const struct path *old_path = &old_fp->filp->f_path; struct dentry *old_child = old_path->dentry; struct path new_path; struct qstr new_last; struct renamedata rd; struct ksmbd_share_config *share_conf = work->tcon->share_conf; - struct ksmbd_file *parent_fp; - int new_type; int err, lookup_flags = LOOKUP_NO_SYMLINKS; if (ksmbd_override_fsids(work)) @@ -678,8 +699,7 @@ int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, retry: err = vfs_path_parent_lookup(to, lookup_flags | LOOKUP_BENEATH, - &new_path, &new_last, &new_type, - &share_conf->vfs_path); + &new_path, &new_last, &share_conf->vfs_path); if (err) goto out1; @@ -701,17 +721,28 @@ retry: if (err) goto out_drop_write; - parent_fp = ksmbd_lookup_fd_inode(old_child->d_parent); - if (parent_fp) { - if (parent_fp->daccess & FILE_DELETE_LE) { - pr_err("parent dir is opened with delete access\n"); - err = -ESHARE; - ksmbd_fd_put(work, parent_fp); - goto out3; - } - ksmbd_fd_put(work, parent_fp); + if (d_is_dir(old_child) && ksmbd_has_nonposix_open_child(old_fp)) { + err = -EACCES; + goto out3; } + /* + * See MS-FSA 2.1.5.15.12. + * An overwrite rename must fail with STATUS_ACCESS_DENIED if the + * existing target still has a non-POSIX open. + */ + if (!(flags & (RENAME_NOREPLACE | RENAME_EXCHANGE)) && + d_inode(rd.new_dentry) && + d_inode(rd.new_dentry) != d_inode(old_child) && + ksmbd_has_other_nonposix_open(rd.new_dentry)) { + err = -EACCES; + goto out3; + } + + err = ksmbd_vfs_check_rename_share(work, old_path); + if (err) + goto out3; + if (d_is_symlink(rd.new_dentry)) { err = -EACCES; goto out3; @@ -919,49 +950,206 @@ void ksmbd_vfs_set_fadvise(struct file *filp, __le32 option) int ksmbd_vfs_zero_data(struct ksmbd_work *work, struct ksmbd_file *fp, loff_t off, loff_t len) { + const struct cred *saved_cred; + loff_t pos = off, size; + char *zero_buf = NULL; + int err; + smb_break_all_levII_oplock(work, fp, 1); - if (fp->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE) - return vfs_fallocate(fp->filp, - FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, - off, len); - - return vfs_fallocate(fp->filp, - FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE, - off, len); + if (!work->tcon->posix_extensions) { + loff_t size = i_size_read(file_inode(fp->filp)); + + if (off < size) { + err = check_lock_range(fp->filp, off, + min(off + len, size) - 1, + WRITE); + if (err) + return -EAGAIN; + } + } + + saved_cred = override_creds(fp->filp->f_cred); + if (fp->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE) { + err = vfs_fallocate(fp->filp, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + off, len); + } else { + size = i_size_read(file_inode(fp->filp)); + if (off >= size) { + err = 0; + goto out; + } + + len = min(len, size - off); + zero_buf = kvzalloc(SZ_64K, GFP_KERNEL); + if (!zero_buf) { + err = -ENOMEM; + goto out; + } + + while (len) { + ssize_t written; + size_t count = min_t(loff_t, len, SZ_64K); + + written = kernel_write(fp->filp, zero_buf, count, &pos); + if (written < 0) { + err = written; + goto out; + } + if (!written) { + err = -EIO; + goto out; + } + len -= written; + } + err = 0; + } +out: + revert_creds(saved_cred); + kvfree(zero_buf); + return err; } -int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, - struct file_allocated_range_buffer *ranges, - unsigned int in_count, unsigned int *out_count) +int ksmbd_vfs_zero_holes(struct ksmbd_file *fp) { struct file *f = fp->filp; - struct inode *inode = file_inode(fp->filp); - loff_t maxbytes = (u64)inode->i_sb->s_maxbytes, end; - loff_t extent_start, extent_end; - int ret = 0; + const struct cred *saved_cred; + loff_t size, pos = 0; + char *zero_buf; + int err; - if (start > maxbytes) - return -EFBIG; + err = file_write_and_wait(f); + if (err) + return err; - if (!in_count) + size = i_size_read(file_inode(f)); + if (!size) return 0; /* - * Shrink request scope to what the fs can actually handle. + * FALLOC_FL_ZERO_RANGE may leave unwritten extents, which SEEK_DATA + * reports as holes. Write zeroes into each hole so that clearing the + * sparse attribute leaves the file fully allocated. */ - if (length > maxbytes || (maxbytes - length) < start) - length = maxbytes - start; + zero_buf = kvzalloc(SZ_64K, GFP_KERNEL); + if (!zero_buf) + return -ENOMEM; - if (start + length > inode->i_size) - length = inode->i_size - start; + saved_cred = override_creds(f->f_cred); + while (pos < size) { + loff_t data, hole; + + hole = vfs_llseek(f, pos, SEEK_HOLE); + if (hole == -ENXIO || hole >= size) + break; + if (hole < 0) { + err = hole; + goto out; + } + + data = vfs_llseek(f, hole, SEEK_DATA); + if (data == -ENXIO) { + data = size; + } else if (data < 0) { + err = data; + goto out; + } + data = min(data, size); + if (data <= hole) { + err = -EIO; + goto out; + } + + pos = hole; + while (pos < data) { + ssize_t written; + size_t count = min_t(loff_t, data - pos, SZ_64K); + + written = kernel_write(f, zero_buf, count, &pos); + if (written < 0) { + err = written; + goto out; + } + if (!written) { + err = -EIO; + goto out; + } + } + } + err = file_write_and_wait(f); +out: + revert_creds(saved_cred); + kvfree(zero_buf); + return err; +} + +int ksmbd_vfs_trim_data(struct ksmbd_work *work, struct ksmbd_file *fp, + loff_t off, loff_t len) +{ + const struct cred *saved_cred; + int err; + + smb_break_all_levII_oplock(work, fp, 1); + if (!work->tcon->posix_extensions) { + loff_t size = i_size_read(file_inode(fp->filp)); + + if (off < size) { + err = check_lock_range(fp->filp, off, + min(off + len, size) - 1, + WRITE); + if (err) + return -EAGAIN; + } + } + + saved_cred = override_creds(fp->filp->f_cred); + err = vfs_fallocate(fp->filp, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + off, len); + revert_creds(saved_cred); + return err; +} + +int ksmbd_vfs_query_allocated_ranges(struct ksmbd_file *fp, loff_t start, + loff_t length, + struct file_allocated_range_buffer *ranges, + unsigned int in_count, + unsigned int *out_count) +{ + struct file *f = fp->filp; + struct inode *inode = file_inode(fp->filp); + loff_t maxbytes = inode->i_sb->s_maxbytes, size; + loff_t extent_start, extent_end, end; + int ret = 0; *out_count = 0; + if (start < 0 || length < 0) + return -EINVAL; + if (start > maxbytes) + return -EFBIG; + if (!in_count) + return 0; + if (length > maxbytes || maxbytes - length < start) + length = maxbytes - start; + size = i_size_read(inode); + if (!length || start >= size) + return 0; + if (length > size - start) + length = size - start; + end = start + length; + if ((fp->f_ci->m_fattr & FILE_ATTRIBUTE_SPARSE_FILE_LE) && + start < end) { + ret = file_write_and_wait_range(f, start, end - 1); + if (ret) + return ret; + } + while (start < end && *out_count < in_count) { extent_start = vfs_llseek(f, start, SEEK_DATA); if (extent_start < 0) { if (extent_start != -ENXIO) - ret = (int)extent_start; + ret = extent_start; break; } @@ -971,7 +1159,7 @@ int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, extent_end = vfs_llseek(f, extent_start, SEEK_HOLE); if (extent_end < 0) { if (extent_end != -ENXIO) - ret = (int)extent_end; + ret = extent_end; break; } else if (extent_start >= extent_end) { break; @@ -980,10 +1168,12 @@ int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, ranges[*out_count].file_offset = cpu_to_le64(extent_start); ranges[(*out_count)++].length = cpu_to_le64(min(extent_end, end) - extent_start); - start = extent_end; } + if (!ret && start < end && *out_count == in_count) + ret = -E2BIG; + return ret; } @@ -1009,13 +1199,15 @@ int ksmbd_vfs_remove_xattr(struct mnt_idmap *idmap, int ksmbd_vfs_unlink(struct file *filp) { + const struct cred *saved_cred; int err = 0; struct dentry *dir, *dentry = filp->f_path.dentry; struct mnt_idmap *idmap = file_mnt_idmap(filp); + saved_cred = override_creds(filp->f_cred); err = mnt_want_write(filp->f_path.mnt); if (err) - return err; + goto out_revert; dir = dget_parent(dentry); dentry = start_removing_dentry(dir, dentry); @@ -1034,7 +1226,8 @@ int ksmbd_vfs_unlink(struct file *filp) out: dput(dir); mnt_drop_write(filp->f_path.mnt); - +out_revert: + revert_creds(saved_cred); return err; } @@ -1149,7 +1342,7 @@ int __ksmbd_vfs_kern_path(struct ksmbd_work *work, char *filepath, retry: err = ksmbd_vfs_path_lookup(share_conf, filepath, flags, path, for_remove); - if (!err || !caseless) + if (!err || err != -ENOENT || !caseless) return err; path_len = strlen(filepath); @@ -1251,15 +1444,30 @@ struct dentry *ksmbd_vfs_kern_path_create(struct ksmbd_work *work, unsigned int flags, struct path *path) { - char *abs_name; + struct ksmbd_share_config *share_conf = work->tcon->share_conf; + struct qstr last; struct dentry *dent; + int err; + + /* resolve the name beneath the share root so ".." cannot escape */ + CLASS(filename_kernel, filename)(name); + + err = vfs_path_parent_lookup(filename, flags | LOOKUP_BENEATH, + path, &last, &share_conf->vfs_path); + if (err) + return ERR_PTR(err); - abs_name = convert_to_unix_name(work->tcon->share_conf, name); - if (!abs_name) - return ERR_PTR(-ENOMEM); + err = mnt_want_write(path->mnt); + if (err) { + path_put(path); + return ERR_PTR(err); + } - dent = start_creating_path(AT_FDCWD, abs_name, path, flags); - kfree(abs_name); + dent = start_creating_noperm(path->dentry, &last); + if (IS_ERR(dent)) { + mnt_drop_write(path->mnt); + path_put(path); + } return dent; } @@ -1456,8 +1664,8 @@ int ksmbd_vfs_set_sd_xattr(struct ksmbd_conn *conn, if (rc < 0) pr_err("Failed to store XATTR ntacl :%d\n", rc); - kfree(sd_ndr.data); out: + kfree(sd_ndr.data); kfree(acl_ndr.data); kfree(smb_acl); kfree(def_smb_acl); @@ -1473,7 +1681,7 @@ int ksmbd_vfs_get_sd_xattr(struct ksmbd_conn *conn, struct ndr n; struct inode *inode = d_inode(dentry); struct ndr acl_ndr = {0}; - struct xattr_ntacl acl; + struct xattr_ntacl acl = {0}; struct xattr_smb_acl *smb_acl = NULL, *def_smb_acl = NULL; __u8 cmp_hash[XATTR_SD_HASH_SIZE] = {0}; @@ -1484,7 +1692,7 @@ int ksmbd_vfs_get_sd_xattr(struct ksmbd_conn *conn, n.length = rc; rc = ndr_decode_v4_ntacl(&n, &acl); if (rc) - goto free_n_data; + goto out_free; smb_acl = ksmbd_vfs_make_xattr_posix_acl(idmap, inode, ACL_TYPE_ACCESS); @@ -1510,6 +1718,7 @@ int ksmbd_vfs_get_sd_xattr(struct ksmbd_conn *conn, *pntsd = acl.sd_buf; if (acl.sd_size < sizeof(struct smb_ntsd)) { pr_err("sd size is invalid\n"); + rc = -EINVAL; goto out_free; } @@ -1529,8 +1738,6 @@ out_free: kfree(acl.sd_buf); *pntsd = NULL; } - -free_n_data: kfree(n.data); return rc; } @@ -1545,14 +1752,15 @@ int ksmbd_vfs_set_dos_attrib_xattr(struct mnt_idmap *idmap, err = ndr_encode_dos_attr(&n, da); if (err) - return err; + goto out; err = ksmbd_vfs_setxattr(idmap, path, XATTR_NAME_DOS_ATTRIBUTE, (void *)n.data, n.offset, 0, get_write); if (err) ksmbd_debug(SMB, "failed to store dos attribute in xattr\n"); - kfree(n.data); +out: + kfree(n.data); return err; } @@ -1655,6 +1863,35 @@ int ksmbd_vfs_fill_dentry_attrs(struct ksmbd_work *work, } } + /* + * Only pay for this when it'll actually be used: AAPL + * READDIR_ATTR_V2's flags field (AAPL_READDIR_ATTR_V2_NO_XATTR) is + * the only consumer. XATTR_NAME_STREAM ("user.DosStream.") is a + * reliable, distinct prefix for genuine ADS/stream xattrs -- unlike + * DOSATTRIB or ACL xattrs, which live under different prefixes, so + * this can't false-positive into telling Finder a file has no extra + * data when it actually does. + */ + ksmbd_kstat->has_ads_stream = false; + if (work->conn->aapl_readdir_attr_v2) { + char *xattr_list = NULL, *name; + ssize_t xattr_list_len; + + xattr_list_len = ksmbd_vfs_listxattr(dentry, &xattr_list); + if (xattr_list_len > 0) { + for (name = xattr_list; + name - xattr_list < xattr_list_len; + name += strlen(name) + 1) { + if (!strncmp(name, XATTR_NAME_STREAM, + XATTR_NAME_STREAM_LEN)) { + ksmbd_kstat->has_ads_stream = true; + break; + } + } + } + kvfree(xattr_list); + } + return 0; } @@ -1705,6 +1942,76 @@ int ksmbd_vfs_xattr_stream_name(char *stream_name, char **xattr_stream_name, return 0; } +static ssize_t ksmbd_vfs_copy_file_range_buffered(struct ksmbd_work *work, + struct ksmbd_file *src_fp, + struct ksmbd_file *dst_fp, + loff_t src_off, + loff_t dst_off, size_t len) +{ + size_t buf_size = min_t(size_t, len, SZ_1M); + size_t copied = 0; + char *buf; + ssize_t ret = 0; + + buf = kvmalloc(buf_size, KSMBD_DEFAULT_GFP); + if (!buf) + return -ENOMEM; + + while (copied < len) { + size_t chunk_size = min(buf_size, len - copied); + size_t done = 0; + loff_t src_pos, dst_pos; + + if (dst_off > src_off) { + src_pos = src_off + len - copied - chunk_size; + dst_pos = dst_off + len - copied - chunk_size; + } else { + src_pos = src_off + copied; + dst_pos = dst_off + copied; + } + + while (done < chunk_size) { + loff_t pos = src_pos + done; + + ret = ksmbd_vfs_read(work, src_fp, chunk_size - done, + &pos, buf + done); + if (ret <= 0) { + if (!ret) + ret = -EIO; + goto out; + } + done += ret; + } + + done = 0; + while (done < chunk_size) { + loff_t pos = dst_pos + done; + ssize_t written = 0; + + ret = ksmbd_vfs_write(work, dst_fp, buf + done, + chunk_size - done, &pos, false, + &written); + if (ret < 0) + goto out; + if (!written) { + ret = -EIO; + goto out; + } + done += written; + } + copied += chunk_size; + } + ret = copied; +out: + kvfree(buf); + return ret; +} + +static bool ksmbd_vfs_copy_range_valid(loff_t offset, size_t len) +{ + return offset >= 0 && (loff_t)len <= MAX_LFS_FILESIZE - offset; +} + int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work, struct ksmbd_file *src_fp, struct ksmbd_file *dst_fp, @@ -1732,9 +2039,6 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work, return -EACCES; } - if (ksmbd_stream_fd(src_fp) || ksmbd_stream_fd(dst_fp)) - return -EBADF; - smb_break_all_levII_oplock(work, dst_fp, 1); if (!work->tcon->posix_extensions) { @@ -1743,6 +2047,10 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work, dst_off = le64_to_cpu(chunks[i].TargetOffset); len = le32_to_cpu(chunks[i].Length); + if (!ksmbd_vfs_copy_range_valid(src_off, len) || + !ksmbd_vfs_copy_range_valid(dst_off, len)) + return -E2BIG; + if (check_lock_range(src_fp->filp, src_off, src_off + len - 1, READ)) return -EAGAIN; @@ -1752,35 +2060,139 @@ int ksmbd_vfs_copy_file_ranges(struct ksmbd_work *work, } } - src_file_size = i_size_read(file_inode(src_fp->filp)); + if (ksmbd_stream_fd(src_fp)) { + const struct cred *saved_cred; + + saved_cred = override_creds(src_fp->filp->f_cred); + 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); + revert_creds(saved_cred); + if (src_file_size < 0) + return src_file_size; + } else { + src_file_size = i_size_read(file_inode(src_fp->filp)); + } + + /* + * macOS Finder's Cmd+D duplicate sends FSCTL_SRV_COPYCHUNK with + * ChunkCount=0 meaning "copy the whole file/stream", not the + * standard SMB2 "query my copy limits, no data" semantics -- + * fsctl_copychunk() only reaches here with chunk_count == 0 for + * AAPL-negotiated connections, so this doesn't affect compliant + * non-AAPL clients. Without this, the destination stays at its + * just-created 0 bytes / empty stream: the for loop below is a + * no-op when chunk_count is 0, since it never has an iteration to + * treat as "copy everything". + */ + if (chunk_count == 0 && work->conn->is_aapl) { + loff_t off = 0; + + while (off < src_file_size) { + size_t remaining = src_file_size - off; + ssize_t copied; + + /* Same source/destination offset here: an in-place, + * same-inode copy at matching offsets is a degenerate + * no-op range, not a real overlap, but vfs_copy_file_range + * still doesn't support streams -- route those (and the + * same-inode case defensively) through the buffered path. + */ + if (ksmbd_stream_fd(src_fp) || ksmbd_stream_fd(dst_fp) || + file_inode(src_fp->filp) == file_inode(dst_fp->filp)) { + copied = ksmbd_vfs_copy_file_range_buffered(work, src_fp, dst_fp, + off, off, remaining); + } else { + copied = vfs_copy_file_range(src_fp->filp, off, + dst_fp->filp, off, + remaining, 0); + if (copied == -EOPNOTSUPP || copied == -EXDEV) + copied = vfs_copy_file_range(src_fp->filp, off, + dst_fp->filp, off, + remaining, + COPY_FILE_SPLICE); + } + if (copied < 0) + return copied; + if (copied == 0) + break; + off += copied; + } + + /* + * This is a synthesized whole-file copy, not a response to + * any chunk descriptor the client actually sent (it sent + * none -- chunk_count is 0). Report zero chunks/chunk-bytes + * rather than inventing a chunk that doesn't correspond to + * anything in the request; only total_size_written (bytes + * actually copied) is meaningful here. + */ + *chunk_count_written = 0; + *chunk_size_written = 0; + *total_size_written = off; + return 0; + } for (i = 0; i < chunk_count; i++) { + bool stream_len_mismatch = false; + size_t copy_len; + src_off = le64_to_cpu(chunks[i].SourceOffset); dst_off = le64_to_cpu(chunks[i].TargetOffset); len = le32_to_cpu(chunks[i].Length); + copy_len = len; - if (src_off + len > src_file_size) + if (!ksmbd_vfs_copy_range_valid(src_off, len) || + !ksmbd_vfs_copy_range_valid(dst_off, len)) return -E2BIG; + if (src_off > src_file_size || len > src_file_size - src_off) { + /* + * macOS can reuse the main file's chunk list when copying + * streams, so the requested range can exceed the size of + * the xattr-backed stream. For an AAPL connection, copy the + * available stream data and report the requested length to + * avoid a copy length mismatch. + */ + if (!work->conn->is_aapl || + !ksmbd_stream_fd(src_fp) || + !ksmbd_stream_fd(dst_fp)) + return -E2BIG; + + stream_len_mismatch = true; + if (src_off < src_file_size) + copy_len = src_file_size - src_off; + else + copy_len = 0; + } + /* - * vfs_copy_file_range does not allow overlapped copying - * within the same file. + * vfs_copy_file_range does not support streams or overlapping + * ranges within the same file. */ - if (file_inode(src_fp->filp) == file_inode(dst_fp->filp) && - dst_off + len > src_off && - dst_off < src_off + len) - ret = do_splice_direct(src_fp->filp, &src_off, - dst_fp->filp, &dst_off, - min_t(size_t, len, MAX_RW_COUNT), 0); - else - ret = vfs_copy_file_range(src_fp->filp, src_off, - dst_fp->filp, dst_off, len, 0); - if (ret == -EOPNOTSUPP || ret == -EXDEV) + if (!copy_len) { + ret = 0; + } else if (ksmbd_stream_fd(src_fp) || ksmbd_stream_fd(dst_fp) || + (file_inode(src_fp->filp) == file_inode(dst_fp->filp) && + dst_off + copy_len > src_off && + dst_off < src_off + copy_len)) { + ret = ksmbd_vfs_copy_file_range_buffered(work, src_fp, + dst_fp, src_off, + dst_off, copy_len); + } else { ret = vfs_copy_file_range(src_fp->filp, src_off, - dst_fp->filp, dst_off, len, - COPY_FILE_SPLICE); + dst_fp->filp, dst_off, copy_len, 0); + if (ret == -EOPNOTSUPP || ret == -EXDEV) + ret = vfs_copy_file_range(src_fp->filp, src_off, + dst_fp->filp, dst_off, + copy_len, + COPY_FILE_SPLICE); + } if (ret < 0) return ret; + if (stream_len_mismatch) + ret = len; *chunk_count_written += 1; *total_size_written += ret; @@ -1855,10 +2267,6 @@ int ksmbd_vfs_inherit_posix_acl(struct mnt_idmap *idmap, const struct path *path, struct inode *parent_inode) { struct posix_acl *acls; - struct posix_acl_entry *pace; - struct dentry *dentry = path->dentry; - struct inode *inode = d_inode(dentry); - int rc, i; if (!IS_ENABLED(CONFIG_FS_POSIX_ACL)) return -EOPNOTSUPP; @@ -1866,27 +2274,148 @@ int ksmbd_vfs_inherit_posix_acl(struct mnt_idmap *idmap, acls = get_inode_acl(parent_inode, ACL_TYPE_DEFAULT); if (IS_ERR_OR_NULL(acls)) return -ENOENT; - pace = acls->a_entries; - for (i = 0; i < acls->a_count; i++, pace++) { - if (pace->e_tag == ACL_MASK) { - pace->e_perm = 0x07; - break; - } + posix_acl_release(acls); + return 0; +} + +void ksmbd_vfs_update_compressed_fattr(struct dentry *dentry, __le32 *fattr) +{ + int rc; + struct file_kattr fa = { .flags_valid = true }; + + rc = vfs_fileattr_get(dentry, &fa); + if (rc) + return; + + if (fa.flags & FS_COMPR_FL) + *fattr |= FILE_ATTRIBUTE_COMPRESSED_LE; +} + +int ksmbd_vfs_get_compression(struct ksmbd_file *fp, u16 *fmt) +{ + struct file_kattr fa = { .flags_valid = true }; + int rc; + + rc = vfs_fileattr_get(fp->filp->f_path.dentry, &fa); + if (rc == -ENOIOCTLCMD || rc == -ENOTTY || rc == -EINVAL || + rc == -EOPNOTSUPP) { + if (fp->f_ci->m_fattr & FILE_ATTRIBUTE_COMPRESSED_LE) + *fmt = COMPRESSION_FORMAT_LZNT1; + else + *fmt = COMPRESSION_FORMAT_NONE; + rc = 0; + goto out; } + if (rc) + goto out; - rc = set_posix_acl(idmap, dentry, ACL_TYPE_ACCESS, acls); - if (rc < 0) - ksmbd_debug(SMB, "Set posix acl(ACL_TYPE_ACCESS) failed, rc : %d\n", - rc); - if (S_ISDIR(inode->i_mode)) { - rc = set_posix_acl(idmap, dentry, ACL_TYPE_DEFAULT, - acls); - if (rc < 0) - ksmbd_debug(SMB, "Set posix acl(ACL_TYPE_DEFAULT) failed, rc : %d\n", - rc); + if (fp->f_ci->m_fattr & FILE_ATTRIBUTE_COMPRESSED_LE) + *fmt = COMPRESSION_FORMAT_LZNT1; + else + *fmt = COMPRESSION_FORMAT_NONE; + +out: + return rc; +} + +static int __ksmbd_vfs_set_compression(struct ksmbd_work *work, + struct ksmbd_file *fp, u16 fmt, + bool check_access) +{ + const struct cred *saved_cred = NULL; + struct file_kattr fa; + struct dentry *dentry = fp->filp->f_path.dentry; + struct mnt_idmap *idmap = file_mnt_idmap(fp->filp); + u32 flags; + __le32 old_fattr; + int rc; + + if (check_access && !(fp->daccess & FILE_WRITE_DATA_LE)) { + rc = -EACCES; + goto out; } - posix_acl_release(acls); + if (fmt != COMPRESSION_FORMAT_NONE && + fmt != COMPRESSION_FORMAT_DEFAULT && + fmt != COMPRESSION_FORMAT_LZNT1) { + rc = -EINVAL; + goto out; + } + + saved_cred = override_creds(fp->filp->f_cred); + rc = vfs_fileattr_get(dentry, &fa); + if (rc == -ENOIOCTLCMD || rc == -ENOTTY || rc == -EINVAL || + rc == -EOPNOTSUPP) + goto update_fattr; + if (rc) + goto out; + + flags = fa.flags; + if (fmt == COMPRESSION_FORMAT_NONE) { + flags &= ~FS_COMPR_FL; + } else if (fmt == COMPRESSION_FORMAT_DEFAULT || + fmt == COMPRESSION_FORMAT_LZNT1) { + flags |= FS_COMPR_FL; + } + + if (flags != fa.flags) { + fileattr_fill_flags(&fa, flags); + rc = mnt_want_write_file(fp->filp); + if (rc) + goto out; + + rc = vfs_fileattr_set(idmap, dentry, &fa); + mnt_drop_write_file(fp->filp); + if (rc == -ENOIOCTLCMD || rc == -ENOTTY || rc == -EINVAL || + rc == -EOPNOTSUPP) + goto update_fattr; + if (rc) + goto out; + } + +update_fattr: + old_fattr = fp->f_ci->m_fattr; + if (fmt == COMPRESSION_FORMAT_NONE) + fp->f_ci->m_fattr &= ~FILE_ATTRIBUTE_COMPRESSED_LE; + else + fp->f_ci->m_fattr |= FILE_ATTRIBUTE_COMPRESSED_LE; + + if (fp->f_ci->m_fattr != old_fattr) { + struct xattr_dos_attrib da = {0}; + + rc = ksmbd_vfs_get_dos_attrib_xattr(idmap, dentry, &da); + if (rc <= 0) { + da.version = 4; + da.itime = fp->itime; + da.create_time = fp->create_time; + da.flags = XATTR_DOSINFO_CREATE_TIME | + XATTR_DOSINFO_ITIME; + } + + da.attr = le32_to_cpu(fp->f_ci->m_fattr); + da.flags |= XATTR_DOSINFO_ATTRIB; + rc = ksmbd_vfs_set_dos_attrib_xattr(idmap, + &fp->filp->f_path, + &da, true); + if (rc) + rc = 0; + } + +out: + if (saved_cred) + revert_creds(saved_cred); return rc; } + +int ksmbd_vfs_set_compression(struct ksmbd_work *work, + struct ksmbd_file *fp, u16 fmt) +{ + return __ksmbd_vfs_set_compression(work, fp, fmt, true); +} + +int ksmbd_vfs_set_compression_create(struct ksmbd_work *work, + struct ksmbd_file *fp, u16 fmt) +{ + return __ksmbd_vfs_set_compression(work, fp, fmt, false); +} diff --git a/fs/smb/server/vfs.h b/fs/smb/server/vfs.h index 16ca29ee16e5..55d099de71f5 100644 --- a/fs/smb/server/vfs.h +++ b/fs/smb/server/vfs.h @@ -70,6 +70,7 @@ struct ksmbd_kstat { struct kstat *kstat; unsigned long long create_time; __le32 file_attributes; + bool has_ads_stream; /* AAPL READDIR_ATTR V2 xattr-presence flag */ }; int ksmbd_vfs_lock_parent(struct dentry *parent, struct dentry *child); @@ -87,8 +88,10 @@ int ksmbd_vfs_remove_file(struct ksmbd_work *work, const struct path *path); int ksmbd_vfs_link(struct ksmbd_work *work, const char *oldname, const char *newname); int ksmbd_vfs_getattr(const struct path *path, struct kstat *stat); -int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, - char *newname, int flags); +int ksmbd_vfs_rename(struct ksmbd_work *work, struct ksmbd_file *old_fp, + char *newname, int flags); +int ksmbd_vfs_check_rename_share(struct ksmbd_work *work, + const struct path *old_path); int ksmbd_vfs_truncate(struct ksmbd_work *work, struct ksmbd_file *fp, loff_t size); struct srv_copychunk; @@ -132,10 +135,15 @@ int ksmbd_vfs_empty_dir(struct ksmbd_file *fp); void ksmbd_vfs_set_fadvise(struct file *filp, __le32 option); int ksmbd_vfs_zero_data(struct ksmbd_work *work, struct ksmbd_file *fp, loff_t off, loff_t len); +int ksmbd_vfs_zero_holes(struct ksmbd_file *fp); +int ksmbd_vfs_trim_data(struct ksmbd_work *work, struct ksmbd_file *fp, + loff_t off, loff_t len); struct file_allocated_range_buffer; -int ksmbd_vfs_fqar_lseek(struct ksmbd_file *fp, loff_t start, loff_t length, - struct file_allocated_range_buffer *ranges, - unsigned int in_count, unsigned int *out_count); +int ksmbd_vfs_query_allocated_ranges(struct ksmbd_file *fp, loff_t start, + loff_t length, + struct file_allocated_range_buffer *ranges, + unsigned int in_count, + unsigned int *out_count); int ksmbd_vfs_unlink(struct file *filp); void *ksmbd_vfs_init_kstat(char **p, struct ksmbd_kstat *ksmbd_kstat); int ksmbd_vfs_fill_dentry_attrs(struct ksmbd_work *work, @@ -168,4 +176,9 @@ int ksmbd_vfs_set_init_posix_acl(struct mnt_idmap *idmap, int ksmbd_vfs_inherit_posix_acl(struct mnt_idmap *idmap, const struct path *path, struct inode *parent_inode); +void ksmbd_vfs_update_compressed_fattr(struct dentry *dentry, __le32 *fattr); +int ksmbd_vfs_get_compression(struct ksmbd_file *fp, u16 *fmt); +int ksmbd_vfs_set_compression(struct ksmbd_work *work, struct ksmbd_file *fp, u16 fmt); +int ksmbd_vfs_set_compression_create(struct ksmbd_work *work, + struct ksmbd_file *fp, u16 fmt); #endif /* __KSMBD_VFS_H__ */ diff --git a/fs/smb/server/vfs_cache.c b/fs/smb/server/vfs_cache.c index ff4ea412d900..fd2c595f0486 100644 --- a/fs/smb/server/vfs_cache.c +++ b/fs/smb/server/vfs_cache.c @@ -10,6 +10,7 @@ #include <linux/vmalloc.h> #include <linux/kthread.h> #include <linux/freezer.h> +#include <linux/dcache.h> #include "glob.h" #include "vfs_cache.h" @@ -19,6 +20,7 @@ #include "misc.h" #include "mgmt/tree_connect.h" #include "mgmt/user_session.h" +#include "mgmt/user_config.h" #include "smb_common.h" #include "server.h" #include "smb2pdu.h" @@ -36,6 +38,8 @@ static struct ksmbd_file_table global_ft; static atomic_long_t fd_limit; static struct kmem_cache *filp_cache; +static int ksmbd_mark_fp_closed(struct ksmbd_file *fp); + #define OPLOCK_NONE 0 #define OPLOCK_EXCLUSIVE 1 #define OPLOCK_BATCH 2 @@ -62,37 +66,73 @@ static const struct ksmbd_const_name ksmbd_lease_const_names[] = { static const struct ksmbd_const_name ksmbd_oplock_const_names[] = { {SMB2_OPLOCK_LEVEL_NONE, "OPLOCK_NONE"}, {SMB2_OPLOCK_LEVEL_II, "OPLOCK_II"}, - {SMB2_OPLOCK_LEVEL_EXCLUSIVE, "OPLOCK_EXECL"}, + {SMB2_OPLOCK_LEVEL_EXCLUSIVE, "OPLOCK_EXCLUSIVE"}, {SMB2_OPLOCK_LEVEL_BATCH, "OPLOCK_BATCH"}, }; +static const struct ksmbd_const_name ksmbd_file_state_names[] = { + {FP_NEW, "new"}, + {FP_INITED, "open"}, + {FP_CLOSED, "closed"}, +}; + +#define KSMBD_PROC_FILE_DURABLE BIT(0) +#define KSMBD_PROC_FILE_PERSISTENT BIT(1) +#define KSMBD_PROC_FILE_RESILIENT BIT(2) +#define KSMBD_PROC_FILE_DELETE_ON_CLOSE BIT(3) +#define KSMBD_PROC_FILE_STREAM BIT(4) +#define KSMBD_PROC_FILE_POSIX BIT(5) +#define KSMBD_PROC_FILE_ATTRIB_ONLY BIT(6) + +static const struct ksmbd_const_name ksmbd_file_flag_names[] = { + {KSMBD_PROC_FILE_DURABLE, "durable"}, + {KSMBD_PROC_FILE_PERSISTENT, "persistent"}, + {KSMBD_PROC_FILE_RESILIENT, "resilient"}, + {KSMBD_PROC_FILE_DELETE_ON_CLOSE, "delete-on-close"}, + {KSMBD_PROC_FILE_STREAM, "stream"}, + {KSMBD_PROC_FILE_POSIX, "posix"}, + {KSMBD_PROC_FILE_ATTRIB_ONLY, "attrib-only"}, +}; + +static unsigned int ksmbd_proc_file_flags(struct ksmbd_file *fp) +{ + unsigned int flags = 0; + + if (fp->is_durable) + flags |= KSMBD_PROC_FILE_DURABLE; + if (fp->is_persistent) + flags |= KSMBD_PROC_FILE_PERSISTENT; + if (fp->is_resilient) + flags |= KSMBD_PROC_FILE_RESILIENT; + if (fp->coption & FILE_DELETE_ON_CLOSE_LE) + flags |= KSMBD_PROC_FILE_DELETE_ON_CLOSE; + if (fp->stream.name) + flags |= KSMBD_PROC_FILE_STREAM; + if (fp->is_posix_ctxt) + flags |= KSMBD_PROC_FILE_POSIX; + if (fp->attrib_only) + flags |= KSMBD_PROC_FILE_ATTRIB_ONLY; + return flags; +} + static int proc_show_files(struct seq_file *m, void *v) { struct ksmbd_file *fp = NULL; unsigned int id; struct oplock_info *opinfo; - seq_printf(m, "#%-10s %-10s %-10s %-10s %-15s %-10s %-10s %s\n", - "<tree id>", "<pid>", "<vid>", "<refcnt>", - "<oplock>", "<daccess>", "<saccess>", - "<name>"); - read_lock(&global_ft.lock); idr_for_each_entry(global_ft.idr, fp, id) { - seq_printf(m, "%#-10x %#-10llx %#-10llx %#-10x", - fp->tcon->id, - fp->persistent_id, - fp->volatile_id, - atomic_read(&fp->refcount)); + seq_printf(m, "tree_id:\t0x%x\n", fp->tcon ? fp->tcon->id : 0); + seq_printf(m, "persistent_id:\t0x%llx\n", fp->persistent_id); + seq_printf(m, "volatile_id:\t0x%llx\n", fp->volatile_id); + seq_printf(m, "refcount:\t%d\n", atomic_read(&fp->refcount)); rcu_read_lock(); opinfo = rcu_dereference(fp->f_opinfo); - rcu_read_unlock(); - - if (!opinfo) { - seq_printf(m, " %-15s", " "); - } else { + if (opinfo) { const struct ksmbd_const_name *const_names; + const char *name; int count; unsigned int level; @@ -105,13 +145,33 @@ static int proc_show_files(struct seq_file *m, void *v) count = ARRAY_SIZE(ksmbd_oplock_const_names); level = opinfo->level; } - ksmbd_proc_show_const_name(m, " %-15s", - const_names, count, level); + rcu_read_unlock(); + name = ksmbd_proc_const_name(const_names, count, level); + if (name) + seq_printf(m, "oplock:\t%s\n", name); + else + seq_printf(m, "oplock:\t0x%x\n", level); + } else { + rcu_read_unlock(); + seq_puts(m, "oplock:\tnone\n"); } - seq_printf(m, " %#010x %#010x %s\n", - le32_to_cpu(fp->daccess), - le32_to_cpu(fp->saccess), + seq_printf(m, "state:\t%s\n", + ksmbd_proc_const_name(ksmbd_file_state_names, + ARRAY_SIZE(ksmbd_file_state_names), + fp->f_state)); + seq_printf(m, "durable_timeout:\t%u\n", fp->durable_timeout); + seq_printf(m, "create_options:\t0x%08x\n", + le32_to_cpu(fp->coption)); + seq_printf(m, "desired_access:\t0x%08x\n", + le32_to_cpu(fp->daccess)); + seq_printf(m, "share_access:\t0x%08x\n", + le32_to_cpu(fp->saccess)); + seq_puts(m, "flags:\t"); + ksmbd_proc_show_flag_names(m, ksmbd_file_flag_names, + ARRAY_SIZE(ksmbd_file_flag_names), + ksmbd_proc_file_flags(fp)); + seq_printf(m, "\nname:\t%s\n\n", fp->filp->f_path.dentry->d_name.name); } read_unlock(&global_ft.lock); @@ -120,7 +180,8 @@ static int proc_show_files(struct seq_file *m, void *v) static int create_proc_files(void) { - ksmbd_proc_create("files", proc_show_files, NULL); + if (!ksmbd_proc_create("files", proc_show_files, NULL)) + return -ENOMEM; return 0; } #else @@ -131,6 +192,16 @@ static bool durable_scavenger_running; static DEFINE_MUTEX(durable_scavenger_lock); static wait_queue_head_t dh_wq; +bool ksmbd_durable_scavenger_active(void) +{ + bool active; + + mutex_lock(&durable_scavenger_lock); + active = durable_scavenger_running; + mutex_unlock(&durable_scavenger_lock); + return active; +} + void ksmbd_set_fd_limit(unsigned long limit) { limit = min(limit, get_max_files()); @@ -210,13 +281,13 @@ int ksmbd_query_inode_status(struct dentry *dentry) return ret; down_read(&ci->m_lock); - if (ci->m_flags & (S_DEL_PENDING | S_DEL_ON_CLS)) + if (ci->m_flags & S_DEL_PENDING) ret = KSMBD_INODE_STATUS_PENDING_DELETE; else ret = KSMBD_INODE_STATUS_OK; up_read(&ci->m_lock); - atomic_dec(&ci->m_count); + ksmbd_inode_put(ci); return ret; } @@ -226,8 +297,14 @@ bool ksmbd_inode_pending_delete(struct ksmbd_file *fp) int ret; down_read(&ci->m_lock); - ret = (ci->m_flags & (S_DEL_PENDING | S_DEL_ON_CLS)); + ret = (ci->m_flags & S_DEL_PENDING); up_read(&ci->m_lock); + if (ret || !ksmbd_stream_fd(fp)) + return ret; + + spin_lock(&fp->f_lock); + ret = fp->stream_del_pending; + spin_unlock(&fp->f_lock); return ret; } @@ -250,6 +327,33 @@ void ksmbd_clear_inode_pending_delete(struct ksmbd_file *fp) up_write(&ci->m_lock); } +bool ksmbd_has_stream_without_delete_share(struct ksmbd_file *fp) +{ + struct ksmbd_file *prev_fp; + struct ksmbd_inode *ci = fp->f_ci; + bool ret = false; + + if (ksmbd_stream_fd(fp)) + return false; + + down_read(&ci->m_lock); + list_for_each_entry(prev_fp, &ci->m_fp_list, node) { + if (prev_fp == fp || !ksmbd_stream_fd(prev_fp)) + continue; + + if (file_inode(fp->filp) != file_inode(prev_fp->filp)) + continue; + + if (!(prev_fp->saccess & FILE_SHARE_DELETE_LE)) { + ret = true; + break; + } + } + up_read(&ci->m_lock); + + return ret; +} + void ksmbd_fd_set_delete_on_close(struct ksmbd_file *fp, int file_info) { @@ -263,6 +367,40 @@ void ksmbd_fd_set_delete_on_close(struct ksmbd_file *fp, up_write(&ci->m_lock); } +/* + * FileDispositionInformation (SET_INFO) on a stream handle must only + * mark the stream for deletion, not the whole file -- otherwise + * deleting a single alternate data stream (e.g. AFP_AfpInfo) deletes + * the entire file's data along with it. + * + * This is tracked on fp itself (stream_del_pending), not the shared + * ksmbd_inode: the inode-wide S_DEL_ON_CLS_STREAM flag used by + * ksmbd_fd_set_delete_on_close() can't record *which* stream should be + * deleted, so if a different stream handle on the same file closed + * first, it would delete the wrong stream. + */ +void ksmbd_fd_set_delete_pending(struct ksmbd_file *fp) +{ + if (ksmbd_stream_fd(fp)) { + spin_lock(&fp->f_lock); + fp->stream_del_pending = true; + spin_unlock(&fp->f_lock); + } else { + ksmbd_set_inode_pending_delete(fp); + } +} + +void ksmbd_fd_clear_delete_pending(struct ksmbd_file *fp) +{ + if (ksmbd_stream_fd(fp)) { + spin_lock(&fp->f_lock); + fp->stream_del_pending = false; + spin_unlock(&fp->f_lock); + } else { + ksmbd_clear_inode_pending_delete(fp); + } +} + static void ksmbd_inode_hash(struct ksmbd_inode *ci) { struct hlist_head *b = inode_hashtable + @@ -383,23 +521,48 @@ static void __ksmbd_inode_close(struct ksmbd_file *fp) } up_write(&ci->m_lock); + /* + * Per-handle delete-pending from ksmbd_fd_set_delete_pending() + * (FileDispositionInformation on this stream) -- separate from + * the inode-wide flag above, which only ever meant "some + * stream on this file" with no way to say which one. + */ + spin_lock(&fp->f_lock); + if (fp->stream_del_pending) { + fp->stream_del_pending = false; + remove_stream_xattr = true; + } + spin_unlock(&fp->f_lock); + if (remove_stream_xattr) { + const struct cred *saved_cred; + + saved_cred = override_creds(filp->f_cred); err = ksmbd_vfs_remove_xattr(file_mnt_idmap(filp), &filp->f_path, fp->stream.name, true); + revert_creds(saved_cred); if (err) pr_err("remove xattr failed : %s\n", fp->stream.name); } } + down_write(&ci->m_lock); + /* Promote S_DEL_ON_CLS to S_DEL_PENDING when close */ + if (ci->m_flags & S_DEL_ON_CLS) { + ci->m_flags &= ~S_DEL_ON_CLS; + ci->m_flags |= S_DEL_PENDING; + } + up_write(&ci->m_lock); + if (atomic_dec_and_test(&ci->m_count)) { bool do_unlink = false; down_write(&ci->m_lock); - if (ci->m_flags & (S_DEL_ON_CLS | S_DEL_PENDING)) { - ci->m_flags &= ~(S_DEL_ON_CLS | S_DEL_PENDING); + if (ci->m_flags & S_DEL_PENDING) { + ci->m_flags &= ~S_DEL_PENDING; do_unlink = true; } up_write(&ci->m_lock); @@ -417,6 +580,14 @@ static void __ksmbd_remove_durable_fd(struct ksmbd_file *fp) return; idr_remove(global_ft.idr, fp->persistent_id); + /* + * Clear persistent_id so a later __ksmbd_close_fd() that runs from a + * delayed putter (e.g. when a concurrent ksmbd_lookup_fd_inode() + * walker held the final reference) does not re-issue idr_remove() on + * an id that idr_alloc_cyclic() may have already handed out to a new + * durable handle. + */ + fp->persistent_id = KSMBD_NO_FID; } static void ksmbd_remove_durable_fd(struct ksmbd_file *fp) @@ -430,13 +601,13 @@ static void ksmbd_remove_durable_fd(struct ksmbd_file *fp) static void __ksmbd_remove_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) { - if (!has_file_id(fp->volatile_id)) - return; - down_write(&fp->f_ci->m_lock); list_del_init(&fp->node); up_write(&fp->f_ci->m_lock); + if (!has_file_id(fp->volatile_id)) + return; + write_lock(&ft->lock); idr_remove(ft->idr, fp->volatile_id); write_unlock(&ft->lock); @@ -446,6 +617,7 @@ 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); @@ -459,24 +631,163 @@ static void __ksmbd_close_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp) if (!IS_ERR_OR_NULL(filp)) fput(filp); - /* because the reference count of fp is 0, it is guaranteed that - * there are not accesses to fp->lock_list. + /* + * 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) { - spin_lock(&fp->conn->llist_lock); - list_del(&smb_lock->clist); - spin_unlock(&fp->conn->llist_lock); + 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(&smb_lock->flist); + 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. + */ + if (fp->conn) { + ksmbd_conn_put(fp->conn); + fp->conn = NULL; + } + if (ksmbd_stream_fd(fp)) kfree(fp->stream.name); + kfree(fp->owner.name); + kmem_cache_free(filp_cache, fp); } +/** + * ksmbd_close_disconnected_durable_delete_on_close() - drop a delete-on-close + * file kept present only by disconnected durable handles + * @dentry: dentry of the file being opened + * + * 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 + * (non-reconnect) open arrives for the same name instead, the disconnected + * handle has to give way. Close such handles so their delete-on-close is + * applied and the file is removed once the last handle is gone, letting the + * new open create a fresh file. + * + * The caller's inode reference is dropped before closing so that the final + * close can promote S_DEL_ON_CLS to S_DEL_PENDING and unlink the file. + * + * Return: true if a disconnected durable handle was closed. + */ +bool ksmbd_close_disconnected_durable_delete_on_close(struct dentry *dentry) +{ + struct ksmbd_inode *ci; + struct ksmbd_file *fp, *tmp; + LIST_HEAD(dispose); + bool closed = false; + + ci = ksmbd_inode_lookup_lock(dentry); + if (!ci) + return false; + + down_write(&ci->m_lock); + if (ci->m_flags & (S_DEL_ON_CLS | S_DEL_ON_CLS_STREAM | S_DEL_PENDING)) { + list_for_each_entry_safe(fp, tmp, &ci->m_fp_list, node) { + if (fp->conn || !fp->is_durable || + fp->f_state != FP_INITED) + continue; + + /* + * Claim the close before unlinking fp from m_fp_list. + * refcount == 1 means only the durable lifetime ref is + * left. Add a transient ref so final close can drop both. + */ + write_lock(&global_ft.lock); + if (atomic_read(&fp->refcount) == 1) { + atomic_inc(&fp->refcount); + __ksmbd_remove_durable_fd(fp); + ksmbd_mark_fp_closed(fp); + list_move_tail(&fp->node, &dispose); + } + write_unlock(&global_ft.lock); + } + } + up_write(&ci->m_lock); + + /* + * Drop our lookup reference before closing so the last __ksmbd_close_fd() + * can drop m_count to zero and unlink the delete-on-close file. The + * collected handles still hold the transient reference taken above, so + * ci stays valid until they are closed below. + */ + ksmbd_inode_put(ci); + + while (!list_empty(&dispose)) { + fp = list_first_entry(&dispose, struct ksmbd_file, node); + list_del_init(&fp->node); + if (atomic_sub_and_test(2, &fp->refcount)) { + __ksmbd_close_fd(NULL, fp); + closed = true; + } + } + + return closed; +} + static struct ksmbd_file *ksmbd_fp_get(struct ksmbd_file *fp) { if (fp->f_state != FP_INITED) @@ -487,6 +798,11 @@ static struct ksmbd_file *ksmbd_fp_get(struct ksmbd_file *fp) return fp; } +struct ksmbd_file *ksmbd_file_get(struct ksmbd_file *fp) +{ + return ksmbd_fp_get(fp); +} + static struct ksmbd_file *__ksmbd_lookup_fd(struct ksmbd_file_table *ft, u64 id) { @@ -505,6 +821,20 @@ 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); } @@ -516,16 +846,30 @@ static void set_close_state_blocked_works(struct ksmbd_file *fp) spin_lock(&fp->f_lock); list_for_each_entry(cancel_work, &fp->blocked_works, fp_entry) { - cancel_work->state = KSMBD_WORK_CLOSED; - cancel_work->cancel_fn(cancel_work->cancel_argv); + if (xchg(&cancel_work->state, KSMBD_WORK_CLOSED) == + KSMBD_WORK_ACTIVE) + cancel_work->cancel_fn(cancel_work->cancel_argv); } spin_unlock(&fp->f_lock); } +void ksmbd_wake_session_blocked_works(struct ksmbd_session *sess) +{ + struct ksmbd_file_table *ft = &sess->file_table; + struct ksmbd_file *fp; + unsigned int id; + + read_lock(&ft->lock); + idr_for_each_entry(ft->idr, fp, id) + set_close_state_blocked_works(fp); + read_unlock(&ft->lock); +} + int ksmbd_close_fd(struct ksmbd_work *work, u64 id) { struct ksmbd_file *fp; struct ksmbd_file_table *ft; + bool closed = false; if (!has_file_id(id)) return 0; @@ -540,6 +884,9 @@ int ksmbd_close_fd(struct ksmbd_work *work, u64 id) fp = NULL; else { fp->f_state = FP_CLOSED; + idr_remove(ft->idr, id); + fp->volatile_id = KSMBD_NO_FID; + closed = true; if (!atomic_dec_and_test(&fp->refcount)) fp = NULL; } @@ -547,7 +894,7 @@ int ksmbd_close_fd(struct ksmbd_work *work, u64 id) write_unlock(&ft->lock); if (!fp) - return -EINVAL; + return closed ? 0 : -EINVAL; __put_fd_final(work, fp); return 0; @@ -620,7 +967,8 @@ struct ksmbd_file *ksmbd_lookup_durable_fd(unsigned long long id) struct ksmbd_file *fp; fp = __ksmbd_lookup_fd(&global_ft, id); - if (fp && (fp->conn || + if (fp && (fp->durable_reconnect_disabled || + fp->conn || (fp->durable_scavenger_timeout && (fp->durable_scavenger_timeout < jiffies_to_msecs(jiffies))))) { @@ -639,6 +987,126 @@ void ksmbd_put_durable_fd(struct ksmbd_file *fp) __ksmbd_close_fd(NULL, fp); } +bool ksmbd_has_other_active_fd(struct ksmbd_file *fp) +{ + struct ksmbd_file *lfp; + struct ksmbd_inode *ci = fp->f_ci; + bool ret = false; + + down_read(&ci->m_lock); + list_for_each_entry(lfp, &ci->m_fp_list, node) { + if (lfp == fp) + continue; + + if (lfp->f_state == FP_INITED && + (READ_ONCE(lfp->conn) || READ_ONCE(lfp->tcon))) { + ret = true; + break; + } + } + up_read(&ci->m_lock); + + return ret; +} + +struct ksmbd_file *ksmbd_lookup_fd_app_instance_id(char *app_instance_id) +{ + struct ksmbd_file *fp = NULL; + unsigned int id; + + 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, + SMB2_CREATE_GUID_SIZE)) { + fp = ksmbd_fp_get(fp); + break; + } + } + read_unlock(&global_ft.lock); + + return fp; +} + +int ksmbd_close_fd_app_instance_id(char *app_instance_id) +{ + 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; + + opinfo = opinfo_get(fp); + if (!opinfo) + goto out; + + down_read(&fp->f_ci->m_lock); + if (!opinfo->conn) { + up_read(&fp->f_ci->m_lock); + goto out; + } + + ft = &opinfo->sess->file_table; + write_lock(&ft->lock); + if (fp->f_state == FP_INITED && has_file_id(fp->volatile_id)) { + idr_remove(ft->idr, fp->volatile_id); + fp->volatile_id = KSMBD_NO_FID; + n_to_drop = ksmbd_mark_fp_closed(fp); + } + write_unlock(&ft->lock); + up_read(&fp->f_ci->m_lock); + opinfo_put(opinfo); + opinfo = NULL; + + if (!n_to_drop) + goto out; + + down_write(&fp->f_ci->m_lock); + 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); + __ksmbd_close_fd(NULL, fp); + } + return 0; + +out: + if (opinfo) + opinfo_put(opinfo); + ksmbd_put_durable_fd(fp); + return 0; +} + +int ksmbd_invalidate_durable_fd(unsigned long long id) +{ + struct ksmbd_file *fp; + + fp = ksmbd_lookup_global_fd(id); + if (!fp) + return -ENOENT; + + fp->durable_reconnect_disabled = true; + + if (fp->conn) { + ksmbd_put_durable_fd(fp); + return -ENOENT; + } + + fp->durable_timeout = 1; + fp->durable_scavenger_timeout = jiffies_to_msecs(jiffies); + ksmbd_put_durable_fd(fp); + if (waitqueue_active(&dh_wq)) + wake_up(&dh_wq); + + return -ENOENT; +} + struct ksmbd_file *ksmbd_lookup_fd_cguid(char *cguid) { struct ksmbd_file *fp = NULL; @@ -673,17 +1141,71 @@ struct ksmbd_file *ksmbd_lookup_fd_inode(struct dentry *dentry) down_read(&ci->m_lock); list_for_each_entry(lfp, &ci->m_fp_list, node) { if (inode == file_inode(lfp->filp)) { - atomic_dec(&ci->m_count); lfp = ksmbd_fp_get(lfp); up_read(&ci->m_lock); + ksmbd_inode_put(ci); return lfp; } } - atomic_dec(&ci->m_count); up_read(&ci->m_lock); + ksmbd_inode_put(ci); return NULL; } +bool ksmbd_has_other_nonposix_open(struct dentry *dentry) +{ + struct ksmbd_file *fp; + struct inode *inode = d_inode(dentry); + unsigned int id; + bool ret = false; + + if (!inode) + return false; + + read_lock(&global_ft.lock); + idr_for_each_entry(global_ft.idr, fp, id) { + if (READ_ONCE(fp->f_state) != FP_INITED) + continue; + if (inode != file_inode(fp->filp)) + continue; + if (fp->is_posix_ctxt) + continue; + + ret = true; + break; + } + read_unlock(&global_ft.lock); + + return ret; +} + +bool ksmbd_has_nonposix_open_child(struct ksmbd_file *old_fp) +{ + struct dentry *dentry = old_fp->filp->f_path.dentry; + struct ksmbd_file *fp; + unsigned int id; + bool ret = false; + + read_lock(&global_ft.lock); + idr_for_each_entry(global_ft.idr, fp, id) { + struct dentry *fp_dentry = fp->filp->f_path.dentry; + + if (fp->f_state != FP_INITED) + continue; + if (fp_dentry == dentry) + continue; + if (old_fp->is_posix_ctxt && fp->is_posix_ctxt) + continue; + if (is_subdir(fp_dentry, dentry)) { + ret = true; + break; + } + } + read_unlock(&global_ft.lock); + + return ret; +} + #define OPEN_ID_TYPE_VOLATILE_ID (0) #define OPEN_ID_TYPE_PERSISTENT_ID (1) @@ -708,7 +1230,8 @@ static int __open_id(struct ksmbd_file_table *ft, struct ksmbd_file *fp, idr_preload(KSMBD_DEFAULT_GFP); write_lock(&ft->lock); - ret = idr_alloc_cyclic(ft->idr, fp, 0, INT_MAX - 1, GFP_NOWAIT); + ret = idr_alloc_cyclic(ft->idr, fp, KSMBD_START_FID, INT_MAX - 1, + GFP_NOWAIT); if (ret >= 0) { id = ret; ret = 0; @@ -743,11 +1266,20 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp) 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->conn = work->conn; + /* + * 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->conn = ksmbd_conn_get(work->conn); fp->tcon = work->tcon; fp->volatile_id = KSMBD_NO_FID; fp->persistent_id = KSMBD_NO_FID; @@ -769,58 +1301,195 @@ 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. */ + ksmbd_conn_put(fp->conn); kmem_cache_free(filp_cache, fp); return ERR_PTR(ret); } -void ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp, - unsigned int state) +/** + * ksmbd_update_fstate() - update an fp state under the file-table lock + * @ft: file table that publishes @fp's volatile id + * @fp: file pointer to update + * @state: new state + * + * 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. + */ +int ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp, + unsigned int state) { + int ret; + if (!fp) - return; + return -ENOENT; write_lock(&ft->lock); - fp->f_state = state; + if (state == FP_INITED && + (fp->f_state != FP_NEW || !has_file_id(fp->volatile_id))) { + ret = -ENOENT; + } else { + fp->f_state = state; + ret = 0; + } write_unlock(&ft->lock); + + return ret; +} + +/* + * ksmbd_mark_fp_closed() - mark fp closed under ft->lock and return how many + * refs the teardown path owns. + * + * FP_INITED has a normal idr-owned reference, so teardown owns both that + * reference and the transient lookup reference. FP_NEW is still owned by the + * in-flight opener/reopener, which will drop the original reference after + * ksmbd_update_fstate(..., FP_INITED) observes the cleared volatile id. + * FP_CLOSED on entry means an earlier ksmbd_close_fd() already consumed the + * idr-owned ref. + */ +static int ksmbd_mark_fp_closed(struct ksmbd_file *fp) +{ + if (fp->f_state == FP_INITED) { + set_close_state_blocked_works(fp); + fp->f_state = FP_CLOSED; + return 2; + } + + return 1; } static int -__close_file_table_ids(struct ksmbd_file_table *ft, +__close_file_table_ids(struct ksmbd_session *sess, struct ksmbd_tree_connect *tcon, bool (*skip)(struct ksmbd_tree_connect *tcon, - struct ksmbd_file *fp)) + struct ksmbd_file *fp, + struct ksmbd_user *user), + bool skip_preserves_fp) { + 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; + write_lock(&ft->lock); fp = idr_get_next(ft->idr, &id); if (!fp) { write_unlock(&ft->lock); break; } - - if (skip(tcon, fp) || - !atomic_dec_and_test(&fp->refcount)) { + if (!atomic_inc_not_zero(&fp->refcount)) { id++; write_unlock(&ft->lock); continue; } - set_close_state_blocked_works(fp); - idr_remove(ft->idr, fp->volatile_id); - fp->volatile_id = KSMBD_NO_FID; - write_unlock(&ft->lock); + if (skip_preserves_fp) { + /* + * Session teardown: skip() is session_fd_check(), + * which may sleep and mutates fp->conn / fp->tcon / + * fp->volatile_id when it chooses to preserve fp + * for durable reconnect. Unpublish fp from the + * session idr here, under ft->lock, so that + * __ksmbd_lookup_fd() through this session cannot + * grant a new ksmbd_fp_get() reference to an fp + * whose fields are about to be rewritten outside + * the lock. Durable reconnect still reaches fp via + * global_ft. + */ + idr_remove(ft->idr, id); + fp->durable_volatile_id = fp->volatile_id; + fp->volatile_id = KSMBD_NO_FID; + write_unlock(&ft->lock); + + if (skip(tcon, fp, sess->user)) { + /* + * session_fd_check() has converted fp to + * durable-preserve state and cleared its + * per-conn fields. fp is already unpublished + * above; the original idr-owned ref keeps it + * alive for the durable scavenger. Drop only + * the transient ref. atomic_dec() is safe -- + * atomic_inc_not_zero() succeeded on a + * positive value and we added one more, so + * refcount cannot be zero here. + */ + atomic_dec(&fp->refcount); + id++; + continue; + } + /* + * Keep the close-state decision under the same lock + * observed by ksmbd_update_fstate(), which is how an + * in-flight FP_NEW opener learns that teardown has + * cleared its volatile id. + */ + write_lock(&ft->lock); + n_to_drop = ksmbd_mark_fp_closed(fp); + write_unlock(&ft->lock); + } else { + /* + * Tree teardown: skip() is tree_conn_fd_check(), a + * cheap pointer compare that doesn't sleep and has + * no side effects, so keep the skip decision plus + * the unpublish-and-mark-closed sequence atomic + * under ft->lock. fps belonging to other tree + * connects (skip() == true) stay fully published in + * the session idr with no lock window. + */ + if (skip(tcon, fp, sess->user)) { + atomic_dec(&fp->refcount); + write_unlock(&ft->lock); + id++; + continue; + } + idr_remove(ft->idr, id); + fp->volatile_id = KSMBD_NO_FID; + n_to_drop = ksmbd_mark_fp_closed(fp); + write_unlock(&ft->lock); + } + + /* + * 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. + */ down_write(&fp->f_ci->m_lock); list_del_init(&fp->node); up_write(&fp->f_ci->m_lock); - __ksmbd_close_fd(ft, fp); - - num++; + /* + * Drop the references this iteration owns: + * + * n_to_drop == 2: we observed FP_INITED and committed + * the FP_CLOSED transition ourselves, so we own the + * transient (+1) and the still-intact idr-owned ref. + * + * n_to_drop == 1: either a prior ksmbd_close_fd() + * already consumed the idr-owned ref, or fp was still + * FP_NEW and the in-flight opener/reopener must keep + * 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. + */ + if (atomic_sub_and_test(n_to_drop, &fp->refcount)) { + __ksmbd_close_fd(NULL, fp); + num++; + } id++; } @@ -854,7 +1523,8 @@ static inline bool is_reconnectable(struct ksmbd_file *fp) } static bool tree_conn_fd_check(struct ksmbd_tree_connect *tcon, - struct ksmbd_file *fp) + struct ksmbd_file *fp, + struct ksmbd_user *user) { return fp->tcon != tcon; } @@ -873,24 +1543,37 @@ static bool ksmbd_durable_scavenger_alive(void) return true; } -static void ksmbd_scavenger_dispose_dh(struct list_head *head) +static void ksmbd_scavenger_dispose_dh(struct ksmbd_file *fp) { - while (!list_empty(head)) { - struct ksmbd_file *fp; + /* + * Durable-preserved fp can remain linked on f_ci->m_fp_list for + * share-mode checks. Unlink it before final close; fp->node is not + * available as a scavenger-private list node because re-adding it to + * another list corrupts m_fp_list. + */ + down_write(&fp->f_ci->m_lock); + list_del_init(&fp->node); + up_write(&fp->f_ci->m_lock); - fp = list_first_entry(head, struct ksmbd_file, node); - list_del_init(&fp->node); + /* + * Drop both the durable lifetime reference and the transient reference + * taken by the scavenger under global_ft.lock. If a concurrent + * ksmbd_lookup_fd_inode() (or any other m_fp_list walker) snatched fp + * before the unlink above, that holder owns the final close via + * ksmbd_fd_put() -> __ksmbd_close_fd(). Otherwise the scavenger is + * the last putter and finalises fp here. + */ + if (atomic_sub_and_test(2, &fp->refcount)) __ksmbd_close_fd(NULL, fp); - } } static int ksmbd_durable_scavenger(void *dummy) { struct ksmbd_file *fp = NULL; + struct ksmbd_file *expired_fp; unsigned int id; unsigned int min_timeout = 1; bool found_fp_timeout; - LIST_HEAD(scavenger_list); unsigned long remaining_jiffies; __module_get(THIS_MODULE); @@ -900,33 +1583,47 @@ static int ksmbd_durable_scavenger(void *dummy) if (try_to_freeze()) continue; - found_fp_timeout = false; - - remaining_jiffies = wait_event_timeout(dh_wq, + remaining_jiffies = wait_event_interruptible_timeout(dh_wq, ksmbd_durable_scavenger_alive() == false, __msecs_to_jiffies(min_timeout)); - if (remaining_jiffies) + if ((long)remaining_jiffies > 0) min_timeout = jiffies_to_msecs(remaining_jiffies); else min_timeout = DURABLE_HANDLE_MAX_TIMEOUT; - write_lock(&global_ft.lock); - idr_for_each_entry(global_ft.idr, fp, id) { - if (!fp->durable_timeout) - continue; - - if (atomic_read(&fp->refcount) > 1 || - fp->conn) - continue; + do { + expired_fp = NULL; + found_fp_timeout = false; - found_fp_timeout = true; - if (fp->durable_scavenger_timeout <= - jiffies_to_msecs(jiffies)) { - __ksmbd_remove_durable_fd(fp); - list_add(&fp->node, &scavenger_list); - } else { + write_lock(&global_ft.lock); + idr_for_each_entry(global_ft.idr, fp, id) { unsigned long durable_timeout; + if (!fp->durable_timeout) + continue; + + if (atomic_read(&fp->refcount) > 1 || + fp->conn) + continue; + + found_fp_timeout = true; + if (fp->durable_scavenger_timeout <= + jiffies_to_msecs(jiffies)) { + __ksmbd_remove_durable_fd(fp); + /* + * Take a transient reference so fp + * cannot be freed by an in-flight + * ksmbd_lookup_fd_inode() that found + * it through f_ci->m_fp_list while we + * drop global_ft.lock and reach the + * m_fp_list unlink in + * ksmbd_scavenger_dispose_dh(). + */ + atomic_inc(&fp->refcount); + expired_fp = fp; + break; + } + durable_timeout = fp->durable_scavenger_timeout - jiffies_to_msecs(jiffies); @@ -934,10 +1631,11 @@ static int ksmbd_durable_scavenger(void *dummy) if (min_timeout > durable_timeout) min_timeout = durable_timeout; } - } - write_unlock(&global_ft.lock); + write_unlock(&global_ft.lock); - ksmbd_scavenger_dispose_dh(&scavenger_list); + if (expired_fp) + ksmbd_scavenger_dispose_dh(expired_fp); + } while (expired_fp); if (found_fp_timeout == false) break; @@ -965,9 +1663,12 @@ void ksmbd_launch_ksmbd_durable_scavenger(void) server_conf.dh_task = kthread_run(ksmbd_durable_scavenger, (void *)NULL, "ksmbd-durable-scavenger"); - if (IS_ERR(server_conf.dh_task)) + if (IS_ERR(server_conf.dh_task)) { pr_err("cannot start conn thread, err : %ld\n", PTR_ERR(server_conf.dh_task)); + server_conf.dh_task = NULL; + durable_scavenger_running = false; + } mutex_unlock(&durable_scavenger_lock); } @@ -989,28 +1690,123 @@ void ksmbd_stop_durable_scavenger(void) kthread_stop(server_conf.dh_task); } +/* + * ksmbd_vfs_set_durable_owner - Store owner info for durable replay/reconnect + * @fp: ksmbd file pointer to store owner info + * @user: user pointer to copy from + * + * This function binds the current user's identity to the file handle + * to satisfy MS-SMB2 Step 8 (SecurityContext matching) during reconnect. + * + * Return: 0 on success, or negative error code on failure + */ +int ksmbd_vfs_set_durable_owner(struct ksmbd_file *fp, + struct ksmbd_user *user) +{ + char *name, *old_name; + + if (!user) + return -EINVAL; + + /* Duplicate the user name to ensure identity persistence */ + name = kstrdup(user->name, GFP_KERNEL); + if (!name) + return -ENOMEM; + + spin_lock(&fp->f_lock); + old_name = fp->owner.name; + fp->owner.uid = user->uid; + fp->owner.gid = user->gid; + fp->owner.name = name; + spin_unlock(&fp->f_lock); + kfree(old_name); + + return 0; +} + +/** + * ksmbd_vfs_compare_durable_owner - Verify if the requester is original owner + * @fp: existing ksmbd file pointer + * @user: user pointer of the reconnect requester + * + * Compares the UID, GID, and name of the current requester against the + * original owner stored in the file handle. + * + * Return: true if the user matches, false otherwise + */ +bool ksmbd_vfs_compare_durable_owner(struct ksmbd_file *fp, + struct ksmbd_user *user) +{ + bool ret = false; + + if (!user) + return false; + + spin_lock(&fp->f_lock); + if (!fp->owner.name) + goto out; + + /* Check if the UID and GID match first (fast path) */ + if (fp->owner.uid != user->uid || fp->owner.gid != user->gid) + goto out; + + /* Validate the account name to ensure the same SecurityContext */ + ret = (strcmp(fp->owner.name, user->name) == 0); +out: + spin_unlock(&fp->f_lock); + return ret; +} + static bool session_fd_check(struct ksmbd_tree_connect *tcon, - struct ksmbd_file *fp) + struct ksmbd_file *fp, struct ksmbd_user *user) { struct ksmbd_inode *ci; struct oplock_info *op; struct ksmbd_conn *conn; + struct ksmbd_lock *smb_lock, *tmp_lock; if (!is_reconnectable(fp)) return false; + if (fp->f_state != FP_INITED) + return false; + + if (WARN_ON_ONCE(!fp->conn)) + return false; + + if (ksmbd_vfs_set_durable_owner(fp, user)) + return false; + + /* + * fp owns a strong reference on fp->conn (taken in ksmbd_open_fd() + * / 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); - list_for_each_entry_rcu(op, &ci->m_op_list, op_entry) { + list_for_each_entry_rcu(op, &ci->m_op_list, op_entry, + lockdep_is_held(&ci->m_lock)) { if (op->conn != conn) continue; - if (op->conn && atomic_dec_and_test(&op->conn->refcnt)) - kfree(op->conn); + ksmbd_conn_put(op->conn); op->conn = NULL; + op->sess = NULL; } 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); + } + fp->conn = NULL; fp->tcon = NULL; fp->volatile_id = KSMBD_NO_FID; @@ -1019,30 +1815,35 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon, fp->durable_scavenger_timeout = jiffies_to_msecs(jiffies) + fp->durable_timeout; + /* Drop fp's own reference on conn. */ + ksmbd_conn_put(conn); return true; } void ksmbd_close_tree_conn_fds(struct ksmbd_work *work) { - int num = __close_file_table_ids(&work->sess->file_table, + int num = __close_file_table_ids(work->sess, work->tcon, - tree_conn_fd_check); + tree_conn_fd_check, + false); atomic_sub(num, &work->conn->stats.open_files_count); } void ksmbd_close_session_fds(struct ksmbd_work *work) { - int num = __close_file_table_ids(&work->sess->file_table, + int num = __close_file_table_ids(work->sess, work->tcon, - session_fd_check); + session_fd_check, + true); atomic_sub(num, &work->conn->stats.open_files_count); } int ksmbd_init_global_file_table(void) { - create_proc_files(); + if (create_proc_files()) + pr_warn("Unable to create files procfs entry\n"); return ksmbd_init_file_table(&global_ft); } @@ -1090,37 +1891,73 @@ int ksmbd_reopen_durable_fd(struct ksmbd_work *work, struct ksmbd_file *fp) { struct ksmbd_inode *ci; struct oplock_info *op; + struct ksmbd_conn *conn = work->conn; + struct ksmbd_lock *smb_lock; + unsigned int old_f_state; - if (!fp->is_durable || fp->conn || fp->tcon) { + write_lock(&global_ft.lock); + if ((!fp->is_durable && !fp->is_persistent) || fp->conn || fp->tcon) { + write_unlock(&global_ft.lock); pr_err("Invalid durable fd [%p:%p]\n", fp->conn, fp->tcon); return -EBADF; } 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; } - fp->conn = work->conn; + /* + * Initialize fp's connection binding before publishing fp into the + * session's file table. If __open_id() is ordered first, a + * 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. + */ + fp->conn = ksmbd_conn_get(conn); fp->tcon = work->tcon; + write_unlock(&global_ft.lock); - ci = fp->f_ci; - down_write(&ci->m_lock); - list_for_each_entry_rcu(op, &ci->m_op_list, op_entry) { - if (op->conn) - continue; - op->conn = fp->conn; - atomic_inc(&op->conn->refcnt); - } - up_write(&ci->m_lock); - + old_f_state = fp->f_state; fp->f_state = FP_NEW; + __open_id(&work->sess->file_table, fp, OPEN_ID_TYPE_VOLATILE_ID); if (!has_file_id(fp->volatile_id)) { + write_lock(&global_ft.lock); fp->conn = NULL; fp->tcon = NULL; + 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); + } + + 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; + } + 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); + return 0; } @@ -1135,12 +1972,14 @@ int ksmbd_init_file_table(struct ksmbd_file_table *ft) return 0; } -void ksmbd_destroy_file_table(struct ksmbd_file_table *ft) +void ksmbd_destroy_file_table(struct ksmbd_session *sess) { + struct ksmbd_file_table *ft = &sess->file_table; + if (!ft->idr) return; - __close_file_table_ids(ft, NULL, session_fd_check); + __close_file_table_ids(sess, NULL, session_fd_check, true); idr_destroy(ft->idr); kfree(ft->idr); ft->idr = NULL; diff --git a/fs/smb/server/vfs_cache.h b/fs/smb/server/vfs_cache.h index 78b506c5ef03..1884f6deb9d0 100644 --- a/fs/smb/server/vfs_cache.h +++ b/fs/smb/server/vfs_cache.h @@ -8,6 +8,7 @@ #include <linux/file.h> #include <linux/fs.h> +#include <linux/mutex.h> #include <linux/rwsem.h> #include <linux/spinlock.h> #include <linux/idr.h> @@ -22,7 +23,12 @@ #define FILE_GENERIC_WRITE 0x120116 #define FILE_GENERIC_EXECUTE 0X1200a0 -#define KSMBD_START_FID 0 +/* + * Start volatile/persistent file id allocation at 1. A file id of 0 yields an + * SMB2 FileId of {0, 0}, which clients (e.g. Windows, Samba) treat as a null + * handle and never close, leaking the open on the server. + */ +#define KSMBD_START_FID 1 #define KSMBD_NO_FID (INT_MAX) #define SMB2_NO_FID (0xFFFFFFFFFFFFFFFFULL) @@ -31,6 +37,7 @@ struct ksmbd_session; struct ksmbd_lock { struct file_lock *fl; + struct ksmbd_conn *conn; struct list_head clist; struct list_head flist; struct list_head llist; @@ -68,10 +75,25 @@ enum { FP_CLOSED }; +/* Owner information for durable handle reconnect */ +struct durable_owner { + unsigned int uid; + unsigned int gid; + char *name; +}; + +#define KSMBD_LOCK_SEQ_ARRAY_SIZE 64 + +struct ksmbd_lock_sequence { + bool valid; + u8 sequence; +}; + struct ksmbd_file { struct file *filp; u64 persistent_id; u64 volatile_id; + u64 durable_volatile_id; spinlock_t f_lock; @@ -86,11 +108,16 @@ struct ksmbd_file { __le32 saccess; __le32 coption; __le32 cdoption; + __le32 create_file_attributes; __u64 create_time; + __u64 change_time; + __u64 allocation_size; __u64 itime; + __u64 open_mtime; bool is_nt_open; bool attrib_only; + bool allocation_size_set; char client_guid[16]; char create_guid[16]; @@ -100,20 +127,47 @@ struct ksmbd_file { struct list_head node; struct list_head blocked_works; struct list_head lock_list; + /* + * Per-handle FileDispositionInformation delete-pending state for a + * stream handle -- separate from ksmbd_inode's inode-wide m_flags, + * which have no way to record which stream on a multi-stream file + * was actually marked for deletion. See ksmbd_fd_set_delete_pending(). + */ + bool stream_del_pending; unsigned int durable_timeout; unsigned int durable_scavenger_timeout; + /* CREATE action returned when this durable handle was established. */ + __le32 create_action; /* if ls is happening on directory, below is valid*/ struct ksmbd_readdir_data readdir_data; + struct mutex readdir_lock; int dot_dotdot[2]; unsigned int f_state; bool reserve_lease_break; bool is_durable; bool is_persistent; bool is_resilient; + bool has_app_instance_id; + bool app_instance_version_valid; + u64 app_instance_version_high; + u64 app_instance_version_low; + bool durable_reconnect_disabled; + bool durable_replay_consumed; bool is_posix_ctxt; + 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, @@ -140,32 +194,47 @@ static inline bool ksmbd_stream_fd(struct ksmbd_file *fp) } int ksmbd_init_file_table(struct ksmbd_file_table *ft); -void ksmbd_destroy_file_table(struct ksmbd_file_table *ft); +void ksmbd_destroy_file_table(struct ksmbd_session *sess); int ksmbd_close_fd(struct ksmbd_work *work, u64 id); struct ksmbd_file *ksmbd_lookup_fd_fast(struct ksmbd_work *work, u64 id); struct ksmbd_file *ksmbd_lookup_foreign_fd(struct ksmbd_work *work, u64 id); struct ksmbd_file *ksmbd_lookup_fd_slow(struct ksmbd_work *work, u64 id, u64 pid); +int ksmbd_vfs_set_durable_owner(struct ksmbd_file *fp, + struct ksmbd_user *user); +struct ksmbd_file *ksmbd_file_get(struct ksmbd_file *fp); void ksmbd_fd_put(struct ksmbd_work *work, struct ksmbd_file *fp); struct ksmbd_inode *ksmbd_inode_lookup_lock(struct dentry *d); void ksmbd_inode_put(struct ksmbd_inode *ci); +bool ksmbd_close_disconnected_durable_delete_on_close(struct dentry *dentry); struct ksmbd_file *ksmbd_lookup_global_fd(unsigned long long id); struct ksmbd_file *ksmbd_lookup_durable_fd(unsigned long long id); 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); 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); +bool ksmbd_has_nonposix_open_child(struct ksmbd_file *old_fp); unsigned int ksmbd_open_durable_fd(struct ksmbd_file *fp); struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp); void ksmbd_launch_ksmbd_durable_scavenger(void); void ksmbd_stop_durable_scavenger(void); +bool ksmbd_durable_scavenger_active(void); void ksmbd_close_tree_conn_fds(struct ksmbd_work *work); void ksmbd_close_session_fds(struct ksmbd_work *work); +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_set_fd_limit(unsigned long limit); -void ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp, - unsigned int state); +int ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp, + unsigned int state); +bool ksmbd_vfs_compare_durable_owner(struct ksmbd_file *fp, + struct ksmbd_user *user); /* * INODE hash @@ -185,6 +254,8 @@ void ksmbd_set_inode_pending_delete(struct ksmbd_file *fp); void ksmbd_clear_inode_pending_delete(struct ksmbd_file *fp); void ksmbd_fd_set_delete_on_close(struct ksmbd_file *fp, int file_info); +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_validate_name_reconnect(struct ksmbd_share_config *share, struct ksmbd_file *fp, char *name); |
