A Wine user has requested the ability to interact with Unix symlinks from Win32 code.
While the generic functionality of NT reparse points is still not implemented upstream, it will probably involve a completely different code path than this, so there does not seem to me to be any point in waiting for that to be done before submitting these patches.
This does not implement all the relevant functionality. FILE_OPEN_REPARSE_POINT is now respected, but returns an fd with no fd->unix_fd. Thus far only file disposition (i.e. deletion), renaming and hardlinking are implemented, and all other operations return the no_fd_status, for which STATUS_REPARSE_POINT_NOT_RESOLVED is arbitrarily chosen.
This is probably going to be controversial. As controversial patches tend to get deferred forever, I'm going to at least try to pose some questions related to concept and implementation which may be easier to answer.
* Do we want to expose Unix symlinks at all? This was requested by a Wine user, and it does seem a strong argument that Wine, in general, supports some measure of integration with the host, including exposing custom interfaces for programs written to take advantage of it.
We also, currently, already expose Unix *directory* symlinks via NtQueryAttributesFile(). We don't currently expose such file symlinks (and this series does not change that particular point, but it would be done by follow-up patches). In a sense we are expanding existing partial support.
On the other hand, Unix symlinks which are completely opaque can be useful, and some downstream distributions (CrossOver and Proton) currently rely on them to reduce disk space usage while keeping the application unaware of a symlink (even if it were to use FILE_OPEN_REPARSE_POINT. Not that any applications are currently known that would break if this were to change, and in any case they could simply revert these patches.)
* Implementation-wise, is having unix_fd == -1 a non-starter? This can be true of other real fds in some specific cases...
* It's also true that this current implementation suffers from some degree of TOCTOU problems: because we only store the name and not the FD anymore, the actual underlying object at that path can change. This would not be true on Windows. Is this a problem?
* One alternate approach would be to use O_PATH (Linux, FreeBSD) and O_SYMLINK (Mac). I don't see any equivalent flag for NetBSD or Solaris, so we would probably just not be able to expose this functionality there.
Note that while the documentation seems to imply that O_PATH only stores the path, manual testing shows that (at least on Linux) a fd opened with O_PATH will remain pointing at the same *inode* even if it is moved or deleted. So this would avoid the aforementioned TOCTOU problem.
From: Elizabeth Figura zfigura@codeweavers.com
--- server/fd.c | 59 +++++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 27 deletions(-)
diff --git a/server/fd.c b/server/fd.c index 663f497b7a9..af5f9366c32 100644 --- a/server/fd.c +++ b/server/fd.c @@ -1913,6 +1913,36 @@ void get_nt_name( struct fd *fd, struct unicode_str *name ) name->len = fd->nt_namelen; }
+static int open_unix_fd( struct fd *fd, const char *name, int flags, const mode_t *mode, + unsigned int access, unsigned int options ) +{ + int rw_mode; + + if ((access & FILE_UNIX_WRITE_ACCESS) && !(options & FILE_DIRECTORY_FILE)) + { + if (access & FILE_UNIX_READ_ACCESS) rw_mode = O_RDWR; + else rw_mode = O_WRONLY; + } + else rw_mode = O_RDONLY; + + if ((fd->unix_fd = open( name, rw_mode | (flags & ~O_TRUNC), *mode )) != -1) + return 1; + + /* if we tried to open a directory for write access, retry read-only */ + if (errno == EISDIR && ((access & FILE_UNIX_WRITE_ACCESS) || (flags & O_CREAT))) + { + if ((fd->unix_fd = open( name, O_RDONLY | (flags & ~(O_TRUNC | O_CREAT | O_EXCL)), *mode ))) + return 1; + } + + /* check for trailing slash on file path */ + if ((errno == ENOENT || (errno == ENOTDIR && !(options & FILE_DIRECTORY_FILE))) && name[strlen(name) - 1] == '/') + set_error( STATUS_OBJECT_NAME_INVALID ); + else + file_set_error(); + return 0; +} + /* open() wrapper that returns a struct fd with no fd user set */ struct fd *open_fd( struct fd *root, const char *name, struct unicode_str nt_name, int flags, mode_t *mode, unsigned int access, @@ -1922,7 +1952,6 @@ struct fd *open_fd( struct fd *root, const char *name, struct unicode_str nt_nam struct closed_fd *closed_fd; struct fd *fd; int root_fd = -1; - int rw_mode; char *path;
if (((options & FILE_DELETE_ON_CLOSE) && !(access & DELETE)) || @@ -1966,32 +1995,8 @@ struct fd *open_fd( struct fd *root, const char *name, struct unicode_str nt_nam flags &= ~(O_CREAT | O_EXCL | O_TRUNC); }
- if ((access & FILE_UNIX_WRITE_ACCESS) && !(options & FILE_DIRECTORY_FILE)) - { - if (access & FILE_UNIX_READ_ACCESS) rw_mode = O_RDWR; - else rw_mode = O_WRONLY; - } - else rw_mode = O_RDONLY; - - if ((fd->unix_fd = open( name, rw_mode | (flags & ~O_TRUNC), *mode )) == -1) - { - /* if we tried to open a directory for write access, retry read-only */ - if (errno == EISDIR) - { - if ((access & FILE_UNIX_WRITE_ACCESS) || (flags & O_CREAT)) - fd->unix_fd = open( name, O_RDONLY | (flags & ~(O_TRUNC | O_CREAT | O_EXCL)), *mode ); - } - - if (fd->unix_fd == -1) - { - /* check for trailing slash on file path */ - if ((errno == ENOENT || (errno == ENOTDIR && !(options & FILE_DIRECTORY_FILE))) && name[strlen(name) - 1] == '/') - set_error( STATUS_OBJECT_NAME_INVALID ); - else - file_set_error(); - goto error; - } - } + if (!open_unix_fd( fd, name, flags, mode, access, options )) + goto error;
fd->nt_name = dup_nt_name( root, nt_name, &fd->nt_namelen ); fd->unix_name = NULL;
From: Elizabeth Figura zfigura@codeweavers.com
--- server/fd.c | 54 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 4 deletions(-)
diff --git a/server/fd.c b/server/fd.c index af5f9366c32..39acba1485b 100644 --- a/server/fd.c +++ b/server/fd.c @@ -1935,6 +1935,12 @@ static int open_unix_fd( struct fd *fd, const char *name, int flags, const mode_ return 1; }
+ if (errno == ELOOP && (options & FILE_OPEN_REPARSE_POINT)) + { + fd->no_fd_status = STATUS_REPARSE_POINT_NOT_RESOLVED; + return 1; + } + /* check for trailing slash on file path */ if ((errno == ENOENT || (errno == ENOTDIR && !(options & FILE_DIRECTORY_FILE))) && name[strlen(name) - 1] == '/') set_error( STATUS_OBJECT_NAME_INVALID ); @@ -1943,6 +1949,31 @@ static int open_unix_fd( struct fd *fd, const char *name, int flags, const mode_ return 0; }
+/* resolve the path except for its last element */ +static char *symlink_realpath( const char *path ) +{ + size_t len = strlen( path ); + char *dir, *ret; + + /* remove trailing slashes if any */ + while (len > 0 && path[len - 1] == '/') + --len; + + while (len > 0 && path[len - 1] != '/') + --len; + + dir = malloc( len + 1 ); + memcpy( dir, path, len ); + dir[len] = 0; + ret = realpath( dir, NULL ); + free( dir ); + + ret = realloc( ret, strlen( ret ) + 1 + strlen( path + len )); + strcat( ret, "/" ); + strcat( ret, path + len ); + return ret; +} + /* open() wrapper that returns a struct fd with no fd user set */ struct fd *open_fd( struct fd *root, const char *name, struct unicode_str nt_name, int flags, mode_t *mode, unsigned int access, @@ -1995,16 +2026,28 @@ struct fd *open_fd( struct fd *root, const char *name, struct unicode_str nt_nam flags &= ~(O_CREAT | O_EXCL | O_TRUNC); }
+ if (options & FILE_OPEN_REPARSE_POINT) + flags |= O_NOFOLLOW; + if (!open_unix_fd( fd, name, flags, mode, access, options )) goto error;
fd->nt_name = dup_nt_name( root, nt_name, &fd->nt_namelen ); fd->unix_name = NULL; - fstat( fd->unix_fd, &st ); + if (fd->unix_fd != -1) + fstat( fd->unix_fd, &st ); + else + { + if (lstat( name, &st ) == -1) + { + file_set_error(); + goto error; + } + } *mode = st.st_mode;
/* only bother with an inode for normal files and directories */ - if (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode)) + if (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode)) { unsigned int err; struct inode *inode = get_inode( st.st_dev, st.st_ino, fd->unix_fd ); @@ -2019,7 +2062,10 @@ struct fd *open_fd( struct fd *root, const char *name, struct unicode_str nt_nam
if ((path = dup_fd_name( root, name ))) { - fd->unix_name = realpath( path, NULL ); + if (S_ISLNK(st.st_mode)) + fd->unix_name = symlink_realpath( path ); + else + fd->unix_name = realpath( path, NULL ); free( path ); }
@@ -2033,7 +2079,7 @@ struct fd *open_fd( struct fd *root, const char *name, struct unicode_str nt_nam closed_fd = NULL;
/* check directory options */ - if ((options & FILE_DIRECTORY_FILE) && !S_ISDIR(st.st_mode)) + if ((options & FILE_DIRECTORY_FILE) && S_ISREG(st.st_mode)) { set_error( STATUS_NOT_A_DIRECTORY ); goto error;
From: Elizabeth Figura zfigura@codeweavers.com
--- server/fd.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-)
diff --git a/server/fd.c b/server/fd.c index 39acba1485b..e61c0b2373c 100644 --- a/server/fd.c +++ b/server/fd.c @@ -1090,7 +1090,7 @@ static void unlink_closed_fd( struct inode *inode, struct closed_fd *fd ) { /* make sure it is still the same file */ struct stat st; - if (!stat( fd->unix_name, &st ) && st.st_dev == inode->device->dev && st.st_ino == inode->ino) + if (!lstat( fd->unix_name, &st ) && st.st_dev == inode->device->dev && st.st_ino == inode->ino) { if (S_ISDIR(st.st_mode)) rmdir( fd->unix_name ); else unlink( fd->unix_name ); @@ -2604,15 +2604,10 @@ static void set_fd_disposition( struct fd *fd, unsigned int flags ) return; }
- if (fd->unix_fd == -1) - { - set_error( fd->no_fd_status ); - return; - } - if (flags & FILE_DISPOSITION_DELETE) { struct fd *fd_ptr; + int ret;
LIST_FOR_EACH_ENTRY( fd_ptr, &fd->inode->open, struct fd, inode_entry ) { @@ -2623,12 +2618,16 @@ static void set_fd_disposition( struct fd *fd, unsigned int flags ) } }
- if (fstat( fd->unix_fd, &st ) == -1) + if (fd->unix_fd != -1) + ret = fstat( fd->unix_fd, &st ); + else + ret = lstat( fd->unix_name, &st ); + if (ret == -1) { file_set_error(); return; } - if (S_ISREG( st.st_mode )) /* can't unlink files we don't have permission to write */ + if (S_ISREG( st.st_mode ) || S_ISLNK( st.st_mode )) /* can't unlink files we don't have permission to write */ { if (!(flags & FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE) && !(st.st_mode & (S_IWUSR | S_IWGRP | S_IWOTH))) @@ -2639,6 +2638,11 @@ static void set_fd_disposition( struct fd *fd, unsigned int flags ) } else if (S_ISDIR( st.st_mode )) /* can't remove non-empty directories */ { + if (fd->unix_fd == -1) + { + set_error( fd->no_fd_status ); + return; + } switch (is_dir_empty( fd->unix_fd )) { case -1:
From: Elizabeth Figura zfigura@codeweavers.com
--- server/fd.c | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-)
diff --git a/server/fd.c b/server/fd.c index e61c0b2373c..81b226c11b4 100644 --- a/server/fd.c +++ b/server/fd.c @@ -2676,17 +2676,13 @@ static void set_fd_name( struct fd *fd, struct fd *root, const char *nameptr, da struct stat st, st2; char *name; const unsigned int replace = flags & FILE_RENAME_REPLACE_IF_EXISTS; + int ret;
if (!fd->inode || !fd->unix_name) { set_error( STATUS_OBJECT_TYPE_MISMATCH ); return; } - if (fd->unix_fd == -1) - { - set_error( fd->no_fd_status ); - return; - }
if (!len || ((nameptr[0] == '/') ^ !root)) { @@ -2710,15 +2706,26 @@ static void set_fd_name( struct fd *fd, struct fd *root, const char *nameptr, da }
/* when creating a hard link, source cannot be a dir */ - if (create_link && !fstat( fd->unix_fd, &st ) && S_ISDIR( st.st_mode )) + if (create_link) { - set_error( STATUS_FILE_IS_A_DIRECTORY ); - goto failed; + if (fd->unix_fd != -1) + ret = fstat( fd->unix_fd, &st ); + else + ret = lstat( fd->unix_name, &st ); + if (!ret && S_ISDIR( st.st_mode )) + { + set_error( STATUS_FILE_IS_A_DIRECTORY ); + goto failed; + } }
if (!stat( name, &st )) { - if (!fstat( fd->unix_fd, &st2 ) && st.st_ino == st2.st_ino && st.st_dev == st2.st_dev) + if (fd->unix_fd != -1) + ret = fstat( fd->unix_fd, &st2 ); + else + ret = lstat( fd->unix_name, &st2 ); + if (!ret && st.st_ino == st2.st_ino && st.st_dev == st2.st_dev) { if (create_link && !replace) set_error( STATUS_OBJECT_NAME_COLLISION ); free( name ); @@ -2784,7 +2791,8 @@ static void set_fd_name( struct fd *fd, struct fd *root, const char *nameptr, da goto failed; }
- if (is_file_executable( fd->unix_name ) != is_file_executable( name ) && !fstat( fd->unix_fd, &st )) + if (is_file_executable( fd->unix_name ) != is_file_executable( name ) && + fd->unix_fd != -1 && !fstat( fd->unix_fd, &st )) { if (is_file_executable( name )) /* set executable bit where read bit is set */
A Wine user has requested the ability to interact with Unix symlinks from Win32 code.
Can you be more specific about the use case? What would be the parallel in Windows? I.e. what type of reparse point would we (eventually) expose?
FWIW, if we do have a real use case for viewing and managing Unix filesystem specifics from Wine side, then I'd be happy to see MR !6855 get another chance as well :)
A Wine user has requested the ability to interact with Unix symlinks from Win32 code.
Can you be more specific about the use case?
The specific desire is to be able to delete and rename the symlinks from cmd.exe. As the recently committed tests show (and 8540 implements) those functions operate on the reparse point rather than the target, much like their Unix equivalents.
I'm not sure why they want to do this rather than interact with them from a Unix script, but that's what's requested, and I broadly think that exposing Unix symlinks is reasonable, or at least worth a try.
That's largely why these are the only things I've implemented for now.
What would be the parallel in Windows? I.e. what type of reparse point would we (eventually) expose?
I'm not sure what the reparse tag would be, but probably either IO_REPARSE_TAG_SYMLINK, or maybe some custom type. I don't know in what cases it would matter, honestly; there's no Windows equivalent of readlink() other than manually calling into ntdll and parsing the target path (which would imply IO_REPARSE_TAG_SYMLINK, otherwise programs wouldn't understand it). But I also don't know in what cases it would even be useful for a program to know. I guess a file manager could display the link target.
IO_REPARSE_TAG_SYMLINK is the most equivalent on Windows; it's created by CreateSymbolicLink(), but does require adminstrator privileges. There is no CRT symlink().
One other thing I should mention is that some of the code paths introduced here could also apply to Unix sockets, which also can't have a unix_fd. We could again use O_PATH in that case, but presumably then we *cannot* use O_SYMLINK, so on Mac there would be no way to delete an AF_UNIX socket once created.
Can you be more specific about the use case?
There is at least one scenario where three things happen that would make Unix symlink support in Wine useful:
1. The application expects to be installed to the C drive, but its files are physically stored on a different drive, and copying them from that drive would take too long, so they are symlinked instead.
2. The application needs certain files to be completely replaced with different files from a specific language pack in order to change the user interface language. For example, there may be an "EN" directory with English-language files and an "ES" directory with Spanish-language files, and those files need to be moved or copied into the application's main directory.
3. The application is sometimes run on Windows and sometimes on Wine, including the script that moves or copies the language-specific files. Right now, if such a setup script is run on Wine, Wine is unaware of the fact that the language pack files are symlinks to files on another drive, so the `move` command makes deep copies of them, which is slow. When the same script is run on Windows, `move` just moves the symlinks themselves instead of copying the file contents over from the other drive. (Or the script could use `xcopy /B` to copy the symlinks instead of moving them, but that doesn't work at all on current Wine.)
It’s also worth noting that the Linux ntfs3 driver exposes Windows symlinks as if they were Unix symlinks. I think the fact that the symlinks might have been created by Windows on an NTFS volume is a good argument for continuing to treat them as symlinks in Wine.