[PATCH 1/2] ksmbd: pass through NTLM authentication to a domain controller
From: Jan Koester
Date: Sun Oct 18 2026 - 13:54:08 EST
A ksmbd server that is a member of an Active Directory domain cannot
check the NTLM response of a domain user on its own: it has no password
hash for that user. Add a way to defer the check to user space, which
can ask a domain controller over NETLOGON (NetrLogonSamLogonEx).
When the login response for a user carries the new
KSMBD_USER_FLAG_PASSTHROUGH flag, ksmbd no longer computes the NTLMv2
response from a local hash. Instead ksmbd_decode_ntlmssp_auth_blob()
hands the server challenge and the client's user name, domain name,
workstation and NT/LM responses to user space through the new
KSMBD_EVENT_NTLM_AUTHEN_REQUEST event and takes the session base key
from the KSMBD_EVENT_NTLM_AUTHEN_RESPONSE, then continues with the
existing key-exchange path.
The two new events get a strict netlink policy and are validated in
ipc_validate_msg(); ksmbd_nl_init_fixup() keeps strict validation for
commands at or after resv_start_op so the new fixed-size response is
checked.
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Jan Koester <jan.koester@xxxxxxx>
---
fs/smb/server/auth.c | 68 ++++++++++++++++++++++++++++++++
fs/smb/server/ksmbd_netlink.h | 41 ++++++++++++++++++++
fs/smb/server/transport_ipc.c | 73 ++++++++++++++++++++++++++++++++++-
fs/smb/server/transport_ipc.h | 3 ++
4 files changed, 183 insertions(+), 2 deletions(-)
diff --git a/fs/smb/server/auth.c b/fs/smb/server/auth.c
index 1b24eb3f39..9a2a3c6a37 100644
--- a/fs/smb/server/auth.c
+++ b/fs/smb/server/auth.c
@@ -79,6 +79,16 @@ static int calc_ntlmv2_hash(struct ksmbd_conn *conn, struct ksmbd_session *sess,
__le16 *uniname = NULL;
struct hmac_md5_ctx ctx;
+ /*
+ * A pass-through user (KSMBD_USER_FLAG_PASSTHROUGH) has no local
+ * password hash and must never reach the NTLMv2 code -- it is handled
+ * in ksmbd_decode_ntlmssp_auth_blob() instead. Guard against a NULL
+ * passkey anyway so a user space that sets KSMBD_USER_FLAG_OK without a
+ * hash can only fail the login, not crash the kernel.
+ */
+ if (!user_passkey(sess->user))
+ return -EINVAL;
+
hmac_md5_init_usingrawkey(&ctx, user_passkey(sess->user),
CIFS_ENCPWD_SIZE);
@@ -184,6 +194,55 @@ int ksmbd_auth_ntlmv2(struct ksmbd_conn *conn, struct ksmbd_session *sess,
return rc;
}
+/**
+ * ksmbd_auth_ntlm_passthrough() - NTLM authentication by user space
+ * @conn: connection
+ * @authblob: authenticate blob
+ * @blob_len: length of @authblob
+ * @sess_key: session base key output buffer
+ *
+ * For a user that user space has no password hash for (a domain user): it
+ * checks the response, e.g. with a domain controller, and returns the
+ * session base key.
+ *
+ * Return: 0 on success, error number on error
+ */
+static int ksmbd_auth_ntlm_passthrough(struct ksmbd_conn *conn,
+ struct authenticate_message *authblob,
+ int blob_len, char *sess_key)
+{
+ const struct security_buffer *bufs[5] = {
+ &authblob->UserName, &authblob->DomainName,
+ &authblob->WorkstationName, &authblob->NtChallengeResponse,
+ &authblob->LmChallengeResponse,
+ };
+ struct ksmbd_ntlm_authen_response *resp;
+ const char *parts[5];
+ unsigned short lens[5];
+ int i, rc;
+
+ for (i = 0; i < 5; i++) {
+ unsigned int off = le32_to_cpu(bufs[i]->BufferOffset);
+
+ lens[i] = le16_to_cpu(bufs[i]->Length);
+ if (blob_len < (u64)off + lens[i])
+ return -EINVAL;
+ parts[i] = (const char *)authblob + off;
+ }
+
+ resp = ksmbd_ipc_ntlm_authen_request(conn->ntlmssp.cryptkey, parts,
+ lens);
+ if (!resp)
+ return -EINVAL;
+
+ rc = resp->status & KSMBD_USER_FLAG_OK ? 0 : -EINVAL;
+ if (!rc)
+ memcpy(sess_key, resp->session_key, SMB2_NTLMV2_SESSKEY_SIZE);
+ memzero_explicit(resp->session_key, sizeof(resp->session_key));
+ kvfree(resp);
+ return rc;
+}
+
/**
* ksmbd_decode_ntlmssp_auth_blob() - helper function to construct
* authenticate blob
@@ -225,6 +284,14 @@ int ksmbd_decode_ntlmssp_auth_blob(struct authenticate_message *authblob,
nt_len < CIFS_ENCPWD_SIZE)
return -EINVAL;
+ if (test_user_flag(sess->user, KSMBD_USER_FLAG_PASSTHROUGH)) {
+ ret = ksmbd_auth_ntlm_passthrough(conn, authblob, blob_len,
+ sess_key);
+ if (ret)
+ return ret;
+ goto key_exchange;
+ }
+
/* TODO : use domain name that imported from configuration file */
domain_name = smb_strndup_from_utf16((const char *)authblob + dn_off,
dn_len, true, conn->local_nls);
@@ -242,6 +309,7 @@ int ksmbd_decode_ntlmssp_auth_blob(struct authenticate_message *authblob,
if (ret)
return ret;
+key_exchange:
/* The recovered secondary session key */
if (conn->ntlmssp.client_flags & NTLMSSP_NEGOTIATE_KEY_XCH) {
struct arc4_ctx *ctx_arc4;
diff --git a/fs/smb/server/ksmbd_netlink.h b/fs/smb/server/ksmbd_netlink.h
index 2673522c76..7438e0b0ba 100644
--- a/fs/smb/server/ksmbd_netlink.h
+++ b/fs/smb/server/ksmbd_netlink.h
@@ -54,6 +54,11 @@
*
* - KSMBD_EVENT_LOGIN_REQUEST_EXT/RESPONSE_EXT(ksmbd_login_request_ext/response_ext)
* This event is to get user account extension info to user IPC daemon.
+ *
+ * - KSMBD_EVENT_NTLM_AUTHEN_REQUEST/RESPONSE(ksmbd_ntlm_authen_request/response)
+ * This event is to make NTLM authentication of a user the login response
+ * marked KSMBD_USER_FLAG_PASSTHROUGH to be processed in userspace, e.g.
+ * by a domain controller.
*/
#define KSMBD_GENL_NAME "SMBD_GENL"
@@ -161,6 +166,38 @@ struct ksmbd_login_response_ext {
__s8 ____payload[];
};
+/*
+ * IPC request to check the NTLM response of a user whose login response
+ * had KSMBD_USER_FLAG_PASSTHROUGH: user space has no password hash for it
+ * and asks someone who has, e.g. a domain controller (NETLOGON).
+ */
+struct ksmbd_ntlm_authen_request {
+ __u32 handle;
+ __u8 challenge[8]; /* the server challenge */
+ __u16 user_name_len; /* lengths of the payload parts */
+ __u16 domain_name_len;
+ __u16 workstation_len;
+ __u16 nt_response_len;
+ __u16 lm_response_len;
+ __u16 reserved16;
+ __u32 reserved[16]; /* Reserved room */
+ __u8 payload[]; /*
+ * user name, domain name, workstation
+ * (UTF-16LE, as the client sent them),
+ * NT response, LM response
+ */
+};
+
+/*
+ * IPC response to an NTLM authentication request
+ */
+struct ksmbd_ntlm_authen_response {
+ __u32 handle;
+ __u32 status; /* KSMBD_USER_FLAG_OK or _BAD_PASSWORD */
+ __u8 session_key[16]; /* the session base key */
+ __u32 reserved[16]; /* Reserved room */
+};
+
/*
* IPC request to fetch net share config.
*/
@@ -326,6 +363,9 @@ enum ksmbd_event {
KSMBD_EVENT_LOGIN_REQUEST_EXT,
KSMBD_EVENT_LOGIN_RESPONSE_EXT,
+ KSMBD_EVENT_NTLM_AUTHEN_REQUEST,
+ KSMBD_EVENT_NTLM_AUTHEN_RESPONSE,
+
__KSMBD_EVENT_MAX,
KSMBD_EVENT_MAX = __KSMBD_EVENT_MAX - 1
};
@@ -357,6 +397,7 @@ enum KSMBD_TREE_CONN_STATUS {
#define KSMBD_USER_FLAG_GUEST_ACCOUNT BIT(4)
#define KSMBD_USER_FLAG_DELAY_SESSION BIT(5)
#define KSMBD_USER_FLAG_EXTENSION BIT(6)
+#define KSMBD_USER_FLAG_PASSTHROUGH BIT(7)
/*
* Share config flags.
diff --git a/fs/smb/server/transport_ipc.c b/fs/smb/server/transport_ipc.c
index e550aa41ad..578a22dbff 100644
--- a/fs/smb/server/transport_ipc.c
+++ b/fs/smb/server/transport_ipc.c
@@ -127,6 +127,10 @@ static const struct nla_policy ksmbd_nl_policy[KSMBD_EVENT_MAX + 1] = {
[KSMBD_EVENT_LOGIN_RESPONSE_EXT] = {
.len = sizeof(struct ksmbd_login_response_ext),
},
+ [KSMBD_EVENT_NTLM_AUTHEN_REQUEST] =
+ NLA_POLICY_MIN_LEN(sizeof(struct ksmbd_ntlm_authen_request)),
+ [KSMBD_EVENT_NTLM_AUTHEN_RESPONSE] =
+ NLA_POLICY_EXACT_LEN(sizeof(struct ksmbd_ntlm_authen_response)),
};
static struct genl_ops ksmbd_genl_ops[] = {
@@ -202,6 +206,14 @@ static struct genl_ops ksmbd_genl_ops[] = {
.cmd = KSMBD_EVENT_LOGIN_RESPONSE_EXT,
.doit = handle_generic_event,
},
+ {
+ .cmd = KSMBD_EVENT_NTLM_AUTHEN_REQUEST,
+ .doit = handle_unsupported_event,
+ },
+ {
+ .cmd = KSMBD_EVENT_NTLM_AUTHEN_RESPONSE,
+ .doit = handle_generic_event,
+ },
};
static struct genl_family ksmbd_genl_family = {
@@ -220,9 +232,11 @@ static void ksmbd_nl_init_fixup(void)
{
int i;
+ /* the commands from resv_start_op on are validated strictly */
for (i = 0; i < ARRAY_SIZE(ksmbd_genl_ops); i++)
- ksmbd_genl_ops[i].validate = GENL_DONT_VALIDATE_STRICT |
- GENL_DONT_VALIDATE_DUMP;
+ if (ksmbd_genl_ops[i].cmd < ksmbd_genl_family.resv_start_op)
+ ksmbd_genl_ops[i].validate = GENL_DONT_VALIDATE_STRICT |
+ GENL_DONT_VALIDATE_DUMP;
ksmbd_genl_family.policy = ksmbd_nl_policy;
}
@@ -549,6 +563,10 @@ static int ipc_validate_msg(struct ipc_msg_table_entry *entry)
return -EINVAL;
break;
}
+ case KSMBD_EVENT_NTLM_AUTHEN_REQUEST:
+ if (entry->msg_sz != sizeof(struct ksmbd_ntlm_authen_response))
+ return -EINVAL;
+ break;
case KSMBD_EVENT_LOGIN_REQUEST_EXT:
{
struct ksmbd_login_response_ext *resp = entry->response;
@@ -702,6 +720,57 @@ ksmbd_ipc_spnego_authen_request(const char *spnego_blob, int blob_len)
return resp;
}
+/**
+ * ksmbd_ipc_ntlm_authen_request() - have user space check an NTLM response
+ * @challenge: the server challenge (CIFS_CRYPTO_KEY_SIZE bytes)
+ * @parts: user name, domain name, workstation (UTF-16LE),
+ * NT response, LM response
+ * @lens: their lengths
+ *
+ * For a user whose login response had KSMBD_USER_FLAG_PASSTHROUGH.
+ *
+ * Return: the response (kvfree() it), NULL on error
+ */
+struct ksmbd_ntlm_authen_response *
+ksmbd_ipc_ntlm_authen_request(const char *challenge, const char *parts[5],
+ const unsigned short lens[5])
+{
+ struct ksmbd_ipc_msg *msg;
+ struct ksmbd_ntlm_authen_request *req;
+ struct ksmbd_ntlm_authen_response *resp;
+ size_t payload_sz = 0, off = 0;
+ int i;
+
+ for (i = 0; i < 5; i++)
+ payload_sz += lens[i];
+ if (payload_sz > KSMBD_IPC_MAX_PAYLOAD)
+ return NULL;
+
+ msg = ipc_msg_alloc(sizeof(struct ksmbd_ntlm_authen_request) +
+ payload_sz);
+ if (!msg)
+ return NULL;
+
+ msg->type = KSMBD_EVENT_NTLM_AUTHEN_REQUEST;
+ req = (struct ksmbd_ntlm_authen_request *)msg->payload;
+ req->handle = ksmbd_acquire_id(&ipc_ida);
+ memcpy(req->challenge, challenge, sizeof(req->challenge));
+ req->user_name_len = lens[0];
+ req->domain_name_len = lens[1];
+ req->workstation_len = lens[2];
+ req->nt_response_len = lens[3];
+ req->lm_response_len = lens[4];
+ for (i = 0; i < 5; i++) {
+ memcpy(req->payload + off, parts[i], lens[i]);
+ off += lens[i];
+ }
+
+ resp = ipc_msg_send_request(msg, req->handle);
+ ipc_msg_handle_free(req->handle);
+ ipc_msg_free(msg);
+ return resp;
+}
+
struct ksmbd_tree_connect_response *
ksmbd_ipc_tree_connect_request(struct ksmbd_session *sess,
struct ksmbd_share_config *share,
diff --git a/fs/smb/server/transport_ipc.h b/fs/smb/server/transport_ipc.h
index e51850f142..f957a33ae3 100644
--- a/fs/smb/server/transport_ipc.h
+++ b/fs/smb/server/transport_ipc.h
@@ -32,6 +32,9 @@ struct ksmbd_share_config_response *
ksmbd_ipc_share_config_request(const char *name);
struct ksmbd_spnego_authen_response *
ksmbd_ipc_spnego_authen_request(const char *spnego_blob, int blob_len);
+struct ksmbd_ntlm_authen_response *
+ksmbd_ipc_ntlm_authen_request(const char *challenge, const char *parts[5],
+ const unsigned short lens[5]);
int ksmbd_ipc_id_alloc(void);
void ksmbd_rpc_id_free(int handle);
struct ksmbd_rpc_command *ksmbd_rpc_open(struct ksmbd_session *sess, int handle);
--
2.56.0