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