]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
smb: client: handle STATUS_STOPPED_ON_SYMLINK responses without a symlink target
authorCarl Johnson <carl@jpartners.org>
Tue, 21 Jul 2026 17:27:55 +0000 (13:27 -0400)
committerSteve French <stfrench@microsoft.com>
Tue, 21 Jul 2026 20:11:07 +0000 (15:11 -0500)
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.

Cc: stable@vger.kernel.org
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>
Signed-off-by: Steve French <stfrench@microsoft.com>
fs/smb/client/smb2file.c
fs/smb/client/smb2inode.c

index 5ef919bce52d0a334139e60dfbaa440b0be1f710..f35b6488d810d06b2668860df2d8bb4ab89ff8ad 100644 (file)
@@ -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;
index 6c9c229b91f654cde7688c7b9eff57cd134ba899..213bc298cdf22e490f64a10384918db6bf03ce87 100644 (file)
@@ -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);
                }