* [PATCH 1/7] cifs: Rename smb2_get_reparse_inode to smb2_create_reparse_inode
2024-09-29 18:50 [PATCH 0/7] cifs: Improve support for native SMB symlinks Pali Rohár
@ 2024-09-29 18:50 ` Pali Rohár
2024-09-29 21:16 ` Steve French
2024-09-29 18:50 ` [PATCH 2/7] cifs: Improve creating native symlinks pointing to directory Pali Rohár
` (7 subsequent siblings)
8 siblings, 1 reply; 38+ messages in thread
From: Pali Rohár @ 2024-09-29 18:50 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
This function creates a new reparse point, so put "create" into its name.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/reparse.c | 6 +++---
fs/smb/client/smb2inode.c | 2 +-
fs/smb/client/smb2proto.h | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
index a23ea2f78c09..507e17244ed3 100644
--- a/fs/smb/client/reparse.c
+++ b/fs/smb/client/reparse.c
@@ -68,7 +68,7 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
convert_delimiter(sym, '/');
iov.iov_base = buf;
iov.iov_len = len;
- new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
+ new = smb2_create_reparse_inode(&data, inode->i_sb, xid,
tcon, full_path, &iov, NULL);
if (!IS_ERR(new))
d_instantiate(dentry, new);
@@ -136,7 +136,7 @@ static int mknod_nfs(unsigned int xid, struct inode *inode,
.reparse = { .tag = IO_REPARSE_TAG_NFS, .nfs = p, },
};
- new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
+ new = smb2_create_reparse_inode(&data, inode->i_sb, xid,
tcon, full_path, &iov, NULL);
if (!IS_ERR(new))
d_instantiate(dentry, new);
@@ -282,7 +282,7 @@ static int mknod_wsl(unsigned int xid, struct inode *inode,
memcpy(data.wsl.eas, &cc->ea, len);
data.wsl.eas_len = len;
- new = smb2_get_reparse_inode(&data, inode->i_sb,
+ new = smb2_create_reparse_inode(&data, inode->i_sb,
xid, tcon, full_path,
&reparse_iov, &xattr_iov);
if (!IS_ERR(new))
diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
index 6e69a3b98be3..0fc73035d6dc 100644
--- a/fs/smb/client/smb2inode.c
+++ b/fs/smb/client/smb2inode.c
@@ -1193,7 +1193,7 @@ smb2_set_file_info(struct inode *inode, const char *full_path,
return rc;
}
-struct inode *smb2_get_reparse_inode(struct cifs_open_info_data *data,
+struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
struct super_block *sb,
const unsigned int xid,
struct cifs_tcon *tcon,
diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
index b208232b12a2..4ac30d29d5a1 100644
--- a/fs/smb/client/smb2proto.h
+++ b/fs/smb/client/smb2proto.h
@@ -56,7 +56,7 @@ extern int smb3_handle_read_data(struct TCP_Server_Info *server,
extern int smb2_query_reparse_tag(const unsigned int xid, struct cifs_tcon *tcon,
struct cifs_sb_info *cifs_sb, const char *path,
__u32 *reparse_tag);
-struct inode *smb2_get_reparse_inode(struct cifs_open_info_data *data,
+struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
struct super_block *sb,
const unsigned int xid,
struct cifs_tcon *tcon,
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* Re: [PATCH 1/7] cifs: Rename smb2_get_reparse_inode to smb2_create_reparse_inode
2024-09-29 18:50 ` [PATCH 1/7] cifs: Rename smb2_get_reparse_inode to smb2_create_reparse_inode Pali Rohár
@ 2024-09-29 21:16 ` Steve French
2024-09-29 22:05 ` Pali Rohár
0 siblings, 1 reply; 38+ messages in thread
From: Steve French @ 2024-09-29 21:16 UTC (permalink / raw)
To: Pali Rohár
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
I lean against minor renames in rc2, better to focus on the bug fixes
in your patch (the renaming also makes it a bit harder to backport
fixes, so often better after all the fixes in, if important rename)
On Sun, Sep 29, 2024 at 1:51 PM Pali Rohár <pali@kernel.org> wrote:
>
> This function creates a new reparse point, so put "create" into its name.
>
> Signed-off-by: Pali Rohár <pali@kernel.org>
> ---
> fs/smb/client/reparse.c | 6 +++---
> fs/smb/client/smb2inode.c | 2 +-
> fs/smb/client/smb2proto.h | 2 +-
> 3 files changed, 5 insertions(+), 5 deletions(-)
>
> diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> index a23ea2f78c09..507e17244ed3 100644
> --- a/fs/smb/client/reparse.c
> +++ b/fs/smb/client/reparse.c
> @@ -68,7 +68,7 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> convert_delimiter(sym, '/');
> iov.iov_base = buf;
> iov.iov_len = len;
> - new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
> + new = smb2_create_reparse_inode(&data, inode->i_sb, xid,
> tcon, full_path, &iov, NULL);
> if (!IS_ERR(new))
> d_instantiate(dentry, new);
> @@ -136,7 +136,7 @@ static int mknod_nfs(unsigned int xid, struct inode *inode,
> .reparse = { .tag = IO_REPARSE_TAG_NFS, .nfs = p, },
> };
>
> - new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
> + new = smb2_create_reparse_inode(&data, inode->i_sb, xid,
> tcon, full_path, &iov, NULL);
> if (!IS_ERR(new))
> d_instantiate(dentry, new);
> @@ -282,7 +282,7 @@ static int mknod_wsl(unsigned int xid, struct inode *inode,
> memcpy(data.wsl.eas, &cc->ea, len);
> data.wsl.eas_len = len;
>
> - new = smb2_get_reparse_inode(&data, inode->i_sb,
> + new = smb2_create_reparse_inode(&data, inode->i_sb,
> xid, tcon, full_path,
> &reparse_iov, &xattr_iov);
> if (!IS_ERR(new))
> diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
> index 6e69a3b98be3..0fc73035d6dc 100644
> --- a/fs/smb/client/smb2inode.c
> +++ b/fs/smb/client/smb2inode.c
> @@ -1193,7 +1193,7 @@ smb2_set_file_info(struct inode *inode, const char *full_path,
> return rc;
> }
>
> -struct inode *smb2_get_reparse_inode(struct cifs_open_info_data *data,
> +struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
> struct super_block *sb,
> const unsigned int xid,
> struct cifs_tcon *tcon,
> diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
> index b208232b12a2..4ac30d29d5a1 100644
> --- a/fs/smb/client/smb2proto.h
> +++ b/fs/smb/client/smb2proto.h
> @@ -56,7 +56,7 @@ extern int smb3_handle_read_data(struct TCP_Server_Info *server,
> extern int smb2_query_reparse_tag(const unsigned int xid, struct cifs_tcon *tcon,
> struct cifs_sb_info *cifs_sb, const char *path,
> __u32 *reparse_tag);
> -struct inode *smb2_get_reparse_inode(struct cifs_open_info_data *data,
> +struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
> struct super_block *sb,
> const unsigned int xid,
> struct cifs_tcon *tcon,
> --
> 2.20.1
>
>
--
Thanks,
Steve
^ permalink raw reply [flat|nested] 38+ messages in thread* Re: [PATCH 1/7] cifs: Rename smb2_get_reparse_inode to smb2_create_reparse_inode
2024-09-29 21:16 ` Steve French
@ 2024-09-29 22:05 ` Pali Rohár
0 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-09-29 22:05 UTC (permalink / raw)
To: Steve French
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
That is fine. You can also postpone changes in this patch series for
next major version if it is needed to more testing. And if you think
that this rename change cause issues, you can drop it. It is not
functional change at all.
On Sunday 29 September 2024 16:16:44 Steve French wrote:
> I lean against minor renames in rc2, better to focus on the bug fixes
> in your patch (the renaming also makes it a bit harder to backport
> fixes, so often better after all the fixes in, if important rename)
>
> On Sun, Sep 29, 2024 at 1:51 PM Pali Rohár <pali@kernel.org> wrote:
> >
> > This function creates a new reparse point, so put "create" into its name.
> >
> > Signed-off-by: Pali Rohár <pali@kernel.org>
> > ---
> > fs/smb/client/reparse.c | 6 +++---
> > fs/smb/client/smb2inode.c | 2 +-
> > fs/smb/client/smb2proto.h | 2 +-
> > 3 files changed, 5 insertions(+), 5 deletions(-)
> >
> > diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> > index a23ea2f78c09..507e17244ed3 100644
> > --- a/fs/smb/client/reparse.c
> > +++ b/fs/smb/client/reparse.c
> > @@ -68,7 +68,7 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> > convert_delimiter(sym, '/');
> > iov.iov_base = buf;
> > iov.iov_len = len;
> > - new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
> > + new = smb2_create_reparse_inode(&data, inode->i_sb, xid,
> > tcon, full_path, &iov, NULL);
> > if (!IS_ERR(new))
> > d_instantiate(dentry, new);
> > @@ -136,7 +136,7 @@ static int mknod_nfs(unsigned int xid, struct inode *inode,
> > .reparse = { .tag = IO_REPARSE_TAG_NFS, .nfs = p, },
> > };
> >
> > - new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
> > + new = smb2_create_reparse_inode(&data, inode->i_sb, xid,
> > tcon, full_path, &iov, NULL);
> > if (!IS_ERR(new))
> > d_instantiate(dentry, new);
> > @@ -282,7 +282,7 @@ static int mknod_wsl(unsigned int xid, struct inode *inode,
> > memcpy(data.wsl.eas, &cc->ea, len);
> > data.wsl.eas_len = len;
> >
> > - new = smb2_get_reparse_inode(&data, inode->i_sb,
> > + new = smb2_create_reparse_inode(&data, inode->i_sb,
> > xid, tcon, full_path,
> > &reparse_iov, &xattr_iov);
> > if (!IS_ERR(new))
> > diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
> > index 6e69a3b98be3..0fc73035d6dc 100644
> > --- a/fs/smb/client/smb2inode.c
> > +++ b/fs/smb/client/smb2inode.c
> > @@ -1193,7 +1193,7 @@ smb2_set_file_info(struct inode *inode, const char *full_path,
> > return rc;
> > }
> >
> > -struct inode *smb2_get_reparse_inode(struct cifs_open_info_data *data,
> > +struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
> > struct super_block *sb,
> > const unsigned int xid,
> > struct cifs_tcon *tcon,
> > diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
> > index b208232b12a2..4ac30d29d5a1 100644
> > --- a/fs/smb/client/smb2proto.h
> > +++ b/fs/smb/client/smb2proto.h
> > @@ -56,7 +56,7 @@ extern int smb3_handle_read_data(struct TCP_Server_Info *server,
> > extern int smb2_query_reparse_tag(const unsigned int xid, struct cifs_tcon *tcon,
> > struct cifs_sb_info *cifs_sb, const char *path,
> > __u32 *reparse_tag);
> > -struct inode *smb2_get_reparse_inode(struct cifs_open_info_data *data,
> > +struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
> > struct super_block *sb,
> > const unsigned int xid,
> > struct cifs_tcon *tcon,
> > --
> > 2.20.1
> >
> >
>
>
> --
> Thanks,
>
> Steve
^ permalink raw reply [flat|nested] 38+ messages in thread
* [PATCH 2/7] cifs: Improve creating native symlinks pointing to directory
2024-09-29 18:50 [PATCH 0/7] cifs: Improve support for native SMB symlinks Pali Rohár
2024-09-29 18:50 ` [PATCH 1/7] cifs: Rename smb2_get_reparse_inode to smb2_create_reparse_inode Pali Rohár
@ 2024-09-29 18:50 ` Pali Rohár
2024-09-29 21:54 ` Steve French
2024-09-30 15:09 ` Paulo Alcantara
2024-09-29 18:50 ` [PATCH 3/7] cifs: Fix creating native symlinks pointing to current or parent directory Pali Rohár
` (6 subsequent siblings)
8 siblings, 2 replies; 38+ messages in thread
From: Pali Rohár @ 2024-09-29 18:50 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
SMB protocol for native symlinks distinguish between symlink to directory
and symlink to file. These two symlink types cannot be exchanged, which
means that symlink of file type pointing to directory cannot be resolved at
all (and vice-versa).
Windows follows this rule for local filesystems (NTFS) and also for SMB.
Linux SMB client currenly creates all native symlinks of file type. Which
means that Windows (and some other SMB clients) cannot resolve symlinks
pointing to directory created by Linux SMB client.
As Linux system does not distinguish between directory and file symlinks,
its API does not provide enough information for Linux SMB client during
creating of native symlinks.
Add some heuristic into the Linux SMB client for choosing the correct
symlink type during symlink creation. Check if the symlink target location
ends with slash, or last path component is dot or dot dot, and check if the
target location on SMB share exists and is a directory. If at least one
condition is truth then create a new SMB symlink of directory type.
Otherwise create it as file type symlink.
This change improves interoperability with Windows systems. Windows systems
would be able to resolve more SMB symlinks created by Linux SMB client
which points to existing directory.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/reparse.c | 131 ++++++++++++++++++++++++++++++++++++--
fs/smb/client/smb2inode.c | 3 +-
fs/smb/client/smb2proto.h | 1 +
3 files changed, 130 insertions(+), 5 deletions(-)
diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
index 507e17244ed3..9390ab801696 100644
--- a/fs/smb/client/reparse.c
+++ b/fs/smb/client/reparse.c
@@ -24,13 +24,16 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
struct inode *new;
struct kvec iov;
__le16 *path;
+ bool directory = false;
char *sym, sep = CIFS_DIR_SEP(cifs_sb);
u16 len, plen;
int rc = 0;
- sym = kstrdup(symname, GFP_KERNEL);
+ len = strlen(symname)+1;
+ sym = kzalloc(len+1, GFP_KERNEL); /* +1 for possible directory slash */
if (!sym)
return -ENOMEM;
+ memcpy(sym, symname, len);
data = (struct cifs_open_info_data) {
.reparse_point = true,
@@ -45,6 +48,125 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
goto out;
}
+ /*
+ * SMB distinguish between symlink to directory and symlink to file.
+ * They cannot be exchanged (symlink of file type which points to
+ * directory cannot be resolved and vice-versa). First do some simple
+ * check, if the original Linux symlink target ends with slash, or
+ * last path component is dot or dot dot then it is for sure symlink
+ * to the directory.
+ */
+ if (!directory) {
+ const char *basename = kbasename(symname);
+ int basename_len = strlen(basename);
+ if (basename_len == 0 || /* symname ends with slash */
+ (basename_len == 1 && basename[0] == '.') || /* last component is "." */
+ (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) /* last component is ".." */
+ directory = true;
+ }
+
+ /*
+ * If it was not detected as directory yet and the symlink is relative
+ * then try to resolve the path on the SMB server, check if the path
+ * exists and determinate if it is a directory or not.
+ */
+ if (!directory && symname[0] != '/') {
+ __u32 oplock;
+ struct tcon_link *tlink;
+ struct cifs_tcon *tcon;
+ struct cifs_fid fid;
+ struct cifs_open_parms oparms;
+ char *resolved_path;
+ char *path_sep;
+ int open_rc;
+ int full_path_len = strlen(full_path);
+ int symname_len = strlen(symname);
+
+ tlink = cifs_sb_tlink(cifs_sb);
+ if (IS_ERR(tlink)) {
+ rc = PTR_ERR(tlink);
+ goto out;
+ }
+
+ resolved_path = kzalloc(full_path_len + symname_len + 1, GFP_KERNEL);
+ if (!resolved_path) {
+ rc = -ENOMEM;
+ goto out;
+ }
+
+ /*
+ * Compose the resolved SMB symlink path from the SMB full path
+ * and Linux target symlink path.
+ */
+ memcpy(resolved_path, full_path, full_path_len+1);
+ path_sep = strrchr(resolved_path, sep);
+ if (path_sep)
+ path_sep++;
+ else
+ path_sep = resolved_path;
+ memcpy(path_sep, symname, symname_len+1);
+ if (sep == '\\')
+ convert_delimiter(path_sep, sep);
+
+ tcon = tlink_tcon(tlink);
+
+ oparms = (struct cifs_open_parms) {
+ .tcon = tcon,
+ .cifs_sb = cifs_sb,
+ .desired_access = FILE_READ_ATTRIBUTES,
+ .disposition = FILE_OPEN,
+ .path = resolved_path,
+ .fid = &fid,
+ };
+
+ /* Try to open as NOT_FILE */
+ oplock = 0;
+ oparms.create_options = cifs_create_options(cifs_sb, CREATE_NOT_FILE);
+ open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
+ if (open_rc == 0) {
+ /* Successful open means that the target path is definitely a directory. */
+ directory = true;
+ tcon->ses->server->ops->close(xid, tcon, &fid);
+ } else if (open_rc != -ENOTDIR) {
+ /* Try to open as NOT_DIR */
+ oplock = 0;
+ oparms.create_options = cifs_create_options(cifs_sb, CREATE_NOT_DIR);
+ open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
+ if (open_rc == 0) {
+ tcon->ses->server->ops->close(xid, tcon, &fid);
+ } else if (open_rc == -EISDIR) {
+ /* -EISDIR means that the target path is definitely a directory. */
+ directory = true;
+ } else {
+ cifs_dbg(FYI,
+ "%s: cannot determinate if the symlink target path '%s' "
+ "is directory or not, creating '%s' as file symlink\n",
+ __func__, symname, full_path);
+ }
+ }
+
+ kfree(resolved_path);
+ cifs_put_tlink(tlink);
+ }
+
+ /*
+ * For absolute symlinks it is not possible to determinate
+ * if it should point to directory or file.
+ */
+ if (!directory && symname[0] == '/')
+ cifs_dbg(FYI,
+ "%s: cannot determinate if the symlink target path '%s' "
+ "is directory or not, creating '%s' as file symlink\n",
+ __func__, symname, full_path);
+
+ /* Ensure that directory symlink target in inode would have trailing slash */
+ len = strlen(data.symlink_target);
+ if (directory && data.symlink_target[len-1] != '/') {
+ /* symlink_target has already preallocated one byte more */
+ data.symlink_target[len] = '/';
+ data.symlink_target[len+1] = '\0';
+ }
+
plen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
len = sizeof(*buf) + plen * 2;
buf = kzalloc(len, GFP_KERNEL);
@@ -69,7 +191,8 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
iov.iov_base = buf;
iov.iov_len = len;
new = smb2_create_reparse_inode(&data, inode->i_sb, xid,
- tcon, full_path, &iov, NULL);
+ tcon, full_path, directory,
+ &iov, NULL);
if (!IS_ERR(new))
d_instantiate(dentry, new);
else
@@ -137,7 +260,7 @@ static int mknod_nfs(unsigned int xid, struct inode *inode,
};
new = smb2_create_reparse_inode(&data, inode->i_sb, xid,
- tcon, full_path, &iov, NULL);
+ tcon, full_path, false, &iov, NULL);
if (!IS_ERR(new))
d_instantiate(dentry, new);
else
@@ -283,7 +406,7 @@ static int mknod_wsl(unsigned int xid, struct inode *inode,
data.wsl.eas_len = len;
new = smb2_create_reparse_inode(&data, inode->i_sb,
- xid, tcon, full_path,
+ xid, tcon, full_path, false,
&reparse_iov, &xattr_iov);
if (!IS_ERR(new))
d_instantiate(dentry, new);
diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
index 0fc73035d6dc..fffb9df4faeb 100644
--- a/fs/smb/client/smb2inode.c
+++ b/fs/smb/client/smb2inode.c
@@ -1198,6 +1198,7 @@ struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
const unsigned int xid,
struct cifs_tcon *tcon,
const char *full_path,
+ bool directory,
struct kvec *reparse_iov,
struct kvec *xattr_iov)
{
@@ -1217,7 +1218,7 @@ struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
FILE_READ_ATTRIBUTES |
FILE_WRITE_ATTRIBUTES,
FILE_CREATE,
- CREATE_NOT_DIR | OPEN_REPARSE_POINT,
+ (directory ? CREATE_NOT_FILE : CREATE_NOT_DIR) | OPEN_REPARSE_POINT,
ACL_NO_MODE);
if (xattr_iov)
oparms.ea_cctx = xattr_iov;
diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
index 4ac30d29d5a1..fd092f2bb8c5 100644
--- a/fs/smb/client/smb2proto.h
+++ b/fs/smb/client/smb2proto.h
@@ -61,6 +61,7 @@ struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
const unsigned int xid,
struct cifs_tcon *tcon,
const char *full_path,
+ bool directory,
struct kvec *reparse_iov,
struct kvec *xattr_iov);
int smb2_query_reparse_point(const unsigned int xid,
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* Re: [PATCH 2/7] cifs: Improve creating native symlinks pointing to directory
2024-09-29 18:50 ` [PATCH 2/7] cifs: Improve creating native symlinks pointing to directory Pali Rohár
@ 2024-09-29 21:54 ` Steve French
2024-09-29 22:11 ` Pali Rohár
2024-09-30 15:09 ` Paulo Alcantara
1 sibling, 1 reply; 38+ messages in thread
From: Steve French @ 2024-09-29 21:54 UTC (permalink / raw)
To: Pali Rohár
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
[-- Attachment #1: Type: text/plain, Size: 12239 bytes --]
Here is a version of the same patch without the function rename (so
presumably easier to backport) and also that fixes to minor checkpatch
warnings (and merged this and also patches 3, 4 and 6 into
cifs-2.6.git for-next pending additional review and tesitng):
On Sun, Sep 29, 2024 at 1:51 PM Pali Rohár <pali@kernel.org> wrote:
>
> SMB protocol for native symlinks distinguish between symlink to directory
> and symlink to file. These two symlink types cannot be exchanged, which
> means that symlink of file type pointing to directory cannot be resolved at
> all (and vice-versa).
>
> Windows follows this rule for local filesystems (NTFS) and also for SMB.
>
> Linux SMB client currenly creates all native symlinks of file type. Which
> means that Windows (and some other SMB clients) cannot resolve symlinks
> pointing to directory created by Linux SMB client.
>
> As Linux system does not distinguish between directory and file symlinks,
> its API does not provide enough information for Linux SMB client during
> creating of native symlinks.
>
> Add some heuristic into the Linux SMB client for choosing the correct
> symlink type during symlink creation. Check if the symlink target location
> ends with slash, or last path component is dot or dot dot, and check if the
> target location on SMB share exists and is a directory. If at least one
> condition is truth then create a new SMB symlink of directory type.
> Otherwise create it as file type symlink.
>
> This change improves interoperability with Windows systems. Windows systems
> would be able to resolve more SMB symlinks created by Linux SMB client
> which points to existing directory.
>
> Signed-off-by: Pali Rohár <pali@kernel.org>
> ---
> fs/smb/client/reparse.c | 131 ++++++++++++++++++++++++++++++++++++--
> fs/smb/client/smb2inode.c | 3 +-
> fs/smb/client/smb2proto.h | 1 +
> 3 files changed, 130 insertions(+), 5 deletions(-)
>
> diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> index 507e17244ed3..9390ab801696 100644
> --- a/fs/smb/client/reparse.c
> +++ b/fs/smb/client/reparse.c
> @@ -24,13 +24,16 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> struct inode *new;
> struct kvec iov;
> __le16 *path;
> + bool directory = false;
> char *sym, sep = CIFS_DIR_SEP(cifs_sb);
> u16 len, plen;
> int rc = 0;
>
> - sym = kstrdup(symname, GFP_KERNEL);
> + len = strlen(symname)+1;
> + sym = kzalloc(len+1, GFP_KERNEL); /* +1 for possible directory slash */
> if (!sym)
> return -ENOMEM;
> + memcpy(sym, symname, len);
>
> data = (struct cifs_open_info_data) {
> .reparse_point = true,
> @@ -45,6 +48,125 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> goto out;
> }
>
> + /*
> + * SMB distinguish between symlink to directory and symlink to file.
> + * They cannot be exchanged (symlink of file type which points to
> + * directory cannot be resolved and vice-versa). First do some simple
> + * check, if the original Linux symlink target ends with slash, or
> + * last path component is dot or dot dot then it is for sure symlink
> + * to the directory.
> + */
> + if (!directory) {
> + const char *basename = kbasename(symname);
> + int basename_len = strlen(basename);
> + if (basename_len == 0 || /* symname ends with slash */
> + (basename_len == 1 && basename[0] == '.') || /* last component is "." */
> + (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) /* last component is ".." */
> + directory = true;
> + }
> +
> + /*
> + * If it was not detected as directory yet and the symlink is relative
> + * then try to resolve the path on the SMB server, check if the path
> + * exists and determinate if it is a directory or not.
> + */
> + if (!directory && symname[0] != '/') {
> + __u32 oplock;
> + struct tcon_link *tlink;
> + struct cifs_tcon *tcon;
> + struct cifs_fid fid;
> + struct cifs_open_parms oparms;
> + char *resolved_path;
> + char *path_sep;
> + int open_rc;
> + int full_path_len = strlen(full_path);
> + int symname_len = strlen(symname);
> +
> + tlink = cifs_sb_tlink(cifs_sb);
> + if (IS_ERR(tlink)) {
> + rc = PTR_ERR(tlink);
> + goto out;
> + }
> +
> + resolved_path = kzalloc(full_path_len + symname_len + 1, GFP_KERNEL);
> + if (!resolved_path) {
> + rc = -ENOMEM;
> + goto out;
> + }
> +
> + /*
> + * Compose the resolved SMB symlink path from the SMB full path
> + * and Linux target symlink path.
> + */
> + memcpy(resolved_path, full_path, full_path_len+1);
> + path_sep = strrchr(resolved_path, sep);
> + if (path_sep)
> + path_sep++;
> + else
> + path_sep = resolved_path;
> + memcpy(path_sep, symname, symname_len+1);
> + if (sep == '\\')
> + convert_delimiter(path_sep, sep);
> +
> + tcon = tlink_tcon(tlink);
> +
> + oparms = (struct cifs_open_parms) {
> + .tcon = tcon,
> + .cifs_sb = cifs_sb,
> + .desired_access = FILE_READ_ATTRIBUTES,
> + .disposition = FILE_OPEN,
> + .path = resolved_path,
> + .fid = &fid,
> + };
> +
> + /* Try to open as NOT_FILE */
> + oplock = 0;
> + oparms.create_options = cifs_create_options(cifs_sb, CREATE_NOT_FILE);
> + open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
> + if (open_rc == 0) {
> + /* Successful open means that the target path is definitely a directory. */
> + directory = true;
> + tcon->ses->server->ops->close(xid, tcon, &fid);
> + } else if (open_rc != -ENOTDIR) {
> + /* Try to open as NOT_DIR */
> + oplock = 0;
> + oparms.create_options = cifs_create_options(cifs_sb, CREATE_NOT_DIR);
> + open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
> + if (open_rc == 0) {
> + tcon->ses->server->ops->close(xid, tcon, &fid);
> + } else if (open_rc == -EISDIR) {
> + /* -EISDIR means that the target path is definitely a directory. */
> + directory = true;
> + } else {
> + cifs_dbg(FYI,
> + "%s: cannot determinate if the symlink target path '%s' "
> + "is directory or not, creating '%s' as file symlink\n",
> + __func__, symname, full_path);
> + }
> + }
> +
> + kfree(resolved_path);
> + cifs_put_tlink(tlink);
> + }
> +
> + /*
> + * For absolute symlinks it is not possible to determinate
> + * if it should point to directory or file.
> + */
> + if (!directory && symname[0] == '/')
> + cifs_dbg(FYI,
> + "%s: cannot determinate if the symlink target path '%s' "
> + "is directory or not, creating '%s' as file symlink\n",
> + __func__, symname, full_path);
> +
> + /* Ensure that directory symlink target in inode would have trailing slash */
> + len = strlen(data.symlink_target);
> + if (directory && data.symlink_target[len-1] != '/') {
> + /* symlink_target has already preallocated one byte more */
> + data.symlink_target[len] = '/';
> + data.symlink_target[len+1] = '\0';
> + }
> +
> plen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
> len = sizeof(*buf) + plen * 2;
> buf = kzalloc(len, GFP_KERNEL);
> @@ -69,7 +191,8 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> iov.iov_base = buf;
> iov.iov_len = len;
> new = smb2_create_reparse_inode(&data, inode->i_sb, xid,
> - tcon, full_path, &iov, NULL);
> + tcon, full_path, directory,
> + &iov, NULL);
> if (!IS_ERR(new))
> d_instantiate(dentry, new);
> else
> @@ -137,7 +260,7 @@ static int mknod_nfs(unsigned int xid, struct inode *inode,
> };
>
> new = smb2_create_reparse_inode(&data, inode->i_sb, xid,
> - tcon, full_path, &iov, NULL);
> + tcon, full_path, false, &iov, NULL);
> if (!IS_ERR(new))
> d_instantiate(dentry, new);
> else
> @@ -283,7 +406,7 @@ static int mknod_wsl(unsigned int xid, struct inode *inode,
> data.wsl.eas_len = len;
>
> new = smb2_create_reparse_inode(&data, inode->i_sb,
> - xid, tcon, full_path,
> + xid, tcon, full_path, false,
> &reparse_iov, &xattr_iov);
> if (!IS_ERR(new))
> d_instantiate(dentry, new);
> diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
> index 0fc73035d6dc..fffb9df4faeb 100644
> --- a/fs/smb/client/smb2inode.c
> +++ b/fs/smb/client/smb2inode.c
> @@ -1198,6 +1198,7 @@ struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
> const unsigned int xid,
> struct cifs_tcon *tcon,
> const char *full_path,
> + bool directory,
> struct kvec *reparse_iov,
> struct kvec *xattr_iov)
> {
> @@ -1217,7 +1218,7 @@ struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
> FILE_READ_ATTRIBUTES |
> FILE_WRITE_ATTRIBUTES,
> FILE_CREATE,
> - CREATE_NOT_DIR | OPEN_REPARSE_POINT,
> + (directory ? CREATE_NOT_FILE : CREATE_NOT_DIR) | OPEN_REPARSE_POINT,
> ACL_NO_MODE);
> if (xattr_iov)
> oparms.ea_cctx = xattr_iov;
> diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
> index 4ac30d29d5a1..fd092f2bb8c5 100644
> --- a/fs/smb/client/smb2proto.h
> +++ b/fs/smb/client/smb2proto.h
> @@ -61,6 +61,7 @@ struct inode *smb2_create_reparse_inode(struct cifs_open_info_data *data,
> const unsigned int xid,
> struct cifs_tcon *tcon,
> const char *full_path,
> + bool directory,
> struct kvec *reparse_iov,
> struct kvec *xattr_iov);
> int smb2_query_reparse_point(const unsigned int xid,
> --
> 2.20.1
>
>
--
Thanks,
Steve
[-- Attachment #2: 0001-cifs-Improve-creating-native-symlinks-pointing-to-di.patch --]
[-- Type: text/x-patch, Size: 9008 bytes --]
From 79bc8f1a0da93a2b6e97ba2eb55546c08a01c168 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pali=20Roh=C3=A1r?= <pali@kernel.org>
Date: Sun, 29 Sep 2024 20:50:48 +0200
Subject: [PATCH] cifs: Improve creating native symlinks pointing to directory
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
SMB protocol for native symlinks distinguish between symlink to directory
and symlink to file. These two symlink types cannot be exchanged, which
means that symlink of file type pointing to directory cannot be resolved at
all (and vice-versa).
Windows follows this rule for local filesystems (NTFS) and also for SMB.
Linux SMB client currenly creates all native symlinks of file type. Which
means that Windows (and some other SMB clients) cannot resolve symlinks
pointing to directory created by Linux SMB client.
As Linux system does not distinguish between directory and file symlinks,
its API does not provide enough information for Linux SMB client during
creating of native symlinks.
Add some heuristic into the Linux SMB client for choosing the correct
symlink type during symlink creation. Check if the symlink target location
ends with slash, or last path component is dot or dot dot, and check if the
target location on SMB share exists and is a directory. If at least one
condition is truth then create a new SMB symlink of directory type.
Otherwise create it as file type symlink.
This change improves interoperability with Windows systems. Windows systems
would be able to resolve more SMB symlinks created by Linux SMB client
which points to existing directory.
Signed-off-by: Pali Rohár <pali@kernel.org>
Signed-off-by: Steve French <stfrench@microsoft.com>
---
fs/smb/client/reparse.c | 132 ++++++++++++++++++++++++++++++++++++--
fs/smb/client/smb2inode.c | 3 +-
fs/smb/client/smb2proto.h | 1 +
3 files changed, 131 insertions(+), 5 deletions(-)
diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
index c848b5e88d32..4fae4e7343e6 100644
--- a/fs/smb/client/reparse.c
+++ b/fs/smb/client/reparse.c
@@ -24,13 +24,16 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
struct inode *new;
struct kvec iov;
__le16 *path;
+ bool directory = false;
char *sym, sep = CIFS_DIR_SEP(cifs_sb);
u16 len, plen;
int rc = 0;
- sym = kstrdup(symname, GFP_KERNEL);
+ len = strlen(symname)+1;
+ sym = kzalloc(len+1, GFP_KERNEL); /* +1 for possible directory slash */
if (!sym)
return -ENOMEM;
+ memcpy(sym, symname, len);
data = (struct cifs_open_info_data) {
.reparse_point = true,
@@ -45,6 +48,126 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
goto out;
}
+ /*
+ * SMB distinguish between symlink to directory and symlink to file.
+ * They cannot be exchanged (symlink of file type which points to
+ * directory cannot be resolved and vice-versa). First do some simple
+ * check, if the original Linux symlink target ends with slash, or
+ * last path component is dot or dot dot then it is for sure symlink
+ * to the directory.
+ */
+ if (!directory) {
+ const char *basename = kbasename(symname);
+ int basename_len = strlen(basename);
+
+ if (basename_len == 0 || /* symname ends with slash */
+ (basename_len == 1 && basename[0] == '.') || /* last component is "." */
+ (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) /* or ".." */
+ directory = true;
+ }
+
+ /*
+ * If it was not detected as directory yet and the symlink is relative
+ * then try to resolve the path on the SMB server, check if the path
+ * exists and determinate if it is a directory or not.
+ */
+ if (!directory && symname[0] != '/') {
+ __u32 oplock;
+ struct tcon_link *tlink;
+ struct cifs_tcon *tcon;
+ struct cifs_fid fid;
+ struct cifs_open_parms oparms;
+ char *resolved_path;
+ char *path_sep;
+ int open_rc;
+ int full_path_len = strlen(full_path);
+ int symname_len = strlen(symname);
+
+ tlink = cifs_sb_tlink(cifs_sb);
+ if (IS_ERR(tlink)) {
+ rc = PTR_ERR(tlink);
+ goto out;
+ }
+
+ resolved_path = kzalloc(full_path_len + symname_len + 1, GFP_KERNEL);
+ if (!resolved_path) {
+ rc = -ENOMEM;
+ goto out;
+ }
+
+ /*
+ * Compose the resolved SMB symlink path from the SMB full path
+ * and Linux target symlink path.
+ */
+ memcpy(resolved_path, full_path, full_path_len+1);
+ path_sep = strrchr(resolved_path, sep);
+ if (path_sep)
+ path_sep++;
+ else
+ path_sep = resolved_path;
+ memcpy(path_sep, symname, symname_len+1);
+ if (sep == '\\')
+ convert_delimiter(path_sep, sep);
+
+ tcon = tlink_tcon(tlink);
+
+ oparms = (struct cifs_open_parms) {
+ .tcon = tcon,
+ .cifs_sb = cifs_sb,
+ .desired_access = FILE_READ_ATTRIBUTES,
+ .disposition = FILE_OPEN,
+ .path = resolved_path,
+ .fid = &fid,
+ };
+
+ /* Try to open as NOT_FILE */
+ oplock = 0;
+ oparms.create_options = cifs_create_options(cifs_sb, CREATE_NOT_FILE);
+ open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
+ if (open_rc == 0) {
+ /* Successful open means that the target path is definitely a directory. */
+ directory = true;
+ tcon->ses->server->ops->close(xid, tcon, &fid);
+ } else if (open_rc != -ENOTDIR) {
+ /* Try to open as NOT_DIR */
+ oplock = 0;
+ oparms.create_options = cifs_create_options(cifs_sb, CREATE_NOT_DIR);
+ open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
+ if (open_rc == 0) {
+ tcon->ses->server->ops->close(xid, tcon, &fid);
+ } else if (open_rc == -EISDIR) {
+ /* -EISDIR means that the target path is definitely a directory. */
+ directory = true;
+ } else {
+ cifs_dbg(FYI,
+ "%s: cannot determinate if the symlink target path '%s' "
+ "is directory or not, creating '%s' as file symlink\n",
+ __func__, symname, full_path);
+ }
+ }
+
+ kfree(resolved_path);
+ cifs_put_tlink(tlink);
+ }
+
+ /*
+ * For absolute symlinks it is not possible to determinate
+ * if it should point to directory or file.
+ */
+ if (!directory && symname[0] == '/')
+ cifs_dbg(FYI,
+ "%s: cannot determinate if the symlink target path '%s' "
+ "is directory or not, creating '%s' as file symlink\n",
+ __func__, symname, full_path);
+
+ /* Ensure that directory symlink target in inode would have trailing slash */
+ len = strlen(data.symlink_target);
+ if (directory && data.symlink_target[len-1] != '/') {
+ /* symlink_target has already preallocated one byte more */
+ data.symlink_target[len] = '/';
+ data.symlink_target[len+1] = '\0';
+ }
+
plen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
len = sizeof(*buf) + plen * 2;
buf = kzalloc(len, GFP_KERNEL);
@@ -69,7 +192,8 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
iov.iov_base = buf;
iov.iov_len = len;
new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
- tcon, full_path, &iov, NULL);
+ tcon, full_path, directory,
+ &iov, NULL);
if (!IS_ERR(new))
d_instantiate(dentry, new);
else
@@ -137,7 +261,7 @@ static int mknod_nfs(unsigned int xid, struct inode *inode,
};
new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
- tcon, full_path, &iov, NULL);
+ tcon, full_path, false, &iov, NULL);
if (!IS_ERR(new))
d_instantiate(dentry, new);
else
@@ -283,7 +407,7 @@ static int mknod_wsl(unsigned int xid, struct inode *inode,
data.wsl.eas_len = len;
new = smb2_get_reparse_inode(&data, inode->i_sb,
- xid, tcon, full_path,
+ xid, tcon, full_path, false,
&reparse_iov, &xattr_iov);
if (!IS_ERR(new))
d_instantiate(dentry, new);
diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
index 05df7ab7dfed..c71d86db8ba6 100644
--- a/fs/smb/client/smb2inode.c
+++ b/fs/smb/client/smb2inode.c
@@ -1198,6 +1198,7 @@ struct inode *smb2_get_reparse_inode(struct cifs_open_info_data *data,
const unsigned int xid,
struct cifs_tcon *tcon,
const char *full_path,
+ bool directory,
struct kvec *reparse_iov,
struct kvec *xattr_iov)
{
@@ -1217,7 +1218,7 @@ struct inode *smb2_get_reparse_inode(struct cifs_open_info_data *data,
FILE_READ_ATTRIBUTES |
FILE_WRITE_ATTRIBUTES,
FILE_CREATE,
- CREATE_NOT_DIR | OPEN_REPARSE_POINT,
+ (directory ? CREATE_NOT_FILE : CREATE_NOT_DIR) | OPEN_REPARSE_POINT,
ACL_NO_MODE);
if (xattr_iov)
oparms.ea_cctx = xattr_iov;
diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
index 56a896ff7cd9..ae1a766af664 100644
--- a/fs/smb/client/smb2proto.h
+++ b/fs/smb/client/smb2proto.h
@@ -61,6 +61,7 @@ struct inode *smb2_get_reparse_inode(struct cifs_open_info_data *data,
const unsigned int xid,
struct cifs_tcon *tcon,
const char *full_path,
+ bool directory,
struct kvec *reparse_iov,
struct kvec *xattr_iov);
int smb2_query_reparse_point(const unsigned int xid,
--
2.43.0
^ permalink raw reply related [flat|nested] 38+ messages in thread* Re: [PATCH 2/7] cifs: Improve creating native symlinks pointing to directory
2024-09-29 21:54 ` Steve French
@ 2024-09-29 22:11 ` Pali Rohár
0 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-09-29 22:11 UTC (permalink / raw)
To: Steve French
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
On Sunday 29 September 2024 16:54:20 Steve French wrote:
> Here is a version of the same patch without the function rename (so
> presumably easier to backport) and also that fixes to minor checkpatch
> warnings (and merged this and also patches 3, 4 and 6 into
> cifs-2.6.git for-next pending additional review and tesitng):
That is fine for me. Just one minor nit suggestion.
> @@ -69,7 +192,8 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> iov.iov_base = buf;
> iov.iov_len = len;
> new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
> - tcon, full_path, &iov, NULL);
> + tcon, full_path, directory,
> + &iov, NULL);
After reverting back the original function name, you can indent/align
arguments for all smb2_get_reparse_inode occurrences.
> if (!IS_ERR(new))
> d_instantiate(dentry, new);
> else
> @@ -137,7 +261,7 @@ static int mknod_nfs(unsigned int xid, struct inode *inode,
> };
>
> new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
> - tcon, full_path, &iov, NULL);
> + tcon, full_path, false, &iov, NULL);
> if (!IS_ERR(new))
> d_instantiate(dentry, new);
> else
> @@ -283,7 +407,7 @@ static int mknod_wsl(unsigned int xid, struct inode *inode,
> data.wsl.eas_len = len;
>
> new = smb2_get_reparse_inode(&data, inode->i_sb,
> - xid, tcon, full_path,
> + xid, tcon, full_path, false,
> &reparse_iov, &xattr_iov);
Here the misaligning on + line is visible too.
> if (!IS_ERR(new))
> d_instantiate(dentry, new);
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH 2/7] cifs: Improve creating native symlinks pointing to directory
2024-09-29 18:50 ` [PATCH 2/7] cifs: Improve creating native symlinks pointing to directory Pali Rohár
2024-09-29 21:54 ` Steve French
@ 2024-09-30 15:09 ` Paulo Alcantara
2024-09-30 17:17 ` Pali Rohár
1 sibling, 1 reply; 38+ messages in thread
From: Paulo Alcantara @ 2024-09-30 15:09 UTC (permalink / raw)
To: Pali Rohár, Steve French, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
Pali Rohár <pali@kernel.org> writes:
> SMB protocol for native symlinks distinguish between symlink to directory
> and symlink to file. These two symlink types cannot be exchanged, which
> means that symlink of file type pointing to directory cannot be resolved at
> all (and vice-versa).
>
> Windows follows this rule for local filesystems (NTFS) and also for SMB.
>
> Linux SMB client currenly creates all native symlinks of file type. Which
> means that Windows (and some other SMB clients) cannot resolve symlinks
> pointing to directory created by Linux SMB client.
>
> As Linux system does not distinguish between directory and file symlinks,
> its API does not provide enough information for Linux SMB client during
> creating of native symlinks.
>
> Add some heuristic into the Linux SMB client for choosing the correct
> symlink type during symlink creation. Check if the symlink target location
> ends with slash, or last path component is dot or dot dot, and check if the
> target location on SMB share exists and is a directory. If at least one
> condition is truth then create a new SMB symlink of directory type.
> Otherwise create it as file type symlink.
>
> This change improves interoperability with Windows systems. Windows systems
> would be able to resolve more SMB symlinks created by Linux SMB client
> which points to existing directory.
>
> Signed-off-by: Pali Rohár <pali@kernel.org>
> ---
> fs/smb/client/reparse.c | 131 ++++++++++++++++++++++++++++++++++++--
> fs/smb/client/smb2inode.c | 3 +-
> fs/smb/client/smb2proto.h | 1 +
> 3 files changed, 130 insertions(+), 5 deletions(-)
>
> diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> index 507e17244ed3..9390ab801696 100644
> --- a/fs/smb/client/reparse.c
> +++ b/fs/smb/client/reparse.c
> @@ -24,13 +24,16 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> struct inode *new;
> struct kvec iov;
> __le16 *path;
> + bool directory = false;
> char *sym, sep = CIFS_DIR_SEP(cifs_sb);
> u16 len, plen;
> int rc = 0;
>
> - sym = kstrdup(symname, GFP_KERNEL);
> + len = strlen(symname)+1;
> + sym = kzalloc(len+1, GFP_KERNEL); /* +1 for possible directory slash */
> if (!sym)
> return -ENOMEM;
> + memcpy(sym, symname, len);
>
> data = (struct cifs_open_info_data) {
> .reparse_point = true,
> @@ -45,6 +48,125 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> goto out;
> }
>
> + /*
> + * SMB distinguish between symlink to directory and symlink to file.
> + * They cannot be exchanged (symlink of file type which points to
> + * directory cannot be resolved and vice-versa). First do some simple
> + * check, if the original Linux symlink target ends with slash, or
> + * last path component is dot or dot dot then it is for sure symlink
> + * to the directory.
> + */
> + if (!directory) {
> + const char *basename = kbasename(symname);
> + int basename_len = strlen(basename);
> + if (basename_len == 0 || /* symname ends with slash */
> + (basename_len == 1 && basename[0] == '.') || /* last component is "." */
> + (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) /* last component is ".." */
> + directory = true;
> + }
> +
> + /*
> + * If it was not detected as directory yet and the symlink is relative
> + * then try to resolve the path on the SMB server, check if the path
> + * exists and determinate if it is a directory or not.
> + */
> + if (!directory && symname[0] != '/') {
> + __u32 oplock;
> + struct tcon_link *tlink;
> + struct cifs_tcon *tcon;
> + struct cifs_fid fid;
> + struct cifs_open_parms oparms;
> + char *resolved_path;
> + char *path_sep;
> + int open_rc;
> + int full_path_len = strlen(full_path);
> + int symname_len = strlen(symname);
> +
> + tlink = cifs_sb_tlink(cifs_sb);
> + if (IS_ERR(tlink)) {
> + rc = PTR_ERR(tlink);
> + goto out;
> + }
> +
> + resolved_path = kzalloc(full_path_len + symname_len + 1, GFP_KERNEL);
> + if (!resolved_path) {
> + rc = -ENOMEM;
> + goto out;
> + }
If !@resolved_path, then you will end up leaking @tlink.
> +
> + /*
> + * Compose the resolved SMB symlink path from the SMB full path
> + * and Linux target symlink path.
> + */
> + memcpy(resolved_path, full_path, full_path_len+1);
> + path_sep = strrchr(resolved_path, sep);
> + if (path_sep)
> + path_sep++;
> + else
> + path_sep = resolved_path;
> + memcpy(path_sep, symname, symname_len+1);
> + if (sep == '\\')
> + convert_delimiter(path_sep, sep);
> +
> + tcon = tlink_tcon(tlink);
> +
> + oparms = (struct cifs_open_parms) {
> + .tcon = tcon,
> + .cifs_sb = cifs_sb,
> + .desired_access = FILE_READ_ATTRIBUTES,
> + .disposition = FILE_OPEN,
> + .path = resolved_path,
> + .fid = &fid,
> + };
Please use CIFS_OPARMS().
> +
> + /* Try to open as NOT_FILE */
> + oplock = 0;
> + oparms.create_options = cifs_create_options(cifs_sb, CREATE_NOT_FILE);
> + open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
> + if (open_rc == 0) {
> + /* Successful open means that the target path is definitely a directory. */
> + directory = true;
> + tcon->ses->server->ops->close(xid, tcon, &fid);
> + } else if (open_rc != -ENOTDIR) {
> + /* Try to open as NOT_DIR */
> + oplock = 0;
> + oparms.create_options = cifs_create_options(cifs_sb, CREATE_NOT_DIR);
> + open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
> + if (open_rc == 0) {
> + tcon->ses->server->ops->close(xid, tcon, &fid);
> + } else if (open_rc == -EISDIR) {
> + /* -EISDIR means that the target path is definitely a directory. */
> + directory = true;
> + } else {
> + cifs_dbg(FYI,
> + "%s: cannot determinate if the symlink target path '%s' "
> + "is directory or not, creating '%s' as file symlink\n",
> + __func__, symname, full_path);
> + }
> + }
> +
> + kfree(resolved_path);
> + cifs_put_tlink(tlink);
> + }
> +
> + /*
> + * For absolute symlinks it is not possible to determinate
> + * if it should point to directory or file.
> + */
> + if (!directory && symname[0] == '/')
> + cifs_dbg(FYI,
> + "%s: cannot determinate if the symlink target path '%s' "
> + "is directory or not, creating '%s' as file symlink\n",
> + __func__, symname, full_path);
> +
Create a helper with all of this and then call it in
smb2_create_reparse_symlink() to determine whether symlink target is a
directory or file.
^ permalink raw reply [flat|nested] 38+ messages in thread* Re: [PATCH 2/7] cifs: Improve creating native symlinks pointing to directory
2024-09-30 15:09 ` Paulo Alcantara
@ 2024-09-30 17:17 ` Pali Rohár
2024-10-05 14:08 ` Pali Rohár
0 siblings, 1 reply; 38+ messages in thread
From: Pali Rohár @ 2024-09-30 17:17 UTC (permalink / raw)
To: Paulo Alcantara; +Cc: Steve French, Ronnie Sahlberg, linux-cifs, linux-kernel
On Monday 30 September 2024 12:09:48 Paulo Alcantara wrote:
> Pali Rohár <pali@kernel.org> writes:
>
> > SMB protocol for native symlinks distinguish between symlink to directory
> > and symlink to file. These two symlink types cannot be exchanged, which
> > means that symlink of file type pointing to directory cannot be resolved at
> > all (and vice-versa).
> >
> > Windows follows this rule for local filesystems (NTFS) and also for SMB.
> >
> > Linux SMB client currenly creates all native symlinks of file type. Which
> > means that Windows (and some other SMB clients) cannot resolve symlinks
> > pointing to directory created by Linux SMB client.
> >
> > As Linux system does not distinguish between directory and file symlinks,
> > its API does not provide enough information for Linux SMB client during
> > creating of native symlinks.
> >
> > Add some heuristic into the Linux SMB client for choosing the correct
> > symlink type during symlink creation. Check if the symlink target location
> > ends with slash, or last path component is dot or dot dot, and check if the
> > target location on SMB share exists and is a directory. If at least one
> > condition is truth then create a new SMB symlink of directory type.
> > Otherwise create it as file type symlink.
> >
> > This change improves interoperability with Windows systems. Windows systems
> > would be able to resolve more SMB symlinks created by Linux SMB client
> > which points to existing directory.
> >
> > Signed-off-by: Pali Rohár <pali@kernel.org>
> > ---
> > fs/smb/client/reparse.c | 131 ++++++++++++++++++++++++++++++++++++--
> > fs/smb/client/smb2inode.c | 3 +-
> > fs/smb/client/smb2proto.h | 1 +
> > 3 files changed, 130 insertions(+), 5 deletions(-)
> >
> > diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> > index 507e17244ed3..9390ab801696 100644
> > --- a/fs/smb/client/reparse.c
> > +++ b/fs/smb/client/reparse.c
> > @@ -24,13 +24,16 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> > struct inode *new;
> > struct kvec iov;
> > __le16 *path;
> > + bool directory = false;
> > char *sym, sep = CIFS_DIR_SEP(cifs_sb);
> > u16 len, plen;
> > int rc = 0;
> >
> > - sym = kstrdup(symname, GFP_KERNEL);
> > + len = strlen(symname)+1;
> > + sym = kzalloc(len+1, GFP_KERNEL); /* +1 for possible directory slash */
> > if (!sym)
> > return -ENOMEM;
> > + memcpy(sym, symname, len);
> >
> > data = (struct cifs_open_info_data) {
> > .reparse_point = true,
> > @@ -45,6 +48,125 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> > goto out;
> > }
> >
> > + /*
> > + * SMB distinguish between symlink to directory and symlink to file.
> > + * They cannot be exchanged (symlink of file type which points to
> > + * directory cannot be resolved and vice-versa). First do some simple
> > + * check, if the original Linux symlink target ends with slash, or
> > + * last path component is dot or dot dot then it is for sure symlink
> > + * to the directory.
> > + */
> > + if (!directory) {
> > + const char *basename = kbasename(symname);
> > + int basename_len = strlen(basename);
> > + if (basename_len == 0 || /* symname ends with slash */
> > + (basename_len == 1 && basename[0] == '.') || /* last component is "." */
> > + (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) /* last component is ".." */
> > + directory = true;
> > + }
> > +
> > + /*
> > + * If it was not detected as directory yet and the symlink is relative
> > + * then try to resolve the path on the SMB server, check if the path
> > + * exists and determinate if it is a directory or not.
> > + */
> > + if (!directory && symname[0] != '/') {
> > + __u32 oplock;
> > + struct tcon_link *tlink;
> > + struct cifs_tcon *tcon;
> > + struct cifs_fid fid;
> > + struct cifs_open_parms oparms;
> > + char *resolved_path;
> > + char *path_sep;
> > + int open_rc;
> > + int full_path_len = strlen(full_path);
> > + int symname_len = strlen(symname);
> > +
> > + tlink = cifs_sb_tlink(cifs_sb);
> > + if (IS_ERR(tlink)) {
> > + rc = PTR_ERR(tlink);
> > + goto out;
> > + }
> > +
> > + resolved_path = kzalloc(full_path_len + symname_len + 1, GFP_KERNEL);
> > + if (!resolved_path) {
> > + rc = -ENOMEM;
> > + goto out;
> > + }
>
> If !@resolved_path, then you will end up leaking @tlink.
>
> > +
> > + /*
> > + * Compose the resolved SMB symlink path from the SMB full path
> > + * and Linux target symlink path.
> > + */
> > + memcpy(resolved_path, full_path, full_path_len+1);
> > + path_sep = strrchr(resolved_path, sep);
> > + if (path_sep)
> > + path_sep++;
> > + else
> > + path_sep = resolved_path;
> > + memcpy(path_sep, symname, symname_len+1);
> > + if (sep == '\\')
> > + convert_delimiter(path_sep, sep);
> > +
> > + tcon = tlink_tcon(tlink);
> > +
> > + oparms = (struct cifs_open_parms) {
> > + .tcon = tcon,
> > + .cifs_sb = cifs_sb,
> > + .desired_access = FILE_READ_ATTRIBUTES,
> > + .disposition = FILE_OPEN,
> > + .path = resolved_path,
> > + .fid = &fid,
> > + };
>
> Please use CIFS_OPARMS().
>
> > +
> > + /* Try to open as NOT_FILE */
> > + oplock = 0;
> > + oparms.create_options = cifs_create_options(cifs_sb, CREATE_NOT_FILE);
> > + open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
> > + if (open_rc == 0) {
> > + /* Successful open means that the target path is definitely a directory. */
> > + directory = true;
> > + tcon->ses->server->ops->close(xid, tcon, &fid);
> > + } else if (open_rc != -ENOTDIR) {
> > + /* Try to open as NOT_DIR */
> > + oplock = 0;
> > + oparms.create_options = cifs_create_options(cifs_sb, CREATE_NOT_DIR);
> > + open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
> > + if (open_rc == 0) {
> > + tcon->ses->server->ops->close(xid, tcon, &fid);
> > + } else if (open_rc == -EISDIR) {
> > + /* -EISDIR means that the target path is definitely a directory. */
> > + directory = true;
> > + } else {
> > + cifs_dbg(FYI,
> > + "%s: cannot determinate if the symlink target path '%s' "
> > + "is directory or not, creating '%s' as file symlink\n",
> > + __func__, symname, full_path);
> > + }
> > + }
> > +
> > + kfree(resolved_path);
> > + cifs_put_tlink(tlink);
> > + }
> > +
> > + /*
> > + * For absolute symlinks it is not possible to determinate
> > + * if it should point to directory or file.
> > + */
> > + if (!directory && symname[0] == '/')
> > + cifs_dbg(FYI,
> > + "%s: cannot determinate if the symlink target path '%s' "
> > + "is directory or not, creating '%s' as file symlink\n",
> > + __func__, symname, full_path);
> > +
>
> Create a helper with all of this and then call it in
> smb2_create_reparse_symlink() to determine whether symlink target is a
> directory or file.
Hello, thanks for review! Of course I will update this patch to address
all your points.
^ permalink raw reply [flat|nested] 38+ messages in thread* Re: [PATCH 2/7] cifs: Improve creating native symlinks pointing to directory
2024-09-30 17:17 ` Pali Rohár
@ 2024-10-05 14:08 ` Pali Rohár
0 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-10-05 14:08 UTC (permalink / raw)
To: Paulo Alcantara; +Cc: Steve French, Ronnie Sahlberg, linux-cifs, linux-kernel
On Monday 30 September 2024 19:17:15 Pali Rohár wrote:
> On Monday 30 September 2024 12:09:48 Paulo Alcantara wrote:
> > Pali Rohár <pali@kernel.org> writes:
> >
> > > SMB protocol for native symlinks distinguish between symlink to directory
> > > and symlink to file. These two symlink types cannot be exchanged, which
> > > means that symlink of file type pointing to directory cannot be resolved at
> > > all (and vice-versa).
> > >
> > > Windows follows this rule for local filesystems (NTFS) and also for SMB.
> > >
> > > Linux SMB client currenly creates all native symlinks of file type. Which
> > > means that Windows (and some other SMB clients) cannot resolve symlinks
> > > pointing to directory created by Linux SMB client.
> > >
> > > As Linux system does not distinguish between directory and file symlinks,
> > > its API does not provide enough information for Linux SMB client during
> > > creating of native symlinks.
> > >
> > > Add some heuristic into the Linux SMB client for choosing the correct
> > > symlink type during symlink creation. Check if the symlink target location
> > > ends with slash, or last path component is dot or dot dot, and check if the
> > > target location on SMB share exists and is a directory. If at least one
> > > condition is truth then create a new SMB symlink of directory type.
> > > Otherwise create it as file type symlink.
> > >
> > > This change improves interoperability with Windows systems. Windows systems
> > > would be able to resolve more SMB symlinks created by Linux SMB client
> > > which points to existing directory.
> > >
> > > Signed-off-by: Pali Rohár <pali@kernel.org>
> > > ---
> > > fs/smb/client/reparse.c | 131 ++++++++++++++++++++++++++++++++++++--
> > > fs/smb/client/smb2inode.c | 3 +-
> > > fs/smb/client/smb2proto.h | 1 +
> > > 3 files changed, 130 insertions(+), 5 deletions(-)
> > >
> > > diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> > > index 507e17244ed3..9390ab801696 100644
> > > --- a/fs/smb/client/reparse.c
> > > +++ b/fs/smb/client/reparse.c
> > > @@ -24,13 +24,16 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> > > struct inode *new;
> > > struct kvec iov;
> > > __le16 *path;
> > > + bool directory = false;
> > > char *sym, sep = CIFS_DIR_SEP(cifs_sb);
> > > u16 len, plen;
> > > int rc = 0;
> > >
> > > - sym = kstrdup(symname, GFP_KERNEL);
> > > + len = strlen(symname)+1;
> > > + sym = kzalloc(len+1, GFP_KERNEL); /* +1 for possible directory slash */
> > > if (!sym)
> > > return -ENOMEM;
> > > + memcpy(sym, symname, len);
> > >
> > > data = (struct cifs_open_info_data) {
> > > .reparse_point = true,
> > > @@ -45,6 +48,125 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> > > goto out;
> > > }
> > >
> > > + /*
> > > + * SMB distinguish between symlink to directory and symlink to file.
> > > + * They cannot be exchanged (symlink of file type which points to
> > > + * directory cannot be resolved and vice-versa). First do some simple
> > > + * check, if the original Linux symlink target ends with slash, or
> > > + * last path component is dot or dot dot then it is for sure symlink
> > > + * to the directory.
> > > + */
> > > + if (!directory) {
> > > + const char *basename = kbasename(symname);
> > > + int basename_len = strlen(basename);
> > > + if (basename_len == 0 || /* symname ends with slash */
> > > + (basename_len == 1 && basename[0] == '.') || /* last component is "." */
> > > + (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) /* last component is ".." */
> > > + directory = true;
> > > + }
> > > +
> > > + /*
> > > + * If it was not detected as directory yet and the symlink is relative
> > > + * then try to resolve the path on the SMB server, check if the path
> > > + * exists and determinate if it is a directory or not.
> > > + */
> > > + if (!directory && symname[0] != '/') {
> > > + __u32 oplock;
> > > + struct tcon_link *tlink;
> > > + struct cifs_tcon *tcon;
> > > + struct cifs_fid fid;
> > > + struct cifs_open_parms oparms;
> > > + char *resolved_path;
> > > + char *path_sep;
> > > + int open_rc;
> > > + int full_path_len = strlen(full_path);
> > > + int symname_len = strlen(symname);
> > > +
> > > + tlink = cifs_sb_tlink(cifs_sb);
> > > + if (IS_ERR(tlink)) {
> > > + rc = PTR_ERR(tlink);
> > > + goto out;
> > > + }
> > > +
> > > + resolved_path = kzalloc(full_path_len + symname_len + 1, GFP_KERNEL);
> > > + if (!resolved_path) {
> > > + rc = -ENOMEM;
> > > + goto out;
> > > + }
> >
> > If !@resolved_path, then you will end up leaking @tlink.
> >
> > > +
> > > + /*
> > > + * Compose the resolved SMB symlink path from the SMB full path
> > > + * and Linux target symlink path.
> > > + */
> > > + memcpy(resolved_path, full_path, full_path_len+1);
> > > + path_sep = strrchr(resolved_path, sep);
> > > + if (path_sep)
> > > + path_sep++;
> > > + else
> > > + path_sep = resolved_path;
> > > + memcpy(path_sep, symname, symname_len+1);
> > > + if (sep == '\\')
> > > + convert_delimiter(path_sep, sep);
> > > +
> > > + tcon = tlink_tcon(tlink);
> > > +
> > > + oparms = (struct cifs_open_parms) {
> > > + .tcon = tcon,
> > > + .cifs_sb = cifs_sb,
> > > + .desired_access = FILE_READ_ATTRIBUTES,
> > > + .disposition = FILE_OPEN,
> > > + .path = resolved_path,
> > > + .fid = &fid,
> > > + };
> >
> > Please use CIFS_OPARMS().
> >
> > > +
> > > + /* Try to open as NOT_FILE */
> > > + oplock = 0;
> > > + oparms.create_options = cifs_create_options(cifs_sb, CREATE_NOT_FILE);
> > > + open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
> > > + if (open_rc == 0) {
> > > + /* Successful open means that the target path is definitely a directory. */
> > > + directory = true;
> > > + tcon->ses->server->ops->close(xid, tcon, &fid);
> > > + } else if (open_rc != -ENOTDIR) {
> > > + /* Try to open as NOT_DIR */
> > > + oplock = 0;
> > > + oparms.create_options = cifs_create_options(cifs_sb, CREATE_NOT_DIR);
> > > + open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
> > > + if (open_rc == 0) {
> > > + tcon->ses->server->ops->close(xid, tcon, &fid);
> > > + } else if (open_rc == -EISDIR) {
> > > + /* -EISDIR means that the target path is definitely a directory. */
> > > + directory = true;
> > > + } else {
> > > + cifs_dbg(FYI,
> > > + "%s: cannot determinate if the symlink target path '%s' "
> > > + "is directory or not, creating '%s' as file symlink\n",
> > > + __func__, symname, full_path);
> > > + }
> > > + }
> > > +
> > > + kfree(resolved_path);
> > > + cifs_put_tlink(tlink);
> > > + }
> > > +
> > > + /*
> > > + * For absolute symlinks it is not possible to determinate
> > > + * if it should point to directory or file.
> > > + */
> > > + if (!directory && symname[0] == '/')
> > > + cifs_dbg(FYI,
> > > + "%s: cannot determinate if the symlink target path '%s' "
> > > + "is directory or not, creating '%s' as file symlink\n",
> > > + __func__, symname, full_path);
> > > +
> >
> > Create a helper with all of this and then call it in
> > smb2_create_reparse_symlink() to determine whether symlink target is a
> > directory or file.
>
> Hello, thanks for review! Of course I will update this patch to address
> all your points.
Hello, I addresses this issues in V2 of the patch series. Please look at it.
^ permalink raw reply [flat|nested] 38+ messages in thread
* [PATCH 3/7] cifs: Fix creating native symlinks pointing to current or parent directory
2024-09-29 18:50 [PATCH 0/7] cifs: Improve support for native SMB symlinks Pali Rohár
2024-09-29 18:50 ` [PATCH 1/7] cifs: Rename smb2_get_reparse_inode to smb2_create_reparse_inode Pali Rohár
2024-09-29 18:50 ` [PATCH 2/7] cifs: Improve creating native symlinks pointing to directory Pali Rohár
@ 2024-09-29 18:50 ` Pali Rohár
2024-09-29 18:50 ` [PATCH 4/7] cifs: Fix parsing native symlinks relative to the export Pali Rohár
` (5 subsequent siblings)
8 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-09-29 18:50 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
Calling 'ln -s . symlink' or 'ln -s .. symlink' creates symlink pointing to
some object name which ends with U+F029 unicode codepoint. This is because
trailing dot in the object name is replaced by non-ASCII unicode codepoint.
So Linux SMB client currently is not able to create native symlink pointing
to current or parent directory on Windows SMB server which can be read by
either on local Windows server or by any other SMB client which does not
implement compatible-reverse character replacement.
Fix this problem in cifsConvertToUTF16() function which is doing that
character replacement. Function comment already says that it does not need
to handle special cases '.' and '..', but after introduction of native
symlinks in reparse point form, this handling is needed.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/cifs_unicode.c | 17 ++++++++++++++---
1 file changed, 14 insertions(+), 3 deletions(-)
diff --git a/fs/smb/client/cifs_unicode.c b/fs/smb/client/cifs_unicode.c
index 79d99a913944..4cc6e0896fad 100644
--- a/fs/smb/client/cifs_unicode.c
+++ b/fs/smb/client/cifs_unicode.c
@@ -484,10 +484,21 @@ cifsConvertToUTF16(__le16 *target, const char *source, int srclen,
/**
* Remap spaces and periods found at the end of every
* component of the path. The special cases of '.' and
- * '..' do not need to be dealt with explicitly because
- * they are addressed in namei.c:link_path_walk().
+ * '..' are need to be handled because of symlinks.
+ * They are treated as non-end-of-string to avoid
+ * remapping and breaking symlinks pointing to . or ..
**/
- if ((i == srclen - 1) || (source[i+1] == '\\'))
+ if ((i == 0 || source[i-1] == '\\') &&
+ source[i] == '.' &&
+ (i == srclen-1 || source[i+1] == '\\'))
+ end_of_string = false; /* "." case */
+ else if (i >= 1 &&
+ (i == 1 || source[i-2] == '\\') &&
+ source[i-1] == '.' &&
+ source[i] == '.' &&
+ (i == srclen-1 || source[i+1] == '\\'))
+ end_of_string = false; /* ".." case */
+ else if ((i == srclen - 1) || (source[i+1] == '\\'))
end_of_string = true;
else
end_of_string = false;
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* [PATCH 4/7] cifs: Fix parsing native symlinks relative to the export
2024-09-29 18:50 [PATCH 0/7] cifs: Improve support for native SMB symlinks Pali Rohár
` (2 preceding siblings ...)
2024-09-29 18:50 ` [PATCH 3/7] cifs: Fix creating native symlinks pointing to current or parent directory Pali Rohár
@ 2024-09-29 18:50 ` Pali Rohár
2024-09-29 18:50 ` [PATCH 5/7] cifs: Fix parsing native symlinks directory/file type Pali Rohár
` (4 subsequent siblings)
8 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-09-29 18:50 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
SMB symlink which has SYMLINK_FLAG_RELATIVE set is relative (as opposite of
the absolute) and it can be relative either to the current directory (where
is the symlink stored) or relative to the top level export path. To what it
is relative depends on the first character of the symlink target path.
If the first character is path separator then symlink is relative to the
export, otherwise to the current directory. Linux (and generally POSIX
systems) supports only symlink paths relative to the current directory
where is symlink stored.
Currently if Linux SMB client reads relative SMB symlink with first
character as path separator (slash), it let as is. Which means that Linux
interpret it as absolute symlink pointing from the root (/). But this
location is different than the top level directory of SMB export (unless
SMB export was mounted to the root) and thefore SMB symlinks relative to
the export are interpreted wrongly by Linux SMB client.
Fix this problem. As Linux does not have equivalent of the path relative to
the top of the mount point, convert such symlink target path relative to
the current directory. Do this by prepending "../" pattern N times before
the SMB target path, where N is the number of path separators found in SMB
symlink path.
So for example, if SMB share is mounted to Linux path /mnt/share/, symlink
is stored in file /mnt/share/test/folder1/symlink (so SMB symlink path is
test\folder1\symlink) and SMB symlink target points to \test\folder2\file,
then convert symlink target path to Linux path ../../test/folder2/file.
Deduplicate code for parsing SMB symlinks in native form from functions
smb2_parse_symlink_response() and parse_reparse_native_symlink() into new
function smb2_parse_native_symlink() and pass into this new function a new
full_path parameter from callers, which specify SMB full path where is
symlink stored.
This change fixes resolving of the native Windows symlinks relative to the
top level directory of the SMB share.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/cifsglob.h | 1 +
fs/smb/client/cifsproto.h | 1 +
fs/smb/client/inode.c | 1 +
fs/smb/client/reparse.c | 92 ++++++++++++++++++++++++++++++++++-----
fs/smb/client/reparse.h | 4 +-
fs/smb/client/smb1ops.c | 3 +-
fs/smb/client/smb2file.c | 21 +++++----
fs/smb/client/smb2inode.c | 6 ++-
fs/smb/client/smb2proto.h | 9 +++-
9 files changed, 110 insertions(+), 28 deletions(-)
diff --git a/fs/smb/client/cifsglob.h b/fs/smb/client/cifsglob.h
index 119537e98f77..7c8d242b98e5 100644
--- a/fs/smb/client/cifsglob.h
+++ b/fs/smb/client/cifsglob.h
@@ -589,6 +589,7 @@ struct smb_version_operations {
/* Check for STATUS_NETWORK_NAME_DELETED */
bool (*is_network_name_deleted)(char *buf, struct TCP_Server_Info *srv);
int (*parse_reparse_point)(struct cifs_sb_info *cifs_sb,
+ const char *full_path,
struct kvec *rsp_iov,
struct cifs_open_info_data *data);
int (*create_reparse_symlink)(const unsigned int xid,
diff --git a/fs/smb/client/cifsproto.h b/fs/smb/client/cifsproto.h
index 791bddac0396..44555a0e4df6 100644
--- a/fs/smb/client/cifsproto.h
+++ b/fs/smb/client/cifsproto.h
@@ -675,6 +675,7 @@ char *extract_hostname(const char *unc);
char *extract_sharename(const char *unc);
int parse_reparse_point(struct reparse_data_buffer *buf,
u32 plen, struct cifs_sb_info *cifs_sb,
+ const char *full_path,
bool unicode, struct cifs_open_info_data *data);
int __cifs_sfu_make_node(unsigned int xid, struct inode *inode,
struct dentry *dentry, struct cifs_tcon *tcon,
diff --git a/fs/smb/client/inode.c b/fs/smb/client/inode.c
index 0c23634438e5..0fe54b2d2561 100644
--- a/fs/smb/client/inode.c
+++ b/fs/smb/client/inode.c
@@ -1107,6 +1107,7 @@ static int reparse_info_to_fattr(struct cifs_open_info_data *data,
rc = 0;
} else if (iov && server->ops->parse_reparse_point) {
rc = server->ops->parse_reparse_point(cifs_sb,
+ full_path,
iov, data);
}
break;
diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
index 9390ab801696..d8edb513556f 100644
--- a/fs/smb/client/reparse.c
+++ b/fs/smb/client/reparse.c
@@ -497,13 +497,81 @@ static int parse_reparse_nfs(struct reparse_nfs_data *buf,
return 0;
}
+int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
+ bool unicode, bool relative,
+ const char *full_path,
+ struct cifs_sb_info *cifs_sb)
+{
+ char sep = CIFS_DIR_SEP(cifs_sb);
+ char *linux_target = NULL;
+ char *smb_target = NULL;
+ int levels;
+ int rc;
+ int i;
+
+ smb_target = cifs_strndup_from_utf16(buf, len, unicode, cifs_sb->local_nls);
+ if (!smb_target) {
+ rc = -ENOMEM;
+ goto out;
+ }
+
+ if (smb_target[0] == sep && relative) {
+ /*
+ * This is a relative SMB symlink from the top of the share,
+ * which is the top level directory of the Linux mount point.
+ * Linux does not support such relative symlinks, so convert
+ * it to the relative symlink from the current directory.
+ * full_path is the SMB path to the symlink (from which is
+ * extracted current directory) and smb_target is the SMB path
+ * where symlink points, therefore full_path must always be on
+ * the SMB share.
+ */
+ int smb_target_len = strlen(smb_target)+1;
+ levels = 0;
+ for (i = 1; full_path[i]; i++) { /* i=1 to skip leading sep */
+ if (full_path[i] == sep)
+ levels++;
+ }
+ linux_target = kmalloc(levels*3 + smb_target_len, GFP_KERNEL);
+ if (!linux_target) {
+ rc = -ENOMEM;
+ goto out;
+ }
+ for (i = 0; i < levels; i++) {
+ linux_target[i*3 + 0] = '.';
+ linux_target[i*3 + 1] = '.';
+ linux_target[i*3 + 2] = sep;
+ }
+ memcpy(linux_target + levels*3, smb_target+1, smb_target_len); /* +1 to skip leading sep */
+ } else {
+ linux_target = smb_target;
+ smb_target = NULL;
+ }
+
+ if (sep == '\\')
+ convert_delimiter(linux_target, '/');
+
+ rc = 0;
+ *target = linux_target;
+
+ cifs_dbg(FYI, "%s: symlink target: %s\n", __func__, *target);
+
+out:
+ if (rc != 0)
+ kfree(linux_target);
+ kfree(smb_target);
+ return rc;
+}
+
static int parse_reparse_symlink(struct reparse_symlink_data_buffer *sym,
u32 plen, bool unicode,
struct cifs_sb_info *cifs_sb,
+ const char *full_path,
struct cifs_open_info_data *data)
{
unsigned int len;
unsigned int offs;
+ int rc;
/* We handle Symbolic Link reparse tag here. See: MS-FSCC 2.1.2.4 */
@@ -514,20 +582,19 @@ static int parse_reparse_symlink(struct reparse_symlink_data_buffer *sym,
return -EIO;
}
- data->symlink_target = cifs_strndup_from_utf16(sym->PathBuffer + offs,
- len, unicode,
- cifs_sb->local_nls);
- if (!data->symlink_target)
- return -ENOMEM;
-
- convert_delimiter(data->symlink_target, '/');
- cifs_dbg(FYI, "%s: target path: %s\n", __func__, data->symlink_target);
-
- return 0;
+ rc = smb2_parse_native_symlink(&data->symlink_target,
+ sym->PathBuffer + offs,
+ len,
+ unicode,
+ le32_to_cpu(sym->Flags) & SYMLINK_FLAG_RELATIVE,
+ full_path,
+ cifs_sb);
+ return rc;
}
int parse_reparse_point(struct reparse_data_buffer *buf,
u32 plen, struct cifs_sb_info *cifs_sb,
+ const char *full_path,
bool unicode, struct cifs_open_info_data *data)
{
struct cifs_tcon *tcon = cifs_sb_master_tcon(cifs_sb);
@@ -542,7 +609,7 @@ int parse_reparse_point(struct reparse_data_buffer *buf,
case IO_REPARSE_TAG_SYMLINK:
return parse_reparse_symlink(
(struct reparse_symlink_data_buffer *)buf,
- plen, unicode, cifs_sb, data);
+ plen, unicode, cifs_sb, full_path, data);
case IO_REPARSE_TAG_LX_SYMLINK:
case IO_REPARSE_TAG_AF_UNIX:
case IO_REPARSE_TAG_LX_FIFO:
@@ -558,6 +625,7 @@ int parse_reparse_point(struct reparse_data_buffer *buf,
}
int smb2_parse_reparse_point(struct cifs_sb_info *cifs_sb,
+ const char *full_path,
struct kvec *rsp_iov,
struct cifs_open_info_data *data)
{
@@ -567,7 +635,7 @@ int smb2_parse_reparse_point(struct cifs_sb_info *cifs_sb,
buf = (struct reparse_data_buffer *)((u8 *)io +
le32_to_cpu(io->OutputOffset));
- return parse_reparse_point(buf, plen, cifs_sb, true, data);
+ return parse_reparse_point(buf, plen, cifs_sb, full_path, true, data);
}
static void wsl_to_fattr(struct cifs_open_info_data *data,
diff --git a/fs/smb/client/reparse.h b/fs/smb/client/reparse.h
index 2a91f64de557..0203f6d669b3 100644
--- a/fs/smb/client/reparse.h
+++ b/fs/smb/client/reparse.h
@@ -128,7 +128,9 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
int smb2_mknod_reparse(unsigned int xid, struct inode *inode,
struct dentry *dentry, struct cifs_tcon *tcon,
const char *full_path, umode_t mode, dev_t dev);
-int smb2_parse_reparse_point(struct cifs_sb_info *cifs_sb, struct kvec *rsp_iov,
+int smb2_parse_reparse_point(struct cifs_sb_info *cifs_sb,
+ const char *full_path,
+ struct kvec *rsp_iov,
struct cifs_open_info_data *data);
#endif /* _CIFS_REPARSE_H */
diff --git a/fs/smb/client/smb1ops.c b/fs/smb/client/smb1ops.c
index e03c91a49650..fed32db32a46 100644
--- a/fs/smb/client/smb1ops.c
+++ b/fs/smb/client/smb1ops.c
@@ -994,6 +994,7 @@ static int cifs_query_symlink(const unsigned int xid,
}
static int cifs_parse_reparse_point(struct cifs_sb_info *cifs_sb,
+ const char *full_path,
struct kvec *rsp_iov,
struct cifs_open_info_data *data)
{
@@ -1004,7 +1005,7 @@ static int cifs_parse_reparse_point(struct cifs_sb_info *cifs_sb,
buf = (struct reparse_data_buffer *)((__u8 *)&io->hdr.Protocol +
le32_to_cpu(io->DataOffset));
- return parse_reparse_point(buf, plen, cifs_sb, unicode, data);
+ return parse_reparse_point(buf, plen, cifs_sb, full_path, unicode, data);
}
static bool
diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
index c23478ab1cf8..dc52995f5591 100644
--- a/fs/smb/client/smb2file.c
+++ b/fs/smb/client/smb2file.c
@@ -63,12 +63,12 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
return sym;
}
-int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov, char **path)
+int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov,
+ const char *full_path, char **path)
{
struct smb2_symlink_err_rsp *sym;
unsigned int sub_offs, sub_len;
unsigned int print_offs, print_len;
- char *s;
if (!cifs_sb || !iov || !iov->iov_base || !iov->iov_len || !path)
return -EINVAL;
@@ -86,15 +86,13 @@ int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec
iov->iov_len < SMB2_SYMLINK_STRUCT_SIZE + print_offs + print_len)
return -EINVAL;
- s = cifs_strndup_from_utf16((char *)sym->PathBuffer + sub_offs, sub_len, true,
- cifs_sb->local_nls);
- if (!s)
- return -ENOMEM;
- convert_delimiter(s, '/');
- cifs_dbg(FYI, "%s: symlink target: %s\n", __func__, s);
-
- *path = s;
- return 0;
+ return smb2_parse_native_symlink(path,
+ (char *)sym->PathBuffer + sub_offs,
+ sub_len,
+ true,
+ le32_to_cpu(sym->Flags) & SYMLINK_FLAG_RELATIVE,
+ full_path,
+ cifs_sb);
}
int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32 *oplock, void *buf)
@@ -126,6 +124,7 @@ int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32
goto out;
if (hdr->Status == STATUS_STOPPED_ON_SYMLINK) {
rc = smb2_parse_symlink_response(oparms->cifs_sb, &err_iov,
+ oparms->path,
&data->symlink_target);
if (!rc) {
memset(smb2_data, 0, sizeof(*smb2_data));
diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
index fffb9df4faeb..c9cdac7d2d50 100644
--- a/fs/smb/client/smb2inode.c
+++ b/fs/smb/client/smb2inode.c
@@ -828,6 +828,7 @@ static int smb2_compound_op(const unsigned int xid, struct cifs_tcon *tcon,
static int parse_create_response(struct cifs_open_info_data *data,
struct cifs_sb_info *cifs_sb,
+ const char *full_path,
const struct kvec *iov)
{
struct smb2_create_rsp *rsp = iov->iov_base;
@@ -841,6 +842,7 @@ static int parse_create_response(struct cifs_open_info_data *data,
break;
case STATUS_STOPPED_ON_SYMLINK:
rc = smb2_parse_symlink_response(cifs_sb, iov,
+ full_path,
&data->symlink_target);
if (rc)
return rc;
@@ -930,14 +932,14 @@ int smb2_query_path_info(const unsigned int xid,
switch (rc) {
case 0:
- rc = parse_create_response(data, cifs_sb, &out_iov[0]);
+ rc = parse_create_response(data, cifs_sb, full_path, &out_iov[0]);
break;
case -EOPNOTSUPP:
/*
* BB TODO: When support for special files added to Samba
* re-verify this path.
*/
- rc = parse_create_response(data, cifs_sb, &out_iov[0]);
+ rc = parse_create_response(data, cifs_sb, full_path, &out_iov[0]);
if (rc || !data->reparse_point)
goto out;
diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
index fd092f2bb8c5..11cef65fa831 100644
--- a/fs/smb/client/smb2proto.h
+++ b/fs/smb/client/smb2proto.h
@@ -113,7 +113,14 @@ extern int smb3_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
struct cifs_sb_info *cifs_sb,
const unsigned char *path, char *pbuf,
unsigned int *pbytes_read);
-int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov, char **path);
+int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
+ bool unicode, bool relative,
+ const char *full_path,
+ struct cifs_sb_info *cifs_sb);
+int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb,
+ const struct kvec *iov,
+ const char *full_path,
+ char **path);
int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32 *oplock,
void *buf);
extern int smb2_unlock_range(struct cifsFileInfo *cfile,
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* [PATCH 5/7] cifs: Fix parsing native symlinks directory/file type
2024-09-29 18:50 [PATCH 0/7] cifs: Improve support for native SMB symlinks Pali Rohár
` (3 preceding siblings ...)
2024-09-29 18:50 ` [PATCH 4/7] cifs: Fix parsing native symlinks relative to the export Pali Rohár
@ 2024-09-29 18:50 ` Pali Rohár
2024-09-29 21:47 ` Steve French
2024-09-29 18:50 ` [PATCH 6/7] cifs: Validate content of native symlink Pali Rohár
` (3 subsequent siblings)
8 siblings, 1 reply; 38+ messages in thread
From: Pali Rohár @ 2024-09-29 18:50 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
As SMB protocol distinguish between symlink to directory and symlink to
file, add some mechanism to disallow resolving incompatible types.
When SMB symlink is of the directory type, ensure that its target path ends
with slash. This forces Linux to not allow resolving such symlink to file.
And when SMB symlink is of the file type and its target path ends with
slash then returns an error as such symlink is unresolvable. Such symlink
always points to invalid location as file cannot end with slash.
This mimics Windows behavior of native SMB symlinks.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/reparse.c | 4 ++++
fs/smb/client/smb2file.c | 46 +++++++++++++++++++++++++++++++++++++++
fs/smb/client/smb2inode.c | 4 ++++
fs/smb/client/smb2proto.h | 1 +
4 files changed, 55 insertions(+)
diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
index d8edb513556f..5a738f65b190 100644
--- a/fs/smb/client/reparse.c
+++ b/fs/smb/client/reparse.c
@@ -589,6 +589,10 @@ static int parse_reparse_symlink(struct reparse_symlink_data_buffer *sym,
le32_to_cpu(sym->Flags) & SYMLINK_FLAG_RELATIVE,
full_path,
cifs_sb);
+ if (!rc) {
+ bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
+ rc = smb2_fix_symlink_target_type(&data->symlink_target, directory);
+ }
return rc;
}
diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
index dc52995f5591..8a1a1b2a1c81 100644
--- a/fs/smb/client/smb2file.c
+++ b/fs/smb/client/smb2file.c
@@ -63,6 +63,48 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
return sym;
}
+int smb2_fix_symlink_target_type(char **target, bool directory)
+{
+ char *buf;
+ int len = strlen(*target);
+
+ if (!len)
+ return -EIO;
+
+ /*
+ * If this is directory symlink and it does not have trailing slash then
+ * append it. Trailing slash simulates Windows/SMB behavior which do not
+ * allow resolving directory symlink to file.
+ */
+ if (directory && (*target)[len-1] != '/') {
+ buf = kzalloc(len+2, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+ memcpy(buf, *target, len);
+ buf[len] = '/';
+ kfree(*target);
+ *target = buf;
+ }
+
+ /*
+ * If this is a symlink which points to file name with trailing slash,
+ * or to file named "." or file named ".." then this symlink cannot be
+ * resolved on Linux because Linux does not allow files with such names.
+ * So return an error to prevent resolving this file type symlink to
+ * directory, as it do not point to directory at all.
+ */
+ if (!directory) {
+ const char *basename = kbasename(*target);
+ int basename_len = strlen(basename);
+ if (basename_len == 0 || /* symname ends with slash */
+ (basename_len == 1 && basename[0] == '.') || /* last component is "." */
+ (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) /* last component is ".." */
+ return -EIO;
+ }
+
+ return 0;
+}
+
int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov,
const char *full_path, char **path)
{
@@ -132,6 +174,10 @@ int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32
rc = SMB2_open(xid, oparms, smb2_path, &smb2_oplock, smb2_data,
NULL, NULL, NULL);
oparms->create_options &= ~OPEN_REPARSE_POINT;
+ if (!rc) {
+ bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
+ rc = smb2_fix_symlink_target_type(&data->symlink_target, directory);
+ }
}
}
}
diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
index c9cdac7d2d50..faf0a8344faa 100644
--- a/fs/smb/client/smb2inode.c
+++ b/fs/smb/client/smb2inode.c
@@ -960,6 +960,10 @@ int smb2_query_path_info(const unsigned int xid,
rc = smb2_compound_op(xid, tcon, cifs_sb, full_path,
&oparms, in_iov, cmds, num_cmds,
cfile, NULL, NULL, NULL);
+ if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
+ bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
+ rc = smb2_fix_symlink_target_type(&data->symlink_target, directory);
+ }
break;
case -EREMOTE:
break;
diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
index 11cef65fa831..d308f3c2f8df 100644
--- a/fs/smb/client/smb2proto.h
+++ b/fs/smb/client/smb2proto.h
@@ -113,6 +113,7 @@ extern int smb3_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
struct cifs_sb_info *cifs_sb,
const unsigned char *path, char *pbuf,
unsigned int *pbytes_read);
+int smb2_fix_symlink_target_type(char **target, bool directory);
int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
bool unicode, bool relative,
const char *full_path,
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* Re: [PATCH 5/7] cifs: Fix parsing native symlinks directory/file type
2024-09-29 18:50 ` [PATCH 5/7] cifs: Fix parsing native symlinks directory/file type Pali Rohár
@ 2024-09-29 21:47 ` Steve French
2024-09-29 21:58 ` Pali Rohár
0 siblings, 1 reply; 38+ messages in thread
From: Steve French @ 2024-09-29 21:47 UTC (permalink / raw)
To: Pali Rohár
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
obvious question is ... is there any risk that this could break POSIX
behavior when creating server side symlinks (ie not using mfsylmlinks,
but native symlink reparse point) remotely ...?
On Sun, Sep 29, 2024 at 1:51 PM Pali Rohár <pali@kernel.org> wrote:
>
> As SMB protocol distinguish between symlink to directory and symlink to
> file, add some mechanism to disallow resolving incompatible types.
>
> When SMB symlink is of the directory type, ensure that its target path ends
> with slash. This forces Linux to not allow resolving such symlink to file.
>
> And when SMB symlink is of the file type and its target path ends with
> slash then returns an error as such symlink is unresolvable. Such symlink
> always points to invalid location as file cannot end with slash.
>
> This mimics Windows behavior of native SMB symlinks.
>
> Signed-off-by: Pali Rohár <pali@kernel.org>
> ---
> fs/smb/client/reparse.c | 4 ++++
> fs/smb/client/smb2file.c | 46 +++++++++++++++++++++++++++++++++++++++
> fs/smb/client/smb2inode.c | 4 ++++
> fs/smb/client/smb2proto.h | 1 +
> 4 files changed, 55 insertions(+)
>
> diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> index d8edb513556f..5a738f65b190 100644
> --- a/fs/smb/client/reparse.c
> +++ b/fs/smb/client/reparse.c
> @@ -589,6 +589,10 @@ static int parse_reparse_symlink(struct reparse_symlink_data_buffer *sym,
> le32_to_cpu(sym->Flags) & SYMLINK_FLAG_RELATIVE,
> full_path,
> cifs_sb);
> + if (!rc) {
> + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> + rc = smb2_fix_symlink_target_type(&data->symlink_target, directory);
> + }
> return rc;
> }
>
> diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
> index dc52995f5591..8a1a1b2a1c81 100644
> --- a/fs/smb/client/smb2file.c
> +++ b/fs/smb/client/smb2file.c
> @@ -63,6 +63,48 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
> return sym;
> }
>
> +int smb2_fix_symlink_target_type(char **target, bool directory)
> +{
> + char *buf;
> + int len = strlen(*target);
> +
> + if (!len)
> + return -EIO;
> +
> + /*
> + * If this is directory symlink and it does not have trailing slash then
> + * append it. Trailing slash simulates Windows/SMB behavior which do not
> + * allow resolving directory symlink to file.
> + */
> + if (directory && (*target)[len-1] != '/') {
> + buf = kzalloc(len+2, GFP_KERNEL);
> + if (!buf)
> + return -ENOMEM;
> + memcpy(buf, *target, len);
> + buf[len] = '/';
> + kfree(*target);
> + *target = buf;
> + }
> +
> + /*
> + * If this is a symlink which points to file name with trailing slash,
> + * or to file named "." or file named ".." then this symlink cannot be
> + * resolved on Linux because Linux does not allow files with such names.
> + * So return an error to prevent resolving this file type symlink to
> + * directory, as it do not point to directory at all.
> + */
> + if (!directory) {
> + const char *basename = kbasename(*target);
> + int basename_len = strlen(basename);
> + if (basename_len == 0 || /* symname ends with slash */
> + (basename_len == 1 && basename[0] == '.') || /* last component is "." */
> + (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) /* last component is ".." */
> + return -EIO;
> + }
> +
> + return 0;
> +}
> +
> int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov,
> const char *full_path, char **path)
> {
> @@ -132,6 +174,10 @@ int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32
> rc = SMB2_open(xid, oparms, smb2_path, &smb2_oplock, smb2_data,
> NULL, NULL, NULL);
> oparms->create_options &= ~OPEN_REPARSE_POINT;
> + if (!rc) {
> + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> + rc = smb2_fix_symlink_target_type(&data->symlink_target, directory);
> + }
> }
> }
> }
> diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
> index c9cdac7d2d50..faf0a8344faa 100644
> --- a/fs/smb/client/smb2inode.c
> +++ b/fs/smb/client/smb2inode.c
> @@ -960,6 +960,10 @@ int smb2_query_path_info(const unsigned int xid,
> rc = smb2_compound_op(xid, tcon, cifs_sb, full_path,
> &oparms, in_iov, cmds, num_cmds,
> cfile, NULL, NULL, NULL);
> + if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
> + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> + rc = smb2_fix_symlink_target_type(&data->symlink_target, directory);
> + }
> break;
> case -EREMOTE:
> break;
> diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
> index 11cef65fa831..d308f3c2f8df 100644
> --- a/fs/smb/client/smb2proto.h
> +++ b/fs/smb/client/smb2proto.h
> @@ -113,6 +113,7 @@ extern int smb3_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
> struct cifs_sb_info *cifs_sb,
> const unsigned char *path, char *pbuf,
> unsigned int *pbytes_read);
> +int smb2_fix_symlink_target_type(char **target, bool directory);
> int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> bool unicode, bool relative,
> const char *full_path,
> --
> 2.20.1
>
>
--
Thanks,
Steve
^ permalink raw reply [flat|nested] 38+ messages in thread* Re: [PATCH 5/7] cifs: Fix parsing native symlinks directory/file type
2024-09-29 21:47 ` Steve French
@ 2024-09-29 21:58 ` Pali Rohár
0 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-09-29 21:58 UTC (permalink / raw)
To: Steve French
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
I was thinking about it and in my opinion this should be OK. If you
create native directory symlink on Windows via mklink /D and it would
point to file, then trying to open that symlink fails on Windows with
error. On Linux if you append slash to symlink target path then opening
it will fail if the target path is file. On Linux the symlink pointing
to directory may have trailing slash but it not required.
But testing by other people would be useful to confirm that there is not
some hidden issue.
On Sunday 29 September 2024 16:47:48 Steve French wrote:
> obvious question is ... is there any risk that this could break POSIX
> behavior when creating server side symlinks (ie not using mfsylmlinks,
> but native symlink reparse point) remotely ...?
>
> On Sun, Sep 29, 2024 at 1:51 PM Pali Rohár <pali@kernel.org> wrote:
> >
> > As SMB protocol distinguish between symlink to directory and symlink to
> > file, add some mechanism to disallow resolving incompatible types.
> >
> > When SMB symlink is of the directory type, ensure that its target path ends
> > with slash. This forces Linux to not allow resolving such symlink to file.
> >
> > And when SMB symlink is of the file type and its target path ends with
> > slash then returns an error as such symlink is unresolvable. Such symlink
> > always points to invalid location as file cannot end with slash.
> >
> > This mimics Windows behavior of native SMB symlinks.
> >
> > Signed-off-by: Pali Rohár <pali@kernel.org>
> > ---
> > fs/smb/client/reparse.c | 4 ++++
> > fs/smb/client/smb2file.c | 46 +++++++++++++++++++++++++++++++++++++++
> > fs/smb/client/smb2inode.c | 4 ++++
> > fs/smb/client/smb2proto.h | 1 +
> > 4 files changed, 55 insertions(+)
> >
> > diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> > index d8edb513556f..5a738f65b190 100644
> > --- a/fs/smb/client/reparse.c
> > +++ b/fs/smb/client/reparse.c
> > @@ -589,6 +589,10 @@ static int parse_reparse_symlink(struct reparse_symlink_data_buffer *sym,
> > le32_to_cpu(sym->Flags) & SYMLINK_FLAG_RELATIVE,
> > full_path,
> > cifs_sb);
> > + if (!rc) {
> > + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> > + rc = smb2_fix_symlink_target_type(&data->symlink_target, directory);
> > + }
> > return rc;
> > }
> >
> > diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
> > index dc52995f5591..8a1a1b2a1c81 100644
> > --- a/fs/smb/client/smb2file.c
> > +++ b/fs/smb/client/smb2file.c
> > @@ -63,6 +63,48 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
> > return sym;
> > }
> >
> > +int smb2_fix_symlink_target_type(char **target, bool directory)
> > +{
> > + char *buf;
> > + int len = strlen(*target);
> > +
> > + if (!len)
> > + return -EIO;
> > +
> > + /*
> > + * If this is directory symlink and it does not have trailing slash then
> > + * append it. Trailing slash simulates Windows/SMB behavior which do not
> > + * allow resolving directory symlink to file.
> > + */
> > + if (directory && (*target)[len-1] != '/') {
> > + buf = kzalloc(len+2, GFP_KERNEL);
> > + if (!buf)
> > + return -ENOMEM;
> > + memcpy(buf, *target, len);
> > + buf[len] = '/';
> > + kfree(*target);
> > + *target = buf;
> > + }
> > +
> > + /*
> > + * If this is a symlink which points to file name with trailing slash,
> > + * or to file named "." or file named ".." then this symlink cannot be
> > + * resolved on Linux because Linux does not allow files with such names.
> > + * So return an error to prevent resolving this file type symlink to
> > + * directory, as it do not point to directory at all.
> > + */
> > + if (!directory) {
> > + const char *basename = kbasename(*target);
> > + int basename_len = strlen(basename);
> > + if (basename_len == 0 || /* symname ends with slash */
> > + (basename_len == 1 && basename[0] == '.') || /* last component is "." */
> > + (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) /* last component is ".." */
> > + return -EIO;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov,
> > const char *full_path, char **path)
> > {
> > @@ -132,6 +174,10 @@ int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32
> > rc = SMB2_open(xid, oparms, smb2_path, &smb2_oplock, smb2_data,
> > NULL, NULL, NULL);
> > oparms->create_options &= ~OPEN_REPARSE_POINT;
> > + if (!rc) {
> > + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> > + rc = smb2_fix_symlink_target_type(&data->symlink_target, directory);
> > + }
> > }
> > }
> > }
> > diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
> > index c9cdac7d2d50..faf0a8344faa 100644
> > --- a/fs/smb/client/smb2inode.c
> > +++ b/fs/smb/client/smb2inode.c
> > @@ -960,6 +960,10 @@ int smb2_query_path_info(const unsigned int xid,
> > rc = smb2_compound_op(xid, tcon, cifs_sb, full_path,
> > &oparms, in_iov, cmds, num_cmds,
> > cfile, NULL, NULL, NULL);
> > + if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
> > + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> > + rc = smb2_fix_symlink_target_type(&data->symlink_target, directory);
> > + }
> > break;
> > case -EREMOTE:
> > break;
> > diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
> > index 11cef65fa831..d308f3c2f8df 100644
> > --- a/fs/smb/client/smb2proto.h
> > +++ b/fs/smb/client/smb2proto.h
> > @@ -113,6 +113,7 @@ extern int smb3_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
> > struct cifs_sb_info *cifs_sb,
> > const unsigned char *path, char *pbuf,
> > unsigned int *pbytes_read);
> > +int smb2_fix_symlink_target_type(char **target, bool directory);
> > int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> > bool unicode, bool relative,
> > const char *full_path,
> > --
> > 2.20.1
> >
> >
>
>
> --
> Thanks,
>
> Steve
^ permalink raw reply [flat|nested] 38+ messages in thread
* [PATCH 6/7] cifs: Validate content of native symlink
2024-09-29 18:50 [PATCH 0/7] cifs: Improve support for native SMB symlinks Pali Rohár
` (4 preceding siblings ...)
2024-09-29 18:50 ` [PATCH 5/7] cifs: Fix parsing native symlinks directory/file type Pali Rohár
@ 2024-09-29 18:50 ` Pali Rohár
2024-09-29 21:48 ` Steve French
2024-09-29 18:50 ` [PATCH 7/7] cifs: Fix creating and resolving absolute NT-style symlinks Pali Rohár
` (2 subsequent siblings)
8 siblings, 1 reply; 38+ messages in thread
From: Pali Rohár @ 2024-09-29 18:50 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
Check that buffer does not contain UTF-16 null codepoint
because Linux cannot process symlink with null byte.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/reparse.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
index 5a738f65b190..ca4f96c43508 100644
--- a/fs/smb/client/reparse.c
+++ b/fs/smb/client/reparse.c
@@ -509,6 +509,16 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
int rc;
int i;
+ /*
+ * Check that buffer does not contain UTF-16 null codepoint
+ * because Linux cannot process symlink with null byte.
+ */
+ if (unicode && UniStrnlen((wchar_t *)buf, len/2) != len/2) {
+ cifs_dbg(VFS, "srv returned null byte in native symlink target location\n");
+ rc = -EIO;
+ goto out;
+ }
+
smb_target = cifs_strndup_from_utf16(buf, len, unicode, cifs_sb->local_nls);
if (!smb_target) {
rc = -ENOMEM;
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* Re: [PATCH 6/7] cifs: Validate content of native symlink
2024-09-29 18:50 ` [PATCH 6/7] cifs: Validate content of native symlink Pali Rohár
@ 2024-09-29 21:48 ` Steve French
2024-09-29 22:19 ` Pali Rohár
0 siblings, 1 reply; 38+ messages in thread
From: Steve French @ 2024-09-29 21:48 UTC (permalink / raw)
To: Pali Rohár
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
Is there any easy way to create such a symlink (with null in it)?
On Sun, Sep 29, 2024 at 1:51 PM Pali Rohár <pali@kernel.org> wrote:
>
> Check that buffer does not contain UTF-16 null codepoint
> because Linux cannot process symlink with null byte.
>
> Signed-off-by: Pali Rohár <pali@kernel.org>
> ---
> fs/smb/client/reparse.c | 10 ++++++++++
> 1 file changed, 10 insertions(+)
>
> diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> index 5a738f65b190..ca4f96c43508 100644
> --- a/fs/smb/client/reparse.c
> +++ b/fs/smb/client/reparse.c
> @@ -509,6 +509,16 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> int rc;
> int i;
>
> + /*
> + * Check that buffer does not contain UTF-16 null codepoint
> + * because Linux cannot process symlink with null byte.
> + */
> + if (unicode && UniStrnlen((wchar_t *)buf, len/2) != len/2) {
> + cifs_dbg(VFS, "srv returned null byte in native symlink target location\n");
> + rc = -EIO;
> + goto out;
> + }
> +
> smb_target = cifs_strndup_from_utf16(buf, len, unicode, cifs_sb->local_nls);
> if (!smb_target) {
> rc = -ENOMEM;
> --
> 2.20.1
>
>
--
Thanks,
Steve
^ permalink raw reply [flat|nested] 38+ messages in thread* Re: [PATCH 6/7] cifs: Validate content of native symlink
2024-09-29 21:48 ` Steve French
@ 2024-09-29 22:19 ` Pali Rohár
2024-09-30 20:00 ` Pali Rohár
0 siblings, 1 reply; 38+ messages in thread
From: Pali Rohár @ 2024-09-29 22:19 UTC (permalink / raw)
To: Steve French
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
I think that via pike it could be possible or via windows application
running locally (to create reparse point manually with prepared buffer
with such content). I will check it later.
Just a side note: Windows NT kernel allows for object names any
characters except backslash. For object names is not used nul-term
string, but rather string with explicit length. So even a null character
is a valid in a object name. NT NTFS driver has for file names more
restrictions and null is not valid. But it does not mean that somebody
can write own filesystem which allows null bytes in file names...
And this design of explicit lengths is also in SMB, so NT kernel may
export nul characters in symlink path buffers...
On Sunday 29 September 2024 16:48:46 Steve French wrote:
> Is there any easy way to create such a symlink (with null in it)?
>
> On Sun, Sep 29, 2024 at 1:51 PM Pali Rohár <pali@kernel.org> wrote:
> >
> > Check that buffer does not contain UTF-16 null codepoint
> > because Linux cannot process symlink with null byte.
> >
> > Signed-off-by: Pali Rohár <pali@kernel.org>
> > ---
> > fs/smb/client/reparse.c | 10 ++++++++++
> > 1 file changed, 10 insertions(+)
> >
> > diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> > index 5a738f65b190..ca4f96c43508 100644
> > --- a/fs/smb/client/reparse.c
> > +++ b/fs/smb/client/reparse.c
> > @@ -509,6 +509,16 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> > int rc;
> > int i;
> >
> > + /*
> > + * Check that buffer does not contain UTF-16 null codepoint
> > + * because Linux cannot process symlink with null byte.
> > + */
> > + if (unicode && UniStrnlen((wchar_t *)buf, len/2) != len/2) {
> > + cifs_dbg(VFS, "srv returned null byte in native symlink target location\n");
> > + rc = -EIO;
> > + goto out;
> > + }
> > +
> > smb_target = cifs_strndup_from_utf16(buf, len, unicode, cifs_sb->local_nls);
> > if (!smb_target) {
> > rc = -ENOMEM;
> > --
> > 2.20.1
> >
> >
>
>
> --
> Thanks,
>
> Steve
^ permalink raw reply [flat|nested] 38+ messages in thread* Re: [PATCH 6/7] cifs: Validate content of native symlink
2024-09-29 22:19 ` Pali Rohár
@ 2024-09-30 20:00 ` Pali Rohár
0 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-09-30 20:00 UTC (permalink / raw)
To: Steve French
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
[-- Attachment #1: Type: text/plain, Size: 2733 bytes --]
Now I tested it. FSCTL_SET_REPARSE_POINT ioctl call on Windows accepts
also symlink path with zero characters. In attachment I'm sending simple
program which creates Windows relative symlink and accepts binary target
path via \xFF sequence. You can compile it with gcc/mingw option -municode.
So calling "set_reparse_symlink.exe symlink file\x00file" creates new
symlink which points to target path "file<nul>file".
On Monday 30 September 2024 00:19:08 Pali Rohár wrote:
> I think that via pike it could be possible or via windows application
> running locally (to create reparse point manually with prepared buffer
> with such content). I will check it later.
>
> Just a side note: Windows NT kernel allows for object names any
> characters except backslash. For object names is not used nul-term
> string, but rather string with explicit length. So even a null character
> is a valid in a object name. NT NTFS driver has for file names more
> restrictions and null is not valid. But it does not mean that somebody
> can write own filesystem which allows null bytes in file names...
> And this design of explicit lengths is also in SMB, so NT kernel may
> export nul characters in symlink path buffers...
>
> On Sunday 29 September 2024 16:48:46 Steve French wrote:
> > Is there any easy way to create such a symlink (with null in it)?
> >
> > On Sun, Sep 29, 2024 at 1:51 PM Pali Rohár <pali@kernel.org> wrote:
> > >
> > > Check that buffer does not contain UTF-16 null codepoint
> > > because Linux cannot process symlink with null byte.
> > >
> > > Signed-off-by: Pali Rohár <pali@kernel.org>
> > > ---
> > > fs/smb/client/reparse.c | 10 ++++++++++
> > > 1 file changed, 10 insertions(+)
> > >
> > > diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> > > index 5a738f65b190..ca4f96c43508 100644
> > > --- a/fs/smb/client/reparse.c
> > > +++ b/fs/smb/client/reparse.c
> > > @@ -509,6 +509,16 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> > > int rc;
> > > int i;
> > >
> > > + /*
> > > + * Check that buffer does not contain UTF-16 null codepoint
> > > + * because Linux cannot process symlink with null byte.
> > > + */
> > > + if (unicode && UniStrnlen((wchar_t *)buf, len/2) != len/2) {
> > > + cifs_dbg(VFS, "srv returned null byte in native symlink target location\n");
> > > + rc = -EIO;
> > > + goto out;
> > > + }
> > > +
> > > smb_target = cifs_strndup_from_utf16(buf, len, unicode, cifs_sb->local_nls);
> > > if (!smb_target) {
> > > rc = -ENOMEM;
> > > --
> > > 2.20.1
> > >
> > >
> >
> >
> > --
> > Thanks,
> >
> > Steve
[-- Attachment #2: set_reparse_symlink.c --]
[-- Type: text/x-csrc, Size: 3199 bytes --]
#include <stdio.h>
#include <windows.h>
#include <ntdef.h>
int wmain(int argc, wchar_t *argv[]) {
static BYTE reparse_data_buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
REPARSE_DATA_BUFFER *reparse_buffer = (REPARSE_DATA_BUFFER *)reparse_data_buffer;
DWORD reparse_buffer_length;
TOKEN_PRIVILEGES privileges;
DWORD target_length;
wchar_t *target;
HANDLE handle;
HANDLE token;
BOOL success;
DWORD i;
if (argc != 3) {
printf("Usage: %ls new_file target\n", argv[0]);
return 1;
}
target = argv[2];
for (i = 0, target_length = 0; target[i]; i++, target_length++) {
if (target[i] == L'\\') {
if (target[i+1] == L'\\') {
target[target_length] = L'\\';
i++;
continue;
} else if (target[i+1] == L'x' && target[i+2] >= L'0' && target[i+2] <= L'9' && target[i+3] >= L'0' && target[i+3] <= L'9') {
target[target_length] = ((target[i+2]-L'0') << 4) | (target[i+3]-L'0');
i += 3;
continue;
}
}
target[target_length] = target[i];
}
target_length *= sizeof(target[0]);
reparse_buffer_length = FIELD_OFFSET(REPARSE_DATA_BUFFER, SymbolicLinkReparseBuffer.PathBuffer) + target_length*2;
if (reparse_buffer_length > MAXIMUM_REPARSE_DATA_BUFFER_SIZE) {
printf("Target path is too long\n");
return 1;
}
reparse_buffer->ReparseTag = IO_REPARSE_TAG_SYMLINK;
reparse_buffer->ReparseDataLength = reparse_buffer_length - REPARSE_DATA_BUFFER_HEADER_SIZE;
reparse_buffer->SymbolicLinkReparseBuffer.SubstituteNameOffset = 0;
reparse_buffer->SymbolicLinkReparseBuffer.SubstituteNameLength = target_length;
reparse_buffer->SymbolicLinkReparseBuffer.PrintNameOffset = target_length;
reparse_buffer->SymbolicLinkReparseBuffer.PrintNameLength = target_length;
reparse_buffer->SymbolicLinkReparseBuffer.Flags = SYMLINK_FLAG_RELATIVE;
memcpy(reparse_buffer->SymbolicLinkReparseBuffer.PathBuffer, target, target_length);
memcpy(((BYTE*)reparse_buffer->SymbolicLinkReparseBuffer.PathBuffer)+target_length, target, target_length);
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token)) {
if (LookupPrivilegeValue(NULL, SE_CREATE_SYMBOLIC_LINK_NAME, &privileges.Privileges[0].Luid)) {
privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
privileges.PrivilegeCount = 1;
AdjustTokenPrivileges(token, FALSE, &privileges, sizeof(privileges), NULL, NULL);
}
CloseHandle(token);
}
handle = CreateFileW(argv[1], FILE_WRITE_DATA, FILE_SHARE_VALID_FLAGS, NULL, OPEN_ALWAYS, FILE_FLAG_OPEN_REPARSE_POINT, NULL);
if (handle == INVALID_HANDLE_VALUE && GetLastError() == ERROR_ACCESS_DENIED)
handle = CreateFileW(argv[1], FILE_WRITE_DATA, FILE_SHARE_VALID_FLAGS, NULL, OPEN_ALWAYS, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (handle == INVALID_HANDLE_VALUE) {
printf("CreateFileW failed: %lu\n", GetLastError());
return 1;
}
success = DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, reparse_buffer, reparse_buffer_length, NULL, 0, NULL, NULL);
CloseHandle(handle);
if (!success) {
printf("FSCTL_SET_REPARSE_POINT failed: %lu\n", GetLastError());
return 1;
}
return 0;
}
^ permalink raw reply [flat|nested] 38+ messages in thread
* [PATCH 7/7] cifs: Fix creating and resolving absolute NT-style symlinks
2024-09-29 18:50 [PATCH 0/7] cifs: Improve support for native SMB symlinks Pali Rohár
` (5 preceding siblings ...)
2024-09-29 18:50 ` [PATCH 6/7] cifs: Validate content of native symlink Pali Rohár
@ 2024-09-29 18:50 ` Pali Rohár
2024-09-29 22:03 ` [PATCH 0/7] cifs: Improve support for native SMB symlinks Steve French
2024-10-05 14:02 ` [PATCH v2 0/6] " Pali Rohár
8 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-09-29 18:50 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
If the SMB symlink is stored on NT server in absolute form then it points
to the NT object hierarchy, which is different from POSIX one and needs
some conversion / mapping.
To make interoperability with Windows SMB server and WSL subsystem, reuse
its logic of mapping between NT paths and POSIX paths into Linux SMB
client.
WSL subsystem on Windows uses for -t drvfs mount option -o symlinkroot=
which specifies the POSIX path where are expected to be mounted lowercase
Windows drive letters (without colon).
Do same for Linux SMB client and add a new mount option -o symlinkroot=
which mimics the drvfs mount option of the same name. It specifies where in
the Linux VFS hierarchy is the root of the DOS / Windows drive letters, and
translates between absolute NT-style symlinks and absolute Linux VFS
symlinks. Default value of symlinkroot is "/mnt", same what is using WSL.
Note that DOS / Windows drive letter symlinks are just subset of all
possible NT-style symlinks. Drive letters live in NT subtree \??\ and
important details about NT paths and object hierarchy are in the comments
in this change.
When symlink target location from non-POSIX SMB server is in absolute form
(indicated by absence of SYMLINK_FLAG_RELATIVE) then it is converted to
Linux absolute symlink according to symlinkroot configuration.
And when creating a new symlink on non-POSIX SMB server in absolute form
then Linux absolute target is converted to NT-style according to
symlinkroot configuration.
When SMB server is POSIX, then this change does not affect neither reading
target location of symlink, nor creating a new symlink. It is expected that
POSIX SMB server works with POSIX paths where the absolute root is /.
This change improves interoperability of absolute SMB symlinks with Windows
SMB servers.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/fs_context.c | 22 +++
fs/smb/client/fs_context.h | 2 +
fs/smb/client/reparse.c | 276 ++++++++++++++++++++++++++++++++++---
3 files changed, 281 insertions(+), 19 deletions(-)
diff --git a/fs/smb/client/fs_context.c b/fs/smb/client/fs_context.c
index 2f0c3894b0f7..22b550860cc8 100644
--- a/fs/smb/client/fs_context.c
+++ b/fs/smb/client/fs_context.c
@@ -178,6 +178,7 @@ const struct fs_parameter_spec smb3_fs_parameters[] = {
fsparam_string("sec", Opt_sec),
fsparam_string("cache", Opt_cache),
fsparam_string("reparse", Opt_reparse),
+ fsparam_string("symlinkroot", Opt_symlinkroot),
/* Arguments that should be ignored */
fsparam_flag("guest", Opt_ignore),
@@ -355,6 +356,7 @@ smb3_fs_context_dup(struct smb3_fs_context *new_ctx, struct smb3_fs_context *ctx
new_ctx->source = NULL;
new_ctx->iocharset = NULL;
new_ctx->leaf_fullpath = NULL;
+ new_ctx->symlinkroot = NULL;
/*
* Make sure to stay in sync with smb3_cleanup_fs_context_contents()
*/
@@ -369,6 +371,7 @@ smb3_fs_context_dup(struct smb3_fs_context *new_ctx, struct smb3_fs_context *ctx
DUP_CTX_STR(nodename);
DUP_CTX_STR(iocharset);
DUP_CTX_STR(leaf_fullpath);
+ DUP_CTX_STR(symlinkroot);
return 0;
}
@@ -1614,9 +1617,26 @@ static int smb3_fs_context_parse_param(struct fs_context *fc,
if (parse_reparse_flavor(fc, param->string, ctx))
goto cifs_parse_mount_err;
break;
+ case Opt_symlinkroot:
+ if (param->string[0] != '/') {
+ cifs_errorf(fc, "symlinkroot mount options must be absolute path\n");
+ goto cifs_parse_mount_err;
+ }
+ kfree(ctx->symlinkroot);
+ ctx->symlinkroot = kstrdup(param->string, GFP_KERNEL);
+ if (!ctx->symlinkroot)
+ goto cifs_parse_mount_err;
+ break;
}
/* case Opt_ignore: - is ignored as expected ... */
+ /*
+ * By default resolve all native absolute symlinks relative to "/mnt/".
+ * Same default has drvfs driver running in WSL for resolving SMB shares.
+ */
+ if (!ctx->symlinkroot)
+ ctx->symlinkroot = kstrdup("/mnt/", GFP_KERNEL);
+
return 0;
cifs_parse_mount_err:
@@ -1747,6 +1767,8 @@ smb3_cleanup_fs_context_contents(struct smb3_fs_context *ctx)
ctx->prepath = NULL;
kfree(ctx->leaf_fullpath);
ctx->leaf_fullpath = NULL;
+ kfree(ctx->symlinkroot);
+ ctx->symlinkroot = NULL;
}
void
diff --git a/fs/smb/client/fs_context.h b/fs/smb/client/fs_context.h
index cf577ec0dd0a..8dd12498ffd8 100644
--- a/fs/smb/client/fs_context.h
+++ b/fs/smb/client/fs_context.h
@@ -157,6 +157,7 @@ enum cifs_param {
Opt_sec,
Opt_cache,
Opt_reparse,
+ Opt_symlinkroot,
/* Mount options to be ignored */
Opt_ignore,
@@ -284,6 +285,7 @@ struct smb3_fs_context {
struct cifs_ses *dfs_root_ses;
bool dfs_automount:1; /* set for dfs automount only */
enum cifs_reparse_type reparse_type;
+ char *symlinkroot; /* top level directory for native SMB symlinks in absolute format */
};
extern const struct fs_parameter_spec smb3_fs_parameters[];
diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
index ca4f96c43508..5845dfde950b 100644
--- a/fs/smb/client/reparse.c
+++ b/fs/smb/client/reparse.c
@@ -19,35 +19,137 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
const char *full_path, const char *symname)
{
struct reparse_symlink_data_buffer *buf = NULL;
- struct cifs_open_info_data data;
+ struct cifs_open_info_data data = {};
struct cifs_sb_info *cifs_sb = CIFS_SB(inode->i_sb);
struct inode *new;
struct kvec iov;
- __le16 *path;
+ char *sym = NULL;
+ __le16 *path = NULL;
bool directory = false;
- char *sym, sep = CIFS_DIR_SEP(cifs_sb);
- u16 len, plen;
+ char *symlink_target = NULL;
+ char sep = CIFS_DIR_SEP(cifs_sb);
+ u16 len, plen, poff, slen;
int rc = 0;
len = strlen(symname)+1;
- sym = kzalloc(len+1, GFP_KERNEL); /* +1 for possible directory slash */
- if (!sym)
- return -ENOMEM;
- memcpy(sym, symname, len);
+ symlink_target = kzalloc(len+1, GFP_KERNEL); /* +1 for possible directory slash */
+ if (!symlink_target) {
+ rc = -ENOMEM;
+ goto out;
+ }
+ memcpy(symlink_target, symname, len);
data = (struct cifs_open_info_data) {
.reparse_point = true,
.reparse = { .tag = IO_REPARSE_TAG_SYMLINK, },
- .symlink_target = sym,
+ .symlink_target = symlink_target,
};
- convert_delimiter(sym, sep);
+ if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
+ /*
+ * This is a request to create an absolute symlink on the server
+ * which does not support POSIX paths, and expects symlink in
+ * NT-style path. So convert absolute Linux symlink target path
+ * to the absolute NT-style path. Root of the NT-style path for
+ * symlinks is specified in "symlinkroot" mount option. This will
+ * ensure compatibility of this symlink stored in absolute form
+ * on the SMB server.
+ */
+ if (!strstarts(symname, cifs_sb->ctx->symlinkroot)) {
+ /*
+ * If the absolute Linux symlink target path is not
+ * inside "symlinkroot" location then there is no way
+ * to convert such Linux symlink to NT-style path.
+ */
+ cifs_dbg(VFS,
+ "absolute symlink '%s' cannot be converted to NT format "
+ "because it is outside of symlinkroot='%s'\n",
+ symname, cifs_sb->ctx->symlinkroot);
+ rc = -EINVAL;
+ goto out;
+ }
+ len = strlen(cifs_sb->ctx->symlinkroot);
+ if (cifs_sb->ctx->symlinkroot[len-1] != '/')
+ len++;
+ if (symname[len] >= 'a' && symname[len] <= 'z' &&
+ (symname[len+1] == '/' || symname[len+1] == '\0')) {
+ /*
+ * Symlink points to Linux target /symlinkroot/x/path/...
+ * where 'x' is the lowercase local Windows drive.
+ * NT-style path for 'x' has common form \??\X:\path\...
+ * with uppercase local Windows drive.
+ */
+ int common_path_len = strlen(symname+len+1)+1;
+ sym = kzalloc(6+common_path_len, GFP_KERNEL);
+ if (!sym) {
+ rc = -ENOMEM;
+ goto out;
+ }
+ memcpy(sym, "\\??\\", 4);
+ sym[4] = symname[len] - ('a'-'A');
+ sym[5] = ':';
+ memcpy(sym+6, symname+len+1, common_path_len);
+ if (sym[6] == '\0') {
+ /*
+ * Symlink which target path is just a Windows
+ * drive letter is definitely directory symlink.
+ */
+ directory = true;
+ }
+ } else {
+ /* Unhandled absolute symlink. Report an error. */
+ cifs_dbg(
+ VFS,
+ "absolute symlink '%s' cannot be converted to NT format "
+ "because it points to unknown target\n",
+ symname);
+ rc = -EINVAL;
+ goto out;
+ }
+ } else {
+ /*
+ * This is request to either create an absolute symlink on
+ * server which expects POSIX paths or it is an request to
+ * create a relative symlink from the current directory.
+ * These paths have same format as relative SMB symlinks,
+ * so no conversion is needed. So just take symname as-is.
+ */
+ sym = kstrdup(symname, GFP_KERNEL);
+ if (!sym) {
+ rc = -ENOMEM;
+ goto out;
+ }
+ }
+
+ if (sep == '\\')
+ convert_delimiter(sym, sep);
+
+ /*
+ * For absolute NT symlinks it is required to pass also leading
+ * backslash and to not mangle NT object prefix "\\??\\" and not to
+ * mangle colon in drive letter. But cifs_convert_path_to_utf16()
+ * removes leading backslash and replaces '?' and ':'. So temporary
+ * mask these characters in NT object prefix by '_' and then change
+ * them back.
+ */
+ if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/')
+ sym[0] = sym[1] = sym[2] = sym[5] = '_';
+
path = cifs_convert_path_to_utf16(sym, cifs_sb);
if (!path) {
rc = -ENOMEM;
goto out;
}
+ if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
+ sym[0] = '\\';
+ sym[1] = sym[2] = '?';
+ sym[5] = ':';
+ path[0] = '\\';
+ path[1] = path[2] = '?';
+ path[5] = ':';
+ }
+
/*
* SMB distinguish between symlink to directory and symlink to file.
* They cannot be exchanged (symlink of file type which points to
@@ -167,8 +269,18 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
data.symlink_target[len+1] = '\0';
}
- plen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
- len = sizeof(*buf) + plen * 2;
+ slen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
+ poff = 0;
+ plen = slen;
+ if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
+ /*
+ * For absolute NT symlinks skip leading "\\??\\" in PrintName as
+ * PrintName is user visible location in DOS/Win32 format (not in NT format).
+ */
+ poff = 4;
+ plen -= 2 * poff;
+ }
+ len = sizeof(*buf) + plen + slen;
buf = kzalloc(len, GFP_KERNEL);
if (!buf) {
rc = -ENOMEM;
@@ -177,17 +289,17 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
buf->ReparseTag = cpu_to_le32(IO_REPARSE_TAG_SYMLINK);
buf->ReparseDataLength = cpu_to_le16(len - sizeof(struct reparse_data_buffer));
+
buf->SubstituteNameOffset = cpu_to_le16(plen);
- buf->SubstituteNameLength = cpu_to_le16(plen);
- memcpy(&buf->PathBuffer[plen], path, plen);
+ buf->SubstituteNameLength = cpu_to_le16(slen);
+ memcpy(&buf->PathBuffer[plen], path, slen);
+
buf->PrintNameOffset = 0;
buf->PrintNameLength = cpu_to_le16(plen);
- memcpy(buf->PathBuffer, path, plen);
+ memcpy(buf->PathBuffer, path+poff, plen);
+
buf->Flags = cpu_to_le32(*symname != '/' ? SYMLINK_FLAG_RELATIVE : 0);
- if (*sym != sep)
- buf->Flags = cpu_to_le32(SYMLINK_FLAG_RELATIVE);
- convert_delimiter(sym, '/');
iov.iov_base = buf;
iov.iov_len = len;
new = smb2_create_reparse_inode(&data, inode->i_sb, xid,
@@ -198,6 +310,7 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
else
rc = PTR_ERR(new);
out:
+ kfree(sym);
kfree(path);
cifs_free_open_info(&data);
kfree(buf);
@@ -505,6 +618,9 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
char sep = CIFS_DIR_SEP(cifs_sb);
char *linux_target = NULL;
char *smb_target = NULL;
+ int symlinkroot_len;
+ int abs_path_len;
+ char *abs_path;
int levels;
int rc;
int i;
@@ -525,7 +641,123 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
goto out;
}
- if (smb_target[0] == sep && relative) {
+ if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && !relative) {
+ /*
+ * This is an absolute symlink from the server which does not
+ * support POSIX paths, so the symlink is in NT-style path.
+ * So convert it to absolute Linux symlink target path. Root of
+ * the NT-style path for symlinks is specified in "symlinkroot"
+ * mount option.
+ *
+ * Root of the DOS and Win32 paths is at NT path \??\
+ * It means that DOS/Win32 path C:\folder\file.txt is
+ * NT path \??\C:\folder\file.txt
+ *
+ * NT systems have some well-known object symlinks in their NT
+ * hierarchy, which is needed to take into account when resolving
+ * other symlinks. Most commonly used symlink paths are:
+ * \?? -> \GLOBAL??
+ * \DosDevices -> \??
+ * \GLOBAL??\GLOBALROOT -> \
+ * \GLOBAL??\Global -> \GLOBAL??
+ * \GLOBAL??\NUL -> \Device\Null
+ * \GLOBAL??\UNC -> \Device\Mup
+ * \GLOBAL??\PhysicalDrive0 -> \Device\Harddisk0\DR0 (for each harddisk)
+ * \GLOBAL??\A: -> \Device\Floppy0 (if A: is the first floppy)
+ * \GLOBAL??\C: -> \Device\HarddiskVolume1 (if C: is the first harddisk)
+ * \GLOBAL??\D: -> \Device\CdRom0 (if D: is first cdrom)
+ * \SystemRoot -> \Device\Harddisk0\Partition1\WINDOWS (or where is NT system installed)
+ * \Volume{...} -> \Device\HarddiskVolume1 (where ... is system generated guid)
+ *
+ * In most common cases, absolute NT symlinks points to path on
+ * DOS/Win32 drive letter, system-specific Volume or on UNC share.
+ * Here are few examples of commonly used absolute NT symlinks
+ * created by mklink.exe tool:
+ * \??\C:\folder\file.txt
+ * \??\\C:\folder\file.txt
+ * \??\UNC\server\share\file.txt
+ * \??\\UNC\server\share\file.txt
+ * \??\Volume{b75e2c83-0000-0000-0000-602f00000000}\folder\file.txt
+ *
+ * It means that the most common path prefix \??\ is also NT path
+ * symlink (to \GLOBAL??). It is less common that second path
+ * separator is double backslash, but it is valid.
+ *
+ * Volume guid is randomly generated by the target system and so
+ * only the target system knows the mapping between guid and the
+ * hardisk number. Over SMB it is not possible to resolve this
+ * mapping, therefore symlinks pointing to target location of
+ * volume guids are totally unusable over SMB.
+ *
+ * For now parse only symlink paths available for DOS and Win32.
+ * Those are paths with \??\ prefix or paths which points to \??\
+ * via other NT symlink (\DosDevices\, \GLOBAL??\, ...).
+ */
+ abs_path = smb_target;
+globalroot:
+ if (strstarts(abs_path, "\\??\\"))
+ abs_path += sizeof("\\??\\")-1;
+ else if (strstarts(abs_path, "\\DosDevices\\"))
+ abs_path += sizeof("\\DosDevices\\")-1;
+ else if (strstarts(abs_path, "\\GLOBAL??\\"))
+ abs_path += sizeof("\\GLOBAL??\\")-1;
+ else {
+ /* Unhandled absolute symlink, points outside of DOS/Win32 */
+ cifs_dbg(VFS,
+ "absolute symlink '%s' cannot be converted from NT format "
+ "because points to unknown target\n",
+ smb_target);
+ rc = -EIO;
+ goto out;
+ }
+
+ /* Sometimes path separator after \?? is double backslash */
+ if (abs_path[0] == '\\')
+ abs_path++;
+
+ while (strstarts(abs_path, "Global\\"))
+ abs_path += sizeof("Global\\")-1;
+
+ if (strstarts(abs_path, "GLOBALROOT\\")) {
+ /* Label globalroot requires path with leading '\\', so do not trim '\\' */
+ abs_path += sizeof("GLOBALROOT")-1;
+ goto globalroot;
+ }
+
+ /* For now parse only paths to drive letters */
+ if (((abs_path[0] >= 'A' && abs_path[0] <= 'Z') ||
+ (abs_path[0] >= 'a' && abs_path[0] <= 'z')) &&
+ abs_path[1] == ':' &&
+ (abs_path[2] == '\\' || abs_path[2] == '\0')) {
+ /* Convert drive letter to lowercase and drop colon */
+ char drive_letter = abs_path[0];
+ if (drive_letter >= 'A' && drive_letter <= 'Z')
+ drive_letter += 'a'-'A';
+ abs_path++;
+ abs_path[0] = drive_letter;
+ } else {
+ /* Unhandled absolute symlink. Report an error. */
+ cifs_dbg(VFS,
+ "absolute symlink '%s' cannot be converted from NT format "
+ "because points to unknown target\n",
+ smb_target);
+ rc = -EIO;
+ goto out;
+ }
+
+ abs_path_len = strlen(abs_path)+1;
+ symlinkroot_len = strlen(cifs_sb->ctx->symlinkroot);
+ if (cifs_sb->ctx->symlinkroot[symlinkroot_len-1] == '/')
+ symlinkroot_len--;
+ linux_target = kmalloc(symlinkroot_len + 1 + abs_path_len, GFP_KERNEL);
+ if (!linux_target) {
+ rc = -ENOMEM;
+ goto out;
+ }
+ memcpy(linux_target, cifs_sb->ctx->symlinkroot, symlinkroot_len);
+ linux_target[symlinkroot_len] = '/';
+ memcpy(linux_target + symlinkroot_len + 1, abs_path, abs_path_len);
+ } else if (smb_target[0] == sep && relative) {
/*
* This is a relative SMB symlink from the top of the share,
* which is the top level directory of the Linux mount point.
@@ -554,6 +786,12 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
}
memcpy(linux_target + levels*3, smb_target+1, smb_target_len); /* +1 to skip leading sep */
} else {
+ /*
+ * This is either an absolute symlink in POSIX-style format
+ * or relative SMB symlink from the current directory.
+ * These paths have same format as Linux symlinks, so no
+ * conversion is needed.
+ */
linux_target = smb_target;
smb_target = NULL;
}
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* Re: [PATCH 0/7] cifs: Improve support for native SMB symlinks
2024-09-29 18:50 [PATCH 0/7] cifs: Improve support for native SMB symlinks Pali Rohár
` (6 preceding siblings ...)
2024-09-29 18:50 ` [PATCH 7/7] cifs: Fix creating and resolving absolute NT-style symlinks Pali Rohár
@ 2024-09-29 22:03 ` Steve French
2024-10-05 14:07 ` Pali Rohár
2024-10-05 14:02 ` [PATCH v2 0/6] " Pali Rohár
8 siblings, 1 reply; 38+ messages in thread
From: Steve French @ 2024-09-29 22:03 UTC (permalink / raw)
To: Pali Rohár
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
Merged patches 2, 3, 4 and 6 from this series into cifs-2.6.git for-next
For patch 1, seems like rename could get in the way of backports, and
is only a minor improvement (could consider it in the future for
6.13-rc if part of other cleanup/improvements, but lower priority)
For patch 5 it could be ok - but wanted more opinions on that ...
since wouldn't want it to break POSIX - e.g. what would happen with
this patch if a Linux server changed the target of a symlink from a
file to directory (or the other way around) - is there any risk of
breaking POSIX symlink semantics in a pure Linux->Linux case with this
patch?
For patch 7, it is relatively larger fix (and less important for pure
Linux workloads) so wanted some additional opinions/feedback on this
from others
On Sun, Sep 29, 2024 at 1:51 PM Pali Rohár <pali@kernel.org> wrote:
>
> This patch series improves interoperability of native SMB symlinks
> (stored in IO_REPARSE_TAG_SYMLINK reparse point) between Linux SMB
> client and Windows SMB server storage (NTFS).
>
> Fixed test cases by this patch series are:
>
> Fixes parsing of symlinks relative to the top of the export which can be
> created on Windows by:
>
> mklink symlink \relative\path\from\export
>
>
> Fixes parsing of symlinks in absolute form which can be created on
> Windows by:
>
> mklink symlink C:\absolute\path
>
>
> Fixes creating of symlinks pointing to directory. So Linux commands:
>
> mkdir dir
> ln -s dir symlink1
> ln -s another_dir/ symlink2
>
> creates a symlink which would be now understood also by Windows as
> symlinks to directories.
>
>
> Fixes creating of symlinks pointing to current or parent directory.
> So following commands create valid symlink understood by Windows:
>
> ln -s . symlink_cur
> ln -s .. symlink_parent
>
>
> Fixes creating of absolute symlinks. Absolute symlinks on Windows are
> quite complicated due to nature of DOS/Win32 path form used by Windows
> applications and NT path form in which are symlink paths stored. Also
> complication is that NT object paths have different hierarchy than POSIX
> paths generally. Required information about NT object hierarchy used in
> native absolute symlinks are in comments in the last patch from this
> series.
>
> To resolve mentioned problems I chosse way which is used by WSL, its
> -t drvfs has mount option -o symlinkroot= which specify Linux path there
> should point absolute windows drive letter symlinks. This could make
> -t cifs mounts in WSL2 more compatible with symlinks coming from local
> NTFS disks mounted by -t drvfs.
>
> I'm not sure how good are these changes, but I think that they improve
> compatibility of symlinks between Linux and Windows systems. Maybe there
> is some better solution how to handle some of those issues?
>
>
> Pali Rohár (7):
> cifs: Rename smb2_get_reparse_inode to smb2_create_reparse_inode
> cifs: Improve creating native symlinks pointing to directory
> cifs: Fix creating native symlinks pointing to current or parent
> directory
> cifs: Fix parsing native symlinks relative to the export
> cifs: Fix parsing native symlinks directory/file type
> cifs: Validate content of native symlink
> cifs: Fix creating and resolving absolute NT-style symlinks
>
> fs/smb/client/cifs_unicode.c | 17 +-
> fs/smb/client/cifsglob.h | 1 +
> fs/smb/client/cifsproto.h | 1 +
> fs/smb/client/fs_context.c | 22 ++
> fs/smb/client/fs_context.h | 2 +
> fs/smb/client/inode.c | 1 +
> fs/smb/client/reparse.c | 513 ++++++++++++++++++++++++++++++++---
> fs/smb/client/reparse.h | 4 +-
> fs/smb/client/smb1ops.c | 3 +-
> fs/smb/client/smb2file.c | 67 ++++-
> fs/smb/client/smb2inode.c | 15 +-
> fs/smb/client/smb2proto.h | 13 +-
> 12 files changed, 602 insertions(+), 57 deletions(-)
>
> --
> 2.20.1
>
>
--
Thanks,
Steve
^ permalink raw reply [flat|nested] 38+ messages in thread* Re: [PATCH 0/7] cifs: Improve support for native SMB symlinks
2024-09-29 22:03 ` [PATCH 0/7] cifs: Improve support for native SMB symlinks Steve French
@ 2024-10-05 14:07 ` Pali Rohár
0 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-10-05 14:07 UTC (permalink / raw)
To: Steve French
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
On Sunday 29 September 2024 17:03:31 Steve French wrote:
> For patch 5 it could be ok - but wanted more opinions on that ...
> since wouldn't want it to break POSIX - e.g. what would happen with
> this patch if a Linux server changed the target of a symlink from a
> file to directory (or the other way around) - is there any risk of
> breaking POSIX symlink semantics in a pure Linux->Linux case with this
> patch?
I reworked the "cifs: Fix parsing native symlinks directory/file type"
patch to not change anything when server announce POSIX extension.
So V2 version of that patch would be active only for non-POSIX SMB
servers, like Windows SMB.
I hope that this would address all possible problems (not sure if there
are any) with Linux client <-> POSIX SMB server scenarios.
^ permalink raw reply [flat|nested] 38+ messages in thread
* [PATCH v2 0/6] cifs: Improve support for native SMB symlinks
2024-09-29 18:50 [PATCH 0/7] cifs: Improve support for native SMB symlinks Pali Rohár
` (7 preceding siblings ...)
2024-09-29 22:03 ` [PATCH 0/7] cifs: Improve support for native SMB symlinks Steve French
@ 2024-10-05 14:02 ` Pali Rohár
2024-10-05 14:02 ` [PATCH v2 1/6] cifs: Improve creating native symlinks pointing to directory Pali Rohár
` (5 more replies)
8 siblings, 6 replies; 38+ messages in thread
From: Pali Rohár @ 2024-10-05 14:02 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
This is second version of patch series which improves support for native
SMB symlinks.
Changes since v1:
- Patch which renames function was dropped
- Patch "Improve creating native symlinks pointing to directory" address
issues pointed by Paulo Alcantara
- Patch "Fix parsing native symlinks directory/file type" was changed to
apply fixups only for non-POSIX servers (Windows SMB/NT)
- Patch "Validate content of native symlink" was improved to check also
UTF-16 buffer length of symlink
Pali Rohár (6):
cifs: Improve creating native symlinks pointing to directory
cifs: Fix creating native symlinks pointing to current or parent
directory
cifs: Fix parsing native symlinks relative to the export
cifs: Fix parsing native symlinks directory/file type
cifs: Validate content of native symlink
cifs: Fix creating and resolving absolute NT-style symlinks
fs/smb/client/cifs_unicode.c | 17 +-
fs/smb/client/cifsglob.h | 1 +
fs/smb/client/cifsproto.h | 1 +
fs/smb/client/fs_context.c | 22 ++
fs/smb/client/fs_context.h | 2 +
fs/smb/client/inode.c | 6 +
fs/smb/client/reparse.c | 538 ++++++++++++++++++++++++++++++++---
fs/smb/client/reparse.h | 4 +-
fs/smb/client/smb1ops.c | 3 +-
fs/smb/client/smb2file.c | 76 ++++-
fs/smb/client/smb2inode.c | 13 +-
fs/smb/client/smb2proto.h | 11 +-
12 files changed, 642 insertions(+), 52 deletions(-)
--
2.20.1
^ permalink raw reply [flat|nested] 38+ messages in thread* [PATCH v2 1/6] cifs: Improve creating native symlinks pointing to directory
2024-10-05 14:02 ` [PATCH v2 0/6] " Pali Rohár
@ 2024-10-05 14:02 ` Pali Rohár
2024-10-05 14:02 ` [PATCH v2 2/6] cifs: Fix creating native symlinks pointing to current or parent directory Pali Rohár
` (4 subsequent siblings)
5 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-10-05 14:02 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
SMB protocol for native symlinks distinguish between symlink to directory
and symlink to file. These two symlink types cannot be exchanged, which
means that symlink of file type pointing to directory cannot be resolved at
all (and vice-versa).
Windows follows this rule for local filesystems (NTFS) and also for SMB.
Linux SMB client currenly creates all native symlinks of file type. Which
means that Windows (and some other SMB clients) cannot resolve symlinks
pointing to directory created by Linux SMB client.
As Linux system does not distinguish between directory and file symlinks,
its API does not provide enough information for Linux SMB client during
creating of native symlinks.
Add some heuristic into the Linux SMB client for choosing the correct
symlink type during symlink creation. Check if the symlink target location
ends with slash, or last path component is dot or dot-dot, and check if the
target location on SMB share exists and is a directory. If at least one
condition is truth then create a new SMB symlink of directory type.
Otherwise create it as file type symlink.
This change improves interoperability with Windows systems. Windows systems
would be able to resolve more SMB symlinks created by Linux SMB client
which points to existing directory.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/reparse.c | 164 +++++++++++++++++++++++++++++++++++++-
fs/smb/client/smb2inode.c | 3 +-
fs/smb/client/smb2proto.h | 1 +
3 files changed, 164 insertions(+), 4 deletions(-)
diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
index 35e8f2e18530..8403e1b94059 100644
--- a/fs/smb/client/reparse.c
+++ b/fs/smb/client/reparse.c
@@ -14,6 +14,12 @@
#include "fs_context.h"
#include "reparse.h"
+static int detect_directory_symlink_target(struct cifs_sb_info *cifs_sb,
+ const unsigned int xid,
+ const char *full_path,
+ const char *symname,
+ bool *directory);
+
int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
struct dentry *dentry, struct cifs_tcon *tcon,
const char *full_path, const char *symname)
@@ -24,6 +30,7 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
struct inode *new;
struct kvec iov;
__le16 *path;
+ bool directory;
char *sym, sep = CIFS_DIR_SEP(cifs_sb);
u16 len, plen;
int rc = 0;
@@ -45,6 +52,18 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
goto out;
}
+ /*
+ * SMB distinguish between symlink to directory and symlink to file.
+ * They cannot be exchanged (symlink of file type which points to
+ * directory cannot be resolved and vice-versa). Try to detect if
+ * the symlink target could be a directory or not. When detection
+ * fails then treat symlink as a file (non-directory) symlink.
+ */
+ directory = false;
+ rc = detect_directory_symlink_target(cifs_sb, xid, full_path, symname, &directory);
+ if (rc < 0)
+ goto out;
+
plen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
len = sizeof(*buf) + plen * 2;
buf = kzalloc(len, GFP_KERNEL);
@@ -69,7 +88,8 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
iov.iov_base = buf;
iov.iov_len = len;
new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
- tcon, full_path, &iov, NULL);
+ tcon, full_path, directory,
+ &iov, NULL);
if (!IS_ERR(new))
d_instantiate(dentry, new);
else
@@ -81,6 +101,144 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
return rc;
}
+static int detect_directory_symlink_target(struct cifs_sb_info *cifs_sb,
+ const unsigned int xid,
+ const char *full_path,
+ const char *symname,
+ bool *directory)
+{
+ char sep = CIFS_DIR_SEP(cifs_sb);
+ struct cifs_open_parms oparms;
+ struct tcon_link *tlink;
+ struct cifs_tcon *tcon;
+ const char *basename;
+ struct cifs_fid fid;
+ char *resolved_path;
+ int full_path_len;
+ int basename_len;
+ int symname_len;
+ char *path_sep;
+ __u32 oplock;
+ int open_rc;
+
+ /*
+ * First do some simple check. If the original Linux symlink target ends
+ * with slash, or last path component is dot or dot-dot then it is for
+ * sure symlink to the directory.
+ */
+ basename = kbasename(symname);
+ basename_len = strlen(basename);
+ if (basename_len == 0 || /* symname ends with slash */
+ (basename_len == 1 && basename[0] == '.') || /* last component is "." */
+ (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) { /* or ".." */
+ *directory = true;
+ return 0;
+ }
+
+ /*
+ * For absolute symlinks it is not possible to determinate
+ * if it should point to directory or file.
+ */
+ if (symname[0] == '/') {
+ cifs_dbg(FYI,
+ "%s: cannot determinate if the symlink target path '%s' "
+ "is directory or not, creating '%s' as file symlink\n",
+ __func__, symname, full_path);
+ return 0;
+ }
+
+ /*
+ * If it was not detected as directory yet and the symlink is relative
+ * then try to resolve the path on the SMB server, check if the path
+ * exists and determinate if it is a directory or not.
+ */
+
+ full_path_len = strlen(full_path);
+ symname_len = strlen(symname);
+
+ tlink = cifs_sb_tlink(cifs_sb);
+ if (IS_ERR(tlink))
+ return PTR_ERR(tlink);
+
+ resolved_path = kzalloc(full_path_len + symname_len + 1, GFP_KERNEL);
+ if (!resolved_path) {
+ cifs_put_tlink(tlink);
+ return -ENOMEM;
+ }
+
+ /*
+ * Compose the resolved SMB symlink path from the SMB full path
+ * and Linux target symlink path.
+ */
+ memcpy(resolved_path, full_path, full_path_len+1);
+ path_sep = strrchr(resolved_path, sep);
+ if (path_sep)
+ path_sep++;
+ else
+ path_sep = resolved_path;
+ memcpy(path_sep, symname, symname_len+1);
+ if (sep == '\\')
+ convert_delimiter(path_sep, sep);
+
+ tcon = tlink_tcon(tlink);
+ oparms = CIFS_OPARMS(cifs_sb, tcon, resolved_path,
+ FILE_READ_ATTRIBUTES, FILE_OPEN, 0, ACL_NO_MODE);
+ oparms.fid = &fid;
+
+ /* Try to open as a directory (NOT_FILE) */
+ oplock = 0;
+ oparms.create_options = cifs_create_options(cifs_sb,
+ CREATE_NOT_FILE | OPEN_REPARSE_POINT);
+ open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
+ if (open_rc == 0) {
+ /* Successful open means that the target path is definitely a directory. */
+ *directory = true;
+ tcon->ses->server->ops->close(xid, tcon, &fid);
+ } else if (open_rc == -ENOTDIR) {
+ /* -ENOTDIR means that the target path is definitely a file. */
+ *directory = false;
+ } else if (open_rc == -ENOENT) {
+ /* -ENOENT means that the target path does not exist. */
+ cifs_dbg(FYI,
+ "%s: symlink target path '%s' does not exist, "
+ "creating '%s' as file symlink\n",
+ __func__, symname, full_path);
+ } else {
+ /* Try to open as a file (NOT_DIR) */
+ oplock = 0;
+ oparms.create_options = cifs_create_options(cifs_sb,
+ CREATE_NOT_DIR | OPEN_REPARSE_POINT);
+ open_rc = tcon->ses->server->ops->open(xid, &oparms, &oplock, NULL);
+ if (open_rc == 0) {
+ /* Successful open means that the target path is definitely a file. */
+ *directory = false;
+ tcon->ses->server->ops->close(xid, tcon, &fid);
+ } else if (open_rc == -EISDIR) {
+ /* -EISDIR means that the target path is definitely a directory. */
+ *directory = true;
+ } else {
+ /*
+ * This code branch is called when we do not have a permission to
+ * open the resolved_path or some other client/process denied
+ * opening the resolved_path.
+ *
+ * TODO: Try to use ops->query_dir_first on the parent directory
+ * of resolved_path, search for basename of resolved_path and
+ * check if the ATTR_DIRECTORY is set in fi.Attributes. In some
+ * case this could work also when opening of the path is denied.
+ */
+ cifs_dbg(FYI,
+ "%s: cannot determinate if the symlink target path '%s' "
+ "is directory or not, creating '%s' as file symlink\n",
+ __func__, symname, full_path);
+ }
+ }
+
+ kfree(resolved_path);
+ cifs_put_tlink(tlink);
+ return 0;
+}
+
static int nfs_set_reparse_buf(struct reparse_posix_data *buf,
mode_t mode, dev_t dev,
struct kvec *iov)
@@ -137,7 +295,7 @@ static int mknod_nfs(unsigned int xid, struct inode *inode,
};
new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
- tcon, full_path, &iov, NULL);
+ tcon, full_path, false, &iov, NULL);
if (!IS_ERR(new))
d_instantiate(dentry, new);
else
@@ -283,7 +441,7 @@ static int mknod_wsl(unsigned int xid, struct inode *inode,
data.wsl.eas_len = len;
new = smb2_get_reparse_inode(&data, inode->i_sb,
- xid, tcon, full_path,
+ xid, tcon, full_path, false,
&reparse_iov, &xattr_iov);
if (!IS_ERR(new))
d_instantiate(dentry, new);
diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
index a6dab60e2c01..cdb0e028e73c 100644
--- a/fs/smb/client/smb2inode.c
+++ b/fs/smb/client/smb2inode.c
@@ -1198,6 +1198,7 @@ struct inode *smb2_get_reparse_inode(struct cifs_open_info_data *data,
const unsigned int xid,
struct cifs_tcon *tcon,
const char *full_path,
+ bool directory,
struct kvec *reparse_iov,
struct kvec *xattr_iov)
{
@@ -1217,7 +1218,7 @@ struct inode *smb2_get_reparse_inode(struct cifs_open_info_data *data,
FILE_READ_ATTRIBUTES |
FILE_WRITE_ATTRIBUTES,
FILE_CREATE,
- CREATE_NOT_DIR | OPEN_REPARSE_POINT,
+ (directory ? CREATE_NOT_FILE : CREATE_NOT_DIR) | OPEN_REPARSE_POINT,
ACL_NO_MODE);
if (xattr_iov)
oparms.ea_cctx = xattr_iov;
diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
index b208232b12a2..5e0855fefcfe 100644
--- a/fs/smb/client/smb2proto.h
+++ b/fs/smb/client/smb2proto.h
@@ -61,6 +61,7 @@ struct inode *smb2_get_reparse_inode(struct cifs_open_info_data *data,
const unsigned int xid,
struct cifs_tcon *tcon,
const char *full_path,
+ bool directory,
struct kvec *reparse_iov,
struct kvec *xattr_iov);
int smb2_query_reparse_point(const unsigned int xid,
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* [PATCH v2 2/6] cifs: Fix creating native symlinks pointing to current or parent directory
2024-10-05 14:02 ` [PATCH v2 0/6] " Pali Rohár
2024-10-05 14:02 ` [PATCH v2 1/6] cifs: Improve creating native symlinks pointing to directory Pali Rohár
@ 2024-10-05 14:02 ` Pali Rohár
2024-10-05 14:02 ` [PATCH v2 3/6] cifs: Fix parsing native symlinks relative to the export Pali Rohár
` (3 subsequent siblings)
5 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-10-05 14:02 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
Calling 'ln -s . symlink' or 'ln -s .. symlink' creates symlink pointing to
some object name which ends with U+F029 unicode codepoint. This is because
trailing dot in the object name is replaced by non-ASCII unicode codepoint.
So Linux SMB client currently is not able to create native symlink pointing
to current or parent directory on Windows SMB server which can be read by
either on local Windows server or by any other SMB client which does not
implement compatible-reverse character replacement.
Fix this problem in cifsConvertToUTF16() function which is doing that
character replacement. Function comment already says that it does not need
to handle special cases '.' and '..', but after introduction of native
symlinks in reparse point form, this handling is needed.
Note that this change depends on the previous change
"cifs: Improve creating native symlinks pointing to directory".
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/cifs_unicode.c | 17 ++++++++++++++---
1 file changed, 14 insertions(+), 3 deletions(-)
diff --git a/fs/smb/client/cifs_unicode.c b/fs/smb/client/cifs_unicode.c
index 79d99a913944..4cc6e0896fad 100644
--- a/fs/smb/client/cifs_unicode.c
+++ b/fs/smb/client/cifs_unicode.c
@@ -484,10 +484,21 @@ cifsConvertToUTF16(__le16 *target, const char *source, int srclen,
/**
* Remap spaces and periods found at the end of every
* component of the path. The special cases of '.' and
- * '..' do not need to be dealt with explicitly because
- * they are addressed in namei.c:link_path_walk().
+ * '..' are need to be handled because of symlinks.
+ * They are treated as non-end-of-string to avoid
+ * remapping and breaking symlinks pointing to . or ..
**/
- if ((i == srclen - 1) || (source[i+1] == '\\'))
+ if ((i == 0 || source[i-1] == '\\') &&
+ source[i] == '.' &&
+ (i == srclen-1 || source[i+1] == '\\'))
+ end_of_string = false; /* "." case */
+ else if (i >= 1 &&
+ (i == 1 || source[i-2] == '\\') &&
+ source[i-1] == '.' &&
+ source[i] == '.' &&
+ (i == srclen-1 || source[i+1] == '\\'))
+ end_of_string = false; /* ".." case */
+ else if ((i == srclen - 1) || (source[i+1] == '\\'))
end_of_string = true;
else
end_of_string = false;
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* [PATCH v2 3/6] cifs: Fix parsing native symlinks relative to the export
2024-10-05 14:02 ` [PATCH v2 0/6] " Pali Rohár
2024-10-05 14:02 ` [PATCH v2 1/6] cifs: Improve creating native symlinks pointing to directory Pali Rohár
2024-10-05 14:02 ` [PATCH v2 2/6] cifs: Fix creating native symlinks pointing to current or parent directory Pali Rohár
@ 2024-10-05 14:02 ` Pali Rohár
2024-10-05 14:02 ` [PATCH v2 4/6] cifs: Fix parsing native symlinks directory/file type Pali Rohár
` (2 subsequent siblings)
5 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-10-05 14:02 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
SMB symlink which has SYMLINK_FLAG_RELATIVE set is relative (as opposite of
the absolute) and it can be relative either to the current directory (where
is the symlink stored) or relative to the top level export path. To what it
is relative depends on the first character of the symlink target path.
If the first character is path separator then symlink is relative to the
export, otherwise to the current directory. Linux (and generally POSIX
systems) supports only symlink paths relative to the current directory
where is symlink stored.
Currently if Linux SMB client reads relative SMB symlink with first
character as path separator (slash), it let as is. Which means that Linux
interpret it as absolute symlink pointing from the root (/). But this
location is different than the top level directory of SMB export (unless
SMB export was mounted to the root) and thefore SMB symlinks relative to
the export are interpreted wrongly by Linux SMB client.
Fix this problem. As Linux does not have equivalent of the path relative to
the top of the mount point, convert such symlink target path relative to
the current directory. Do this by prepending "../" pattern N times before
the SMB target path, where N is the number of path separators found in SMB
symlink path.
So for example, if SMB share is mounted to Linux path /mnt/share/, symlink
is stored in file /mnt/share/test/folder1/symlink (so SMB symlink path is
test\folder1\symlink) and SMB symlink target points to \test\folder2\file,
then convert symlink target path to Linux path ../../test/folder2/file.
Deduplicate code for parsing SMB symlinks in native form from functions
smb2_parse_symlink_response() and parse_reparse_native_symlink() into new
function smb2_parse_native_symlink() and pass into this new function a new
full_path parameter from callers, which specify SMB full path where is
symlink stored.
This change fixes resolving of the native Windows symlinks relative to the
top level directory of the SMB share.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/cifsglob.h | 1 +
fs/smb/client/cifsproto.h | 1 +
fs/smb/client/inode.c | 1 +
fs/smb/client/reparse.c | 90 +++++++++++++++++++++++++++++++++------
fs/smb/client/reparse.h | 4 +-
fs/smb/client/smb1ops.c | 3 +-
fs/smb/client/smb2file.c | 21 +++++----
fs/smb/client/smb2inode.c | 6 ++-
fs/smb/client/smb2proto.h | 9 +++-
9 files changed, 108 insertions(+), 28 deletions(-)
diff --git a/fs/smb/client/cifsglob.h b/fs/smb/client/cifsglob.h
index 9eae8649f90c..260b553283ef 100644
--- a/fs/smb/client/cifsglob.h
+++ b/fs/smb/client/cifsglob.h
@@ -589,6 +589,7 @@ struct smb_version_operations {
/* Check for STATUS_NETWORK_NAME_DELETED */
bool (*is_network_name_deleted)(char *buf, struct TCP_Server_Info *srv);
int (*parse_reparse_point)(struct cifs_sb_info *cifs_sb,
+ const char *full_path,
struct kvec *rsp_iov,
struct cifs_open_info_data *data);
int (*create_reparse_symlink)(const unsigned int xid,
diff --git a/fs/smb/client/cifsproto.h b/fs/smb/client/cifsproto.h
index 791bddac0396..44555a0e4df6 100644
--- a/fs/smb/client/cifsproto.h
+++ b/fs/smb/client/cifsproto.h
@@ -675,6 +675,7 @@ char *extract_hostname(const char *unc);
char *extract_sharename(const char *unc);
int parse_reparse_point(struct reparse_data_buffer *buf,
u32 plen, struct cifs_sb_info *cifs_sb,
+ const char *full_path,
bool unicode, struct cifs_open_info_data *data);
int __cifs_sfu_make_node(unsigned int xid, struct inode *inode,
struct dentry *dentry, struct cifs_tcon *tcon,
diff --git a/fs/smb/client/inode.c b/fs/smb/client/inode.c
index 0c23634438e5..0fe54b2d2561 100644
--- a/fs/smb/client/inode.c
+++ b/fs/smb/client/inode.c
@@ -1107,6 +1107,7 @@ static int reparse_info_to_fattr(struct cifs_open_info_data *data,
rc = 0;
} else if (iov && server->ops->parse_reparse_point) {
rc = server->ops->parse_reparse_point(cifs_sb,
+ full_path,
iov, data);
}
break;
diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
index 8403e1b94059..0d1cea64ab6e 100644
--- a/fs/smb/client/reparse.c
+++ b/fs/smb/client/reparse.c
@@ -532,9 +532,76 @@ static int parse_reparse_posix(struct reparse_posix_data *buf,
return 0;
}
+int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
+ bool unicode, bool relative,
+ const char *full_path,
+ struct cifs_sb_info *cifs_sb)
+{
+ char sep = CIFS_DIR_SEP(cifs_sb);
+ char *linux_target = NULL;
+ char *smb_target = NULL;
+ int levels;
+ int rc;
+ int i;
+
+ smb_target = cifs_strndup_from_utf16(buf, len, unicode, cifs_sb->local_nls);
+ if (!smb_target) {
+ rc = -ENOMEM;
+ goto out;
+ }
+
+ if (smb_target[0] == sep && relative) {
+ /*
+ * This is a relative SMB symlink from the top of the share,
+ * which is the top level directory of the Linux mount point.
+ * Linux does not support such relative symlinks, so convert
+ * it to the relative symlink from the current directory.
+ * full_path is the SMB path to the symlink (from which is
+ * extracted current directory) and smb_target is the SMB path
+ * where symlink points, therefore full_path must always be on
+ * the SMB share.
+ */
+ int smb_target_len = strlen(smb_target)+1;
+ levels = 0;
+ for (i = 1; full_path[i]; i++) { /* i=1 to skip leading sep */
+ if (full_path[i] == sep)
+ levels++;
+ }
+ linux_target = kmalloc(levels*3 + smb_target_len, GFP_KERNEL);
+ if (!linux_target) {
+ rc = -ENOMEM;
+ goto out;
+ }
+ for (i = 0; i < levels; i++) {
+ linux_target[i*3 + 0] = '.';
+ linux_target[i*3 + 1] = '.';
+ linux_target[i*3 + 2] = sep;
+ }
+ memcpy(linux_target + levels*3, smb_target+1, smb_target_len); /* +1 to skip leading sep */
+ } else {
+ linux_target = smb_target;
+ smb_target = NULL;
+ }
+
+ if (sep == '\\')
+ convert_delimiter(linux_target, '/');
+
+ rc = 0;
+ *target = linux_target;
+
+ cifs_dbg(FYI, "%s: symlink target: %s\n", __func__, *target);
+
+out:
+ if (rc != 0)
+ kfree(linux_target);
+ kfree(smb_target);
+ return rc;
+}
+
static int parse_reparse_symlink(struct reparse_symlink_data_buffer *sym,
u32 plen, bool unicode,
struct cifs_sb_info *cifs_sb,
+ const char *full_path,
struct cifs_open_info_data *data)
{
unsigned int len;
@@ -549,20 +616,18 @@ static int parse_reparse_symlink(struct reparse_symlink_data_buffer *sym,
return -EIO;
}
- data->symlink_target = cifs_strndup_from_utf16(sym->PathBuffer + offs,
- len, unicode,
- cifs_sb->local_nls);
- if (!data->symlink_target)
- return -ENOMEM;
-
- convert_delimiter(data->symlink_target, '/');
- cifs_dbg(FYI, "%s: target path: %s\n", __func__, data->symlink_target);
-
- return 0;
+ return smb2_parse_native_symlink(&data->symlink_target,
+ sym->PathBuffer + offs,
+ len,
+ unicode,
+ le32_to_cpu(sym->Flags) & SYMLINK_FLAG_RELATIVE,
+ full_path,
+ cifs_sb);
}
int parse_reparse_point(struct reparse_data_buffer *buf,
u32 plen, struct cifs_sb_info *cifs_sb,
+ const char *full_path,
bool unicode, struct cifs_open_info_data *data)
{
struct cifs_tcon *tcon = cifs_sb_master_tcon(cifs_sb);
@@ -577,7 +642,7 @@ int parse_reparse_point(struct reparse_data_buffer *buf,
case IO_REPARSE_TAG_SYMLINK:
return parse_reparse_symlink(
(struct reparse_symlink_data_buffer *)buf,
- plen, unicode, cifs_sb, data);
+ plen, unicode, cifs_sb, full_path, data);
case IO_REPARSE_TAG_LX_SYMLINK:
case IO_REPARSE_TAG_AF_UNIX:
case IO_REPARSE_TAG_LX_FIFO:
@@ -593,6 +658,7 @@ int parse_reparse_point(struct reparse_data_buffer *buf,
}
int smb2_parse_reparse_point(struct cifs_sb_info *cifs_sb,
+ const char *full_path,
struct kvec *rsp_iov,
struct cifs_open_info_data *data)
{
@@ -602,7 +668,7 @@ int smb2_parse_reparse_point(struct cifs_sb_info *cifs_sb,
buf = (struct reparse_data_buffer *)((u8 *)io +
le32_to_cpu(io->OutputOffset));
- return parse_reparse_point(buf, plen, cifs_sb, true, data);
+ return parse_reparse_point(buf, plen, cifs_sb, full_path, true, data);
}
static void wsl_to_fattr(struct cifs_open_info_data *data,
diff --git a/fs/smb/client/reparse.h b/fs/smb/client/reparse.h
index 5be54878265e..eb6854e65e08 100644
--- a/fs/smb/client/reparse.h
+++ b/fs/smb/client/reparse.h
@@ -128,7 +128,9 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
int smb2_mknod_reparse(unsigned int xid, struct inode *inode,
struct dentry *dentry, struct cifs_tcon *tcon,
const char *full_path, umode_t mode, dev_t dev);
-int smb2_parse_reparse_point(struct cifs_sb_info *cifs_sb, struct kvec *rsp_iov,
+int smb2_parse_reparse_point(struct cifs_sb_info *cifs_sb,
+ const char *full_path,
+ struct kvec *rsp_iov,
struct cifs_open_info_data *data);
#endif /* _CIFS_REPARSE_H */
diff --git a/fs/smb/client/smb1ops.c b/fs/smb/client/smb1ops.c
index e03c91a49650..fed32db32a46 100644
--- a/fs/smb/client/smb1ops.c
+++ b/fs/smb/client/smb1ops.c
@@ -994,6 +994,7 @@ static int cifs_query_symlink(const unsigned int xid,
}
static int cifs_parse_reparse_point(struct cifs_sb_info *cifs_sb,
+ const char *full_path,
struct kvec *rsp_iov,
struct cifs_open_info_data *data)
{
@@ -1004,7 +1005,7 @@ static int cifs_parse_reparse_point(struct cifs_sb_info *cifs_sb,
buf = (struct reparse_data_buffer *)((__u8 *)&io->hdr.Protocol +
le32_to_cpu(io->DataOffset));
- return parse_reparse_point(buf, plen, cifs_sb, unicode, data);
+ return parse_reparse_point(buf, plen, cifs_sb, full_path, unicode, data);
}
static bool
diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
index c23478ab1cf8..dc52995f5591 100644
--- a/fs/smb/client/smb2file.c
+++ b/fs/smb/client/smb2file.c
@@ -63,12 +63,12 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
return sym;
}
-int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov, char **path)
+int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov,
+ const char *full_path, char **path)
{
struct smb2_symlink_err_rsp *sym;
unsigned int sub_offs, sub_len;
unsigned int print_offs, print_len;
- char *s;
if (!cifs_sb || !iov || !iov->iov_base || !iov->iov_len || !path)
return -EINVAL;
@@ -86,15 +86,13 @@ int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec
iov->iov_len < SMB2_SYMLINK_STRUCT_SIZE + print_offs + print_len)
return -EINVAL;
- s = cifs_strndup_from_utf16((char *)sym->PathBuffer + sub_offs, sub_len, true,
- cifs_sb->local_nls);
- if (!s)
- return -ENOMEM;
- convert_delimiter(s, '/');
- cifs_dbg(FYI, "%s: symlink target: %s\n", __func__, s);
-
- *path = s;
- return 0;
+ return smb2_parse_native_symlink(path,
+ (char *)sym->PathBuffer + sub_offs,
+ sub_len,
+ true,
+ le32_to_cpu(sym->Flags) & SYMLINK_FLAG_RELATIVE,
+ full_path,
+ cifs_sb);
}
int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32 *oplock, void *buf)
@@ -126,6 +124,7 @@ int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32
goto out;
if (hdr->Status == STATUS_STOPPED_ON_SYMLINK) {
rc = smb2_parse_symlink_response(oparms->cifs_sb, &err_iov,
+ oparms->path,
&data->symlink_target);
if (!rc) {
memset(smb2_data, 0, sizeof(*smb2_data));
diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
index cdb0e028e73c..9a28a30ec1a3 100644
--- a/fs/smb/client/smb2inode.c
+++ b/fs/smb/client/smb2inode.c
@@ -828,6 +828,7 @@ static int smb2_compound_op(const unsigned int xid, struct cifs_tcon *tcon,
static int parse_create_response(struct cifs_open_info_data *data,
struct cifs_sb_info *cifs_sb,
+ const char *full_path,
const struct kvec *iov)
{
struct smb2_create_rsp *rsp = iov->iov_base;
@@ -841,6 +842,7 @@ static int parse_create_response(struct cifs_open_info_data *data,
break;
case STATUS_STOPPED_ON_SYMLINK:
rc = smb2_parse_symlink_response(cifs_sb, iov,
+ full_path,
&data->symlink_target);
if (rc)
return rc;
@@ -930,14 +932,14 @@ int smb2_query_path_info(const unsigned int xid,
switch (rc) {
case 0:
- rc = parse_create_response(data, cifs_sb, &out_iov[0]);
+ rc = parse_create_response(data, cifs_sb, full_path, &out_iov[0]);
break;
case -EOPNOTSUPP:
/*
* BB TODO: When support for special files added to Samba
* re-verify this path.
*/
- rc = parse_create_response(data, cifs_sb, &out_iov[0]);
+ rc = parse_create_response(data, cifs_sb, full_path, &out_iov[0]);
if (rc || !data->reparse_point)
goto out;
diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
index 5e0855fefcfe..aa01ae234732 100644
--- a/fs/smb/client/smb2proto.h
+++ b/fs/smb/client/smb2proto.h
@@ -113,7 +113,14 @@ extern int smb3_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
struct cifs_sb_info *cifs_sb,
const unsigned char *path, char *pbuf,
unsigned int *pbytes_read);
-int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov, char **path);
+int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
+ bool unicode, bool relative,
+ const char *full_path,
+ struct cifs_sb_info *cifs_sb);
+int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb,
+ const struct kvec *iov,
+ const char *full_path,
+ char **path);
int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32 *oplock,
void *buf);
extern int smb2_unlock_range(struct cifsFileInfo *cfile,
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* [PATCH v2 4/6] cifs: Fix parsing native symlinks directory/file type
2024-10-05 14:02 ` [PATCH v2 0/6] " Pali Rohár
` (2 preceding siblings ...)
2024-10-05 14:02 ` [PATCH v2 3/6] cifs: Fix parsing native symlinks relative to the export Pali Rohár
@ 2024-10-05 14:02 ` Pali Rohár
2024-10-13 17:56 ` Steve French
2024-10-15 14:30 ` [PATCH v3] " Pali Rohár
2024-10-05 14:02 ` [PATCH v2 5/6] cifs: Validate content of native symlink Pali Rohár
2024-10-05 14:03 ` [PATCH v2 6/6] cifs: Fix creating and resolving absolute NT-style symlinks Pali Rohár
5 siblings, 2 replies; 38+ messages in thread
From: Pali Rohár @ 2024-10-05 14:02 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
As SMB protocol distinguish between symlink to directory and symlink to
file, add some mechanism to disallow resolving incompatible types.
When SMB symlink is of the directory type, ensure that its target path ends
with slash. This forces Linux to not allow resolving such symlink to file.
And when SMB symlink is of the file type and its target path ends with
slash then returns an error as such symlink is unresolvable. Such symlink
always points to invalid location as file cannot end with slash.
As POSIX server does not distinguish between symlinks to file and symlink
directory, do not apply this change for symlinks from POSIX SMB server. For
POSIX SMB servers, this change does nothing.
This mimics Windows behavior of native SMB symlinks.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/inode.c | 5 ++++
fs/smb/client/smb2file.c | 55 +++++++++++++++++++++++++++++++++++++++
fs/smb/client/smb2inode.c | 4 +++
fs/smb/client/smb2proto.h | 1 +
4 files changed, 65 insertions(+)
diff --git a/fs/smb/client/inode.c b/fs/smb/client/inode.c
index 0fe54b2d2561..aa38a3935f8f 100644
--- a/fs/smb/client/inode.c
+++ b/fs/smb/client/inode.c
@@ -1110,6 +1110,11 @@ static int reparse_info_to_fattr(struct cifs_open_info_data *data,
full_path,
iov, data);
}
+
+ if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
+ bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
+ rc = smb2_fix_symlink_target_type(&data->symlink_target, directory, cifs_sb);
+ }
break;
}
diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
index dc52995f5591..149449d9c1c0 100644
--- a/fs/smb/client/smb2file.c
+++ b/fs/smb/client/smb2file.c
@@ -63,6 +63,56 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
return sym;
}
+int smb2_fix_symlink_target_type(char **target, bool directory, struct cifs_sb_info *cifs_sb)
+{
+ char *buf;
+ int len;
+
+ /*
+ * POSIX server does not distinguish between symlinks to file and
+ * symlink directory. So nothing is needed to fix on the client side.
+ */
+ if (cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS)
+ return 0;
+
+ len = strlen(*target);
+ if (!len)
+ return -EIO;
+
+ /*
+ * If this is directory symlink and it does not have trailing slash then
+ * append it. Trailing slash simulates Windows/SMB behavior which do not
+ * allow resolving directory symlink to file.
+ */
+ if (directory && (*target)[len-1] != '/') {
+ buf = kzalloc(len+2, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+ memcpy(buf, *target, len);
+ buf[len] = '/';
+ kfree(*target);
+ *target = buf;
+ }
+
+ /*
+ * If this is a symlink which points to file name with trailing slash,
+ * or to file named "." or file named ".." then this symlink cannot be
+ * resolved on Linux because Linux does not allow files with such names.
+ * So return an error to prevent resolving this file type symlink to
+ * directory, as it do not point to directory at all.
+ */
+ if (!directory) {
+ const char *basename = kbasename(*target);
+ int basename_len = strlen(basename);
+ if (basename_len == 0 || /* symname ends with slash */
+ (basename_len == 1 && basename[0] == '.') || /* last component is "." */
+ (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) /* or ".." */
+ return -EIO;
+ }
+
+ return 0;
+}
+
int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov,
const char *full_path, char **path)
{
@@ -133,6 +183,11 @@ int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32
NULL, NULL, NULL);
oparms->create_options &= ~OPEN_REPARSE_POINT;
}
+ if (!rc) {
+ bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
+ rc = smb2_fix_symlink_target_type(&data->symlink_target,
+ directory, oparms->cifs_sb);
+ }
}
}
diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
index 9a28a30ec1a3..06bb6f7fbf0f 100644
--- a/fs/smb/client/smb2inode.c
+++ b/fs/smb/client/smb2inode.c
@@ -960,6 +960,10 @@ int smb2_query_path_info(const unsigned int xid,
rc = smb2_compound_op(xid, tcon, cifs_sb, full_path,
&oparms, in_iov, cmds, num_cmds,
cfile, NULL, NULL, NULL);
+ if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
+ bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
+ rc = smb2_fix_symlink_target_type(&data->symlink_target, directory, cifs_sb);
+ }
break;
case -EREMOTE:
break;
diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
index aa01ae234732..1828b825c7d3 100644
--- a/fs/smb/client/smb2proto.h
+++ b/fs/smb/client/smb2proto.h
@@ -113,6 +113,7 @@ extern int smb3_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
struct cifs_sb_info *cifs_sb,
const unsigned char *path, char *pbuf,
unsigned int *pbytes_read);
+int smb2_fix_symlink_target_type(char **target, bool directory, struct cifs_sb_info *cifs_sb);
int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
bool unicode, bool relative,
const char *full_path,
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* Re: [PATCH v2 4/6] cifs: Fix parsing native symlinks directory/file type
2024-10-05 14:02 ` [PATCH v2 4/6] cifs: Fix parsing native symlinks directory/file type Pali Rohár
@ 2024-10-13 17:56 ` Steve French
2024-10-14 9:52 ` Pali Rohár
2024-10-15 14:30 ` [PATCH v3] " Pali Rohár
1 sibling, 1 reply; 38+ messages in thread
From: Steve French @ 2024-10-13 17:56 UTC (permalink / raw)
To: Pali Rohár
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
Would this break any pure Linux client example, mounted to Windows,
where previously the Linux client created all symlinks as file
symlinks? e.g. If there were two Linux clients writing to the share,
one that included this fix and one that did not.
On Sat, Oct 5, 2024 at 9:03 AM Pali Rohár <pali@kernel.org> wrote:
>
> As SMB protocol distinguish between symlink to directory and symlink to
> file, add some mechanism to disallow resolving incompatible types.
>
> When SMB symlink is of the directory type, ensure that its target path ends
> with slash. This forces Linux to not allow resolving such symlink to file.
>
> And when SMB symlink is of the file type and its target path ends with
> slash then returns an error as such symlink is unresolvable. Such symlink
> always points to invalid location as file cannot end with slash.
>
> As POSIX server does not distinguish between symlinks to file and symlink
> directory, do not apply this change for symlinks from POSIX SMB server. For
> POSIX SMB servers, this change does nothing.
>
> This mimics Windows behavior of native SMB symlinks.
>
> Signed-off-by: Pali Rohár <pali@kernel.org>
> ---
> fs/smb/client/inode.c | 5 ++++
> fs/smb/client/smb2file.c | 55 +++++++++++++++++++++++++++++++++++++++
> fs/smb/client/smb2inode.c | 4 +++
> fs/smb/client/smb2proto.h | 1 +
> 4 files changed, 65 insertions(+)
>
> diff --git a/fs/smb/client/inode.c b/fs/smb/client/inode.c
> index 0fe54b2d2561..aa38a3935f8f 100644
> --- a/fs/smb/client/inode.c
> +++ b/fs/smb/client/inode.c
> @@ -1110,6 +1110,11 @@ static int reparse_info_to_fattr(struct cifs_open_info_data *data,
> full_path,
> iov, data);
> }
> +
> + if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
> + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> + rc = smb2_fix_symlink_target_type(&data->symlink_target, directory, cifs_sb);
> + }
> break;
> }
>
> diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
> index dc52995f5591..149449d9c1c0 100644
> --- a/fs/smb/client/smb2file.c
> +++ b/fs/smb/client/smb2file.c
> @@ -63,6 +63,56 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
> return sym;
> }
>
> +int smb2_fix_symlink_target_type(char **target, bool directory, struct cifs_sb_info *cifs_sb)
> +{
> + char *buf;
> + int len;
> +
> + /*
> + * POSIX server does not distinguish between symlinks to file and
> + * symlink directory. So nothing is needed to fix on the client side.
> + */
> + if (cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS)
> + return 0;
> +
> + len = strlen(*target);
> + if (!len)
> + return -EIO;
> +
> + /*
> + * If this is directory symlink and it does not have trailing slash then
> + * append it. Trailing slash simulates Windows/SMB behavior which do not
> + * allow resolving directory symlink to file.
> + */
> + if (directory && (*target)[len-1] != '/') {
> + buf = kzalloc(len+2, GFP_KERNEL);
> + if (!buf)
> + return -ENOMEM;
> + memcpy(buf, *target, len);
> + buf[len] = '/';
> + kfree(*target);
> + *target = buf;
> + }
> +
> + /*
> + * If this is a symlink which points to file name with trailing slash,
> + * or to file named "." or file named ".." then this symlink cannot be
> + * resolved on Linux because Linux does not allow files with such names.
> + * So return an error to prevent resolving this file type symlink to
> + * directory, as it do not point to directory at all.
> + */
> + if (!directory) {
> + const char *basename = kbasename(*target);
> + int basename_len = strlen(basename);
> + if (basename_len == 0 || /* symname ends with slash */
> + (basename_len == 1 && basename[0] == '.') || /* last component is "." */
> + (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) /* or ".." */
> + return -EIO;
> + }
> +
> + return 0;
> +}
> +
> int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov,
> const char *full_path, char **path)
> {
> @@ -133,6 +183,11 @@ int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32
> NULL, NULL, NULL);
> oparms->create_options &= ~OPEN_REPARSE_POINT;
> }
> + if (!rc) {
> + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> + rc = smb2_fix_symlink_target_type(&data->symlink_target,
> + directory, oparms->cifs_sb);
> + }
> }
> }
>
> diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
> index 9a28a30ec1a3..06bb6f7fbf0f 100644
> --- a/fs/smb/client/smb2inode.c
> +++ b/fs/smb/client/smb2inode.c
> @@ -960,6 +960,10 @@ int smb2_query_path_info(const unsigned int xid,
> rc = smb2_compound_op(xid, tcon, cifs_sb, full_path,
> &oparms, in_iov, cmds, num_cmds,
> cfile, NULL, NULL, NULL);
> + if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
> + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> + rc = smb2_fix_symlink_target_type(&data->symlink_target, directory, cifs_sb);
> + }
> break;
> case -EREMOTE:
> break;
> diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
> index aa01ae234732..1828b825c7d3 100644
> --- a/fs/smb/client/smb2proto.h
> +++ b/fs/smb/client/smb2proto.h
> @@ -113,6 +113,7 @@ extern int smb3_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
> struct cifs_sb_info *cifs_sb,
> const unsigned char *path, char *pbuf,
> unsigned int *pbytes_read);
> +int smb2_fix_symlink_target_type(char **target, bool directory, struct cifs_sb_info *cifs_sb);
> int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> bool unicode, bool relative,
> const char *full_path,
> --
> 2.20.1
>
>
--
Thanks,
Steve
^ permalink raw reply [flat|nested] 38+ messages in thread* Re: [PATCH v2 4/6] cifs: Fix parsing native symlinks directory/file type
2024-10-13 17:56 ` Steve French
@ 2024-10-14 9:52 ` Pali Rohár
0 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-10-14 9:52 UTC (permalink / raw)
To: Steve French
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
I see what you mean. The last check in smb2_fix_symlink_target_type
will disallow to use symlinks to "." and ".." created by older linux
client as it created it as symlink to file, instead of symlink to dir.
This is a good point. I can send a new version with dropped last check
in smb2_fix_symlink_target_type.
On Sunday 13 October 2024 12:56:23 Steve French wrote:
> Would this break any pure Linux client example, mounted to Windows,
> where previously the Linux client created all symlinks as file
> symlinks? e.g. If there were two Linux clients writing to the share,
> one that included this fix and one that did not.
>
> On Sat, Oct 5, 2024 at 9:03 AM Pali Rohár <pali@kernel.org> wrote:
> >
> > As SMB protocol distinguish between symlink to directory and symlink to
> > file, add some mechanism to disallow resolving incompatible types.
> >
> > When SMB symlink is of the directory type, ensure that its target path ends
> > with slash. This forces Linux to not allow resolving such symlink to file.
> >
> > And when SMB symlink is of the file type and its target path ends with
> > slash then returns an error as such symlink is unresolvable. Such symlink
> > always points to invalid location as file cannot end with slash.
> >
> > As POSIX server does not distinguish between symlinks to file and symlink
> > directory, do not apply this change for symlinks from POSIX SMB server. For
> > POSIX SMB servers, this change does nothing.
> >
> > This mimics Windows behavior of native SMB symlinks.
> >
> > Signed-off-by: Pali Rohár <pali@kernel.org>
> > ---
> > fs/smb/client/inode.c | 5 ++++
> > fs/smb/client/smb2file.c | 55 +++++++++++++++++++++++++++++++++++++++
> > fs/smb/client/smb2inode.c | 4 +++
> > fs/smb/client/smb2proto.h | 1 +
> > 4 files changed, 65 insertions(+)
> >
> > diff --git a/fs/smb/client/inode.c b/fs/smb/client/inode.c
> > index 0fe54b2d2561..aa38a3935f8f 100644
> > --- a/fs/smb/client/inode.c
> > +++ b/fs/smb/client/inode.c
> > @@ -1110,6 +1110,11 @@ static int reparse_info_to_fattr(struct cifs_open_info_data *data,
> > full_path,
> > iov, data);
> > }
> > +
> > + if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
> > + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> > + rc = smb2_fix_symlink_target_type(&data->symlink_target, directory, cifs_sb);
> > + }
> > break;
> > }
> >
> > diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
> > index dc52995f5591..149449d9c1c0 100644
> > --- a/fs/smb/client/smb2file.c
> > +++ b/fs/smb/client/smb2file.c
> > @@ -63,6 +63,56 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
> > return sym;
> > }
> >
> > +int smb2_fix_symlink_target_type(char **target, bool directory, struct cifs_sb_info *cifs_sb)
> > +{
> > + char *buf;
> > + int len;
> > +
> > + /*
> > + * POSIX server does not distinguish between symlinks to file and
> > + * symlink directory. So nothing is needed to fix on the client side.
> > + */
> > + if (cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS)
> > + return 0;
> > +
> > + len = strlen(*target);
> > + if (!len)
> > + return -EIO;
> > +
> > + /*
> > + * If this is directory symlink and it does not have trailing slash then
> > + * append it. Trailing slash simulates Windows/SMB behavior which do not
> > + * allow resolving directory symlink to file.
> > + */
> > + if (directory && (*target)[len-1] != '/') {
> > + buf = kzalloc(len+2, GFP_KERNEL);
> > + if (!buf)
> > + return -ENOMEM;
> > + memcpy(buf, *target, len);
> > + buf[len] = '/';
> > + kfree(*target);
> > + *target = buf;
> > + }
> > +
> > + /*
> > + * If this is a symlink which points to file name with trailing slash,
> > + * or to file named "." or file named ".." then this symlink cannot be
> > + * resolved on Linux because Linux does not allow files with such names.
> > + * So return an error to prevent resolving this file type symlink to
> > + * directory, as it do not point to directory at all.
> > + */
> > + if (!directory) {
> > + const char *basename = kbasename(*target);
> > + int basename_len = strlen(basename);
> > + if (basename_len == 0 || /* symname ends with slash */
> > + (basename_len == 1 && basename[0] == '.') || /* last component is "." */
> > + (basename_len == 2 && basename[0] == '.' && basename[1] == '.')) /* or ".." */
> > + return -EIO;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov,
> > const char *full_path, char **path)
> > {
> > @@ -133,6 +183,11 @@ int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32
> > NULL, NULL, NULL);
> > oparms->create_options &= ~OPEN_REPARSE_POINT;
> > }
> > + if (!rc) {
> > + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> > + rc = smb2_fix_symlink_target_type(&data->symlink_target,
> > + directory, oparms->cifs_sb);
> > + }
> > }
> > }
> >
> > diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
> > index 9a28a30ec1a3..06bb6f7fbf0f 100644
> > --- a/fs/smb/client/smb2inode.c
> > +++ b/fs/smb/client/smb2inode.c
> > @@ -960,6 +960,10 @@ int smb2_query_path_info(const unsigned int xid,
> > rc = smb2_compound_op(xid, tcon, cifs_sb, full_path,
> > &oparms, in_iov, cmds, num_cmds,
> > cfile, NULL, NULL, NULL);
> > + if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
> > + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> > + rc = smb2_fix_symlink_target_type(&data->symlink_target, directory, cifs_sb);
> > + }
> > break;
> > case -EREMOTE:
> > break;
> > diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
> > index aa01ae234732..1828b825c7d3 100644
> > --- a/fs/smb/client/smb2proto.h
> > +++ b/fs/smb/client/smb2proto.h
> > @@ -113,6 +113,7 @@ extern int smb3_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
> > struct cifs_sb_info *cifs_sb,
> > const unsigned char *path, char *pbuf,
> > unsigned int *pbytes_read);
> > +int smb2_fix_symlink_target_type(char **target, bool directory, struct cifs_sb_info *cifs_sb);
> > int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> > bool unicode, bool relative,
> > const char *full_path,
> > --
> > 2.20.1
> >
> >
>
>
> --
> Thanks,
>
> Steve
^ permalink raw reply [flat|nested] 38+ messages in thread
* [PATCH v3] cifs: Fix parsing native symlinks directory/file type
2024-10-05 14:02 ` [PATCH v2 4/6] cifs: Fix parsing native symlinks directory/file type Pali Rohár
2024-10-13 17:56 ` Steve French
@ 2024-10-15 14:30 ` Pali Rohár
2024-12-09 18:02 ` Pali Rohár
1 sibling, 1 reply; 38+ messages in thread
From: Pali Rohár @ 2024-10-15 14:30 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
As SMB protocol distinguish between symlink to directory and symlink to
file, add some mechanism to disallow resolving incompatible types.
When SMB symlink is of the directory type, ensure that its target path ends
with slash. This forces Linux to not allow resolving such symlink to file.
And when SMB symlink is of the file type and its target path ends with
slash then returns an error as such symlink is unresolvable. Such symlink
always points to invalid location as file cannot end with slash.
As POSIX server does not distinguish between symlinks to file and symlink
directory, do not apply this change for symlinks from POSIX SMB server. For
POSIX SMB servers, this change does nothing.
This mimics Windows behavior of native SMB symlinks.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
Changes in v3:
* Relax non-directory case condition in smb2_fix_symlink_target_type()
for compatibility with older Linux clients.
* Use krealloc() instead of kzalloc()+memcpy()
---
fs/smb/client/inode.c | 5 ++++
fs/smb/client/smb2file.c | 48 +++++++++++++++++++++++++++++++++++++++
fs/smb/client/smb2inode.c | 4 ++++
fs/smb/client/smb2proto.h | 1 +
4 files changed, 58 insertions(+)
diff --git a/fs/smb/client/inode.c b/fs/smb/client/inode.c
index 2ac9cc8d327d..3fd625b356bd 100644
--- a/fs/smb/client/inode.c
+++ b/fs/smb/client/inode.c
@@ -1140,6 +1140,11 @@ static int reparse_info_to_fattr(struct cifs_open_info_data *data,
full_path,
iov, data);
}
+
+ if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
+ bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
+ rc = smb2_fix_symlink_target_type(&data->symlink_target, directory, cifs_sb);
+ }
break;
}
diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
index e836bc2193dd..4b07274e824a 100644
--- a/fs/smb/client/smb2file.c
+++ b/fs/smb/client/smb2file.c
@@ -63,6 +63,49 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
return sym;
}
+int smb2_fix_symlink_target_type(char **target, bool directory, struct cifs_sb_info *cifs_sb)
+{
+ char *buf;
+ int len;
+
+ /*
+ * POSIX server does not distinguish between symlinks to file and
+ * symlink directory. So nothing is needed to fix on the client side.
+ */
+ if (cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS)
+ return 0;
+
+ len = strlen(*target);
+ if (!len)
+ return -EIO;
+
+ /*
+ * If this is directory symlink and it does not have trailing slash then
+ * append it. Trailing slash simulates Windows/SMB behavior which do not
+ * allow resolving directory symlink to file.
+ */
+ if (directory && (*target)[len-1] != '/') {
+ buf = krealloc(*target, len+2, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+ buf[len] = '/';
+ buf[len+1] = '\0';
+ *target = buf;
+ len++;
+ }
+
+ /*
+ * If this is a file (non-directory) symlink and it points to path name
+ * with trailing slash then this is an invalid symlink because file name
+ * cannot contain slash character. File name with slash is invalid on
+ * both Windows and Linux systems. So return an error for such symlink.
+ */
+ if (!directory && (*target)[len-1] == '/')
+ return -EIO;
+
+ return 0;
+}
+
int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov,
const char *full_path, char **path)
{
@@ -133,6 +176,11 @@ int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32
NULL, NULL, NULL);
oparms->create_options &= ~OPEN_REPARSE_POINT;
}
+ if (!rc) {
+ bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
+ rc = smb2_fix_symlink_target_type(&data->symlink_target,
+ directory, oparms->cifs_sb);
+ }
}
}
diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
index a188908914fe..b8ccc8fd88f2 100644
--- a/fs/smb/client/smb2inode.c
+++ b/fs/smb/client/smb2inode.c
@@ -960,6 +960,10 @@ int smb2_query_path_info(const unsigned int xid,
rc = smb2_compound_op(xid, tcon, cifs_sb, full_path,
&oparms, in_iov, cmds, num_cmds,
cfile, NULL, NULL, NULL);
+ if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
+ bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
+ rc = smb2_fix_symlink_target_type(&data->symlink_target, directory, cifs_sb);
+ }
break;
case -EREMOTE:
break;
diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
index db93447f0f5a..5390d5a61039 100644
--- a/fs/smb/client/smb2proto.h
+++ b/fs/smb/client/smb2proto.h
@@ -113,6 +113,7 @@ extern int smb3_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
struct cifs_sb_info *cifs_sb,
const unsigned char *path, char *pbuf,
unsigned int *pbytes_read);
+int smb2_fix_symlink_target_type(char **target, bool directory, struct cifs_sb_info *cifs_sb);
int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
bool unicode, bool relative,
const char *full_path,
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* Re: [PATCH v3] cifs: Fix parsing native symlinks directory/file type
2024-10-15 14:30 ` [PATCH v3] " Pali Rohár
@ 2024-12-09 18:02 ` Pali Rohár
0 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-12-09 18:02 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
On Tuesday 15 October 2024 16:30:41 Pali Rohár wrote:
> As SMB protocol distinguish between symlink to directory and symlink to
> file, add some mechanism to disallow resolving incompatible types.
>
> When SMB symlink is of the directory type, ensure that its target path ends
> with slash. This forces Linux to not allow resolving such symlink to file.
>
> And when SMB symlink is of the file type and its target path ends with
> slash then returns an error as such symlink is unresolvable. Such symlink
> always points to invalid location as file cannot end with slash.
>
> As POSIX server does not distinguish between symlinks to file and symlink
> directory, do not apply this change for symlinks from POSIX SMB server. For
> POSIX SMB servers, this change does nothing.
>
> This mimics Windows behavior of native SMB symlinks.
>
> Signed-off-by: Pali Rohár <pali@kernel.org>
> ---
> Changes in v3:
> * Relax non-directory case condition in smb2_fix_symlink_target_type()
> for compatibility with older Linux clients.
> * Use krealloc() instead of kzalloc()+memcpy()
> ---
> fs/smb/client/inode.c | 5 ++++
> fs/smb/client/smb2file.c | 48 +++++++++++++++++++++++++++++++++++++++
> fs/smb/client/smb2inode.c | 4 ++++
> fs/smb/client/smb2proto.h | 1 +
> 4 files changed, 58 insertions(+)
>
> diff --git a/fs/smb/client/inode.c b/fs/smb/client/inode.c
> index 2ac9cc8d327d..3fd625b356bd 100644
> --- a/fs/smb/client/inode.c
> +++ b/fs/smb/client/inode.c
> @@ -1140,6 +1140,11 @@ static int reparse_info_to_fattr(struct cifs_open_info_data *data,
> full_path,
> iov, data);
> }
> +
> + if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
> + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> + rc = smb2_fix_symlink_target_type(&data->symlink_target, directory, cifs_sb);
> + }
> break;
> }
>
> diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
> index e836bc2193dd..4b07274e824a 100644
> --- a/fs/smb/client/smb2file.c
> +++ b/fs/smb/client/smb2file.c
> @@ -63,6 +63,49 @@ static struct smb2_symlink_err_rsp *symlink_data(const struct kvec *iov)
> return sym;
> }
>
> +int smb2_fix_symlink_target_type(char **target, bool directory, struct cifs_sb_info *cifs_sb)
> +{
> + char *buf;
> + int len;
> +
> + /*
> + * POSIX server does not distinguish between symlinks to file and
> + * symlink directory. So nothing is needed to fix on the client side.
> + */
> + if (cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS)
> + return 0;
> +
> + len = strlen(*target);
> + if (!len)
> + return -EIO;
> +
> + /*
> + * If this is directory symlink and it does not have trailing slash then
> + * append it. Trailing slash simulates Windows/SMB behavior which do not
> + * allow resolving directory symlink to file.
> + */
> + if (directory && (*target)[len-1] != '/') {
> + buf = krealloc(*target, len+2, GFP_KERNEL);
> + if (!buf)
> + return -ENOMEM;
> + buf[len] = '/';
> + buf[len+1] = '\0';
> + *target = buf;
> + len++;
> + }
> +
> + /*
> + * If this is a file (non-directory) symlink and it points to path name
> + * with trailing slash then this is an invalid symlink because file name
> + * cannot contain slash character. File name with slash is invalid on
> + * both Windows and Linux systems. So return an error for such symlink.
> + */
> + if (!directory && (*target)[len-1] == '/')
> + return -EIO;
> +
> + return 0;
> +}
> +
> int smb2_parse_symlink_response(struct cifs_sb_info *cifs_sb, const struct kvec *iov,
> const char *full_path, char **path)
> {
> @@ -133,6 +176,11 @@ int smb2_open_file(const unsigned int xid, struct cifs_open_parms *oparms, __u32
> NULL, NULL, NULL);
> oparms->create_options &= ~OPEN_REPARSE_POINT;
> }
> + if (!rc) {
> + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> + rc = smb2_fix_symlink_target_type(&data->symlink_target,
> + directory, oparms->cifs_sb);
> + }
> }
> }
>
> diff --git a/fs/smb/client/smb2inode.c b/fs/smb/client/smb2inode.c
> index a188908914fe..b8ccc8fd88f2 100644
> --- a/fs/smb/client/smb2inode.c
> +++ b/fs/smb/client/smb2inode.c
> @@ -960,6 +960,10 @@ int smb2_query_path_info(const unsigned int xid,
> rc = smb2_compound_op(xid, tcon, cifs_sb, full_path,
> &oparms, in_iov, cmds, num_cmds,
> cfile, NULL, NULL, NULL);
> + if (data->reparse.tag == IO_REPARSE_TAG_SYMLINK && !rc) {
> + bool directory = le32_to_cpu(data->fi.Attributes) & ATTR_DIRECTORY;
> + rc = smb2_fix_symlink_target_type(&data->symlink_target, directory, cifs_sb);
> + }
> break;
> case -EREMOTE:
> break;
> diff --git a/fs/smb/client/smb2proto.h b/fs/smb/client/smb2proto.h
> index db93447f0f5a..5390d5a61039 100644
> --- a/fs/smb/client/smb2proto.h
> +++ b/fs/smb/client/smb2proto.h
> @@ -113,6 +113,7 @@ extern int smb3_query_mf_symlink(unsigned int xid, struct cifs_tcon *tcon,
> struct cifs_sb_info *cifs_sb,
> const unsigned char *path, char *pbuf,
> unsigned int *pbytes_read);
> +int smb2_fix_symlink_target_type(char **target, bool directory, struct cifs_sb_info *cifs_sb);
> int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> bool unicode, bool relative,
> const char *full_path,
> --
> 2.20.1
>
I was informed that function smb2_fix_symlink_target_type() may
dereference NULL pointer by strlen() call. So I'm proposing this fixup:
diff --git a/fs/smb/client/smb2file.c b/fs/smb/client/smb2file.c
index b0037058e8d9..47ba0b50f514 100644
--- a/fs/smb/client/smb2file.c
+++ b/fs/smb/client/smb2file.c
@@ -75,6 +75,9 @@ int smb2_fix_symlink_target_type(char **target, bool directory, struct cifs_sb_i
if (cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS)
return 0;
+ if (!*target)
+ return -EIO;
+
len = strlen(*target);
if (!len)
return -EIO;
^ permalink raw reply related [flat|nested] 38+ messages in thread
* [PATCH v2 5/6] cifs: Validate content of native symlink
2024-10-05 14:02 ` [PATCH v2 0/6] " Pali Rohár
` (3 preceding siblings ...)
2024-10-05 14:02 ` [PATCH v2 4/6] cifs: Fix parsing native symlinks directory/file type Pali Rohár
@ 2024-10-05 14:02 ` Pali Rohár
2024-10-13 4:21 ` Steve French
2024-10-05 14:03 ` [PATCH v2 6/6] cifs: Fix creating and resolving absolute NT-style symlinks Pali Rohár
5 siblings, 1 reply; 38+ messages in thread
From: Pali Rohár @ 2024-10-05 14:02 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
Check that path buffer has correct length (it is non-zero and in UNICODE
mode it has even number of bytes) and check that buffer does not contain
null character (UTF-16 null codepoint in UNICODE mode or null byte in
non-unicode mode) because Linux cannot process symlink with null byte.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/reparse.c | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
index 0d1cea64ab6e..fb1d16b17f38 100644
--- a/fs/smb/client/reparse.c
+++ b/fs/smb/client/reparse.c
@@ -544,6 +544,25 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
int rc;
int i;
+ /* Check that length it valid for unicode/non-unicode mode */
+ if (!len || (unicode && (len % 2))) {
+ cifs_dbg(VFS, "srv returned malformed symlink buffer\n");
+ rc = -EIO;
+ goto out;
+ }
+
+ /*
+ * Check that buffer does not contain UTF-16 null codepoint in unicode
+ * mode or null byte in non-unicode mode because Linux cannot process
+ * symlink with null byte.
+ */
+ if ((unicode && UniStrnlen((wchar_t *)buf, len/2) != len/2) ||
+ (!unicode && strnlen(buf, len) != len)) {
+ cifs_dbg(VFS, "srv returned null byte in native symlink target location\n");
+ rc = -EIO;
+ goto out;
+ }
+
smb_target = cifs_strndup_from_utf16(buf, len, unicode, cifs_sb->local_nls);
if (!smb_target) {
rc = -ENOMEM;
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* Re: [PATCH v2 5/6] cifs: Validate content of native symlink
2024-10-05 14:02 ` [PATCH v2 5/6] cifs: Validate content of native symlink Pali Rohár
@ 2024-10-13 4:21 ` Steve French
2024-10-14 9:39 ` Pali Rohár
0 siblings, 1 reply; 38+ messages in thread
From: Steve French @ 2024-10-13 4:21 UTC (permalink / raw)
To: Pali Rohár
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
This looks like one of the more important of the fixes of this series
- let me know if any thoughts or Reviewed-by/Acked-by on it. I may
try to send this earlier than the others
Pali,
Let me know if I missed dependencies that this patch depends on
On Sat, Oct 5, 2024 at 9:03 AM Pali Rohár <pali@kernel.org> wrote:
>
> Check that path buffer has correct length (it is non-zero and in UNICODE
> mode it has even number of bytes) and check that buffer does not contain
> null character (UTF-16 null codepoint in UNICODE mode or null byte in
> non-unicode mode) because Linux cannot process symlink with null byte.
>
> Signed-off-by: Pali Rohár <pali@kernel.org>
> ---
> fs/smb/client/reparse.c | 19 +++++++++++++++++++
> 1 file changed, 19 insertions(+)
>
> diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> index 0d1cea64ab6e..fb1d16b17f38 100644
> --- a/fs/smb/client/reparse.c
> +++ b/fs/smb/client/reparse.c
> @@ -544,6 +544,25 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> int rc;
> int i;
>
> + /* Check that length it valid for unicode/non-unicode mode */
> + if (!len || (unicode && (len % 2))) {
> + cifs_dbg(VFS, "srv returned malformed symlink buffer\n");
> + rc = -EIO;
> + goto out;
> + }
> +
> + /*
> + * Check that buffer does not contain UTF-16 null codepoint in unicode
> + * mode or null byte in non-unicode mode because Linux cannot process
> + * symlink with null byte.
> + */
> + if ((unicode && UniStrnlen((wchar_t *)buf, len/2) != len/2) ||
> + (!unicode && strnlen(buf, len) != len)) {
> + cifs_dbg(VFS, "srv returned null byte in native symlink target location\n");
> + rc = -EIO;
> + goto out;
> + }
> +
> smb_target = cifs_strndup_from_utf16(buf, len, unicode, cifs_sb->local_nls);
> if (!smb_target) {
> rc = -ENOMEM;
> --
> 2.20.1
>
>
--
Thanks,
Steve
^ permalink raw reply [flat|nested] 38+ messages in thread* Re: [PATCH v2 5/6] cifs: Validate content of native symlink
2024-10-13 4:21 ` Steve French
@ 2024-10-14 9:39 ` Pali Rohár
0 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-10-14 9:39 UTC (permalink / raw)
To: Steve French
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
On Saturday 12 October 2024 23:21:43 Steve French wrote:
> This looks like one of the more important of the fixes of this series
> - let me know if any thoughts or Reviewed-by/Acked-by on it. I may
> try to send this earlier than the others
>
> Pali,
> Let me know if I missed dependencies that this patch depends on
This one should be fine. I do not see any dependency for this change.
> On Sat, Oct 5, 2024 at 9:03 AM Pali Rohár <pali@kernel.org> wrote:
> >
> > Check that path buffer has correct length (it is non-zero and in UNICODE
> > mode it has even number of bytes) and check that buffer does not contain
> > null character (UTF-16 null codepoint in UNICODE mode or null byte in
> > non-unicode mode) because Linux cannot process symlink with null byte.
> >
> > Signed-off-by: Pali Rohár <pali@kernel.org>
> > ---
> > fs/smb/client/reparse.c | 19 +++++++++++++++++++
> > 1 file changed, 19 insertions(+)
> >
> > diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> > index 0d1cea64ab6e..fb1d16b17f38 100644
> > --- a/fs/smb/client/reparse.c
> > +++ b/fs/smb/client/reparse.c
> > @@ -544,6 +544,25 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> > int rc;
> > int i;
> >
> > + /* Check that length it valid for unicode/non-unicode mode */
> > + if (!len || (unicode && (len % 2))) {
> > + cifs_dbg(VFS, "srv returned malformed symlink buffer\n");
> > + rc = -EIO;
> > + goto out;
> > + }
> > +
> > + /*
> > + * Check that buffer does not contain UTF-16 null codepoint in unicode
> > + * mode or null byte in non-unicode mode because Linux cannot process
> > + * symlink with null byte.
> > + */
> > + if ((unicode && UniStrnlen((wchar_t *)buf, len/2) != len/2) ||
> > + (!unicode && strnlen(buf, len) != len)) {
> > + cifs_dbg(VFS, "srv returned null byte in native symlink target location\n");
> > + rc = -EIO;
> > + goto out;
> > + }
> > +
> > smb_target = cifs_strndup_from_utf16(buf, len, unicode, cifs_sb->local_nls);
> > if (!smb_target) {
> > rc = -ENOMEM;
> > --
> > 2.20.1
> >
> >
>
>
> --
> Thanks,
>
> Steve
^ permalink raw reply [flat|nested] 38+ messages in thread
* [PATCH v2 6/6] cifs: Fix creating and resolving absolute NT-style symlinks
2024-10-05 14:02 ` [PATCH v2 0/6] " Pali Rohár
` (4 preceding siblings ...)
2024-10-05 14:02 ` [PATCH v2 5/6] cifs: Validate content of native symlink Pali Rohár
@ 2024-10-05 14:03 ` Pali Rohár
2024-10-07 3:59 ` Steve French
2024-12-09 17:58 ` Pali Rohár
5 siblings, 2 replies; 38+ messages in thread
From: Pali Rohár @ 2024-10-05 14:03 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
If the SMB symlink is stored on NT server in absolute form then it points
to the NT object hierarchy, which is different from POSIX one and needs
some conversion / mapping.
To make interoperability with Windows SMB server and WSL subsystem, reuse
its logic of mapping between NT paths and POSIX paths into Linux SMB
client.
WSL subsystem on Windows uses for -t drvfs mount option -o symlinkroot=
which specifies the POSIX path where are expected to be mounted lowercase
Windows drive letters (without colon).
Do same for Linux SMB client and add a new mount option -o symlinkroot=
which mimics the drvfs mount option of the same name. It specifies where in
the Linux VFS hierarchy is the root of the DOS / Windows drive letters, and
translates between absolute NT-style symlinks and absolute Linux VFS
symlinks. Default value of symlinkroot is "/mnt", same what is using WSL.
Note that DOS / Windows drive letter symlinks are just subset of all
possible NT-style symlinks. Drive letters live in NT subtree \??\ and
important details about NT paths and object hierarchy are in the comments
in this change.
When symlink target location from non-POSIX SMB server is in absolute form
(indicated by absence of SYMLINK_FLAG_RELATIVE) then it is converted to
Linux absolute symlink according to symlinkroot configuration.
And when creating a new symlink on non-POSIX SMB server in absolute form
then Linux absolute target is converted to NT-style according to
symlinkroot configuration.
When SMB server is POSIX, then this change does not affect neither reading
target location of symlink, nor creating a new symlink. It is expected that
POSIX SMB server works with POSIX paths where the absolute root is /.
This change improves interoperability of absolute SMB symlinks with Windows
SMB servers.
Signed-off-by: Pali Rohár <pali@kernel.org>
---
fs/smb/client/fs_context.c | 22 +++
fs/smb/client/fs_context.h | 2 +
fs/smb/client/reparse.c | 267 ++++++++++++++++++++++++++++++++++---
3 files changed, 273 insertions(+), 18 deletions(-)
diff --git a/fs/smb/client/fs_context.c b/fs/smb/client/fs_context.c
index 2f0c3894b0f7..22b550860cc8 100644
--- a/fs/smb/client/fs_context.c
+++ b/fs/smb/client/fs_context.c
@@ -178,6 +178,7 @@ const struct fs_parameter_spec smb3_fs_parameters[] = {
fsparam_string("sec", Opt_sec),
fsparam_string("cache", Opt_cache),
fsparam_string("reparse", Opt_reparse),
+ fsparam_string("symlinkroot", Opt_symlinkroot),
/* Arguments that should be ignored */
fsparam_flag("guest", Opt_ignore),
@@ -355,6 +356,7 @@ smb3_fs_context_dup(struct smb3_fs_context *new_ctx, struct smb3_fs_context *ctx
new_ctx->source = NULL;
new_ctx->iocharset = NULL;
new_ctx->leaf_fullpath = NULL;
+ new_ctx->symlinkroot = NULL;
/*
* Make sure to stay in sync with smb3_cleanup_fs_context_contents()
*/
@@ -369,6 +371,7 @@ smb3_fs_context_dup(struct smb3_fs_context *new_ctx, struct smb3_fs_context *ctx
DUP_CTX_STR(nodename);
DUP_CTX_STR(iocharset);
DUP_CTX_STR(leaf_fullpath);
+ DUP_CTX_STR(symlinkroot);
return 0;
}
@@ -1614,9 +1617,26 @@ static int smb3_fs_context_parse_param(struct fs_context *fc,
if (parse_reparse_flavor(fc, param->string, ctx))
goto cifs_parse_mount_err;
break;
+ case Opt_symlinkroot:
+ if (param->string[0] != '/') {
+ cifs_errorf(fc, "symlinkroot mount options must be absolute path\n");
+ goto cifs_parse_mount_err;
+ }
+ kfree(ctx->symlinkroot);
+ ctx->symlinkroot = kstrdup(param->string, GFP_KERNEL);
+ if (!ctx->symlinkroot)
+ goto cifs_parse_mount_err;
+ break;
}
/* case Opt_ignore: - is ignored as expected ... */
+ /*
+ * By default resolve all native absolute symlinks relative to "/mnt/".
+ * Same default has drvfs driver running in WSL for resolving SMB shares.
+ */
+ if (!ctx->symlinkroot)
+ ctx->symlinkroot = kstrdup("/mnt/", GFP_KERNEL);
+
return 0;
cifs_parse_mount_err:
@@ -1747,6 +1767,8 @@ smb3_cleanup_fs_context_contents(struct smb3_fs_context *ctx)
ctx->prepath = NULL;
kfree(ctx->leaf_fullpath);
ctx->leaf_fullpath = NULL;
+ kfree(ctx->symlinkroot);
+ ctx->symlinkroot = NULL;
}
void
diff --git a/fs/smb/client/fs_context.h b/fs/smb/client/fs_context.h
index cf577ec0dd0a..8dd12498ffd8 100644
--- a/fs/smb/client/fs_context.h
+++ b/fs/smb/client/fs_context.h
@@ -157,6 +157,7 @@ enum cifs_param {
Opt_sec,
Opt_cache,
Opt_reparse,
+ Opt_symlinkroot,
/* Mount options to be ignored */
Opt_ignore,
@@ -284,6 +285,7 @@ struct smb3_fs_context {
struct cifs_ses *dfs_root_ses;
bool dfs_automount:1; /* set for dfs automount only */
enum cifs_reparse_type reparse_type;
+ char *symlinkroot; /* top level directory for native SMB symlinks in absolute format */
};
extern const struct fs_parameter_spec smb3_fs_parameters[];
diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
index fb1d16b17f38..a577b2d2a4fc 100644
--- a/fs/smb/client/reparse.c
+++ b/fs/smb/client/reparse.c
@@ -25,33 +25,128 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
const char *full_path, const char *symname)
{
struct reparse_symlink_data_buffer *buf = NULL;
- struct cifs_open_info_data data;
+ struct cifs_open_info_data data = {};
struct cifs_sb_info *cifs_sb = CIFS_SB(inode->i_sb);
struct inode *new;
struct kvec iov;
- __le16 *path;
+ __le16 *path = NULL;
bool directory;
- char *sym, sep = CIFS_DIR_SEP(cifs_sb);
- u16 len, plen;
+ char *symlink_target = NULL;
+ char *sym = NULL;
+ char sep = CIFS_DIR_SEP(cifs_sb);
+ u16 len, plen, poff, slen;
int rc = 0;
- sym = kstrdup(symname, GFP_KERNEL);
- if (!sym)
- return -ENOMEM;
+ symlink_target = kstrdup(symname, GFP_KERNEL);
+ if (!symlink_target) {
+ rc = -ENOMEM;
+ goto out;
+ }
data = (struct cifs_open_info_data) {
.reparse_point = true,
.reparse = { .tag = IO_REPARSE_TAG_SYMLINK, },
- .symlink_target = sym,
+ .symlink_target = symlink_target,
};
- convert_delimiter(sym, sep);
+ if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
+ /*
+ * This is a request to create an absolute symlink on the server
+ * which does not support POSIX paths, and expects symlink in
+ * NT-style path. So convert absolute Linux symlink target path
+ * to the absolute NT-style path. Root of the NT-style path for
+ * symlinks is specified in "symlinkroot" mount option. This will
+ * ensure compatibility of this symlink stored in absolute form
+ * on the SMB server.
+ */
+ if (!strstarts(symname, cifs_sb->ctx->symlinkroot)) {
+ /*
+ * If the absolute Linux symlink target path is not
+ * inside "symlinkroot" location then there is no way
+ * to convert such Linux symlink to NT-style path.
+ */
+ cifs_dbg(VFS,
+ "absolute symlink '%s' cannot be converted to NT format "
+ "because it is outside of symlinkroot='%s'\n",
+ symname, cifs_sb->ctx->symlinkroot);
+ rc = -EINVAL;
+ goto out;
+ }
+ len = strlen(cifs_sb->ctx->symlinkroot);
+ if (cifs_sb->ctx->symlinkroot[len-1] != '/')
+ len++;
+ if (symname[len] >= 'a' && symname[len] <= 'z' &&
+ (symname[len+1] == '/' || symname[len+1] == '\0')) {
+ /*
+ * Symlink points to Linux target /symlinkroot/x/path/...
+ * where 'x' is the lowercase local Windows drive.
+ * NT-style path for 'x' has common form \??\X:\path\...
+ * with uppercase local Windows drive.
+ */
+ int common_path_len = strlen(symname+len+1)+1;
+ sym = kzalloc(6+common_path_len, GFP_KERNEL);
+ if (!sym) {
+ rc = -ENOMEM;
+ goto out;
+ }
+ memcpy(sym, "\\??\\", 4);
+ sym[4] = symname[len] - ('a'-'A');
+ sym[5] = ':';
+ memcpy(sym+6, symname+len+1, common_path_len);
+ } else {
+ /* Unhandled absolute symlink. Report an error. */
+ cifs_dbg(
+ VFS,
+ "absolute symlink '%s' cannot be converted to NT format "
+ "because it points to unknown target\n",
+ symname);
+ rc = -EINVAL;
+ goto out;
+ }
+ } else {
+ /*
+ * This is request to either create an absolute symlink on
+ * server which expects POSIX paths or it is an request to
+ * create a relative symlink from the current directory.
+ * These paths have same format as relative SMB symlinks,
+ * so no conversion is needed. So just take symname as-is.
+ */
+ sym = kstrdup(symname, GFP_KERNEL);
+ if (!sym) {
+ rc = -ENOMEM;
+ goto out;
+ }
+ }
+
+ if (sep == '\\')
+ convert_delimiter(sym, sep);
+
+ /*
+ * For absolute NT symlinks it is required to pass also leading
+ * backslash and to not mangle NT object prefix "\\??\\" and not to
+ * mangle colon in drive letter. But cifs_convert_path_to_utf16()
+ * removes leading backslash and replaces '?' and ':'. So temporary
+ * mask these characters in NT object prefix by '_' and then change
+ * them back.
+ */
+ if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/')
+ sym[0] = sym[1] = sym[2] = sym[5] = '_';
+
path = cifs_convert_path_to_utf16(sym, cifs_sb);
if (!path) {
rc = -ENOMEM;
goto out;
}
+ if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
+ sym[0] = '\\';
+ sym[1] = sym[2] = '?';
+ sym[5] = ':';
+ path[0] = '\\';
+ path[1] = path[2] = '?';
+ path[5] = ':';
+ }
+
/*
* SMB distinguish between symlink to directory and symlink to file.
* They cannot be exchanged (symlink of file type which points to
@@ -64,8 +159,18 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
if (rc < 0)
goto out;
- plen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
- len = sizeof(*buf) + plen * 2;
+ slen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
+ poff = 0;
+ plen = slen;
+ if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
+ /*
+ * For absolute NT symlinks skip leading "\\??\\" in PrintName as
+ * PrintName is user visible location in DOS/Win32 format (not in NT format).
+ */
+ poff = 4;
+ plen -= 2 * poff;
+ }
+ len = sizeof(*buf) + plen + slen;
buf = kzalloc(len, GFP_KERNEL);
if (!buf) {
rc = -ENOMEM;
@@ -74,17 +179,17 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
buf->ReparseTag = cpu_to_le32(IO_REPARSE_TAG_SYMLINK);
buf->ReparseDataLength = cpu_to_le16(len - sizeof(struct reparse_data_buffer));
+
buf->SubstituteNameOffset = cpu_to_le16(plen);
- buf->SubstituteNameLength = cpu_to_le16(plen);
- memcpy(&buf->PathBuffer[plen], path, plen);
+ buf->SubstituteNameLength = cpu_to_le16(slen);
+ memcpy(&buf->PathBuffer[plen], path, slen);
+
buf->PrintNameOffset = 0;
buf->PrintNameLength = cpu_to_le16(plen);
- memcpy(buf->PathBuffer, path, plen);
+ memcpy(buf->PathBuffer, path+poff, plen);
+
buf->Flags = cpu_to_le32(*symname != '/' ? SYMLINK_FLAG_RELATIVE : 0);
- if (*sym != sep)
- buf->Flags = cpu_to_le32(SYMLINK_FLAG_RELATIVE);
- convert_delimiter(sym, '/');
iov.iov_base = buf;
iov.iov_len = len;
new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
@@ -95,6 +200,7 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
else
rc = PTR_ERR(new);
out:
+ kfree(sym);
kfree(path);
cifs_free_open_info(&data);
kfree(buf);
@@ -540,6 +646,9 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
char sep = CIFS_DIR_SEP(cifs_sb);
char *linux_target = NULL;
char *smb_target = NULL;
+ int symlinkroot_len;
+ int abs_path_len;
+ char *abs_path;
int levels;
int rc;
int i;
@@ -569,7 +678,123 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
goto out;
}
- if (smb_target[0] == sep && relative) {
+ if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && !relative) {
+ /*
+ * This is an absolute symlink from the server which does not
+ * support POSIX paths, so the symlink is in NT-style path.
+ * So convert it to absolute Linux symlink target path. Root of
+ * the NT-style path for symlinks is specified in "symlinkroot"
+ * mount option.
+ *
+ * Root of the DOS and Win32 paths is at NT path \??\
+ * It means that DOS/Win32 path C:\folder\file.txt is
+ * NT path \??\C:\folder\file.txt
+ *
+ * NT systems have some well-known object symlinks in their NT
+ * hierarchy, which is needed to take into account when resolving
+ * other symlinks. Most commonly used symlink paths are:
+ * \?? -> \GLOBAL??
+ * \DosDevices -> \??
+ * \GLOBAL??\GLOBALROOT -> \
+ * \GLOBAL??\Global -> \GLOBAL??
+ * \GLOBAL??\NUL -> \Device\Null
+ * \GLOBAL??\UNC -> \Device\Mup
+ * \GLOBAL??\PhysicalDrive0 -> \Device\Harddisk0\DR0 (for each harddisk)
+ * \GLOBAL??\A: -> \Device\Floppy0 (if A: is the first floppy)
+ * \GLOBAL??\C: -> \Device\HarddiskVolume1 (if C: is the first harddisk)
+ * \GLOBAL??\D: -> \Device\CdRom0 (if D: is first cdrom)
+ * \SystemRoot -> \Device\Harddisk0\Partition1\WINDOWS (or where is NT system installed)
+ * \Volume{...} -> \Device\HarddiskVolume1 (where ... is system generated guid)
+ *
+ * In most common cases, absolute NT symlinks points to path on
+ * DOS/Win32 drive letter, system-specific Volume or on UNC share.
+ * Here are few examples of commonly used absolute NT symlinks
+ * created by mklink.exe tool:
+ * \??\C:\folder\file.txt
+ * \??\\C:\folder\file.txt
+ * \??\UNC\server\share\file.txt
+ * \??\\UNC\server\share\file.txt
+ * \??\Volume{b75e2c83-0000-0000-0000-602f00000000}\folder\file.txt
+ *
+ * It means that the most common path prefix \??\ is also NT path
+ * symlink (to \GLOBAL??). It is less common that second path
+ * separator is double backslash, but it is valid.
+ *
+ * Volume guid is randomly generated by the target system and so
+ * only the target system knows the mapping between guid and the
+ * hardisk number. Over SMB it is not possible to resolve this
+ * mapping, therefore symlinks pointing to target location of
+ * volume guids are totally unusable over SMB.
+ *
+ * For now parse only symlink paths available for DOS and Win32.
+ * Those are paths with \??\ prefix or paths which points to \??\
+ * via other NT symlink (\DosDevices\, \GLOBAL??\, ...).
+ */
+ abs_path = smb_target;
+globalroot:
+ if (strstarts(abs_path, "\\??\\"))
+ abs_path += sizeof("\\??\\")-1;
+ else if (strstarts(abs_path, "\\DosDevices\\"))
+ abs_path += sizeof("\\DosDevices\\")-1;
+ else if (strstarts(abs_path, "\\GLOBAL??\\"))
+ abs_path += sizeof("\\GLOBAL??\\")-1;
+ else {
+ /* Unhandled absolute symlink, points outside of DOS/Win32 */
+ cifs_dbg(VFS,
+ "absolute symlink '%s' cannot be converted from NT format "
+ "because points to unknown target\n",
+ smb_target);
+ rc = -EIO;
+ goto out;
+ }
+
+ /* Sometimes path separator after \?? is double backslash */
+ if (abs_path[0] == '\\')
+ abs_path++;
+
+ while (strstarts(abs_path, "Global\\"))
+ abs_path += sizeof("Global\\")-1;
+
+ if (strstarts(abs_path, "GLOBALROOT\\")) {
+ /* Label globalroot requires path with leading '\\', so do not trim '\\' */
+ abs_path += sizeof("GLOBALROOT")-1;
+ goto globalroot;
+ }
+
+ /* For now parse only paths to drive letters */
+ if (((abs_path[0] >= 'A' && abs_path[0] <= 'Z') ||
+ (abs_path[0] >= 'a' && abs_path[0] <= 'z')) &&
+ abs_path[1] == ':' &&
+ (abs_path[2] == '\\' || abs_path[2] == '\0')) {
+ /* Convert drive letter to lowercase and drop colon */
+ char drive_letter = abs_path[0];
+ if (drive_letter >= 'A' && drive_letter <= 'Z')
+ drive_letter += 'a'-'A';
+ abs_path++;
+ abs_path[0] = drive_letter;
+ } else {
+ /* Unhandled absolute symlink. Report an error. */
+ cifs_dbg(VFS,
+ "absolute symlink '%s' cannot be converted from NT format "
+ "because points to unknown target\n",
+ smb_target);
+ rc = -EIO;
+ goto out;
+ }
+
+ abs_path_len = strlen(abs_path)+1;
+ symlinkroot_len = strlen(cifs_sb->ctx->symlinkroot);
+ if (cifs_sb->ctx->symlinkroot[symlinkroot_len-1] == '/')
+ symlinkroot_len--;
+ linux_target = kmalloc(symlinkroot_len + 1 + abs_path_len, GFP_KERNEL);
+ if (!linux_target) {
+ rc = -ENOMEM;
+ goto out;
+ }
+ memcpy(linux_target, cifs_sb->ctx->symlinkroot, symlinkroot_len);
+ linux_target[symlinkroot_len] = '/';
+ memcpy(linux_target + symlinkroot_len + 1, abs_path, abs_path_len);
+ } else if (smb_target[0] == sep && relative) {
/*
* This is a relative SMB symlink from the top of the share,
* which is the top level directory of the Linux mount point.
@@ -598,6 +823,12 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
}
memcpy(linux_target + levels*3, smb_target+1, smb_target_len); /* +1 to skip leading sep */
} else {
+ /*
+ * This is either an absolute symlink in POSIX-style format
+ * or relative SMB symlink from the current directory.
+ * These paths have same format as Linux symlinks, so no
+ * conversion is needed.
+ */
linux_target = smb_target;
smb_target = NULL;
}
--
2.20.1
^ permalink raw reply related [flat|nested] 38+ messages in thread* Re: [PATCH v2 6/6] cifs: Fix creating and resolving absolute NT-style symlinks
2024-10-05 14:03 ` [PATCH v2 6/6] cifs: Fix creating and resolving absolute NT-style symlinks Pali Rohár
@ 2024-10-07 3:59 ` Steve French
2024-10-07 18:09 ` Pali Rohár
2024-12-09 17:58 ` Pali Rohár
1 sibling, 1 reply; 38+ messages in thread
From: Steve French @ 2024-10-07 3:59 UTC (permalink / raw)
To: Pali Rohár
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
This patch had a merge conflict in fs_context.h and wouldn't apply
cleanly. Do you have a git branch with this series (or all three
recent patch series) applied on 6.12-rc2?
On Sat, Oct 5, 2024 at 9:04 AM Pali Rohár <pali@kernel.org> wrote:
>
> If the SMB symlink is stored on NT server in absolute form then it points
> to the NT object hierarchy, which is different from POSIX one and needs
> some conversion / mapping.
>
> To make interoperability with Windows SMB server and WSL subsystem, reuse
> its logic of mapping between NT paths and POSIX paths into Linux SMB
> client.
>
> WSL subsystem on Windows uses for -t drvfs mount option -o symlinkroot=
> which specifies the POSIX path where are expected to be mounted lowercase
> Windows drive letters (without colon).
>
> Do same for Linux SMB client and add a new mount option -o symlinkroot=
> which mimics the drvfs mount option of the same name. It specifies where in
> the Linux VFS hierarchy is the root of the DOS / Windows drive letters, and
> translates between absolute NT-style symlinks and absolute Linux VFS
> symlinks. Default value of symlinkroot is "/mnt", same what is using WSL.
>
> Note that DOS / Windows drive letter symlinks are just subset of all
> possible NT-style symlinks. Drive letters live in NT subtree \??\ and
> important details about NT paths and object hierarchy are in the comments
> in this change.
>
> When symlink target location from non-POSIX SMB server is in absolute form
> (indicated by absence of SYMLINK_FLAG_RELATIVE) then it is converted to
> Linux absolute symlink according to symlinkroot configuration.
>
> And when creating a new symlink on non-POSIX SMB server in absolute form
> then Linux absolute target is converted to NT-style according to
> symlinkroot configuration.
>
> When SMB server is POSIX, then this change does not affect neither reading
> target location of symlink, nor creating a new symlink. It is expected that
> POSIX SMB server works with POSIX paths where the absolute root is /.
>
> This change improves interoperability of absolute SMB symlinks with Windows
> SMB servers.
>
> Signed-off-by: Pali Rohár <pali@kernel.org>
> ---
> fs/smb/client/fs_context.c | 22 +++
> fs/smb/client/fs_context.h | 2 +
> fs/smb/client/reparse.c | 267 ++++++++++++++++++++++++++++++++++---
> 3 files changed, 273 insertions(+), 18 deletions(-)
>
> diff --git a/fs/smb/client/fs_context.c b/fs/smb/client/fs_context.c
> index 2f0c3894b0f7..22b550860cc8 100644
> --- a/fs/smb/client/fs_context.c
> +++ b/fs/smb/client/fs_context.c
> @@ -178,6 +178,7 @@ const struct fs_parameter_spec smb3_fs_parameters[] = {
> fsparam_string("sec", Opt_sec),
> fsparam_string("cache", Opt_cache),
> fsparam_string("reparse", Opt_reparse),
> + fsparam_string("symlinkroot", Opt_symlinkroot),
>
> /* Arguments that should be ignored */
> fsparam_flag("guest", Opt_ignore),
> @@ -355,6 +356,7 @@ smb3_fs_context_dup(struct smb3_fs_context *new_ctx, struct smb3_fs_context *ctx
> new_ctx->source = NULL;
> new_ctx->iocharset = NULL;
> new_ctx->leaf_fullpath = NULL;
> + new_ctx->symlinkroot = NULL;
> /*
> * Make sure to stay in sync with smb3_cleanup_fs_context_contents()
> */
> @@ -369,6 +371,7 @@ smb3_fs_context_dup(struct smb3_fs_context *new_ctx, struct smb3_fs_context *ctx
> DUP_CTX_STR(nodename);
> DUP_CTX_STR(iocharset);
> DUP_CTX_STR(leaf_fullpath);
> + DUP_CTX_STR(symlinkroot);
>
> return 0;
> }
> @@ -1614,9 +1617,26 @@ static int smb3_fs_context_parse_param(struct fs_context *fc,
> if (parse_reparse_flavor(fc, param->string, ctx))
> goto cifs_parse_mount_err;
> break;
> + case Opt_symlinkroot:
> + if (param->string[0] != '/') {
> + cifs_errorf(fc, "symlinkroot mount options must be absolute path\n");
> + goto cifs_parse_mount_err;
> + }
> + kfree(ctx->symlinkroot);
> + ctx->symlinkroot = kstrdup(param->string, GFP_KERNEL);
> + if (!ctx->symlinkroot)
> + goto cifs_parse_mount_err;
> + break;
> }
> /* case Opt_ignore: - is ignored as expected ... */
>
> + /*
> + * By default resolve all native absolute symlinks relative to "/mnt/".
> + * Same default has drvfs driver running in WSL for resolving SMB shares.
> + */
> + if (!ctx->symlinkroot)
> + ctx->symlinkroot = kstrdup("/mnt/", GFP_KERNEL);
> +
> return 0;
>
> cifs_parse_mount_err:
> @@ -1747,6 +1767,8 @@ smb3_cleanup_fs_context_contents(struct smb3_fs_context *ctx)
> ctx->prepath = NULL;
> kfree(ctx->leaf_fullpath);
> ctx->leaf_fullpath = NULL;
> + kfree(ctx->symlinkroot);
> + ctx->symlinkroot = NULL;
> }
>
> void
> diff --git a/fs/smb/client/fs_context.h b/fs/smb/client/fs_context.h
> index cf577ec0dd0a..8dd12498ffd8 100644
> --- a/fs/smb/client/fs_context.h
> +++ b/fs/smb/client/fs_context.h
> @@ -157,6 +157,7 @@ enum cifs_param {
> Opt_sec,
> Opt_cache,
> Opt_reparse,
> + Opt_symlinkroot,
>
> /* Mount options to be ignored */
> Opt_ignore,
> @@ -284,6 +285,7 @@ struct smb3_fs_context {
> struct cifs_ses *dfs_root_ses;
> bool dfs_automount:1; /* set for dfs automount only */
> enum cifs_reparse_type reparse_type;
> + char *symlinkroot; /* top level directory for native SMB symlinks in absolute format */
> };
>
> extern const struct fs_parameter_spec smb3_fs_parameters[];
> diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> index fb1d16b17f38..a577b2d2a4fc 100644
> --- a/fs/smb/client/reparse.c
> +++ b/fs/smb/client/reparse.c
> @@ -25,33 +25,128 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> const char *full_path, const char *symname)
> {
> struct reparse_symlink_data_buffer *buf = NULL;
> - struct cifs_open_info_data data;
> + struct cifs_open_info_data data = {};
> struct cifs_sb_info *cifs_sb = CIFS_SB(inode->i_sb);
> struct inode *new;
> struct kvec iov;
> - __le16 *path;
> + __le16 *path = NULL;
> bool directory;
> - char *sym, sep = CIFS_DIR_SEP(cifs_sb);
> - u16 len, plen;
> + char *symlink_target = NULL;
> + char *sym = NULL;
> + char sep = CIFS_DIR_SEP(cifs_sb);
> + u16 len, plen, poff, slen;
> int rc = 0;
>
> - sym = kstrdup(symname, GFP_KERNEL);
> - if (!sym)
> - return -ENOMEM;
> + symlink_target = kstrdup(symname, GFP_KERNEL);
> + if (!symlink_target) {
> + rc = -ENOMEM;
> + goto out;
> + }
>
> data = (struct cifs_open_info_data) {
> .reparse_point = true,
> .reparse = { .tag = IO_REPARSE_TAG_SYMLINK, },
> - .symlink_target = sym,
> + .symlink_target = symlink_target,
> };
>
> - convert_delimiter(sym, sep);
> + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
> + /*
> + * This is a request to create an absolute symlink on the server
> + * which does not support POSIX paths, and expects symlink in
> + * NT-style path. So convert absolute Linux symlink target path
> + * to the absolute NT-style path. Root of the NT-style path for
> + * symlinks is specified in "symlinkroot" mount option. This will
> + * ensure compatibility of this symlink stored in absolute form
> + * on the SMB server.
> + */
> + if (!strstarts(symname, cifs_sb->ctx->symlinkroot)) {
> + /*
> + * If the absolute Linux symlink target path is not
> + * inside "symlinkroot" location then there is no way
> + * to convert such Linux symlink to NT-style path.
> + */
> + cifs_dbg(VFS,
> + "absolute symlink '%s' cannot be converted to NT format "
> + "because it is outside of symlinkroot='%s'\n",
> + symname, cifs_sb->ctx->symlinkroot);
> + rc = -EINVAL;
> + goto out;
> + }
> + len = strlen(cifs_sb->ctx->symlinkroot);
> + if (cifs_sb->ctx->symlinkroot[len-1] != '/')
> + len++;
> + if (symname[len] >= 'a' && symname[len] <= 'z' &&
> + (symname[len+1] == '/' || symname[len+1] == '\0')) {
> + /*
> + * Symlink points to Linux target /symlinkroot/x/path/...
> + * where 'x' is the lowercase local Windows drive.
> + * NT-style path for 'x' has common form \??\X:\path\...
> + * with uppercase local Windows drive.
> + */
> + int common_path_len = strlen(symname+len+1)+1;
> + sym = kzalloc(6+common_path_len, GFP_KERNEL);
> + if (!sym) {
> + rc = -ENOMEM;
> + goto out;
> + }
> + memcpy(sym, "\\??\\", 4);
> + sym[4] = symname[len] - ('a'-'A');
> + sym[5] = ':';
> + memcpy(sym+6, symname+len+1, common_path_len);
> + } else {
> + /* Unhandled absolute symlink. Report an error. */
> + cifs_dbg(
> + VFS,
> + "absolute symlink '%s' cannot be converted to NT format "
> + "because it points to unknown target\n",
> + symname);
> + rc = -EINVAL;
> + goto out;
> + }
> + } else {
> + /*
> + * This is request to either create an absolute symlink on
> + * server which expects POSIX paths or it is an request to
> + * create a relative symlink from the current directory.
> + * These paths have same format as relative SMB symlinks,
> + * so no conversion is needed. So just take symname as-is.
> + */
> + sym = kstrdup(symname, GFP_KERNEL);
> + if (!sym) {
> + rc = -ENOMEM;
> + goto out;
> + }
> + }
> +
> + if (sep == '\\')
> + convert_delimiter(sym, sep);
> +
> + /*
> + * For absolute NT symlinks it is required to pass also leading
> + * backslash and to not mangle NT object prefix "\\??\\" and not to
> + * mangle colon in drive letter. But cifs_convert_path_to_utf16()
> + * removes leading backslash and replaces '?' and ':'. So temporary
> + * mask these characters in NT object prefix by '_' and then change
> + * them back.
> + */
> + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/')
> + sym[0] = sym[1] = sym[2] = sym[5] = '_';
> +
> path = cifs_convert_path_to_utf16(sym, cifs_sb);
> if (!path) {
> rc = -ENOMEM;
> goto out;
> }
>
> + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
> + sym[0] = '\\';
> + sym[1] = sym[2] = '?';
> + sym[5] = ':';
> + path[0] = '\\';
> + path[1] = path[2] = '?';
> + path[5] = ':';
> + }
> +
> /*
> * SMB distinguish between symlink to directory and symlink to file.
> * They cannot be exchanged (symlink of file type which points to
> @@ -64,8 +159,18 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> if (rc < 0)
> goto out;
>
> - plen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
> - len = sizeof(*buf) + plen * 2;
> + slen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
> + poff = 0;
> + plen = slen;
> + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
> + /*
> + * For absolute NT symlinks skip leading "\\??\\" in PrintName as
> + * PrintName is user visible location in DOS/Win32 format (not in NT format).
> + */
> + poff = 4;
> + plen -= 2 * poff;
> + }
> + len = sizeof(*buf) + plen + slen;
> buf = kzalloc(len, GFP_KERNEL);
> if (!buf) {
> rc = -ENOMEM;
> @@ -74,17 +179,17 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
>
> buf->ReparseTag = cpu_to_le32(IO_REPARSE_TAG_SYMLINK);
> buf->ReparseDataLength = cpu_to_le16(len - sizeof(struct reparse_data_buffer));
> +
> buf->SubstituteNameOffset = cpu_to_le16(plen);
> - buf->SubstituteNameLength = cpu_to_le16(plen);
> - memcpy(&buf->PathBuffer[plen], path, plen);
> + buf->SubstituteNameLength = cpu_to_le16(slen);
> + memcpy(&buf->PathBuffer[plen], path, slen);
> +
> buf->PrintNameOffset = 0;
> buf->PrintNameLength = cpu_to_le16(plen);
> - memcpy(buf->PathBuffer, path, plen);
> + memcpy(buf->PathBuffer, path+poff, plen);
> +
> buf->Flags = cpu_to_le32(*symname != '/' ? SYMLINK_FLAG_RELATIVE : 0);
> - if (*sym != sep)
> - buf->Flags = cpu_to_le32(SYMLINK_FLAG_RELATIVE);
>
> - convert_delimiter(sym, '/');
> iov.iov_base = buf;
> iov.iov_len = len;
> new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
> @@ -95,6 +200,7 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> else
> rc = PTR_ERR(new);
> out:
> + kfree(sym);
> kfree(path);
> cifs_free_open_info(&data);
> kfree(buf);
> @@ -540,6 +646,9 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> char sep = CIFS_DIR_SEP(cifs_sb);
> char *linux_target = NULL;
> char *smb_target = NULL;
> + int symlinkroot_len;
> + int abs_path_len;
> + char *abs_path;
> int levels;
> int rc;
> int i;
> @@ -569,7 +678,123 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> goto out;
> }
>
> - if (smb_target[0] == sep && relative) {
> + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && !relative) {
> + /*
> + * This is an absolute symlink from the server which does not
> + * support POSIX paths, so the symlink is in NT-style path.
> + * So convert it to absolute Linux symlink target path. Root of
> + * the NT-style path for symlinks is specified in "symlinkroot"
> + * mount option.
> + *
> + * Root of the DOS and Win32 paths is at NT path \??\
> + * It means that DOS/Win32 path C:\folder\file.txt is
> + * NT path \??\C:\folder\file.txt
> + *
> + * NT systems have some well-known object symlinks in their NT
> + * hierarchy, which is needed to take into account when resolving
> + * other symlinks. Most commonly used symlink paths are:
> + * \?? -> \GLOBAL??
> + * \DosDevices -> \??
> + * \GLOBAL??\GLOBALROOT -> \
> + * \GLOBAL??\Global -> \GLOBAL??
> + * \GLOBAL??\NUL -> \Device\Null
> + * \GLOBAL??\UNC -> \Device\Mup
> + * \GLOBAL??\PhysicalDrive0 -> \Device\Harddisk0\DR0 (for each harddisk)
> + * \GLOBAL??\A: -> \Device\Floppy0 (if A: is the first floppy)
> + * \GLOBAL??\C: -> \Device\HarddiskVolume1 (if C: is the first harddisk)
> + * \GLOBAL??\D: -> \Device\CdRom0 (if D: is first cdrom)
> + * \SystemRoot -> \Device\Harddisk0\Partition1\WINDOWS (or where is NT system installed)
> + * \Volume{...} -> \Device\HarddiskVolume1 (where ... is system generated guid)
> + *
> + * In most common cases, absolute NT symlinks points to path on
> + * DOS/Win32 drive letter, system-specific Volume or on UNC share.
> + * Here are few examples of commonly used absolute NT symlinks
> + * created by mklink.exe tool:
> + * \??\C:\folder\file.txt
> + * \??\\C:\folder\file.txt
> + * \??\UNC\server\share\file.txt
> + * \??\\UNC\server\share\file.txt
> + * \??\Volume{b75e2c83-0000-0000-0000-602f00000000}\folder\file.txt
> + *
> + * It means that the most common path prefix \??\ is also NT path
> + * symlink (to \GLOBAL??). It is less common that second path
> + * separator is double backslash, but it is valid.
> + *
> + * Volume guid is randomly generated by the target system and so
> + * only the target system knows the mapping between guid and the
> + * hardisk number. Over SMB it is not possible to resolve this
> + * mapping, therefore symlinks pointing to target location of
> + * volume guids are totally unusable over SMB.
> + *
> + * For now parse only symlink paths available for DOS and Win32.
> + * Those are paths with \??\ prefix or paths which points to \??\
> + * via other NT symlink (\DosDevices\, \GLOBAL??\, ...).
> + */
> + abs_path = smb_target;
> +globalroot:
> + if (strstarts(abs_path, "\\??\\"))
> + abs_path += sizeof("\\??\\")-1;
> + else if (strstarts(abs_path, "\\DosDevices\\"))
> + abs_path += sizeof("\\DosDevices\\")-1;
> + else if (strstarts(abs_path, "\\GLOBAL??\\"))
> + abs_path += sizeof("\\GLOBAL??\\")-1;
> + else {
> + /* Unhandled absolute symlink, points outside of DOS/Win32 */
> + cifs_dbg(VFS,
> + "absolute symlink '%s' cannot be converted from NT format "
> + "because points to unknown target\n",
> + smb_target);
> + rc = -EIO;
> + goto out;
> + }
> +
> + /* Sometimes path separator after \?? is double backslash */
> + if (abs_path[0] == '\\')
> + abs_path++;
> +
> + while (strstarts(abs_path, "Global\\"))
> + abs_path += sizeof("Global\\")-1;
> +
> + if (strstarts(abs_path, "GLOBALROOT\\")) {
> + /* Label globalroot requires path with leading '\\', so do not trim '\\' */
> + abs_path += sizeof("GLOBALROOT")-1;
> + goto globalroot;
> + }
> +
> + /* For now parse only paths to drive letters */
> + if (((abs_path[0] >= 'A' && abs_path[0] <= 'Z') ||
> + (abs_path[0] >= 'a' && abs_path[0] <= 'z')) &&
> + abs_path[1] == ':' &&
> + (abs_path[2] == '\\' || abs_path[2] == '\0')) {
> + /* Convert drive letter to lowercase and drop colon */
> + char drive_letter = abs_path[0];
> + if (drive_letter >= 'A' && drive_letter <= 'Z')
> + drive_letter += 'a'-'A';
> + abs_path++;
> + abs_path[0] = drive_letter;
> + } else {
> + /* Unhandled absolute symlink. Report an error. */
> + cifs_dbg(VFS,
> + "absolute symlink '%s' cannot be converted from NT format "
> + "because points to unknown target\n",
> + smb_target);
> + rc = -EIO;
> + goto out;
> + }
> +
> + abs_path_len = strlen(abs_path)+1;
> + symlinkroot_len = strlen(cifs_sb->ctx->symlinkroot);
> + if (cifs_sb->ctx->symlinkroot[symlinkroot_len-1] == '/')
> + symlinkroot_len--;
> + linux_target = kmalloc(symlinkroot_len + 1 + abs_path_len, GFP_KERNEL);
> + if (!linux_target) {
> + rc = -ENOMEM;
> + goto out;
> + }
> + memcpy(linux_target, cifs_sb->ctx->symlinkroot, symlinkroot_len);
> + linux_target[symlinkroot_len] = '/';
> + memcpy(linux_target + symlinkroot_len + 1, abs_path, abs_path_len);
> + } else if (smb_target[0] == sep && relative) {
> /*
> * This is a relative SMB symlink from the top of the share,
> * which is the top level directory of the Linux mount point.
> @@ -598,6 +823,12 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> }
> memcpy(linux_target + levels*3, smb_target+1, smb_target_len); /* +1 to skip leading sep */
> } else {
> + /*
> + * This is either an absolute symlink in POSIX-style format
> + * or relative SMB symlink from the current directory.
> + * These paths have same format as Linux symlinks, so no
> + * conversion is needed.
> + */
> linux_target = smb_target;
> smb_target = NULL;
> }
> --
> 2.20.1
>
>
--
Thanks,
Steve
^ permalink raw reply [flat|nested] 38+ messages in thread* Re: [PATCH v2 6/6] cifs: Fix creating and resolving absolute NT-style symlinks
2024-10-07 3:59 ` Steve French
@ 2024-10-07 18:09 ` Pali Rohár
0 siblings, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-10-07 18:09 UTC (permalink / raw)
To: Steve French
Cc: Steve French, Paulo Alcantara, Ronnie Sahlberg, linux-cifs,
linux-kernel
I have figured it out. There is a new member at the end of the struct
smb3_fs_context. I have resolved this rebase conflict and pushed update
of this change into my cifs branch (based on 6.12-rc2) at:
https://git.kernel.org/pub/scm/linux/kernel/git/pali/linux.git/log/?h=cifs
Beware that this branch contains also some other WIP patches which I
have not sent yet.
If needed I can resend just this one rebased patch to the list.
On Sunday 06 October 2024 22:59:18 Steve French wrote:
> This patch had a merge conflict in fs_context.h and wouldn't apply
> cleanly. Do you have a git branch with this series (or all three
> recent patch series) applied on 6.12-rc2?
>
> On Sat, Oct 5, 2024 at 9:04 AM Pali Rohár <pali@kernel.org> wrote:
> >
> > If the SMB symlink is stored on NT server in absolute form then it points
> > to the NT object hierarchy, which is different from POSIX one and needs
> > some conversion / mapping.
> >
> > To make interoperability with Windows SMB server and WSL subsystem, reuse
> > its logic of mapping between NT paths and POSIX paths into Linux SMB
> > client.
> >
> > WSL subsystem on Windows uses for -t drvfs mount option -o symlinkroot=
> > which specifies the POSIX path where are expected to be mounted lowercase
> > Windows drive letters (without colon).
> >
> > Do same for Linux SMB client and add a new mount option -o symlinkroot=
> > which mimics the drvfs mount option of the same name. It specifies where in
> > the Linux VFS hierarchy is the root of the DOS / Windows drive letters, and
> > translates between absolute NT-style symlinks and absolute Linux VFS
> > symlinks. Default value of symlinkroot is "/mnt", same what is using WSL.
> >
> > Note that DOS / Windows drive letter symlinks are just subset of all
> > possible NT-style symlinks. Drive letters live in NT subtree \??\ and
> > important details about NT paths and object hierarchy are in the comments
> > in this change.
> >
> > When symlink target location from non-POSIX SMB server is in absolute form
> > (indicated by absence of SYMLINK_FLAG_RELATIVE) then it is converted to
> > Linux absolute symlink according to symlinkroot configuration.
> >
> > And when creating a new symlink on non-POSIX SMB server in absolute form
> > then Linux absolute target is converted to NT-style according to
> > symlinkroot configuration.
> >
> > When SMB server is POSIX, then this change does not affect neither reading
> > target location of symlink, nor creating a new symlink. It is expected that
> > POSIX SMB server works with POSIX paths where the absolute root is /.
> >
> > This change improves interoperability of absolute SMB symlinks with Windows
> > SMB servers.
> >
> > Signed-off-by: Pali Rohár <pali@kernel.org>
> > ---
> > fs/smb/client/fs_context.c | 22 +++
> > fs/smb/client/fs_context.h | 2 +
> > fs/smb/client/reparse.c | 267 ++++++++++++++++++++++++++++++++++---
> > 3 files changed, 273 insertions(+), 18 deletions(-)
> >
> > diff --git a/fs/smb/client/fs_context.c b/fs/smb/client/fs_context.c
> > index 2f0c3894b0f7..22b550860cc8 100644
> > --- a/fs/smb/client/fs_context.c
> > +++ b/fs/smb/client/fs_context.c
> > @@ -178,6 +178,7 @@ const struct fs_parameter_spec smb3_fs_parameters[] = {
> > fsparam_string("sec", Opt_sec),
> > fsparam_string("cache", Opt_cache),
> > fsparam_string("reparse", Opt_reparse),
> > + fsparam_string("symlinkroot", Opt_symlinkroot),
> >
> > /* Arguments that should be ignored */
> > fsparam_flag("guest", Opt_ignore),
> > @@ -355,6 +356,7 @@ smb3_fs_context_dup(struct smb3_fs_context *new_ctx, struct smb3_fs_context *ctx
> > new_ctx->source = NULL;
> > new_ctx->iocharset = NULL;
> > new_ctx->leaf_fullpath = NULL;
> > + new_ctx->symlinkroot = NULL;
> > /*
> > * Make sure to stay in sync with smb3_cleanup_fs_context_contents()
> > */
> > @@ -369,6 +371,7 @@ smb3_fs_context_dup(struct smb3_fs_context *new_ctx, struct smb3_fs_context *ctx
> > DUP_CTX_STR(nodename);
> > DUP_CTX_STR(iocharset);
> > DUP_CTX_STR(leaf_fullpath);
> > + DUP_CTX_STR(symlinkroot);
> >
> > return 0;
> > }
> > @@ -1614,9 +1617,26 @@ static int smb3_fs_context_parse_param(struct fs_context *fc,
> > if (parse_reparse_flavor(fc, param->string, ctx))
> > goto cifs_parse_mount_err;
> > break;
> > + case Opt_symlinkroot:
> > + if (param->string[0] != '/') {
> > + cifs_errorf(fc, "symlinkroot mount options must be absolute path\n");
> > + goto cifs_parse_mount_err;
> > + }
> > + kfree(ctx->symlinkroot);
> > + ctx->symlinkroot = kstrdup(param->string, GFP_KERNEL);
> > + if (!ctx->symlinkroot)
> > + goto cifs_parse_mount_err;
> > + break;
> > }
> > /* case Opt_ignore: - is ignored as expected ... */
> >
> > + /*
> > + * By default resolve all native absolute symlinks relative to "/mnt/".
> > + * Same default has drvfs driver running in WSL for resolving SMB shares.
> > + */
> > + if (!ctx->symlinkroot)
> > + ctx->symlinkroot = kstrdup("/mnt/", GFP_KERNEL);
> > +
> > return 0;
> >
> > cifs_parse_mount_err:
> > @@ -1747,6 +1767,8 @@ smb3_cleanup_fs_context_contents(struct smb3_fs_context *ctx)
> > ctx->prepath = NULL;
> > kfree(ctx->leaf_fullpath);
> > ctx->leaf_fullpath = NULL;
> > + kfree(ctx->symlinkroot);
> > + ctx->symlinkroot = NULL;
> > }
> >
> > void
> > diff --git a/fs/smb/client/fs_context.h b/fs/smb/client/fs_context.h
> > index cf577ec0dd0a..8dd12498ffd8 100644
> > --- a/fs/smb/client/fs_context.h
> > +++ b/fs/smb/client/fs_context.h
> > @@ -157,6 +157,7 @@ enum cifs_param {
> > Opt_sec,
> > Opt_cache,
> > Opt_reparse,
> > + Opt_symlinkroot,
> >
> > /* Mount options to be ignored */
> > Opt_ignore,
> > @@ -284,6 +285,7 @@ struct smb3_fs_context {
> > struct cifs_ses *dfs_root_ses;
> > bool dfs_automount:1; /* set for dfs automount only */
> > enum cifs_reparse_type reparse_type;
> > + char *symlinkroot; /* top level directory for native SMB symlinks in absolute format */
> > };
> >
> > extern const struct fs_parameter_spec smb3_fs_parameters[];
> > diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> > index fb1d16b17f38..a577b2d2a4fc 100644
> > --- a/fs/smb/client/reparse.c
> > +++ b/fs/smb/client/reparse.c
> > @@ -25,33 +25,128 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> > const char *full_path, const char *symname)
> > {
> > struct reparse_symlink_data_buffer *buf = NULL;
> > - struct cifs_open_info_data data;
> > + struct cifs_open_info_data data = {};
> > struct cifs_sb_info *cifs_sb = CIFS_SB(inode->i_sb);
> > struct inode *new;
> > struct kvec iov;
> > - __le16 *path;
> > + __le16 *path = NULL;
> > bool directory;
> > - char *sym, sep = CIFS_DIR_SEP(cifs_sb);
> > - u16 len, plen;
> > + char *symlink_target = NULL;
> > + char *sym = NULL;
> > + char sep = CIFS_DIR_SEP(cifs_sb);
> > + u16 len, plen, poff, slen;
> > int rc = 0;
> >
> > - sym = kstrdup(symname, GFP_KERNEL);
> > - if (!sym)
> > - return -ENOMEM;
> > + symlink_target = kstrdup(symname, GFP_KERNEL);
> > + if (!symlink_target) {
> > + rc = -ENOMEM;
> > + goto out;
> > + }
> >
> > data = (struct cifs_open_info_data) {
> > .reparse_point = true,
> > .reparse = { .tag = IO_REPARSE_TAG_SYMLINK, },
> > - .symlink_target = sym,
> > + .symlink_target = symlink_target,
> > };
> >
> > - convert_delimiter(sym, sep);
> > + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
> > + /*
> > + * This is a request to create an absolute symlink on the server
> > + * which does not support POSIX paths, and expects symlink in
> > + * NT-style path. So convert absolute Linux symlink target path
> > + * to the absolute NT-style path. Root of the NT-style path for
> > + * symlinks is specified in "symlinkroot" mount option. This will
> > + * ensure compatibility of this symlink stored in absolute form
> > + * on the SMB server.
> > + */
> > + if (!strstarts(symname, cifs_sb->ctx->symlinkroot)) {
> > + /*
> > + * If the absolute Linux symlink target path is not
> > + * inside "symlinkroot" location then there is no way
> > + * to convert such Linux symlink to NT-style path.
> > + */
> > + cifs_dbg(VFS,
> > + "absolute symlink '%s' cannot be converted to NT format "
> > + "because it is outside of symlinkroot='%s'\n",
> > + symname, cifs_sb->ctx->symlinkroot);
> > + rc = -EINVAL;
> > + goto out;
> > + }
> > + len = strlen(cifs_sb->ctx->symlinkroot);
> > + if (cifs_sb->ctx->symlinkroot[len-1] != '/')
> > + len++;
> > + if (symname[len] >= 'a' && symname[len] <= 'z' &&
> > + (symname[len+1] == '/' || symname[len+1] == '\0')) {
> > + /*
> > + * Symlink points to Linux target /symlinkroot/x/path/...
> > + * where 'x' is the lowercase local Windows drive.
> > + * NT-style path for 'x' has common form \??\X:\path\...
> > + * with uppercase local Windows drive.
> > + */
> > + int common_path_len = strlen(symname+len+1)+1;
> > + sym = kzalloc(6+common_path_len, GFP_KERNEL);
> > + if (!sym) {
> > + rc = -ENOMEM;
> > + goto out;
> > + }
> > + memcpy(sym, "\\??\\", 4);
> > + sym[4] = symname[len] - ('a'-'A');
> > + sym[5] = ':';
> > + memcpy(sym+6, symname+len+1, common_path_len);
> > + } else {
> > + /* Unhandled absolute symlink. Report an error. */
> > + cifs_dbg(
> > + VFS,
> > + "absolute symlink '%s' cannot be converted to NT format "
> > + "because it points to unknown target\n",
> > + symname);
> > + rc = -EINVAL;
> > + goto out;
> > + }
> > + } else {
> > + /*
> > + * This is request to either create an absolute symlink on
> > + * server which expects POSIX paths or it is an request to
> > + * create a relative symlink from the current directory.
> > + * These paths have same format as relative SMB symlinks,
> > + * so no conversion is needed. So just take symname as-is.
> > + */
> > + sym = kstrdup(symname, GFP_KERNEL);
> > + if (!sym) {
> > + rc = -ENOMEM;
> > + goto out;
> > + }
> > + }
> > +
> > + if (sep == '\\')
> > + convert_delimiter(sym, sep);
> > +
> > + /*
> > + * For absolute NT symlinks it is required to pass also leading
> > + * backslash and to not mangle NT object prefix "\\??\\" and not to
> > + * mangle colon in drive letter. But cifs_convert_path_to_utf16()
> > + * removes leading backslash and replaces '?' and ':'. So temporary
> > + * mask these characters in NT object prefix by '_' and then change
> > + * them back.
> > + */
> > + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/')
> > + sym[0] = sym[1] = sym[2] = sym[5] = '_';
> > +
> > path = cifs_convert_path_to_utf16(sym, cifs_sb);
> > if (!path) {
> > rc = -ENOMEM;
> > goto out;
> > }
> >
> > + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
> > + sym[0] = '\\';
> > + sym[1] = sym[2] = '?';
> > + sym[5] = ':';
> > + path[0] = '\\';
> > + path[1] = path[2] = '?';
> > + path[5] = ':';
> > + }
> > +
> > /*
> > * SMB distinguish between symlink to directory and symlink to file.
> > * They cannot be exchanged (symlink of file type which points to
> > @@ -64,8 +159,18 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> > if (rc < 0)
> > goto out;
> >
> > - plen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
> > - len = sizeof(*buf) + plen * 2;
> > + slen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
> > + poff = 0;
> > + plen = slen;
> > + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
> > + /*
> > + * For absolute NT symlinks skip leading "\\??\\" in PrintName as
> > + * PrintName is user visible location in DOS/Win32 format (not in NT format).
> > + */
> > + poff = 4;
> > + plen -= 2 * poff;
> > + }
> > + len = sizeof(*buf) + plen + slen;
> > buf = kzalloc(len, GFP_KERNEL);
> > if (!buf) {
> > rc = -ENOMEM;
> > @@ -74,17 +179,17 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> >
> > buf->ReparseTag = cpu_to_le32(IO_REPARSE_TAG_SYMLINK);
> > buf->ReparseDataLength = cpu_to_le16(len - sizeof(struct reparse_data_buffer));
> > +
> > buf->SubstituteNameOffset = cpu_to_le16(plen);
> > - buf->SubstituteNameLength = cpu_to_le16(plen);
> > - memcpy(&buf->PathBuffer[plen], path, plen);
> > + buf->SubstituteNameLength = cpu_to_le16(slen);
> > + memcpy(&buf->PathBuffer[plen], path, slen);
> > +
> > buf->PrintNameOffset = 0;
> > buf->PrintNameLength = cpu_to_le16(plen);
> > - memcpy(buf->PathBuffer, path, plen);
> > + memcpy(buf->PathBuffer, path+poff, plen);
> > +
> > buf->Flags = cpu_to_le32(*symname != '/' ? SYMLINK_FLAG_RELATIVE : 0);
> > - if (*sym != sep)
> > - buf->Flags = cpu_to_le32(SYMLINK_FLAG_RELATIVE);
> >
> > - convert_delimiter(sym, '/');
> > iov.iov_base = buf;
> > iov.iov_len = len;
> > new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
> > @@ -95,6 +200,7 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> > else
> > rc = PTR_ERR(new);
> > out:
> > + kfree(sym);
> > kfree(path);
> > cifs_free_open_info(&data);
> > kfree(buf);
> > @@ -540,6 +646,9 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> > char sep = CIFS_DIR_SEP(cifs_sb);
> > char *linux_target = NULL;
> > char *smb_target = NULL;
> > + int symlinkroot_len;
> > + int abs_path_len;
> > + char *abs_path;
> > int levels;
> > int rc;
> > int i;
> > @@ -569,7 +678,123 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> > goto out;
> > }
> >
> > - if (smb_target[0] == sep && relative) {
> > + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && !relative) {
> > + /*
> > + * This is an absolute symlink from the server which does not
> > + * support POSIX paths, so the symlink is in NT-style path.
> > + * So convert it to absolute Linux symlink target path. Root of
> > + * the NT-style path for symlinks is specified in "symlinkroot"
> > + * mount option.
> > + *
> > + * Root of the DOS and Win32 paths is at NT path \??\
> > + * It means that DOS/Win32 path C:\folder\file.txt is
> > + * NT path \??\C:\folder\file.txt
> > + *
> > + * NT systems have some well-known object symlinks in their NT
> > + * hierarchy, which is needed to take into account when resolving
> > + * other symlinks. Most commonly used symlink paths are:
> > + * \?? -> \GLOBAL??
> > + * \DosDevices -> \??
> > + * \GLOBAL??\GLOBALROOT -> \
> > + * \GLOBAL??\Global -> \GLOBAL??
> > + * \GLOBAL??\NUL -> \Device\Null
> > + * \GLOBAL??\UNC -> \Device\Mup
> > + * \GLOBAL??\PhysicalDrive0 -> \Device\Harddisk0\DR0 (for each harddisk)
> > + * \GLOBAL??\A: -> \Device\Floppy0 (if A: is the first floppy)
> > + * \GLOBAL??\C: -> \Device\HarddiskVolume1 (if C: is the first harddisk)
> > + * \GLOBAL??\D: -> \Device\CdRom0 (if D: is first cdrom)
> > + * \SystemRoot -> \Device\Harddisk0\Partition1\WINDOWS (or where is NT system installed)
> > + * \Volume{...} -> \Device\HarddiskVolume1 (where ... is system generated guid)
> > + *
> > + * In most common cases, absolute NT symlinks points to path on
> > + * DOS/Win32 drive letter, system-specific Volume or on UNC share.
> > + * Here are few examples of commonly used absolute NT symlinks
> > + * created by mklink.exe tool:
> > + * \??\C:\folder\file.txt
> > + * \??\\C:\folder\file.txt
> > + * \??\UNC\server\share\file.txt
> > + * \??\\UNC\server\share\file.txt
> > + * \??\Volume{b75e2c83-0000-0000-0000-602f00000000}\folder\file.txt
> > + *
> > + * It means that the most common path prefix \??\ is also NT path
> > + * symlink (to \GLOBAL??). It is less common that second path
> > + * separator is double backslash, but it is valid.
> > + *
> > + * Volume guid is randomly generated by the target system and so
> > + * only the target system knows the mapping between guid and the
> > + * hardisk number. Over SMB it is not possible to resolve this
> > + * mapping, therefore symlinks pointing to target location of
> > + * volume guids are totally unusable over SMB.
> > + *
> > + * For now parse only symlink paths available for DOS and Win32.
> > + * Those are paths with \??\ prefix or paths which points to \??\
> > + * via other NT symlink (\DosDevices\, \GLOBAL??\, ...).
> > + */
> > + abs_path = smb_target;
> > +globalroot:
> > + if (strstarts(abs_path, "\\??\\"))
> > + abs_path += sizeof("\\??\\")-1;
> > + else if (strstarts(abs_path, "\\DosDevices\\"))
> > + abs_path += sizeof("\\DosDevices\\")-1;
> > + else if (strstarts(abs_path, "\\GLOBAL??\\"))
> > + abs_path += sizeof("\\GLOBAL??\\")-1;
> > + else {
> > + /* Unhandled absolute symlink, points outside of DOS/Win32 */
> > + cifs_dbg(VFS,
> > + "absolute symlink '%s' cannot be converted from NT format "
> > + "because points to unknown target\n",
> > + smb_target);
> > + rc = -EIO;
> > + goto out;
> > + }
> > +
> > + /* Sometimes path separator after \?? is double backslash */
> > + if (abs_path[0] == '\\')
> > + abs_path++;
> > +
> > + while (strstarts(abs_path, "Global\\"))
> > + abs_path += sizeof("Global\\")-1;
> > +
> > + if (strstarts(abs_path, "GLOBALROOT\\")) {
> > + /* Label globalroot requires path with leading '\\', so do not trim '\\' */
> > + abs_path += sizeof("GLOBALROOT")-1;
> > + goto globalroot;
> > + }
> > +
> > + /* For now parse only paths to drive letters */
> > + if (((abs_path[0] >= 'A' && abs_path[0] <= 'Z') ||
> > + (abs_path[0] >= 'a' && abs_path[0] <= 'z')) &&
> > + abs_path[1] == ':' &&
> > + (abs_path[2] == '\\' || abs_path[2] == '\0')) {
> > + /* Convert drive letter to lowercase and drop colon */
> > + char drive_letter = abs_path[0];
> > + if (drive_letter >= 'A' && drive_letter <= 'Z')
> > + drive_letter += 'a'-'A';
> > + abs_path++;
> > + abs_path[0] = drive_letter;
> > + } else {
> > + /* Unhandled absolute symlink. Report an error. */
> > + cifs_dbg(VFS,
> > + "absolute symlink '%s' cannot be converted from NT format "
> > + "because points to unknown target\n",
> > + smb_target);
> > + rc = -EIO;
> > + goto out;
> > + }
> > +
> > + abs_path_len = strlen(abs_path)+1;
> > + symlinkroot_len = strlen(cifs_sb->ctx->symlinkroot);
> > + if (cifs_sb->ctx->symlinkroot[symlinkroot_len-1] == '/')
> > + symlinkroot_len--;
> > + linux_target = kmalloc(symlinkroot_len + 1 + abs_path_len, GFP_KERNEL);
> > + if (!linux_target) {
> > + rc = -ENOMEM;
> > + goto out;
> > + }
> > + memcpy(linux_target, cifs_sb->ctx->symlinkroot, symlinkroot_len);
> > + linux_target[symlinkroot_len] = '/';
> > + memcpy(linux_target + symlinkroot_len + 1, abs_path, abs_path_len);
> > + } else if (smb_target[0] == sep && relative) {
> > /*
> > * This is a relative SMB symlink from the top of the share,
> > * which is the top level directory of the Linux mount point.
> > @@ -598,6 +823,12 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> > }
> > memcpy(linux_target + levels*3, smb_target+1, smb_target_len); /* +1 to skip leading sep */
> > } else {
> > + /*
> > + * This is either an absolute symlink in POSIX-style format
> > + * or relative SMB symlink from the current directory.
> > + * These paths have same format as Linux symlinks, so no
> > + * conversion is needed.
> > + */
> > linux_target = smb_target;
> > smb_target = NULL;
> > }
> > --
> > 2.20.1
> >
> >
>
>
> --
> Thanks,
>
> Steve
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v2 6/6] cifs: Fix creating and resolving absolute NT-style symlinks
2024-10-05 14:03 ` [PATCH v2 6/6] cifs: Fix creating and resolving absolute NT-style symlinks Pali Rohár
2024-10-07 3:59 ` Steve French
@ 2024-12-09 17:58 ` Pali Rohár
1 sibling, 0 replies; 38+ messages in thread
From: Pali Rohár @ 2024-12-09 17:58 UTC (permalink / raw)
To: Steve French, Paulo Alcantara, Ronnie Sahlberg; +Cc: linux-cifs, linux-kernel
On Saturday 05 October 2024 16:03:00 Pali Rohár wrote:
> If the SMB symlink is stored on NT server in absolute form then it points
> to the NT object hierarchy, which is different from POSIX one and needs
> some conversion / mapping.
>
> To make interoperability with Windows SMB server and WSL subsystem, reuse
> its logic of mapping between NT paths and POSIX paths into Linux SMB
> client.
>
> WSL subsystem on Windows uses for -t drvfs mount option -o symlinkroot=
> which specifies the POSIX path where are expected to be mounted lowercase
> Windows drive letters (without colon).
>
> Do same for Linux SMB client and add a new mount option -o symlinkroot=
> which mimics the drvfs mount option of the same name. It specifies where in
> the Linux VFS hierarchy is the root of the DOS / Windows drive letters, and
> translates between absolute NT-style symlinks and absolute Linux VFS
> symlinks. Default value of symlinkroot is "/mnt", same what is using WSL.
>
> Note that DOS / Windows drive letter symlinks are just subset of all
> possible NT-style symlinks. Drive letters live in NT subtree \??\ and
> important details about NT paths and object hierarchy are in the comments
> in this change.
>
> When symlink target location from non-POSIX SMB server is in absolute form
> (indicated by absence of SYMLINK_FLAG_RELATIVE) then it is converted to
> Linux absolute symlink according to symlinkroot configuration.
>
> And when creating a new symlink on non-POSIX SMB server in absolute form
> then Linux absolute target is converted to NT-style according to
> symlinkroot configuration.
>
> When SMB server is POSIX, then this change does not affect neither reading
> target location of symlink, nor creating a new symlink. It is expected that
> POSIX SMB server works with POSIX paths where the absolute root is /.
>
> This change improves interoperability of absolute SMB symlinks with Windows
> SMB servers.
>
> Signed-off-by: Pali Rohár <pali@kernel.org>
> ---
> fs/smb/client/fs_context.c | 22 +++
> fs/smb/client/fs_context.h | 2 +
> fs/smb/client/reparse.c | 267 ++++++++++++++++++++++++++++++++++---
> 3 files changed, 273 insertions(+), 18 deletions(-)
>
> diff --git a/fs/smb/client/fs_context.c b/fs/smb/client/fs_context.c
> index 2f0c3894b0f7..22b550860cc8 100644
> --- a/fs/smb/client/fs_context.c
> +++ b/fs/smb/client/fs_context.c
> @@ -178,6 +178,7 @@ const struct fs_parameter_spec smb3_fs_parameters[] = {
> fsparam_string("sec", Opt_sec),
> fsparam_string("cache", Opt_cache),
> fsparam_string("reparse", Opt_reparse),
> + fsparam_string("symlinkroot", Opt_symlinkroot),
>
> /* Arguments that should be ignored */
> fsparam_flag("guest", Opt_ignore),
> @@ -355,6 +356,7 @@ smb3_fs_context_dup(struct smb3_fs_context *new_ctx, struct smb3_fs_context *ctx
> new_ctx->source = NULL;
> new_ctx->iocharset = NULL;
> new_ctx->leaf_fullpath = NULL;
> + new_ctx->symlinkroot = NULL;
> /*
> * Make sure to stay in sync with smb3_cleanup_fs_context_contents()
> */
> @@ -369,6 +371,7 @@ smb3_fs_context_dup(struct smb3_fs_context *new_ctx, struct smb3_fs_context *ctx
> DUP_CTX_STR(nodename);
> DUP_CTX_STR(iocharset);
> DUP_CTX_STR(leaf_fullpath);
> + DUP_CTX_STR(symlinkroot);
>
> return 0;
> }
> @@ -1614,9 +1617,26 @@ static int smb3_fs_context_parse_param(struct fs_context *fc,
> if (parse_reparse_flavor(fc, param->string, ctx))
> goto cifs_parse_mount_err;
> break;
> + case Opt_symlinkroot:
> + if (param->string[0] != '/') {
> + cifs_errorf(fc, "symlinkroot mount options must be absolute path\n");
> + goto cifs_parse_mount_err;
> + }
> + kfree(ctx->symlinkroot);
> + ctx->symlinkroot = kstrdup(param->string, GFP_KERNEL);
> + if (!ctx->symlinkroot)
> + goto cifs_parse_mount_err;
> + break;
> }
> /* case Opt_ignore: - is ignored as expected ... */
>
> + /*
> + * By default resolve all native absolute symlinks relative to "/mnt/".
> + * Same default has drvfs driver running in WSL for resolving SMB shares.
> + */
> + if (!ctx->symlinkroot)
> + ctx->symlinkroot = kstrdup("/mnt/", GFP_KERNEL);
> +
> return 0;
>
> cifs_parse_mount_err:
> @@ -1747,6 +1767,8 @@ smb3_cleanup_fs_context_contents(struct smb3_fs_context *ctx)
> ctx->prepath = NULL;
> kfree(ctx->leaf_fullpath);
> ctx->leaf_fullpath = NULL;
> + kfree(ctx->symlinkroot);
> + ctx->symlinkroot = NULL;
> }
>
> void
> diff --git a/fs/smb/client/fs_context.h b/fs/smb/client/fs_context.h
> index cf577ec0dd0a..8dd12498ffd8 100644
> --- a/fs/smb/client/fs_context.h
> +++ b/fs/smb/client/fs_context.h
> @@ -157,6 +157,7 @@ enum cifs_param {
> Opt_sec,
> Opt_cache,
> Opt_reparse,
> + Opt_symlinkroot,
>
> /* Mount options to be ignored */
> Opt_ignore,
> @@ -284,6 +285,7 @@ struct smb3_fs_context {
> struct cifs_ses *dfs_root_ses;
> bool dfs_automount:1; /* set for dfs automount only */
> enum cifs_reparse_type reparse_type;
> + char *symlinkroot; /* top level directory for native SMB symlinks in absolute format */
> };
>
> extern const struct fs_parameter_spec smb3_fs_parameters[];
> diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
> index fb1d16b17f38..a577b2d2a4fc 100644
> --- a/fs/smb/client/reparse.c
> +++ b/fs/smb/client/reparse.c
> @@ -25,33 +25,128 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> const char *full_path, const char *symname)
> {
> struct reparse_symlink_data_buffer *buf = NULL;
> - struct cifs_open_info_data data;
> + struct cifs_open_info_data data = {};
> struct cifs_sb_info *cifs_sb = CIFS_SB(inode->i_sb);
> struct inode *new;
> struct kvec iov;
> - __le16 *path;
> + __le16 *path = NULL;
> bool directory;
> - char *sym, sep = CIFS_DIR_SEP(cifs_sb);
> - u16 len, plen;
> + char *symlink_target = NULL;
> + char *sym = NULL;
> + char sep = CIFS_DIR_SEP(cifs_sb);
> + u16 len, plen, poff, slen;
> int rc = 0;
>
> - sym = kstrdup(symname, GFP_KERNEL);
> - if (!sym)
> - return -ENOMEM;
> + symlink_target = kstrdup(symname, GFP_KERNEL);
> + if (!symlink_target) {
> + rc = -ENOMEM;
> + goto out;
> + }
>
> data = (struct cifs_open_info_data) {
> .reparse_point = true,
> .reparse = { .tag = IO_REPARSE_TAG_SYMLINK, },
> - .symlink_target = sym,
> + .symlink_target = symlink_target,
> };
>
> - convert_delimiter(sym, sep);
> + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
> + /*
> + * This is a request to create an absolute symlink on the server
> + * which does not support POSIX paths, and expects symlink in
> + * NT-style path. So convert absolute Linux symlink target path
> + * to the absolute NT-style path. Root of the NT-style path for
> + * symlinks is specified in "symlinkroot" mount option. This will
> + * ensure compatibility of this symlink stored in absolute form
> + * on the SMB server.
> + */
> + if (!strstarts(symname, cifs_sb->ctx->symlinkroot)) {
> + /*
> + * If the absolute Linux symlink target path is not
> + * inside "symlinkroot" location then there is no way
> + * to convert such Linux symlink to NT-style path.
> + */
> + cifs_dbg(VFS,
> + "absolute symlink '%s' cannot be converted to NT format "
> + "because it is outside of symlinkroot='%s'\n",
> + symname, cifs_sb->ctx->symlinkroot);
> + rc = -EINVAL;
> + goto out;
> + }
> + len = strlen(cifs_sb->ctx->symlinkroot);
> + if (cifs_sb->ctx->symlinkroot[len-1] != '/')
> + len++;
> + if (symname[len] >= 'a' && symname[len] <= 'z' &&
> + (symname[len+1] == '/' || symname[len+1] == '\0')) {
> + /*
> + * Symlink points to Linux target /symlinkroot/x/path/...
> + * where 'x' is the lowercase local Windows drive.
> + * NT-style path for 'x' has common form \??\X:\path\...
> + * with uppercase local Windows drive.
> + */
> + int common_path_len = strlen(symname+len+1)+1;
> + sym = kzalloc(6+common_path_len, GFP_KERNEL);
> + if (!sym) {
> + rc = -ENOMEM;
> + goto out;
> + }
> + memcpy(sym, "\\??\\", 4);
> + sym[4] = symname[len] - ('a'-'A');
> + sym[5] = ':';
> + memcpy(sym+6, symname+len+1, common_path_len);
> + } else {
> + /* Unhandled absolute symlink. Report an error. */
> + cifs_dbg(
> + VFS,
> + "absolute symlink '%s' cannot be converted to NT format "
> + "because it points to unknown target\n",
> + symname);
> + rc = -EINVAL;
> + goto out;
> + }
> + } else {
> + /*
> + * This is request to either create an absolute symlink on
> + * server which expects POSIX paths or it is an request to
> + * create a relative symlink from the current directory.
> + * These paths have same format as relative SMB symlinks,
> + * so no conversion is needed. So just take symname as-is.
> + */
> + sym = kstrdup(symname, GFP_KERNEL);
> + if (!sym) {
> + rc = -ENOMEM;
> + goto out;
> + }
> + }
> +
> + if (sep == '\\')
> + convert_delimiter(sym, sep);
> +
> + /*
> + * For absolute NT symlinks it is required to pass also leading
> + * backslash and to not mangle NT object prefix "\\??\\" and not to
> + * mangle colon in drive letter. But cifs_convert_path_to_utf16()
> + * removes leading backslash and replaces '?' and ':'. So temporary
> + * mask these characters in NT object prefix by '_' and then change
> + * them back.
> + */
> + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/')
> + sym[0] = sym[1] = sym[2] = sym[5] = '_';
> +
> path = cifs_convert_path_to_utf16(sym, cifs_sb);
> if (!path) {
> rc = -ENOMEM;
> goto out;
> }
>
> + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
> + sym[0] = '\\';
> + sym[1] = sym[2] = '?';
> + sym[5] = ':';
> + path[0] = '\\';
> + path[1] = path[2] = '?';
> + path[5] = ':';
> + }
> +
> /*
> * SMB distinguish between symlink to directory and symlink to file.
> * They cannot be exchanged (symlink of file type which points to
> @@ -64,8 +159,18 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> if (rc < 0)
> goto out;
>
> - plen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
> - len = sizeof(*buf) + plen * 2;
> + slen = 2 * UniStrnlen((wchar_t *)path, PATH_MAX);
> + poff = 0;
> + plen = slen;
> + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && symname[0] == '/') {
> + /*
> + * For absolute NT symlinks skip leading "\\??\\" in PrintName as
> + * PrintName is user visible location in DOS/Win32 format (not in NT format).
> + */
> + poff = 4;
> + plen -= 2 * poff;
> + }
> + len = sizeof(*buf) + plen + slen;
> buf = kzalloc(len, GFP_KERNEL);
> if (!buf) {
> rc = -ENOMEM;
> @@ -74,17 +179,17 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
>
> buf->ReparseTag = cpu_to_le32(IO_REPARSE_TAG_SYMLINK);
> buf->ReparseDataLength = cpu_to_le16(len - sizeof(struct reparse_data_buffer));
> +
> buf->SubstituteNameOffset = cpu_to_le16(plen);
> - buf->SubstituteNameLength = cpu_to_le16(plen);
> - memcpy(&buf->PathBuffer[plen], path, plen);
> + buf->SubstituteNameLength = cpu_to_le16(slen);
> + memcpy(&buf->PathBuffer[plen], path, slen);
> +
> buf->PrintNameOffset = 0;
> buf->PrintNameLength = cpu_to_le16(plen);
> - memcpy(buf->PathBuffer, path, plen);
> + memcpy(buf->PathBuffer, path+poff, plen);
> +
> buf->Flags = cpu_to_le32(*symname != '/' ? SYMLINK_FLAG_RELATIVE : 0);
> - if (*sym != sep)
> - buf->Flags = cpu_to_le32(SYMLINK_FLAG_RELATIVE);
>
> - convert_delimiter(sym, '/');
> iov.iov_base = buf;
> iov.iov_len = len;
> new = smb2_get_reparse_inode(&data, inode->i_sb, xid,
> @@ -95,6 +200,7 @@ int smb2_create_reparse_symlink(const unsigned int xid, struct inode *inode,
> else
> rc = PTR_ERR(new);
> out:
> + kfree(sym);
> kfree(path);
> cifs_free_open_info(&data);
> kfree(buf);
> @@ -540,6 +646,9 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> char sep = CIFS_DIR_SEP(cifs_sb);
> char *linux_target = NULL;
> char *smb_target = NULL;
> + int symlinkroot_len;
> + int abs_path_len;
> + char *abs_path;
> int levels;
> int rc;
> int i;
> @@ -569,7 +678,123 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> goto out;
> }
>
> - if (smb_target[0] == sep && relative) {
> + if (!(cifs_sb->mnt_cifs_flags & CIFS_MOUNT_POSIX_PATHS) && !relative) {
> + /*
> + * This is an absolute symlink from the server which does not
> + * support POSIX paths, so the symlink is in NT-style path.
> + * So convert it to absolute Linux symlink target path. Root of
> + * the NT-style path for symlinks is specified in "symlinkroot"
> + * mount option.
> + *
> + * Root of the DOS and Win32 paths is at NT path \??\
> + * It means that DOS/Win32 path C:\folder\file.txt is
> + * NT path \??\C:\folder\file.txt
> + *
> + * NT systems have some well-known object symlinks in their NT
> + * hierarchy, which is needed to take into account when resolving
> + * other symlinks. Most commonly used symlink paths are:
> + * \?? -> \GLOBAL??
> + * \DosDevices -> \??
> + * \GLOBAL??\GLOBALROOT -> \
> + * \GLOBAL??\Global -> \GLOBAL??
> + * \GLOBAL??\NUL -> \Device\Null
> + * \GLOBAL??\UNC -> \Device\Mup
> + * \GLOBAL??\PhysicalDrive0 -> \Device\Harddisk0\DR0 (for each harddisk)
> + * \GLOBAL??\A: -> \Device\Floppy0 (if A: is the first floppy)
> + * \GLOBAL??\C: -> \Device\HarddiskVolume1 (if C: is the first harddisk)
> + * \GLOBAL??\D: -> \Device\CdRom0 (if D: is first cdrom)
> + * \SystemRoot -> \Device\Harddisk0\Partition1\WINDOWS (or where is NT system installed)
> + * \Volume{...} -> \Device\HarddiskVolume1 (where ... is system generated guid)
> + *
> + * In most common cases, absolute NT symlinks points to path on
> + * DOS/Win32 drive letter, system-specific Volume or on UNC share.
> + * Here are few examples of commonly used absolute NT symlinks
> + * created by mklink.exe tool:
> + * \??\C:\folder\file.txt
> + * \??\\C:\folder\file.txt
> + * \??\UNC\server\share\file.txt
> + * \??\\UNC\server\share\file.txt
> + * \??\Volume{b75e2c83-0000-0000-0000-602f00000000}\folder\file.txt
> + *
> + * It means that the most common path prefix \??\ is also NT path
> + * symlink (to \GLOBAL??). It is less common that second path
> + * separator is double backslash, but it is valid.
> + *
> + * Volume guid is randomly generated by the target system and so
> + * only the target system knows the mapping between guid and the
> + * hardisk number. Over SMB it is not possible to resolve this
> + * mapping, therefore symlinks pointing to target location of
> + * volume guids are totally unusable over SMB.
> + *
> + * For now parse only symlink paths available for DOS and Win32.
> + * Those are paths with \??\ prefix or paths which points to \??\
> + * via other NT symlink (\DosDevices\, \GLOBAL??\, ...).
> + */
> + abs_path = smb_target;
> +globalroot:
> + if (strstarts(abs_path, "\\??\\"))
> + abs_path += sizeof("\\??\\")-1;
> + else if (strstarts(abs_path, "\\DosDevices\\"))
> + abs_path += sizeof("\\DosDevices\\")-1;
> + else if (strstarts(abs_path, "\\GLOBAL??\\"))
> + abs_path += sizeof("\\GLOBAL??\\")-1;
> + else {
> + /* Unhandled absolute symlink, points outside of DOS/Win32 */
> + cifs_dbg(VFS,
> + "absolute symlink '%s' cannot be converted from NT format "
> + "because points to unknown target\n",
> + smb_target);
> + rc = -EIO;
> + goto out;
> + }
> +
> + /* Sometimes path separator after \?? is double backslash */
> + if (abs_path[0] == '\\')
> + abs_path++;
> +
> + while (strstarts(abs_path, "Global\\"))
> + abs_path += sizeof("Global\\")-1;
> +
> + if (strstarts(abs_path, "GLOBALROOT\\")) {
> + /* Label globalroot requires path with leading '\\', so do not trim '\\' */
> + abs_path += sizeof("GLOBALROOT")-1;
> + goto globalroot;
> + }
> +
> + /* For now parse only paths to drive letters */
> + if (((abs_path[0] >= 'A' && abs_path[0] <= 'Z') ||
> + (abs_path[0] >= 'a' && abs_path[0] <= 'z')) &&
> + abs_path[1] == ':' &&
> + (abs_path[2] == '\\' || abs_path[2] == '\0')) {
> + /* Convert drive letter to lowercase and drop colon */
> + char drive_letter = abs_path[0];
> + if (drive_letter >= 'A' && drive_letter <= 'Z')
> + drive_letter += 'a'-'A';
> + abs_path++;
> + abs_path[0] = drive_letter;
> + } else {
> + /* Unhandled absolute symlink. Report an error. */
> + cifs_dbg(VFS,
> + "absolute symlink '%s' cannot be converted from NT format "
> + "because points to unknown target\n",
> + smb_target);
> + rc = -EIO;
> + goto out;
> + }
> +
> + abs_path_len = strlen(abs_path)+1;
> + symlinkroot_len = strlen(cifs_sb->ctx->symlinkroot);
> + if (cifs_sb->ctx->symlinkroot[symlinkroot_len-1] == '/')
> + symlinkroot_len--;
> + linux_target = kmalloc(symlinkroot_len + 1 + abs_path_len, GFP_KERNEL);
> + if (!linux_target) {
> + rc = -ENOMEM;
> + goto out;
> + }
> + memcpy(linux_target, cifs_sb->ctx->symlinkroot, symlinkroot_len);
> + linux_target[symlinkroot_len] = '/';
> + memcpy(linux_target + symlinkroot_len + 1, abs_path, abs_path_len);
> + } else if (smb_target[0] == sep && relative) {
> /*
> * This is a relative SMB symlink from the top of the share,
> * which is the top level directory of the Linux mount point.
> @@ -598,6 +823,12 @@ int smb2_parse_native_symlink(char **target, const char *buf, unsigned int len,
> }
> memcpy(linux_target + levels*3, smb_target+1, smb_target_len); /* +1 to skip leading sep */
> } else {
> + /*
> + * This is either an absolute symlink in POSIX-style format
> + * or relative SMB symlink from the current directory.
> + * These paths have same format as Linux symlinks, so no
> + * conversion is needed.
> + */
> linux_target = smb_target;
> smb_target = NULL;
> }
> --
> 2.20.1
>
Hello, this change is missing cpu_to_le16() wrapper on two for big endian systems.
fixup! cifs: Fix creating and resolving absolute NT-style symlinks
diff --git a/fs/smb/client/reparse.c b/fs/smb/client/reparse.c
index a763b3ce809a..af08e5918adb 100644
--- a/fs/smb/client/reparse.c
+++ b/fs/smb/client/reparse.c
@@ -172,9 +172,9 @@ static int create_native_symlink(const unsigned int xid, struct inode *inode,
sym[0] = '\\';
sym[1] = sym[2] = '?';
sym[5] = ':';
- path[0] = '\\';
- path[1] = path[2] = '?';
- path[5] = ':';
+ path[0] = cpu_to_le16('\\');
+ path[1] = path[2] = cpu_to_le16('?');
+ path[5] = cpu_to_le16(':');
}
/*
^ permalink raw reply related [flat|nested] 38+ messages in thread