Skip to content

bpo-31512: Add non-elevated symlink support for dev mode Windows 10 #3652

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2699,19 +2699,15 @@ features:
as a directory if *target_is_directory* is ``True`` or a file symlink (the
default) otherwise. On non-Windows platforms, *target_is_directory* is ignored.

Symbolic link support was introduced in Windows 6.0 (Vista). :func:`symlink`
will raise a :exc:`NotImplementedError` on Windows versions earlier than 6.0.

This function can support :ref:`paths relative to directory descriptors
<dir_fd>`.

.. note::

On Windows, the *SeCreateSymbolicLinkPrivilege* is required in order to
successfully create symlinks. This privilege is not typically granted to
regular users but is available to accounts which can escalate privileges
to the administrator level. Either obtaining the privilege or running your
application as an administrator are ways to successfully create symlinks.
On newer versions of Windows 10, unprivileged accounts can create symlinks
if Developer Mode is enabled. When Developer Mode is not available/enabled,
the *SeCreateSymbolicLinkPrivilege* privilege is required, or the process
must be run as an administrator.


:exc:`OSError` is raised when the function is called by an unprivileged
Expand All @@ -2729,6 +2725,9 @@ features:
.. versionchanged:: 3.6
Accepts a :term:`path-like object` for *src* and *dst*.

.. versionchanged:: 3.8
Added support for unelevated symlinks on Windows with Developer Mode.


.. function:: sync()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
With the Windows 10 Creators Update, non-elevated users can now create
symlinks as long as the computer has Developer Mode enabled.
110 changes: 39 additions & 71 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,7 @@ extern char *ctermid_r(char *);
#include <windows.h>
#include <shellapi.h> /* for ShellExecute() */
#include <lmcons.h> /* for UNLEN */
#ifdef SE_CREATE_SYMBOLIC_LINK_NAME /* Available starting with Vista */
#define HAVE_SYMLINK
static int win32_can_symlink = 0;
#endif
#endif /* _MSC_VER */

#ifndef MAXPATHLEN
Expand Down Expand Up @@ -7755,26 +7752,6 @@ os_readlink_impl(PyObject *module, path_t *path, int dir_fd)

#if defined(MS_WINDOWS)

/* Grab CreateSymbolicLinkW dynamically from kernel32 */
static BOOLEAN (CALLBACK *Py_CreateSymbolicLinkW)(LPCWSTR, LPCWSTR, DWORD) = NULL;

static int
check_CreateSymbolicLink(void)
{
HINSTANCE hKernel32;
/* only recheck */
if (Py_CreateSymbolicLinkW)
return 1;

Py_BEGIN_ALLOW_THREADS
hKernel32 = GetModuleHandleW(L"KERNEL32");
*(FARPROC*)&Py_CreateSymbolicLinkW = GetProcAddress(hKernel32,
"CreateSymbolicLinkW");
Py_END_ALLOW_THREADS

return Py_CreateSymbolicLinkW != NULL;
}

/* Remove the last portion of the path - return 0 on success */
static int
_dirnameW(WCHAR *path)
Expand Down Expand Up @@ -7878,33 +7855,57 @@ os_symlink_impl(PyObject *module, path_t *src, path_t *dst,
{
#ifdef MS_WINDOWS
DWORD result;
DWORD flags = 0;

/* Assumed true, set to false if detected to not be available. */
static int windows_has_symlink_unprivileged_flag = TRUE;
#else
int result;
#endif

#ifdef MS_WINDOWS
if (!check_CreateSymbolicLink()) {
PyErr_SetString(PyExc_NotImplementedError,
"CreateSymbolicLink functions not found");
return NULL;
}
if (!win32_can_symlink) {
PyErr_SetString(PyExc_OSError, "symbolic link privilege not held");
return NULL;
}
#endif

#ifdef MS_WINDOWS
if (windows_has_symlink_unprivileged_flag) {
/* Allow non-admin symlinks if system allows it. */
flags |= SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE;
}

Py_BEGIN_ALLOW_THREADS
_Py_BEGIN_SUPPRESS_IPH
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to suppress the CRT invalid-parameter handler in this code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to remove it, but it caused test failures (test_os.Win32SynLinkTests.test_buffer_overflow). See #5989, which introduced these macros here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. That makes sense. I forgot that _check_dirW was rewritten using the safe string functions. IMO, it needs improvement. We have path_t strings, so they're null terminated, and we know their length. Also, the check shouldn't be limited to MAX_PATH. But that's for another day.

/* if src is a directory, ensure target_is_directory==1 */
target_is_directory |= _check_dirW(src->wide, dst->wide);
result = Py_CreateSymbolicLinkW(dst->wide, src->wide,
target_is_directory);
/* if src is a directory, ensure flags==1 (target_is_directory bit) */
if (target_is_directory || _check_dirW(src->wide, dst->wide)) {
flags |= SYMBOLIC_LINK_FLAG_DIRECTORY;
}

result = CreateSymbolicLinkW(dst->wide, src->wide, flags);
_Py_END_SUPPRESS_IPH
Py_END_ALLOW_THREADS

if (windows_has_symlink_unprivileged_flag && !result &&
ERROR_INVALID_PARAMETER == GetLastError()) {

Py_BEGIN_ALLOW_THREADS
_Py_BEGIN_SUPPRESS_IPH
/* This error might be caused by
SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE not being supported.
Try again, and update windows_has_symlink_unprivileged_flag if we
are successful this time.

NOTE: There is a risk of a race condition here if there are other
conditions than the flag causing ERROR_INVALID_PARAMETER, and
another process (or thread) changes that condition in between our
calls to CreateSymbolicLink.
*/
flags &= ~(SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE);
result = CreateSymbolicLinkW(dst->wide, src->wide, flags);
_Py_END_SUPPRESS_IPH
Py_END_ALLOW_THREADS

if (result || ERROR_INVALID_PARAMETER != GetLastError()) {
windows_has_symlink_unprivileged_flag = FALSE;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A note: on systems without the new flag support, we'll do both calls on each os.symlink invocation until the first time the symlink can be created. This is probably not important, but could be avoided if windows_has_symlink_unprivileged_flag was set to FALSE in the case of failure with any error other than ERROR_INVALID_PARAMETER (suffering from the same race as the case of success).

}

if (!result)
return path_error2(src, dst);

Expand Down Expand Up @@ -13469,35 +13470,6 @@ static PyMethodDef posix_methods[] = {
{NULL, NULL} /* Sentinel */
};


#if defined(HAVE_SYMLINK) && defined(MS_WINDOWS)
static int
enable_symlink()
{
HANDLE tok;
TOKEN_PRIVILEGES tok_priv;
LUID luid;

if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &tok))
return 0;

if (!LookupPrivilegeValue(NULL, SE_CREATE_SYMBOLIC_LINK_NAME, &luid))
return 0;

tok_priv.PrivilegeCount = 1;
tok_priv.Privileges[0].Luid = luid;
tok_priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

if (!AdjustTokenPrivileges(tok, FALSE, &tok_priv,
sizeof(TOKEN_PRIVILEGES),
(PTOKEN_PRIVILEGES) NULL, (PDWORD) NULL))
return 0;

/* ERROR_NOT_ALL_ASSIGNED returned when the privilege can't be assigned. */
return GetLastError() == ERROR_NOT_ALL_ASSIGNED ? 0 : 1;
}
#endif /* defined(HAVE_SYMLINK) && defined(MS_WINDOWS) */

static int
all_ins(PyObject *m)
{
Expand Down Expand Up @@ -14105,10 +14077,6 @@ INITFUNC(void)
PyObject *list;
const char * const *trace;

#if defined(HAVE_SYMLINK) && defined(MS_WINDOWS)
win32_can_symlink = enable_symlink();
#endif

m = PyModule_Create(&posixmodule);
if (m == NULL)
return NULL;
Expand Down
5 changes: 5 additions & 0 deletions Modules/winreparse.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ typedef struct {
FIELD_OFFSET(_Py_REPARSE_DATA_BUFFER, GenericReparseBuffer)
#define _Py_MAXIMUM_REPARSE_DATA_BUFFER_SIZE ( 16 * 1024 )

// Defined in WinBase.h in 'recent' versions of Windows 10 SDK
#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2
#endif

#ifdef __cplusplus
}
#endif
Expand Down