Skip to content

Commit 0e10766

Browse files
vidartfzooba
authored andcommitted
bpo-31512: Add non-elevated symlink support for Windows (GH-3652)
1 parent 8709490 commit 0e10766

File tree

4 files changed

+53
-79
lines changed

4 files changed

+53
-79
lines changed

Doc/library/os.rst

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2699,19 +2699,15 @@ features:
26992699
as a directory if *target_is_directory* is ``True`` or a file symlink (the
27002700
default) otherwise. On non-Windows platforms, *target_is_directory* is ignored.
27012701

2702-
Symbolic link support was introduced in Windows 6.0 (Vista). :func:`symlink`
2703-
will raise a :exc:`NotImplementedError` on Windows versions earlier than 6.0.
2704-
27052702
This function can support :ref:`paths relative to directory descriptors
27062703
<dir_fd>`.
27072704

27082705
.. note::
27092706

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

27162712

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

2728+
.. versionchanged:: 3.8
2729+
Added support for unelevated symlinks on Windows with Developer Mode.
2730+
27322731

27332732
.. function:: sync()
27342733

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
With the Windows 10 Creators Update, non-elevated users can now create
2+
symlinks as long as the computer has Developer Mode enabled.

Modules/posixmodule.c

Lines changed: 39 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,7 @@ extern char *ctermid_r(char *);
284284
#include <windows.h>
285285
#include <shellapi.h> /* for ShellExecute() */
286286
#include <lmcons.h> /* for UNLEN */
287-
#ifdef SE_CREATE_SYMBOLIC_LINK_NAME /* Available starting with Vista */
288287
#define HAVE_SYMLINK
289-
static int win32_can_symlink = 0;
290-
#endif
291288
#endif /* _MSC_VER */
292289

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

77567753
#if defined(MS_WINDOWS)
77577754

7758-
/* Grab CreateSymbolicLinkW dynamically from kernel32 */
7759-
static BOOLEAN (CALLBACK *Py_CreateSymbolicLinkW)(LPCWSTR, LPCWSTR, DWORD) = NULL;
7760-
7761-
static int
7762-
check_CreateSymbolicLink(void)
7763-
{
7764-
HINSTANCE hKernel32;
7765-
/* only recheck */
7766-
if (Py_CreateSymbolicLinkW)
7767-
return 1;
7768-
7769-
Py_BEGIN_ALLOW_THREADS
7770-
hKernel32 = GetModuleHandleW(L"KERNEL32");
7771-
*(FARPROC*)&Py_CreateSymbolicLinkW = GetProcAddress(hKernel32,
7772-
"CreateSymbolicLinkW");
7773-
Py_END_ALLOW_THREADS
7774-
7775-
return Py_CreateSymbolicLinkW != NULL;
7776-
}
7777-
77787755
/* Remove the last portion of the path - return 0 on success */
77797756
static int
77807757
_dirnameW(WCHAR *path)
@@ -7878,33 +7855,57 @@ os_symlink_impl(PyObject *module, path_t *src, path_t *dst,
78787855
{
78797856
#ifdef MS_WINDOWS
78807857
DWORD result;
7858+
DWORD flags = 0;
7859+
7860+
/* Assumed true, set to false if detected to not be available. */
7861+
static int windows_has_symlink_unprivileged_flag = TRUE;
78817862
#else
78827863
int result;
78837864
#endif
78847865

78857866
#ifdef MS_WINDOWS
7886-
if (!check_CreateSymbolicLink()) {
7887-
PyErr_SetString(PyExc_NotImplementedError,
7888-
"CreateSymbolicLink functions not found");
7889-
return NULL;
7890-
}
7891-
if (!win32_can_symlink) {
7892-
PyErr_SetString(PyExc_OSError, "symbolic link privilege not held");
7893-
return NULL;
7894-
}
7895-
#endif
78967867

7897-
#ifdef MS_WINDOWS
7868+
if (windows_has_symlink_unprivileged_flag) {
7869+
/* Allow non-admin symlinks if system allows it. */
7870+
flags |= SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE;
7871+
}
78987872

78997873
Py_BEGIN_ALLOW_THREADS
79007874
_Py_BEGIN_SUPPRESS_IPH
7901-
/* if src is a directory, ensure target_is_directory==1 */
7902-
target_is_directory |= _check_dirW(src->wide, dst->wide);
7903-
result = Py_CreateSymbolicLinkW(dst->wide, src->wide,
7904-
target_is_directory);
7875+
/* if src is a directory, ensure flags==1 (target_is_directory bit) */
7876+
if (target_is_directory || _check_dirW(src->wide, dst->wide)) {
7877+
flags |= SYMBOLIC_LINK_FLAG_DIRECTORY;
7878+
}
7879+
7880+
result = CreateSymbolicLinkW(dst->wide, src->wide, flags);
79057881
_Py_END_SUPPRESS_IPH
79067882
Py_END_ALLOW_THREADS
79077883

7884+
if (windows_has_symlink_unprivileged_flag && !result &&
7885+
ERROR_INVALID_PARAMETER == GetLastError()) {
7886+
7887+
Py_BEGIN_ALLOW_THREADS
7888+
_Py_BEGIN_SUPPRESS_IPH
7889+
/* This error might be caused by
7890+
SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE not being supported.
7891+
Try again, and update windows_has_symlink_unprivileged_flag if we
7892+
are successful this time.
7893+
7894+
NOTE: There is a risk of a race condition here if there are other
7895+
conditions than the flag causing ERROR_INVALID_PARAMETER, and
7896+
another process (or thread) changes that condition in between our
7897+
calls to CreateSymbolicLink.
7898+
*/
7899+
flags &= ~(SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE);
7900+
result = CreateSymbolicLinkW(dst->wide, src->wide, flags);
7901+
_Py_END_SUPPRESS_IPH
7902+
Py_END_ALLOW_THREADS
7903+
7904+
if (result || ERROR_INVALID_PARAMETER != GetLastError()) {
7905+
windows_has_symlink_unprivileged_flag = FALSE;
7906+
}
7907+
}
7908+
79087909
if (!result)
79097910
return path_error2(src, dst);
79107911

@@ -13469,35 +13470,6 @@ static PyMethodDef posix_methods[] = {
1346913470
{NULL, NULL} /* Sentinel */
1347013471
};
1347113472

13472-
13473-
#if defined(HAVE_SYMLINK) && defined(MS_WINDOWS)
13474-
static int
13475-
enable_symlink()
13476-
{
13477-
HANDLE tok;
13478-
TOKEN_PRIVILEGES tok_priv;
13479-
LUID luid;
13480-
13481-
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &tok))
13482-
return 0;
13483-
13484-
if (!LookupPrivilegeValue(NULL, SE_CREATE_SYMBOLIC_LINK_NAME, &luid))
13485-
return 0;
13486-
13487-
tok_priv.PrivilegeCount = 1;
13488-
tok_priv.Privileges[0].Luid = luid;
13489-
tok_priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
13490-
13491-
if (!AdjustTokenPrivileges(tok, FALSE, &tok_priv,
13492-
sizeof(TOKEN_PRIVILEGES),
13493-
(PTOKEN_PRIVILEGES) NULL, (PDWORD) NULL))
13494-
return 0;
13495-
13496-
/* ERROR_NOT_ALL_ASSIGNED returned when the privilege can't be assigned. */
13497-
return GetLastError() == ERROR_NOT_ALL_ASSIGNED ? 0 : 1;
13498-
}
13499-
#endif /* defined(HAVE_SYMLINK) && defined(MS_WINDOWS) */
13500-
1350113473
static int
1350213474
all_ins(PyObject *m)
1350313475
{
@@ -14105,10 +14077,6 @@ INITFUNC(void)
1410514077
PyObject *list;
1410614078
const char * const *trace;
1410714079

14108-
#if defined(HAVE_SYMLINK) && defined(MS_WINDOWS)
14109-
win32_can_symlink = enable_symlink();
14110-
#endif
14111-
1411214080
m = PyModule_Create(&posixmodule);
1411314081
if (m == NULL)
1411414082
return NULL;

Modules/winreparse.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ typedef struct {
4545
FIELD_OFFSET(_Py_REPARSE_DATA_BUFFER, GenericReparseBuffer)
4646
#define _Py_MAXIMUM_REPARSE_DATA_BUFFER_SIZE ( 16 * 1024 )
4747

48+
// Defined in WinBase.h in 'recent' versions of Windows 10 SDK
49+
#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
50+
#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2
51+
#endif
52+
4853
#ifdef __cplusplus
4954
}
5055
#endif

0 commit comments

Comments
 (0)