public inbox for cygwin-patches@cygwin.com
 help / color / mirror / Atom feed
* [PATCH] Add ability to use NTFS native directory symlinks without admin rights on XP and later
@ 2015-09-23 11:07 Evgeny Grin
  0 siblings, 0 replies; 3+ messages in thread
From: Evgeny Grin @ 2015-09-23 11:07 UTC (permalink / raw)
  To: cygwin-patches

This patch will add ability to create directory junction which are supported from Windows 2000 and not require any special rights (unlike file/directory symbolic links).
New three modes for symbolic links creation added: "safenative", "safenativestrict" and "safenativeonly". First two allow fallback to "native" and "nativesctrict", last one use only directory junction for symbolic links (file links will fail with this setting, but it can be useful for derived projects like MSys or Git for Windows).

Only creation of directory junctions is implemented in this patch, reading and resolving junction as symbolic links are already supported by Cygwin.
I'd recommend to set default mode to "safenative" as it allow to use system functionality out-of-box with fallback to Cygwin's functionality where system's symlinks are not available.

---
 winsup/cygwin/environ.cc |   9 ++
 winsup/cygwin/globals.cc |   5 +-
 winsup/cygwin/path.cc    | 215 ++++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 226 insertions(+), 3 deletions(-)

diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc
index 8f25fb1..b02d685 100644
--- a/winsup/cygwin/environ.cc
+++ b/winsup/cygwin/environ.cc
@@ -84,6 +84,15 @@ set_winsymlinks (const char *buf)
     allow_winsymlinks = WSYM_lnk;
   else if (ascii_strncasematch (buf, "lnk", 3))
     allow_winsymlinks = WSYM_lnk;
+  else if (ascii_strncasematch (buf, "safenative", 10))
+    {
+      if (ascii_strcasematch (buf + 10, "strict"))
+        allow_winsymlinks = WSYM_safenativestrict;
+      else if (ascii_strcasematch (buf + 10, "only"))
+        allow_winsymlinks = WSYM_safenativeonly;
+      else
+        allow_winsymlinks = WSYM_safenative;
+    }
   /* Make sure to try native symlinks only on systems supporting them. */
   else if (ascii_strncasematch (buf, "native", 6))
     {
diff --git a/winsup/cygwin/globals.cc b/winsup/cygwin/globals.cc
index 09c08f2..c468741 100644
--- a/winsup/cygwin/globals.cc
+++ b/winsup/cygwin/globals.cc
@@ -59,7 +59,10 @@ enum winsym_t
   WSYM_lnk,
   WSYM_native,
   WSYM_nativestrict,
-  WSYM_nfs
+  WSYM_nfs,
+  WSYM_safenative,
+  WSYM_safenativestrict,
+  WSYM_safenativeonly
 };
 
 exit_states NO_COPY exit_state;
diff --git a/winsup/cygwin/path.cc b/winsup/cygwin/path.cc
index 89dbdab..fb7b191 100644
--- a/winsup/cygwin/path.cc
+++ b/winsup/cygwin/path.cc
@@ -1734,6 +1734,207 @@ symlink_native (const char *oldpath, path_conv &win32_newpath)
   return 0;
 }
 
+static int
+symlink_safenative (const char *oldpath, path_conv &win32_newpath)
+{
+  tmp_pathbuf tp;
+  path_conv win32_oldpath;
+  PUNICODE_STRING nt_oldpath;
+  NTSTATUS status;
+  HANDLE fh;
+  OBJECT_ATTRIBUTES attr;
+  IO_STATUS_BLOCK io;
+  PREPARSE_DATA_BUFFER prdb;
+  ACCESS_MASK access_mask;
+
+  syscall_printf ("symlink_safenative (oldpath: '%s', newpath: '%s')", oldpath,
+                win32_newpath.get_posix ());
+
+  /* Directory junction cannot be created on remote drive.
+     Leave this check here to simplify routing in symlink_worker(). */
+  if (win32_newpath.isremote ())
+    {
+      debug_printf ("'%S' is on remote drive",
+                    win32_newpath.get_nt_native_path ());
+      set_errno (EXDEV);
+      return -1;
+    }
+
+  win32_oldpath.check (oldpath, PC_SYM_NOFOLLOW);
+
+  /* Make sure that symlink target is directory as directory junction
+     can point only to directory. If target is not exist yet then
+     don't create junction to prevent an invalid state with junction
+     pointing to later created regular file. */
+  if (!win32_oldpath.exists () || !win32_oldpath.isdir ())
+    {
+      debug_printf ("'%S' does not exist or is not a directory",
+                    win32_oldpath.get_nt_native_path ());
+      set_errno (ENOTDIR);
+      return -1;
+    }
+
+  nt_oldpath = win32_oldpath.get_nt_native_path ();
+
+  /* Directory junction can point only to local mounted drive. */
+  if (nt_oldpath->Length < 6 * sizeof (wchar_t) ||
+      nt_oldpath->Buffer[5] != L':')
+    {
+      debug_printf ("'%S' is not on local mounted drive", nt_oldpath);
+      set_errno (EXDEV);
+      return -1;
+    }
+
+  /* Make sure that slash for root directory is present otherwise OS will 
+     misbehavior when resolving such junction. This is an extra care as 
+     get_nt_native_path() should return correct path. */
+  if (nt_oldpath->Length < 7 * sizeof (wchar_t) ||
+      nt_oldpath->Buffer[6] != L'\\')
+    {
+      set_errno (ENOTDIR);
+      return -1;
+    }
+
+  if (offsetof (REPARSE_DATA_BUFFER, MountPointReparseBuffer.PathBuffer) +
+      nt_oldpath->Length * 2 + (2 - 4) * sizeof (wchar_t)
+      > MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
+    {
+      set_errno (ENAMETOOLONG);
+      return -1;

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [PATCH] Add ability to use NTFS native directory symlinks without admin rights on XP and later
  2015-09-23 15:24 Evgeny Grin
@ 2015-10-21 18:48 ` Corinna Vinschen
  0 siblings, 0 replies; 3+ messages in thread
From: Corinna Vinschen @ 2015-10-21 18:48 UTC (permalink / raw)
  To: cygwin-patches

[-- Attachment #1: Type: text/plain, Size: 1194 bytes --]

Hi Evgeny,

Preliminaries: we need a copyright assignment from you before being able
to include your patches.  Please see https://cygwin.com/assign.txt.

On Sep 23 18:23, Evgeny Grin wrote:
> This patch will add ability to create directory junction which are
> supported from Windows 2000 and not require any special rights (unlike
> file/directory symbolic links).
> New three modes for symbolic links creation added: "safenative",
> "safenativestrict" and "safenativeonly". First two allow fallback to
> "native" and "nativesctrict",

I'm not opposed to add functionality to create directory junctions
as symlinks, but I am opposed to adding lots of even more confusing
settings to the CYGWIN=winsymlinks option.

I'm ok to fallback from native symlink to dir junction in both
winsymlinks:native and nativestrict modes.  We could even always use dir
junctions for dirs in the first place and only try to create a symlink
if that fails.  The difference is negligible from a user perspective
anyway.


Thanks,
Corinna

-- 
Corinna Vinschen                  Please, send mails regarding Cygwin to
Cygwin Maintainer                 cygwin AT cygwin DOT com
Red Hat

[-- Attachment #2: Type: application/pgp-signature, Size: 819 bytes --]

^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH] Add ability to use NTFS native directory symlinks without admin rights on XP and later
@ 2015-09-23 15:24 Evgeny Grin
  2015-10-21 18:48 ` Corinna Vinschen
  0 siblings, 1 reply; 3+ messages in thread
From: Evgeny Grin @ 2015-09-23 15:24 UTC (permalink / raw)
  To: cygwin-patches

(second try, previous was truncated) 
This patch will add ability to create directory junction which are supported from Windows 2000 and not require any special rights (unlike file/directory symbolic links).
New three modes for symbolic links creation added: "safenative", "safenativestrict" and "safenativeonly". First two allow fallback to "native" and "nativesctrict", last one use only directory junction for symbolic links (file links will fail with this setting, but it can be useful for derived projects like MSys or Git for Windows).

Only creation of directory junctions is implemented in this patch, reading and resolving junction as symbolic links are already supported by Cygwin.
I'd recommend to set default mode to "safenative" as it allow to use system functionality out-of-box with fallback to Cygwin's functionality where system's symlinks are not available.


---
 winsup/cygwin/environ.cc |   9 ++
 winsup/cygwin/globals.cc |   5 +-
 winsup/cygwin/path.cc    | 215 ++++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 226 insertions(+), 3 deletions(-)

diff --git a/winsup/cygwin/environ.cc b/winsup/cygwin/environ.cc
index 8f25fb1..b02d685 100644
--- a/winsup/cygwin/environ.cc
+++ b/winsup/cygwin/environ.cc
@@ -84,6 +84,15 @@ set_winsymlinks (const char *buf)
     allow_winsymlinks = WSYM_lnk;
   else if (ascii_strncasematch (buf, "lnk", 3))
     allow_winsymlinks = WSYM_lnk;
+  else if (ascii_strncasematch (buf, "safenative", 10))
+    {
+      if (ascii_strcasematch (buf + 10, "strict"))
+        allow_winsymlinks = WSYM_safenativestrict;
+      else if (ascii_strcasematch (buf + 10, "only"))
+        allow_winsymlinks = WSYM_safenativeonly;
+      else
+        allow_winsymlinks = WSYM_safenative;
+    }
   /* Make sure to try native symlinks only on systems supporting them. */
   else if (ascii_strncasematch (buf, "native", 6))
     {
diff --git a/winsup/cygwin/globals.cc b/winsup/cygwin/globals.cc
index 09c08f2..c468741 100644
--- a/winsup/cygwin/globals.cc
+++ b/winsup/cygwin/globals.cc
@@ -59,7 +59,10 @@ enum winsym_t
   WSYM_lnk,
   WSYM_native,
   WSYM_nativestrict,
-  WSYM_nfs
+  WSYM_nfs,
+  WSYM_safenative,
+  WSYM_safenativestrict,
+  WSYM_safenativeonly
 };
 
 exit_states NO_COPY exit_state;
diff --git a/winsup/cygwin/path.cc b/winsup/cygwin/path.cc
index 89dbdab..fb7b191 100644
--- a/winsup/cygwin/path.cc
+++ b/winsup/cygwin/path.cc
@@ -1734,6 +1734,207 @@ symlink_native (const char *oldpath, path_conv &win32_newpath)
   return 0;
 }
 
+static int
+symlink_safenative (const char *oldpath, path_conv &win32_newpath)
+{
+  tmp_pathbuf tp;
+  path_conv win32_oldpath;
+  PUNICODE_STRING nt_oldpath;
+  NTSTATUS status;
+  HANDLE fh;
+  OBJECT_ATTRIBUTES attr;
+  IO_STATUS_BLOCK io;
+  PREPARSE_DATA_BUFFER prdb;
+  ACCESS_MASK access_mask;
+
+  syscall_printf ("symlink_safenative (oldpath: '%s', newpath: '%s')", oldpath,
+                win32_newpath.get_posix ());
+
+  /* Directory junction cannot be created on remote drive.
+     Leave this check here to simplify routing in symlink_worker(). */
+  if (win32_newpath.isremote ())
+    {
+      debug_printf ("'%S' is on remote drive",
+                    win32_newpath.get_nt_native_path ());
+      set_errno (EXDEV);
+      return -1;
+    }
+
+  win32_oldpath.check (oldpath, PC_SYM_NOFOLLOW);
+
+  /* Make sure that symlink target is directory as directory junction
+     can point only to directory. If target is not exist yet then
+     don't create junction to prevent an invalid state with junction
+     pointing to later created regular file. */
+  if (!win32_oldpath.exists () || !win32_oldpath.isdir ())
+    {
+      debug_printf ("'%S' does not exist or is not a directory",
+                    win32_oldpath.get_nt_native_path ());
+      set_errno (ENOTDIR);
+      return -1;
+    }
+
+  nt_oldpath = win32_oldpath.get_nt_native_path ();
+
+  /* Directory junction can point only to local mounted drive. */
+  if (nt_oldpath->Length < 6 * sizeof (wchar_t) ||
+      nt_oldpath->Buffer[5] != L':')
+    {
+      debug_printf ("'%S' is not on local mounted drive", nt_oldpath);
+      set_errno (EXDEV);
+      return -1;
+    }
+
+  /* Make sure that slash for root directory is present otherwise OS will 
+     misbehavior when resolving such junction. This is an extra care as 
+     get_nt_native_path() should return correct path. */
+  if (nt_oldpath->Length < 7 * sizeof (wchar_t) ||
+      nt_oldpath->Buffer[6] != L'\\')
+    {
+      set_errno (ENOTDIR);
+      return -1;
+    }
+
+  if (offsetof (REPARSE_DATA_BUFFER, MountPointReparseBuffer.PathBuffer) +
+      nt_oldpath->Length * 2 + (2 - 4) * sizeof (wchar_t)
+      > MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
+    {
+      set_errno (ENAMETOOLONG);
+      return -1;
+    }
+
+#if MAXIMUM_REPARSE_DATA_BUFFER_SIZE > NT_MAX_PATH
+#error REPARSE_DATA_BUFFER do not fit into tmp_pathbuf::c_get() buffer. Rewrite code.
+#endif
+  prdb = (PREPARSE_DATA_BUFFER) tp.c_get ();
+
+  prdb->Reserved = 0;
+  prdb->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
+  prdb->MountPointReparseBuffer.SubstituteNameOffset = 0;
+  memcpy(prdb->MountPointReparseBuffer.PathBuffer, nt_oldpath->Buffer,
+         nt_oldpath->Length);
+  prdb->MountPointReparseBuffer.SubstituteNameLength =
+          (USHORT)(nt_oldpath->Length);
+  prdb->MountPointReparseBuffer.PathBuffer[
+          prdb->MountPointReparseBuffer.SubstituteNameLength / sizeof (wchar_t)] = 0;
+  prdb->MountPointReparseBuffer.PrintNameOffset =
+          prdb->MountPointReparseBuffer.SubstituteNameLength + 1 * sizeof (wchar_t);
+  memcpy(&(prdb->MountPointReparseBuffer.PathBuffer[
+         prdb->MountPointReparseBuffer.PrintNameOffset / sizeof (wchar_t)]),
+         &(nt_oldpath->Buffer[4]), nt_oldpath->Length - 4 * sizeof (wchar_t));
+  prdb->MountPointReparseBuffer.PrintNameLength =
+          (USHORT)(nt_oldpath->Length - 4 * sizeof (wchar_t));
+  prdb->MountPointReparseBuffer.PathBuffer[
+          (prdb->MountPointReparseBuffer.PrintNameOffset + 
+           prdb->MountPointReparseBuffer.PrintNameLength) / sizeof (wchar_t)] = 0;
+  prdb->ReparseDataLength = (USHORT) offsetof(REPARSE_DATA_BUFFER,
+                                              MountPointReparseBuffer.PathBuffer) -
+          REPARSE_DATA_BUFFER_HEADER_SIZE +
+          prdb->MountPointReparseBuffer.PrintNameOffset +
+          prdb->MountPointReparseBuffer.PrintNameLength + 1 * sizeof (wchar_t);
+
+  /* DELETE access is required to delete empty directory if it's not
+     transformed into directory junctions. */
+  access_mask = FILE_WRITE_ATTRIBUTES | FILE_LIST_DIRECTORY |
+      FILE_TRAVERSE | SYNCHRONIZE | DELETE;
+  /* READ_CONTROL and WRITE_DAC are required for reuse handle in
+     set_file_attribute() otherwise function will need to reopen file. */
+  if (win32_newpath.has_acls ())
+    access_mask |= READ_CONTROL | WRITE_DAC;
+
+  status = NtCreateFile (&fh, access_mask,
+                         win32_newpath.get_object_attr (attr, sec_none_nih),
+                         &io, NULL, FILE_ATTRIBUTE_DIRECTORY,
+                         0, FILE_CREATE, FILE_DIRECTORY_FILE |
+                         FILE_OPEN_REPARSE_POINT | FILE_OPEN_FOR_BACKUP_INTENT,
+                         NULL, 0);
+  if (!NT_SUCCESS (status))
+    {
+      if (status == STATUS_ACCESS_DENIED)
+        {
+          ULONG share_access;
+          /* Retry with less requested access rights. */
+          debug_printf ("Creating '%S' with restricted access rights",
+                        win32_newpath.get_nt_native_path ());
+
+          access_mask &= ~(DELETE | READ_CONTROL | WRITE_DAC);
+
+          /* Allow sharing otherwise set_file_attribute() will fail. */
+          share_access = win32_newpath.has_acls () ? (FILE_SHARE_READ |
+                                                      FILE_SHARE_WRITE) : 0;
+          status = NtCreateFile (&fh, access_mask,
+                                 win32_newpath.get_object_attr (attr, sec_none_nih),
+                                 &io, NULL, FILE_ATTRIBUTE_DIRECTORY,
+                                 share_access, FILE_CREATE, FILE_DIRECTORY_FILE |
+                                 FILE_OPEN_REPARSE_POINT |
+                                 FILE_OPEN_FOR_BACKUP_INTENT, NULL, 0);
+        }
+      if (!NT_SUCCESS (status))
+        {
+          debug_printf ("Creating '%S' failed, status = %y", 
+                        win32_newpath.get_nt_native_path (), status);
+          __seterrno_from_nt_status (status);
+          return -1;
+        }
+    }
+
+  if (win32_newpath.has_acls ())
+    set_file_attribute (fh, win32_newpath, ILLEGAL_UID, ILLEGAL_GID,
+                        S_JUSTCREATED | S_IFDIR | S_IFLNK |
+                        STD_RBITS | STD_WBITS | STD_XBITS);
+
+  debug_printf ("Setting SubstituteName '%W' and PrintName '%W' for directory junction '%S'", 
+                prdb->MountPointReparseBuffer.PathBuffer +
+                  prdb->MountPointReparseBuffer.SubstituteNameOffset / sizeof (wchar_t),
+                prdb->MountPointReparseBuffer.PathBuffer +
+                  prdb->MountPointReparseBuffer.PrintNameOffset / sizeof (wchar_t),
+                win32_newpath.get_nt_native_path ());
+  status = NtFsControlFile (fh, NULL, NULL, NULL, &io, FSCTL_SET_REPARSE_POINT,
+                            prdb, (ULONG)(prdb->ReparseDataLength +
+                                    REPARSE_DATA_BUFFER_HEADER_SIZE), NULL, 0);
+  if (status == STATUS_PENDING)
+    {
+      if (WaitForSingleObject (fh, 2000) == WAIT_OBJECT_0)
+        status = io.Status;
+      else
+        status = STATUS_ACCESS_DENIED;
+    }
+
+  if (!NT_SUCCESS (status))
+    {
+      FILE_DISPOSITION_INFORMATION disp = { TRUE };
+      if (status == STATUS_IO_REPARSE_TAG_INVALID || status == STATUS_IO_REPARSE_DATA_INVALID)
+        debug_printf ("Setting reparse point failed because reparse point data is invalid, status = %y", status);
+      else
+        debug_printf ("Setting reparse point failed, status = %y", status);
+      __seterrno_from_nt_status (status);
+
+      /* Delete created junction blank. */
+      status = NtSetInformationFile (fh, &io, &disp, sizeof (disp),
+                                     FileDispositionInformation);
+      NtClose (fh);
+      if (!NT_SUCCESS (status))
+        {
+          /* Reopen junction blank for deletion. */
+          status = NtCreateFile (&fh, DELETE,
+                                 win32_newpath.get_object_attr (attr, sec_none_nih),
+                                 &io, NULL, FILE_ATTRIBUTE_DIRECTORY,
+                                 FILE_SHARE_DELETE, FILE_OPEN, FILE_DIRECTORY_FILE |
+                                 FILE_OPEN_REPARSE_POINT | FILE_OPEN_FOR_BACKUP_INTENT |
+                                 FILE_DELETE_ON_CLOSE, NULL, 0);
+          if (NT_SUCCESS (status))
+            NtClose (fh);
+        }
+      if (!NT_SUCCESS (status) && status != STATUS_DELETE_PENDING)
+        debug_printf ("Removing directory junction blank failed, status = %y", status);
+
+      return -1;
+  }
+
+  NtClose (fh);
+  return 0;
+}
+
 int
 symlink_worker (const char *oldpath, const char *newpath, bool isdevice)
 {
@@ -1798,7 +1999,10 @@ symlink_worker (const char *oldpath, const char *newpath, bool isdevice)
 	  wsym_type = WSYM_nativestrict;
 	}
       /* Don't try native symlinks on FSes not supporting reparse points. */
-      else if ((wsym_type == WSYM_native || wsym_type == WSYM_nativestrict)
+      else if ((wsym_type == WSYM_native || wsym_type == WSYM_nativestrict
+                || wsym_type == WSYM_safenative
+                || wsym_type == WSYM_safenativestrict
+                || wsym_type == WSYM_safenativeonly)
 	       && !(win32_newpath.fs_flags () & FILE_SUPPORTS_REPARSE_POINTS))
 	wsym_type = WSYM_sysfile;
 
@@ -1838,13 +2042,20 @@ symlink_worker (const char *oldpath, const char *newpath, bool isdevice)
 	case WSYM_nfs:
 	  res = symlink_nfs (oldpath, win32_newpath);
 	  __leave;
+        case WSYM_safenative:
+	case WSYM_safenativestrict:
+        case WSYM_safenativeonly:
+          res = symlink_safenative (oldpath, win32_newpath);
+	  if (!res || wsym_type == WSYM_safenativeonly)
+	    __leave;
+	/* Intentional fall-through */
 	case WSYM_native:
 	case WSYM_nativestrict:
 	  res = symlink_native (oldpath, win32_newpath);
 	  if (!res)
 	    __leave;
 	  /* Strictly native?  Too bad. */
-	  if (wsym_type == WSYM_nativestrict)
+	  if (wsym_type == WSYM_nativestrict || wsym_type == WSYM_safenativestrict)
 	    {
 	      __seterrno ();
 	      __leave;
-- 
2.5.1.windows.1

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2015-10-21 18:48 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2015-09-23 11:07 [PATCH] Add ability to use NTFS native directory symlinks without admin rights on XP and later Evgeny Grin
2015-09-23 15:24 Evgeny Grin
2015-10-21 18:48 ` Corinna Vinschen

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).