Skip to content

Commit 4bd91f2

Browse files
committed
pythongh-118486: Support mkdir(mode=0o700) on Windows
1 parent 1161ab9 commit 4bd91f2

File tree

4 files changed

+193
-2
lines changed

4 files changed

+193
-2
lines changed

Doc/library/os.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2430,6 +2430,10 @@ features:
24302430
platform-dependent. On some platforms, they are ignored and you should call
24312431
:func:`chmod` explicitly to set them.
24322432

2433+
On Windows, a *mode* of ``0o700`` is specifically handled to apply access
2434+
control to the new directory such that only the current user and
2435+
administrators have access. Other values of *mode* are ignored.
2436+
24332437
This function can also support :ref:`paths relative to directory descriptors
24342438
<dir_fd>`.
24352439

@@ -2444,6 +2448,9 @@ features:
24442448
.. versionchanged:: 3.6
24452449
Accepts a :term:`path-like object`.
24462450

2451+
.. versionchanged:: 3.13
2452+
Windows now handles a *mode* of ``0o700``.
2453+
24472454

24482455
.. function:: makedirs(name, mode=0o777, exist_ok=False)
24492456

Lib/test/test_tempfile.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import weakref
1414
import gc
1515
import shutil
16+
import subprocess
1617
from unittest import mock
1718

1819
import unittest
@@ -803,6 +804,33 @@ def test_mode(self):
803804
finally:
804805
os.rmdir(dir)
805806

807+
@unittest.skipUnless(os.name == "nt", "Only on Windows.")
808+
def test_mode_win32(self):
809+
# Use icacls.exe to extract the users with some level of access
810+
# Main thing we are testing is that the BUILTIN\Users group has
811+
# no access. The exact ACL is going to vary based on which user
812+
# is running the test.
813+
dir = self.do_create()
814+
try:
815+
out = subprocess.check_output(["icacls.exe", dir], encoding="oem").casefold()
816+
finally:
817+
os.rmdir(dir)
818+
819+
dir = dir.casefold()
820+
users = set()
821+
found_user = False
822+
for line in out.strip().splitlines():
823+
acl = None
824+
# First line of result includes our directory
825+
if line.startswith(dir):
826+
acl = line.removeprefix(dir).strip()
827+
elif line and line[:1].isspace():
828+
acl = line.strip()
829+
if acl:
830+
users.add(acl.partition(":")[0])
831+
832+
self.assertNotIn(r"BUILTIN\Users".casefold(), users)
833+
806834
def test_collision_with_existing_file(self):
807835
# mkdtemp tries another name when a file with
808836
# the chosen name already exists
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`os.mkdir` now accepts *mode* of ``0o700`` to restrict the new
2+
directory to the current user.

Modules/posixmodule.c

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
# include <winioctl.h>
3838
# include <lmcons.h> // UNLEN
3939
# include "osdefs.h" // SEP
40+
# include <aclapi.h> // SetEntriesInAcl
41+
# include <sddl.h> // SDDL_REVISION_1
4042
# if defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM)
4143
# define HAVE_SYMLINK
4244
# endif /* MS_WINDOWS_DESKTOP | MS_WINDOWS_SYSTEM */
@@ -5539,6 +5541,133 @@ os__path_normpath_impl(PyObject *module, PyObject *path)
55395541
return result;
55405542
}
55415543

5544+
#ifdef MS_WINDOWS
5545+
5546+
/* We centralise SECURITY_ATTRIBUTE initialization based around
5547+
templates that will probably mostly match common POSIX mode settings.
5548+
The _Py_SECURITY_ATTRIBUTE_DATA structure contains temporary data, as
5549+
a constructed SECURITY_ATTRIBUTE structure typically refers to memory
5550+
that has to be alive while it's being used.
5551+
5552+
Typical use will look like:
5553+
SECURITY_ATTRIBUTES *pSecAttr = NULL;
5554+
struct _Py_SECURITY_ATTRIBUTE_DATA secAttrData;
5555+
int error, error2;
5556+
5557+
Py_BEGIN_ALLOW_THREADS
5558+
switch (mode) {
5559+
case 0x1C0: // 0o700
5560+
error = initializeMkdir700SecurityAttributes(&pSecAttr, &secAttrData);
5561+
break;
5562+
...
5563+
default:
5564+
error = initializeDefaultSecurityAttributes(&pSecAttr, &secAttrData);
5565+
break;
5566+
}
5567+
5568+
if (!error) {
5569+
// do operation, passing pSecAttr
5570+
}
5571+
5572+
// Unconditionally clear secAttrData.
5573+
error2 = clearSecurityAttributes(&pSecAttr, &secAttrData);
5574+
if (!error) {
5575+
error = error2;
5576+
}
5577+
Py_END_ALLOW_THREADS
5578+
5579+
if (error) {
5580+
PyErr_SetFromWindowsErr(error);
5581+
return NULL;
5582+
}
5583+
*/
5584+
5585+
struct _Py_SECURITY_ATTRIBUTE_DATA {
5586+
SECURITY_ATTRIBUTES securityAttributes;
5587+
PACL acl;
5588+
SECURITY_DESCRIPTOR sd;
5589+
EXPLICIT_ACCESS_W ea[4];
5590+
};
5591+
5592+
static int
5593+
initializeDefaultSecurityAttributes(
5594+
PSECURITY_ATTRIBUTES *securityAttributes,
5595+
struct _Py_SECURITY_ATTRIBUTE_DATA *data
5596+
) {
5597+
assert(securityAttributes);
5598+
assert(data);
5599+
*securityAttributes = NULL;
5600+
memset(data, 0, sizeof(*data));
5601+
return 0;
5602+
}
5603+
5604+
static int
5605+
initializeMkdir700SecurityAttributes(
5606+
PSECURITY_ATTRIBUTES *securityAttributes,
5607+
struct _Py_SECURITY_ATTRIBUTE_DATA *data
5608+
) {
5609+
assert(securityAttributes);
5610+
assert(data);
5611+
*securityAttributes = NULL;
5612+
memset(data, 0, sizeof(*data));
5613+
5614+
if (!InitializeSecurityDescriptor(&data->sd, SECURITY_DESCRIPTOR_REVISION)
5615+
|| !SetSecurityDescriptorGroup(&data->sd, NULL, TRUE)) {
5616+
return GetLastError();
5617+
}
5618+
5619+
data->securityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);
5620+
data->ea[0].grfAccessPermissions = GENERIC_ALL;
5621+
data->ea[0].grfAccessMode = SET_ACCESS;
5622+
data->ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
5623+
data->ea[0].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
5624+
data->ea[0].Trustee.TrusteeType = TRUSTEE_IS_ALIAS;
5625+
data->ea[0].Trustee.ptstrName = L"CURRENT_USER";
5626+
5627+
data->ea[1].grfAccessPermissions = GENERIC_ALL;
5628+
data->ea[1].grfAccessMode = SET_ACCESS;
5629+
data->ea[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
5630+
data->ea[1].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
5631+
data->ea[1].Trustee.TrusteeType = TRUSTEE_IS_ALIAS;
5632+
data->ea[1].Trustee.ptstrName = L"SYSTEM";
5633+
5634+
data->ea[2].grfAccessPermissions = GENERIC_ALL;
5635+
data->ea[2].grfAccessMode = SET_ACCESS;
5636+
data->ea[2].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
5637+
data->ea[2].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
5638+
data->ea[2].Trustee.TrusteeType = TRUSTEE_IS_ALIAS;
5639+
data->ea[2].Trustee.ptstrName = L"ADMINISTRATORS";
5640+
5641+
int r = SetEntriesInAclW(3, data->ea, NULL, &data->acl);
5642+
if (r) {
5643+
return r;
5644+
}
5645+
if (!SetSecurityDescriptorDacl(&data->sd, TRUE, data->acl, FALSE)) {
5646+
return GetLastError();
5647+
}
5648+
data->securityAttributes.lpSecurityDescriptor = &data->sd;
5649+
*securityAttributes = &data->securityAttributes;
5650+
return 0;
5651+
}
5652+
5653+
static int
5654+
clearSecurityAttributes(
5655+
PSECURITY_ATTRIBUTES *securityAttributes,
5656+
struct _Py_SECURITY_ATTRIBUTE_DATA *data
5657+
) {
5658+
assert(securityAttributes);
5659+
assert(data);
5660+
*securityAttributes = NULL;
5661+
if (data->acl) {
5662+
if (LocalFree((void *)data->acl)) {
5663+
return GetLastError();
5664+
}
5665+
}
5666+
return 0;
5667+
}
5668+
5669+
#endif
5670+
55425671
/*[clinic input]
55435672
os.mkdir
55445673
@@ -5568,6 +5697,12 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
55685697
/*[clinic end generated code: output=a70446903abe821f input=a61722e1576fab03]*/
55695698
{
55705699
int result;
5700+
#ifdef MS_WINDOWS
5701+
int error = 0;
5702+
int pathError = 0;
5703+
SECURITY_ATTRIBUTES *pSecAttr = NULL;
5704+
struct _Py_SECURITY_ATTRIBUTE_DATA secAttrData;
5705+
#endif
55715706
#ifdef HAVE_MKDIRAT
55725707
int mkdirat_unavailable = 0;
55735708
#endif
@@ -5579,11 +5714,30 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
55795714

55805715
#ifdef MS_WINDOWS
55815716
Py_BEGIN_ALLOW_THREADS
5582-
result = CreateDirectoryW(path->wide, NULL);
5717+
switch (mode) {
5718+
case 0x1C0: // 0o700
5719+
error = initializeMkdir700SecurityAttributes(&pSecAttr, &secAttrData);
5720+
break;
5721+
default:
5722+
error = initializeDefaultSecurityAttributes(&pSecAttr, &secAttrData);
5723+
break;
5724+
}
5725+
if (!error) {
5726+
result = CreateDirectoryW(path->wide, pSecAttr);
5727+
error = clearSecurityAttributes(&pSecAttr, &secAttrData);
5728+
} else {
5729+
// Ignore errors - we have a more interesting one already
5730+
clearSecurityAttributes(&pSecAttr, &secAttrData);
5731+
}
55835732
Py_END_ALLOW_THREADS
55845733

5585-
if (!result)
5734+
if (error) {
5735+
PyErr_SetFromWindowsErr(error);
5736+
return NULL;
5737+
}
5738+
if (!result) {
55865739
return path_error(path);
5740+
}
55875741
#else
55885742
Py_BEGIN_ALLOW_THREADS
55895743
#if HAVE_MKDIRAT

0 commit comments

Comments
 (0)