Re: [PATCH v2] smb: client: handle STATUS_STOPPED_ON_SYMLINK responses without a symlink target

Steve French <[email protected]> Tue, 21 Jul 2026 15:11:53 -0500
Newsgroups org.kernel.vger.linux-cifs
Message-ID <CAH2r5mvvc=CsqpZxJXh2QFpgwo6MNiQcgq0E87hqXAiKONkXzw@mail.gmail.com>
merged into cifs-2.6.git for-next and added Cc:stable

On Tue, Jul 21, 2026 at 12:36 PM Carl Johnson <[email protected]> wrote:
>
> 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 <[email protected]>
> Signed-off-by: Pali Rohár <[email protected]>
> Signed-off-by: Carl Johnson <[email protected]>
> ---
> 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)
>
>


-- 
Thanks,

Steve