[PATCH] smb: client: validate DFS referral offsets

Yichong Chen <[email protected]>
Newsgroups org.kernel.vger.linux-cifs,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
parse_dfs_referrals() checks that the response contains the fixed
referral entries, but it still trusts offsets stored inside each referral.
A malformed server response can set DfsPathOffset or NetworkAddressOffset
past the end of the received buffer.  The resulting pointer is then used
to derive a remaining length and passed to cifs_strndup_from_utf16().

Validate the per-referral offsets before using them.  Also validate the
Unicode PathConsumed value against the converted search name length before
passing it to cifs_utf16_bytes(), and only publish the parsed referral
array to the caller after the number of referrals has been validated.

Fixes: 6447b0e35556 ("cifs: parse_dfs_referrals: prevent oob on malformed input")
Signed-off-by: Yichong Chen <[email protected]>
---
 fs/smb/client/misc.c | 94 +++++++++++++++++++++++++++++---------------
 1 file changed, 63 insertions(+), 31 deletions(-)

diff --git a/fs/smb/client/misc.c b/fs/smb/client/misc.c
index 0c54b9b..73676d2 100644
--- a/fs/smb/client/misc.c
+++ b/fs/smb/client/misc.c
@@ -678,6 +678,11 @@ parse_dfs_referrals(struct get_dfs_referral_rsp *rsp, u32 rsp_size,
 	int i, rc = 0;
 	char *data_end;
 	struct dfs_referral_level_3 *ref;
+	unsigned int num_refs, path_consumed;
+	size_t search_name_len;
+
+	*target_nodes = NULL;
+	*num_of_nodes = 0;
 
 	if (rsp_size < sizeof(*rsp)) {
 		cifs_dbg(VFS | ONCE,
@@ -687,85 +692,112 @@ parse_dfs_referrals(struct get_dfs_referral_rsp *rsp, u32 rsp_size,
 		goto parse_DFS_referrals_exit;
 	}
 
-	*num_of_nodes = le16_to_cpu(rsp->NumberOfReferrals);
+	num_refs = le16_to_cpu(rsp->NumberOfReferrals);
 
-	if (*num_of_nodes < 1) {
+	if (num_refs < 1) {
 		cifs_dbg(VFS | ONCE, "%s: [path=%s] num_referrals must be at least > 0, but we got %d\n",
-			 __func__, searchName, *num_of_nodes);
+			 __func__, searchName, num_refs);
 		rc = -ENOENT;
 		goto parse_DFS_referrals_exit;
 	}
 
-	if (sizeof(*rsp) + *num_of_nodes * sizeof(REFERRAL3) > rsp_size) {
+	if (sizeof(*rsp) + num_refs * sizeof(REFERRAL3) > rsp_size) {
 		cifs_dbg(VFS | ONCE,
 			 "%s: malformed buffer (size is %u, must be at least %zu)\n",
 			 __func__, rsp_size,
-			 sizeof(*rsp) + *num_of_nodes * sizeof(REFERRAL3));
+			 sizeof(*rsp) + num_refs * sizeof(REFERRAL3));
 		rc = -EINVAL;
 		goto parse_DFS_referrals_exit;
 	}
 
 	ref = (struct dfs_referral_level_3 *) &(rsp->referrals);
-	if (ref->VersionNumber != cpu_to_le16(3)) {
-		cifs_dbg(VFS, "Referrals of V%d version are not supported, should be V3\n",
-			 le16_to_cpu(ref->VersionNumber));
-		rc = -EINVAL;
-		goto parse_DFS_referrals_exit;
-	}
 
 	/* get the upper boundary of the resp buffer */
 	data_end = (char *)rsp + rsp_size;
 
 	cifs_dbg(FYI, "num_referrals: %d dfs flags: 0x%x ...\n",
-		 *num_of_nodes, le32_to_cpu(rsp->DFSFlags));
+		 num_refs, le32_to_cpu(rsp->DFSFlags));
 
-	*target_nodes = kzalloc_objs(struct dfs_info3_param, *num_of_nodes);
-	if (*target_nodes == NULL) {
+	*target_nodes = kzalloc_objs(struct dfs_info3_param, num_refs);
+	if (!*target_nodes) {
 		rc = -ENOMEM;
 		goto parse_DFS_referrals_exit;
 	}
+	*num_of_nodes = num_refs;
+	search_name_len = strlen(searchName);
 
 	/* collect necessary data from referrals */
-	for (i = 0; i < *num_of_nodes; i++) {
+	for (i = 0; i < num_refs; i++) {
 		char *temp;
 		int max_len;
-		struct dfs_info3_param *node = (*target_nodes)+i;
+		unsigned int offset, remaining;
+		struct dfs_info3_param *node = (*target_nodes) + i;
+
+		if (ref->VersionNumber != cpu_to_le16(3)) {
+			cifs_dbg(VFS,
+				 "Referrals of V%d version are not supported, should be V3\n",
+				 le16_to_cpu(ref->VersionNumber));
+			rc = -EINVAL;
+			goto parse_DFS_referrals_exit;
+		}
 
 		node->flags = le32_to_cpu(rsp->DFSFlags);
+		path_consumed = le16_to_cpu(rsp->PathConsumed);
 		if (is_unicode) {
-			__le16 *tmp = kmalloc(strlen(searchName)*2 + 2,
-						GFP_KERNEL);
-			if (tmp == NULL) {
+			size_t search_name_utf16_len = search_name_len * 2 + 2;
+			__le16 *tmp;
+
+			if (path_consumed > search_name_utf16_len) {
+				rc = -EINVAL;
+				goto parse_DFS_referrals_exit;
+			}
+
+			tmp = kmalloc(search_name_utf16_len, GFP_KERNEL);
+			if (!tmp) {
 				rc = -ENOMEM;
 				goto parse_DFS_referrals_exit;
 			}
-			cifsConvertToUTF16((__le16 *) tmp, searchName,
+			cifsConvertToUTF16((__le16 *)tmp, searchName,
 					   PATH_MAX, nls_codepage, remap);
-			node->path_consumed = cifs_utf16_bytes(tmp,
-					le16_to_cpu(rsp->PathConsumed),
-					nls_codepage);
+			node->path_consumed = cifs_utf16_bytes(tmp, path_consumed,
+							       nls_codepage);
 			kfree(tmp);
-		} else
-			node->path_consumed = le16_to_cpu(rsp->PathConsumed);
+		} else {
+			node->path_consumed = path_consumed;
+		}
 
 		node->server_type = le16_to_cpu(ref->ServerType);
 		node->ref_flag = le16_to_cpu(ref->ReferralEntryFlags);
 
+		remaining = data_end - (char *)ref;
+
 		/* copy DfsPath */
-		temp = (char *)ref + le16_to_cpu(ref->DfsPathOffset);
-		max_len = data_end - temp;
+		offset = le16_to_cpu(ref->DfsPathOffset);
+		if (offset > remaining) {
+			rc = -EINVAL;
+			goto parse_DFS_referrals_exit;
+		}
+		temp = (char *)ref + offset;
+		max_len = remaining - offset;
 		node->path_name = cifs_strndup_from_utf16(temp, max_len,
-						is_unicode, nls_codepage);
+							  is_unicode,
+							  nls_codepage);
 		if (!node->path_name) {
 			rc = -ENOMEM;
 			goto parse_DFS_referrals_exit;
 		}
 
 		/* copy link target UNC */
-		temp = (char *)ref + le16_to_cpu(ref->NetworkAddressOffset);
-		max_len = data_end - temp;
+		offset = le16_to_cpu(ref->NetworkAddressOffset);
+		if (offset > remaining) {
+			rc = -EINVAL;
+			goto parse_DFS_referrals_exit;
+		}
+		temp = (char *)ref + offset;
+		max_len = remaining - offset;
 		node->node_name = cifs_strndup_from_utf16(temp, max_len,
-						is_unicode, nls_codepage);
+							  is_unicode,
+							  nls_codepage);
 		if (!node->node_name) {
 			rc = -ENOMEM;
 			goto parse_DFS_referrals_exit;
-- 
2.51.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.