From: Carl Johnson <carl@jpartners.org>
To: linux-cifs@vger.kernel.org
Cc: "Steve French" <sfrench@samba.org>,
"Pali Rohár" <pali@kernel.org>,
"Carl Johnson" <carl@jpartners.org>
Subject: [PATCH v2] smb: client: handle STATUS_STOPPED_ON_SYMLINK responses without a symlink target
Date: Tue, 21 Jul 2026 13:27:55 -0400 [thread overview]
Message-ID: <20260721172755.41346-1-carl@jpartners.org> (raw)
The macOS built-in SMB server returns STATUS_STOPPED_ON_SYMLINK for a
CREATE on a path whose final component is a symlink, but it does not
include a Symbolic Link Error Response in the error data: both
ErrorContextCount and ByteCount are zero, so the symlink target is not
present in the response at all. Per [MS-SMB2] section 2.2.2 such a
response should carry a valid Symbolic Link Error Response, so this is a
server bug, but the target can still be retrieved with
FSCTL_GET_REPARSE_POINT.
Frame from a capture against macOS 26.5.2 (build 25F84):
SMB2 hdr : Status=0x8000002d STATUS_STOPPED_ON_SYMLINK, Cmd=Create
Error Rsp: StructureSize=0x0009
Error Context Count: 0
Byte Count: 0
Error Data: 00
symlink_data() cannot find a struct smb2_symlink_err_rsp in such a
response and returns -EINVAL, which parse_create_response() propagates,
so smb2_query_path_info() bails out at
if (rc || !data->reparse_point)
goto out;
before it can retry with SMB2_OP_GET_REPARSE. stat(), readlink() and ls
of any server-side symlink then fail with -EINVAL:
$ ls -la Config
l????????? ? ? ? ? ? Config.json
$ stat Config/Config.json
stat: cannot statx 'Config/Config.json': Invalid argument
A 5.10 client resolves these symlinks correctly against the same server
and share, so this is a regression for Apple SMB servers.
Handle it in several places:
- symlink_data() detects the empty response (ErrorContextCount and
ByteCount both zero) and returns a distinct -ENODATA, so that "server
did not send the target" can be told apart from a genuinely malformed
response and only this case is worked around.
- parse_create_response() treats -ENODATA like
STATUS_IO_REPARSE_TAG_NOT_HANDLED, which does not carry the target
either: leave the reparse tag unset and clear rc, so the existing
SMB2_OP_GET_REPARSE path retrieves the target.
- smb2_query_path_info() only fixes up the symlink target type when the
target is already known. SMB2_OP_GET_REPARSE sets data->reparse.tag
but does not parse the target out of the reparse buffer; that happens
later, in reparse_info_to_fattr(). Without this check
smb2_fix_symlink_target_type() is called with a NULL target and
returns -EIO. This could not happen with servers that send the target
inline and therefore skip SMB2_OP_GET_REPARSE.
- smb2_open_file() maps -ENODATA to -EIO, matching
STATUS_IO_REPARSE_TAG_NOT_HANDLED, so its callers retrieve the target
with SMB2_OP_GET_REPARSE as well.
Tested on Debian 13, kernel 6.18.38 (armv7), against macOS 26.5.2:
symlinks now resolve, including relative, parent-traversing and directory
symlinks, and reads through symlinks succeed.
Co-developed-by: Pali Rohár <pali@kernel.org>
Signed-off-by: Pali Rohár <pali@kernel.org>
Signed-off-by: Carl Johnson <carl@jpartners.org>
---
v2: rebased onto cifs-2.6 for-next; no functional change. v1 was generated
against the 6.18.38 stable tree and did not apply to mainline/for-next.
fs/smb/client/smb2file.c | 21 +++++++++++++++++++++
fs/smb/client/smb2inode.c | 23 ++++++++++++++++++++---
2 files changed, 41 insertions(+), 3 deletions(-)
diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
index 5ef919b..f35b648 100644
--- a/fs/smb/client/smb2file.c
+++ b/fs/smb/client/smb2file.c
@@ -30,6 +30,19 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
u8 *end = (u8 *)err + iov->iov_len;
u32 len;
+ /*
+ * Per [MS-SMB2] section 2.2.2, a STATUS_STOPPED_ON_SYMLINK response has to
+ * carry a Symbolic Link Error Response, so ByteCount cannot be zero. Some
+ * servers (e.g. the macOS built-in SMB server) violate this and return an
+ * empty error response, with both ErrorContextCount and ByteCount set to
+ * zero, i.e. without the symlink target. Detect this and return -ENODATA
+ * so that callers can tell "server did not send the target" apart from a
+ * malformed response, and retrieve the target with FSCTL_GET_REPARSE_POINT
+ * instead.
+ */
+ if (!err->ErrorContextCount && !le32_to_cpu(err->ByteCount))
+ return ERR_PTR(-ENODATA);
+
if (err->ErrorContextCount) {
struct smb2_error_context_rsp *p;
@@ -199,6 +212,14 @@ int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms,
rc = smb2_parse_symlink_response(oparms->cifs_sb, &err_iov,
oparms->path,
&data->symlink_target);
+ /*
+ * If smb2_parse_symlink_response returned -ENODATA then the
+ * symlink_target was not sent. Treat this as if the SMB2_open()
+ * failed with STATUS_IO_REPARSE_TAG_NOT_HANDLED status, which is
+ * indicated by the -EIO errno.
+ */
+ if (rc == -ENODATA)
+ rc = -EIO;
if (!rc) {
memset(&data->fi, 0, sizeof(data->fi));
oparms->create_options |= OPEN_REPARSE_POINT;
diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
index 6c9c229..213bc29 100644
--- a/fs/smb/client/smb2inode.c
+++ b/fs/smb/client/smb2inode.c
@@ -792,9 +792,19 @@ static int parse_create_response(struct cifs_open_info_data *data,
rc = smb2_parse_symlink_response(cifs_sb, iov,
full_path,
&data->symlink_target);
- if (rc)
+ if (rc != 0 && rc != -ENODATA)
return rc;
- tag = IO_REPARSE_TAG_SYMLINK;
+ /*
+ * -ENODATA means that the response was parsed but did not contain
+ * the symlink target at all (see symlink_data()). Treat it like
+ * STATUS_IO_REPARSE_TAG_NOT_HANDLED, which does not contain it
+ * either: leave the tag unset and clear rc, so that the caller
+ * retrieves the target with SMB2_OP_GET_REPARSE.
+ */
+ if (rc == -ENODATA)
+ rc = 0;
+ else
+ tag = IO_REPARSE_TAG_SYMLINK;
reparse_point = true;
break;
case STATUS_SUCCESS:
@@ -987,7 +997,14 @@ int smb2_query_path_info(const unsigned int xid,
rc = -EOPNOTSUPP;
}
- if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
+ /*
+ * If the symlink was already parsed in create response then it is needed to fix
+ * its type now (after the second call with OPEN_REPARSE_POINT which filled the
+ * data->fi.Attributes). If the symlink was not parsed in create response then
+ * the data->symlink_target was not filled yet and then the type will be fixed
+ * later after data->symlink_target is filled.
+ */
+ if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc && data->symlink_target) {
bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
rc = smb2_fix_symlink_target_type(&data->symlink_target, directory, cifs_sb);
}
--
2.50.1 (Apple Git-155)
next reply other threads:[~2026-07-21 17:27 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-21 17:27 Carl Johnson [this message]
2026-07-21 20:11 ` [PATCH v2] smb: client: handle STATUS_STOPPED_ON_SYMLINK responses without a symlink target Steve French
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260721172755.41346-1-carl@jpartners.org \
--to=carl@jpartners.org \
--cc=linux-cifs@vger.kernel.org \
--cc=pali@kernel.org \
--cc=sfrench@samba.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox