From 86da8debddf2149219cc1fbd9992121e6ff8d849 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 1 May 2024 20:57:29 +0100 Subject: [PATCH 1/5] gh-118486: Support mkdir(mode=0o700) on Windows --- Doc/library/os.rst | 7 + Doc/whatsnew/3.8.rst | 16 ++ Lib/test/test_os.py | 19 ++ Lib/test/test_tempfile.py | 28 +++ ...-05-01-20-57-09.gh-issue-118486.K44KJG.rst | 4 + Modules/posixmodule.c | 175 +++++++++++++++++- 6 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2024-05-01-20-57-09.gh-issue-118486.K44KJG.rst diff --git a/Doc/library/os.rst b/Doc/library/os.rst index d0a37a8bbdbf05..736a59a9216b6f 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -1909,6 +1909,10 @@ features: platform-dependent. On some platforms, they are ignored and you should call :func:`chmod` explicitly to set them. + On Windows, a *mode* of ``0o700`` is specifically handled to apply access + control to the new directory such that only the current user and + administrators have access. Other values of *mode* are ignored. + This function can also support :ref:`paths relative to directory descriptors `. @@ -1923,6 +1927,9 @@ features: .. versionchanged:: 3.6 Accepts a :term:`path-like object`. + .. versionchanged:: 3.8.20 + Windows now handles a *mode* of ``0o700``. + .. function:: makedirs(name, mode=0o777, exist_ok=False) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index e5278da3f6a5be..e0eb5b4dc3f777 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -1046,6 +1046,13 @@ treat junctions as links. (Contributed by Steve Dower in :issue:`37834`.) +As of 3.8.20, :func:`os.mkdir` and :func:`os.makedirs` on Windows now support +passing a *mode* value of ``0o700`` to apply access control to the new +directory. This implicitly affects :func:`tempfile.mkdtemp` and is a +mitigation for :cve:`2024-4030`. Other values for *mode* continue to be +ignored. +(Contributed by Steve Dower in :gh:`118486`.) + os.path ------- @@ -1252,6 +1259,15 @@ in a standardized and extensible format, and offers several other benefits. (Contributed by C.A.M. Gerlach in :issue:`36268`.) +tempfile +-------- + +As of 3.8.20 on Windows, the default mode ``0o700`` used by +:func:`tempfile.mkdtemp` now limits access to the new directory due to +changes to :func:`os.mkdir`. This is a mitigation for :cve:`2024-4030`. +(Contributed by Steve Dower in :gh:`118486`.) + + threading --------- diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 5302b1ce575d4e..0274f791ba79bf 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1380,6 +1380,25 @@ def test_exist_ok_existing_regular_file(self): self.assertRaises(OSError, os.makedirs, path, exist_ok=True) os.remove(path) + @unittest.skipUnless(os.name == 'nt', "requires Windows") + def test_win32_mkdir_700(self): + base = os_helper.TESTFN + path1 = os.path.join(os_helper.TESTFN, 'dir1') + path2 = os.path.join(os_helper.TESTFN, 'dir2') + # mode=0o700 is special-cased to override ACLs on Windows + # There's no way to know exactly how the ACLs will look, so we'll + # check that they are different from a regularly created directory. + os.mkdir(path1, mode=0o700) + os.mkdir(path2, mode=0o777) + + out1 = subprocess.check_output(["icacls.exe", path1], encoding="oem") + out2 = subprocess.check_output(["icacls.exe", path2], encoding="oem") + os.rmdir(path1) + os.rmdir(path2) + out1 = out1.replace(path1, "") + out2 = out2.replace(path2, "") + self.assertNotEqual(out1, out2) + def tearDown(self): path = os.path.join(support.TESTFN, 'dir1', 'dir2', 'dir3', 'dir4', 'dir5', 'dir6') diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index 8cb36f38a2a35c..9fe9d1d651433e 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -11,6 +11,7 @@ import contextlib import stat import weakref +import subprocess from unittest import mock import unittest @@ -760,6 +761,33 @@ def test_mode(self): finally: os.rmdir(dir) + @unittest.skipUnless(os.name == "nt", "Only on Windows.") + def test_mode_win32(self): + # Use icacls.exe to extract the users with some level of access + # Main thing we are testing is that the BUILTIN\Users group has + # no access. The exact ACL is going to vary based on which user + # is running the test. + dir = self.do_create() + try: + out = subprocess.check_output(["icacls.exe", dir], encoding="oem").casefold() + finally: + os.rmdir(dir) + + dir = dir.casefold() + users = set() + found_user = False + for line in out.strip().splitlines(): + acl = None + # First line of result includes our directory + if line.startswith(dir): + acl = line.removeprefix(dir).strip() + elif line and line[:1].isspace(): + acl = line.strip() + if acl: + users.add(acl.partition(":")[0]) + + self.assertNotIn(r"BUILTIN\Users".casefold(), users) + def test_collision_with_existing_file(self): # mkdtemp tries another name when a file with # the chosen name already exists diff --git a/Misc/NEWS.d/next/Security/2024-05-01-20-57-09.gh-issue-118486.K44KJG.rst b/Misc/NEWS.d/next/Security/2024-05-01-20-57-09.gh-issue-118486.K44KJG.rst new file mode 100644 index 00000000000000..8ac48aac816a60 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2024-05-01-20-57-09.gh-issue-118486.K44KJG.rst @@ -0,0 +1,4 @@ +:func:`os.mkdir` on Windows now accepts *mode* of ``0o700`` to restrict +the new directory to the current user. This fixes :cve:`2024-4030` +affecting :func:`tempfile.mkdtemp` in scenarios where the base temporary +directory is more permissive than the default. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index d7edabe5da08d1..4a08b3635dcc3f 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -40,6 +40,12 @@ #include "pycore_pystate.h" /* _PyRuntime */ #include "pythread.h" #include "structmember.h" + +#ifdef MS_WINDOWS +# include // SetEntriesInAcl +# include // SDDL_REVISION_1 +#endif + #ifndef MS_WINDOWS # include "posixmodule.h" #else @@ -4123,6 +4129,146 @@ os__path_splitroot_impl(PyObject *module, path_t *path) #endif /* MS_WINDOWS */ +#ifdef MS_WINDOWS + +/* We centralise SECURITY_ATTRIBUTE initialization based around +templates that will probably mostly match common POSIX mode settings. +The _Py_SECURITY_ATTRIBUTE_DATA structure contains temporary data, as +a constructed SECURITY_ATTRIBUTE structure typically refers to memory +that has to be alive while it's being used. + +Typical use will look like: + SECURITY_ATTRIBUTES *pSecAttr = NULL; + struct _Py_SECURITY_ATTRIBUTE_DATA secAttrData; + int error, error2; + + Py_BEGIN_ALLOW_THREADS + switch (mode) { + case 0x1C0: // 0o700 + error = initializeMkdir700SecurityAttributes(&pSecAttr, &secAttrData); + break; + ... + default: + error = initializeDefaultSecurityAttributes(&pSecAttr, &secAttrData); + break; + } + + if (!error) { + // do operation, passing pSecAttr + } + + // Unconditionally clear secAttrData. + error2 = clearSecurityAttributes(&pSecAttr, &secAttrData); + if (!error) { + error = error2; + } + Py_END_ALLOW_THREADS + + if (error) { + PyErr_SetFromWindowsErr(error); + return NULL; + } +*/ + +struct _Py_SECURITY_ATTRIBUTE_DATA { + SECURITY_ATTRIBUTES securityAttributes; + PACL acl; + SECURITY_DESCRIPTOR sd; + EXPLICIT_ACCESS_W ea[4]; + char sid[64]; +}; + +static int +initializeDefaultSecurityAttributes( + PSECURITY_ATTRIBUTES *securityAttributes, + struct _Py_SECURITY_ATTRIBUTE_DATA *data +) { + assert(securityAttributes); + assert(data); + *securityAttributes = NULL; + memset(data, 0, sizeof(*data)); + return 0; +} + +static int +initializeMkdir700SecurityAttributes( + PSECURITY_ATTRIBUTES *securityAttributes, + struct _Py_SECURITY_ATTRIBUTE_DATA *data +) { + assert(securityAttributes); + assert(data); + *securityAttributes = NULL; + memset(data, 0, sizeof(*data)); + + if (!InitializeSecurityDescriptor(&data->sd, SECURITY_DESCRIPTOR_REVISION) + || !SetSecurityDescriptorGroup(&data->sd, NULL, TRUE)) { + return GetLastError(); + } + + int use_alias = 0; + DWORD cbSid = sizeof(data->sid); + if (!CreateWellKnownSid(WinCreatorOwnerRightsSid, NULL, (PSID)data->sid, &cbSid)) { + use_alias = 1; + } + + data->securityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); + data->ea[0].grfAccessPermissions = GENERIC_ALL; + data->ea[0].grfAccessMode = SET_ACCESS; + data->ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; + if (use_alias) { + data->ea[0].Trustee.TrusteeForm = TRUSTEE_IS_NAME; + data->ea[0].Trustee.TrusteeType = TRUSTEE_IS_ALIAS; + data->ea[0].Trustee.ptstrName = L"CURRENT_USER"; + } else { + data->ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; + data->ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; + data->ea[0].Trustee.ptstrName = (LPWCH)(SID*)data->sid; + } + + data->ea[1].grfAccessPermissions = GENERIC_ALL; + data->ea[1].grfAccessMode = SET_ACCESS; + data->ea[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; + data->ea[1].Trustee.TrusteeForm = TRUSTEE_IS_NAME; + data->ea[1].Trustee.TrusteeType = TRUSTEE_IS_ALIAS; + data->ea[1].Trustee.ptstrName = L"SYSTEM"; + + data->ea[2].grfAccessPermissions = GENERIC_ALL; + data->ea[2].grfAccessMode = SET_ACCESS; + data->ea[2].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; + data->ea[2].Trustee.TrusteeForm = TRUSTEE_IS_NAME; + data->ea[2].Trustee.TrusteeType = TRUSTEE_IS_ALIAS; + data->ea[2].Trustee.ptstrName = L"ADMINISTRATORS"; + + int r = SetEntriesInAclW(3, data->ea, NULL, &data->acl); + if (r) { + return r; + } + if (!SetSecurityDescriptorDacl(&data->sd, TRUE, data->acl, FALSE)) { + return GetLastError(); + } + data->securityAttributes.lpSecurityDescriptor = &data->sd; + *securityAttributes = &data->securityAttributes; + return 0; +} + +static int +clearSecurityAttributes( + PSECURITY_ATTRIBUTES *securityAttributes, + struct _Py_SECURITY_ATTRIBUTE_DATA *data +) { + assert(securityAttributes); + assert(data); + *securityAttributes = NULL; + if (data->acl) { + if (LocalFree((void *)data->acl)) { + return GetLastError(); + } + } + return 0; +} + +#endif + /*[clinic input] os.mkdir @@ -4151,6 +4297,12 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd) /*[clinic end generated code: output=a70446903abe821f input=e965f68377e9b1ce]*/ { int result; +#ifdef MS_WINDOWS + int error = 0; + int pathError = 0; + SECURITY_ATTRIBUTES *pSecAttr = NULL; + struct _Py_SECURITY_ATTRIBUTE_DATA secAttrData; +#endif if (PySys_Audit("os.mkdir", "Oii", path->object, mode, dir_fd == DEFAULT_DIR_FD ? -1 : dir_fd) < 0) { @@ -4159,11 +4311,30 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd) #ifdef MS_WINDOWS Py_BEGIN_ALLOW_THREADS - result = CreateDirectoryW(path->wide, NULL); + switch (mode) { + case 0x1C0: // 0o700 + error = initializeMkdir700SecurityAttributes(&pSecAttr, &secAttrData); + break; + default: + error = initializeDefaultSecurityAttributes(&pSecAttr, &secAttrData); + break; + } + if (!error) { + result = CreateDirectoryW(path->wide, pSecAttr); + error = clearSecurityAttributes(&pSecAttr, &secAttrData); + } else { + // Ignore error from "clear" - we have a more interesting one already + clearSecurityAttributes(&pSecAttr, &secAttrData); + } Py_END_ALLOW_THREADS - if (!result) + if (error) { + PyErr_SetFromWindowsErr(error); + return NULL; + } + if (!result) { return path_error(path); + } #else Py_BEGIN_ALLOW_THREADS #if HAVE_MKDIRAT From 3f5e5f5165beb1cbc368b7693e6e02cf204862e6 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 7 May 2024 22:14:43 +0100 Subject: [PATCH 2/5] Fix CVE in docs --- Doc/whatsnew/3.8.rst | 4 ++-- .../Security/2024-05-01-20-57-09.gh-issue-118486.K44KJG.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index e0eb5b4dc3f777..c1e57456345525 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -1049,7 +1049,7 @@ treat junctions as links. As of 3.8.20, :func:`os.mkdir` and :func:`os.makedirs` on Windows now support passing a *mode* value of ``0o700`` to apply access control to the new directory. This implicitly affects :func:`tempfile.mkdtemp` and is a -mitigation for :cve:`2024-4030`. Other values for *mode* continue to be +mitigation for CVE-2024-4030. Other values for *mode* continue to be ignored. (Contributed by Steve Dower in :gh:`118486`.) @@ -1264,7 +1264,7 @@ tempfile As of 3.8.20 on Windows, the default mode ``0o700`` used by :func:`tempfile.mkdtemp` now limits access to the new directory due to -changes to :func:`os.mkdir`. This is a mitigation for :cve:`2024-4030`. +changes to :func:`os.mkdir`. This is a mitigation for CVE-2024-4030. (Contributed by Steve Dower in :gh:`118486`.) diff --git a/Misc/NEWS.d/next/Security/2024-05-01-20-57-09.gh-issue-118486.K44KJG.rst b/Misc/NEWS.d/next/Security/2024-05-01-20-57-09.gh-issue-118486.K44KJG.rst index 8ac48aac816a60..a28a4e5cdb6991 100644 --- a/Misc/NEWS.d/next/Security/2024-05-01-20-57-09.gh-issue-118486.K44KJG.rst +++ b/Misc/NEWS.d/next/Security/2024-05-01-20-57-09.gh-issue-118486.K44KJG.rst @@ -1,4 +1,4 @@ :func:`os.mkdir` on Windows now accepts *mode* of ``0o700`` to restrict -the new directory to the current user. This fixes :cve:`2024-4030` +the new directory to the current user. This fixes CVE-2024-4030 affecting :func:`tempfile.mkdtemp` in scenarios where the base temporary directory is more permissive than the default. From f7dd957511389a8845782f079997aa45bf77c09d Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 7 May 2024 22:34:04 +0100 Subject: [PATCH 3/5] Apply suggestions from code review --- Lib/test/test_os.py | 6 +++--- Lib/test/test_tempfile.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 0274f791ba79bf..e0fe178cda14be 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1382,9 +1382,9 @@ def test_exist_ok_existing_regular_file(self): @unittest.skipUnless(os.name == 'nt', "requires Windows") def test_win32_mkdir_700(self): - base = os_helper.TESTFN - path1 = os.path.join(os_helper.TESTFN, 'dir1') - path2 = os.path.join(os_helper.TESTFN, 'dir2') + base = support.TESTFN + path1 = os.path.join(support.TESTFN, 'dir1') + path2 = os.path.join(support.TESTFN, 'dir2') # mode=0o700 is special-cased to override ACLs on Windows # There's no way to know exactly how the ACLs will look, so we'll # check that they are different from a regularly created directory. diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index 9fe9d1d651433e..c1fdcea04bb935 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -780,7 +780,7 @@ def test_mode_win32(self): acl = None # First line of result includes our directory if line.startswith(dir): - acl = line.removeprefix(dir).strip() + acl = line[len(dir):].strip() elif line and line[:1].isspace(): acl = line.strip() if acl: From 0069b388ca525a98dfdcc814583581e1a1bedeec Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 9 May 2024 17:46:30 +0100 Subject: [PATCH 4/5] Use language-invariant ACL --- Modules/posixmodule.c | 177 ++++++------------------------------------ 1 file changed, 22 insertions(+), 155 deletions(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 4a08b3635dcc3f..712bef16fde25f 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4128,147 +4128,6 @@ os__path_splitroot_impl(PyObject *module, path_t *path) #endif /* MS_WINDOWS */ - -#ifdef MS_WINDOWS - -/* We centralise SECURITY_ATTRIBUTE initialization based around -templates that will probably mostly match common POSIX mode settings. -The _Py_SECURITY_ATTRIBUTE_DATA structure contains temporary data, as -a constructed SECURITY_ATTRIBUTE structure typically refers to memory -that has to be alive while it's being used. - -Typical use will look like: - SECURITY_ATTRIBUTES *pSecAttr = NULL; - struct _Py_SECURITY_ATTRIBUTE_DATA secAttrData; - int error, error2; - - Py_BEGIN_ALLOW_THREADS - switch (mode) { - case 0x1C0: // 0o700 - error = initializeMkdir700SecurityAttributes(&pSecAttr, &secAttrData); - break; - ... - default: - error = initializeDefaultSecurityAttributes(&pSecAttr, &secAttrData); - break; - } - - if (!error) { - // do operation, passing pSecAttr - } - - // Unconditionally clear secAttrData. - error2 = clearSecurityAttributes(&pSecAttr, &secAttrData); - if (!error) { - error = error2; - } - Py_END_ALLOW_THREADS - - if (error) { - PyErr_SetFromWindowsErr(error); - return NULL; - } -*/ - -struct _Py_SECURITY_ATTRIBUTE_DATA { - SECURITY_ATTRIBUTES securityAttributes; - PACL acl; - SECURITY_DESCRIPTOR sd; - EXPLICIT_ACCESS_W ea[4]; - char sid[64]; -}; - -static int -initializeDefaultSecurityAttributes( - PSECURITY_ATTRIBUTES *securityAttributes, - struct _Py_SECURITY_ATTRIBUTE_DATA *data -) { - assert(securityAttributes); - assert(data); - *securityAttributes = NULL; - memset(data, 0, sizeof(*data)); - return 0; -} - -static int -initializeMkdir700SecurityAttributes( - PSECURITY_ATTRIBUTES *securityAttributes, - struct _Py_SECURITY_ATTRIBUTE_DATA *data -) { - assert(securityAttributes); - assert(data); - *securityAttributes = NULL; - memset(data, 0, sizeof(*data)); - - if (!InitializeSecurityDescriptor(&data->sd, SECURITY_DESCRIPTOR_REVISION) - || !SetSecurityDescriptorGroup(&data->sd, NULL, TRUE)) { - return GetLastError(); - } - - int use_alias = 0; - DWORD cbSid = sizeof(data->sid); - if (!CreateWellKnownSid(WinCreatorOwnerRightsSid, NULL, (PSID)data->sid, &cbSid)) { - use_alias = 1; - } - - data->securityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); - data->ea[0].grfAccessPermissions = GENERIC_ALL; - data->ea[0].grfAccessMode = SET_ACCESS; - data->ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; - if (use_alias) { - data->ea[0].Trustee.TrusteeForm = TRUSTEE_IS_NAME; - data->ea[0].Trustee.TrusteeType = TRUSTEE_IS_ALIAS; - data->ea[0].Trustee.ptstrName = L"CURRENT_USER"; - } else { - data->ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; - data->ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; - data->ea[0].Trustee.ptstrName = (LPWCH)(SID*)data->sid; - } - - data->ea[1].grfAccessPermissions = GENERIC_ALL; - data->ea[1].grfAccessMode = SET_ACCESS; - data->ea[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; - data->ea[1].Trustee.TrusteeForm = TRUSTEE_IS_NAME; - data->ea[1].Trustee.TrusteeType = TRUSTEE_IS_ALIAS; - data->ea[1].Trustee.ptstrName = L"SYSTEM"; - - data->ea[2].grfAccessPermissions = GENERIC_ALL; - data->ea[2].grfAccessMode = SET_ACCESS; - data->ea[2].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; - data->ea[2].Trustee.TrusteeForm = TRUSTEE_IS_NAME; - data->ea[2].Trustee.TrusteeType = TRUSTEE_IS_ALIAS; - data->ea[2].Trustee.ptstrName = L"ADMINISTRATORS"; - - int r = SetEntriesInAclW(3, data->ea, NULL, &data->acl); - if (r) { - return r; - } - if (!SetSecurityDescriptorDacl(&data->sd, TRUE, data->acl, FALSE)) { - return GetLastError(); - } - data->securityAttributes.lpSecurityDescriptor = &data->sd; - *securityAttributes = &data->securityAttributes; - return 0; -} - -static int -clearSecurityAttributes( - PSECURITY_ATTRIBUTES *securityAttributes, - struct _Py_SECURITY_ATTRIBUTE_DATA *data -) { - assert(securityAttributes); - assert(data); - *securityAttributes = NULL; - if (data->acl) { - if (LocalFree((void *)data->acl)) { - return GetLastError(); - } - } - return 0; -} - -#endif - /*[clinic input] os.mkdir @@ -4300,8 +4159,8 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd) #ifdef MS_WINDOWS int error = 0; int pathError = 0; + SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr) }; SECURITY_ATTRIBUTES *pSecAttr = NULL; - struct _Py_SECURITY_ATTRIBUTE_DATA secAttrData; #endif if (PySys_Audit("os.mkdir", "Oii", path->object, mode, @@ -4311,26 +4170,34 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd) #ifdef MS_WINDOWS Py_BEGIN_ALLOW_THREADS - switch (mode) { - case 0x1C0: // 0o700 - error = initializeMkdir700SecurityAttributes(&pSecAttr, &secAttrData); - break; - default: - error = initializeDefaultSecurityAttributes(&pSecAttr, &secAttrData); - break; + if (mode == 0700 /* 0o700 */) { + ULONG sdSize; + pSecAttr = &secAttr; + // Set a discreationary ACL (D) that is protected (P) and includes + // inheritable (OICI) entries that allow (A) full control (FA) to + // SYSTEM (SY), Administrators (BA), and the owner (OW). + if (!ConvertStringSecurityDescriptorToSecurityDescriptorW( + L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)", + SDDL_REVISION_1, + &secAttr.lpSecurityDescriptor, + &sdSize + )) { + error = GetLastError(); + } } if (!error) { result = CreateDirectoryW(path->wide, pSecAttr); - error = clearSecurityAttributes(&pSecAttr, &secAttrData); - } else { - // Ignore error from "clear" - we have a more interesting one already - clearSecurityAttributes(&pSecAttr, &secAttrData); + if (secAttr.lpSecurityDescriptor && + // uncommonly, LocalFree returns non-zero on error, but still uses + // GetLastError() to see what the error code is + LocalFree(secAttr.lpSecurityDescriptor)) { + error = GetLastError(); + } } Py_END_ALLOW_THREADS if (error) { - PyErr_SetFromWindowsErr(error); - return NULL; + return PyErr_SetFromWindowsErr(error); } if (!result) { return path_error(path); From c4b3f7a027ecf8258ecf173812263dde27b9eaaa Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Wed, 15 May 2024 11:59:41 +0100 Subject: [PATCH 5/5] gh-118486: Simplify test_win32_mkdir_700 to check the exact ACL (GH-119056) --- Lib/test/test_os.py | 23 ++++++++--------------- Modules/posixmodule.c | 2 +- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index e0fe178cda14be..84f78c222982a8 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1383,21 +1383,14 @@ def test_exist_ok_existing_regular_file(self): @unittest.skipUnless(os.name == 'nt', "requires Windows") def test_win32_mkdir_700(self): base = support.TESTFN - path1 = os.path.join(support.TESTFN, 'dir1') - path2 = os.path.join(support.TESTFN, 'dir2') - # mode=0o700 is special-cased to override ACLs on Windows - # There's no way to know exactly how the ACLs will look, so we'll - # check that they are different from a regularly created directory. - os.mkdir(path1, mode=0o700) - os.mkdir(path2, mode=0o777) - - out1 = subprocess.check_output(["icacls.exe", path1], encoding="oem") - out2 = subprocess.check_output(["icacls.exe", path2], encoding="oem") - os.rmdir(path1) - os.rmdir(path2) - out1 = out1.replace(path1, "") - out2 = out2.replace(path2, "") - self.assertNotEqual(out1, out2) + path = os.path.abspath(os.path.join(support.TESTFN, 'dir')) + os.mkdir(path, mode=0o700) + out = subprocess.check_output(["cacls.exe", path, "/s"], encoding="oem") + os.rmdir(path) + self.assertEqual( + out.strip(), + f'{path} "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)"', + ) def tearDown(self): path = os.path.join(support.TESTFN, 'dir1', 'dir2', 'dir3', diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 712bef16fde25f..2ec8458cb51bfb 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4173,7 +4173,7 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd) if (mode == 0700 /* 0o700 */) { ULONG sdSize; pSecAttr = &secAttr; - // Set a discreationary ACL (D) that is protected (P) and includes + // Set a discretionary ACL (D) that is protected (P) and includes // inheritable (OICI) entries that allow (A) full control (FA) to // SYSTEM (SY), Administrators (BA), and the owner (OW). if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(