[PATCH] smb: client: fix OOB read in symlink error response parsing

From: Werner Kasselman

Date: Tue Apr 14 2026 - 07:51:59 EST


symlink_data() walks server-supplied SMB2 error contexts to locate the
smb2_symlink_err_rsp before returning it to smb2_parse_symlink_response().
When ErrorContextCount is non-zero, sym can land at an attacker-chosen
offset past the smb2_err_rsp header, bounded only by iov_len.

Reads of p->ErrorId and p->ErrorDataLength in the walk loop occur
without checking that the smb2_error_context_rsp header fits in the
response buffer, and sym is dereferenced for SymLinkErrorTag/ReparseTag
without checking that sym itself fits. A context header placed near
iov_end produces an OOB read.

The bounds check in smb2_parse_symlink_response() uses the compile-time
SMB2_SYMLINK_STRUCT_SIZE as the base for SubstituteName and PrintName
ranges. That only matches the fixed layout when ErrorContextCount is
zero; with contexts, the actual PathBuffer offset in iov is larger, and
the read of sym->PathBuffer + sub_offs for sub_len bytes can extend
past iov_len into adjacent slab memory. The copied bytes reach
userspace via readlink() on data->symlink_target.

STATUS_STOPPED_ON_SYMLINK responses are served from the 448-byte small
buffer pool, so the overread reliably crosses the slab object boundary.

Bound each context header during the walk, verify sym fits in the
response before dereferencing its length fields, and compute the
PathBuffer bound from sym->PathBuffer's actual offset into iov.

Fixes: 76894f3e2f71 ("cifs: improve symlink handling for smb2+")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Werner Kasselman <werner@xxxxxxxxxxx>
---
fs/smb/client/smb2file.c | 22 +++++++++++++++++-----
1 file changed, 17 insertions(+), 5 deletions(-)

diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
index ed651c946251..6fda8ec7fe9b 100644
--- a/fs/smb/client/smb2file.c
+++ b/fs/smb/client/smb2file.c
@@ -41,6 +41,8 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
p = (struct smb2_error_context_rsp *)err->ErrorData;
end = (struct smb2_error_context_rsp *)((u8 *)err + iov->iov_len);
do {
+ if ((u8 *)p + sizeof(*p) > (u8 *)end)
+ return ERR_PTR(-EINVAL);
if (le32_to_cpu(p->ErrorId) == SMB2_ERROR_ID_DEFAULT) {
sym = (struct smb2_symlink_err_rsp *)p->ErrorContextData;
break;
@@ -56,9 +58,15 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
sym = (struct smb2_symlink_err_rsp *)err->ErrorData;
}

- if (!IS_ERR(sym) && (le32_to_cpu(sym->SymLinkErrorTag) != SYMLINK_ERROR_TAG ||
- le32_to_cpu(sym->ReparseTag) != IO_REPARSE_TAG_SYMLINK))
- sym = ERR_PTR(-EINVAL);
+ if (IS_ERR(sym))
+ return sym;
+
+ if ((u8 *)sym + sizeof(*sym) > (u8 *)err + iov->iov_len)
+ return ERR_PTR(-EINVAL);
+
+ if (le32_to_cpu(sym->SymLinkErrorTag) != SYMLINK_ERROR_TAG ||
+ le32_to_cpu(sym->ReparseTag) != IO_REPARSE_TAG_SYMLINK)
+ return ERR_PTR(-EINVAL);

return sym;
}
@@ -115,6 +123,7 @@ int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec
struct smb2_symlink_err_rsp *sym;
unsigned int sub_offs, sub_len;
unsigned int print_offs, print_len;
+ size_t pathbuf_off;

if (!cifs_sb || !iov || !iov->iov_base || !iov->iov_len || !path)
return -EINVAL;
@@ -128,8 +137,11 @@ int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec
print_len = le16_to_cpu(sym->PrintNameLength);
print_offs = le16_to_cpu(sym->PrintNameOffset);

- if (iov->iov_len < SMB2_SYMLINK_STRUCT_SIZE + sub_offs + sub_len ||
- iov->iov_len < SMB2_SYMLINK_STRUCT_SIZE + print_offs + print_len)
+ pathbuf_off = (const u8 *)sym->PathBuffer - (const u8 *)iov->iov_base;
+
+ if (pathbuf_off > iov->iov_len ||
+ iov->iov_len - pathbuf_off < sub_offs + sub_len ||
+ iov->iov_len - pathbuf_off < print_offs + print_len)
return -EINVAL;

return smb2_parse_native_symlink(path,
--
2.43.0