From ee29182a97cc0d8ccceceb6e770b7537e4ef7a50 Mon Sep 17 00:00:00 2001 From: Cory Crooks Date: Thu, 16 Apr 2026 16:54:19 -0400 Subject: [PATCH 1/4] gh-134587: Fix os.mkdir(mode=0o700) in Windows AppContainer In 8af84b5 (gh-118773), which was a follow up to 81939da (gh-118488), an SDDL was used to grant a directory created via os.mkdir with mode=0o700 owner, admin, and system control. However, when running under a Windows AppContainer, objects must ALSO allow access for the AppContainer SID. This changes the logic to do a one time resolution of the SDDL string to use. If we are running under an AppContainer, then the SDDL includes the SID of the AppContainer, otherwise we use the same standard SDDL as before. --- Lib/test/test_os/test_os.py | 232 ++++++++++++++++++ ...-04-20-13-04-26.gh-issue-134587.3_IZSd.rst | 1 + Modules/posixmodule.c | 119 ++++++++- 3 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 7e670e5a139d99..4080305ab06beb 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -2209,6 +2209,238 @@ def test_win32_mkdir_700(self): '"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)"', ) + @unittest.skipUnless(os.name == 'nt', "requires Windows") + def test_win32_mkdir_700_appcontainer(self): + # gh-134587: os.mkdir(mode=0o700) must include the AppContainer SID + # in the protected DACL so that the creating process can still access + # the directory when running inside a Windows AppContainer. + import ctypes + from ctypes import wintypes + + CONTAINER_NAME = "CPythonTestMkdir700" + PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES = 0x00020009 + EXTENDED_STARTUPINFO_PRESENT = 0x00080000 + CREATE_NO_WINDOW = 0x08000000 + + class SECURITY_CAPABILITIES(ctypes.Structure): + _fields_ = [ + ("AppContainerSid", ctypes.c_void_p), + ("Capabilities", ctypes.c_void_p), + ("CapabilityCount", wintypes.DWORD), + ("Reserved", wintypes.DWORD), + ] + + class STARTUPINFOW(ctypes.Structure): + _fields_ = [ + ("cb", wintypes.DWORD), + ("lpReserved", wintypes.LPWSTR), + ("lpDesktop", wintypes.LPWSTR), + ("lpTitle", wintypes.LPWSTR), + ("dwX", wintypes.DWORD), + ("dwY", wintypes.DWORD), + ("dwXSize", wintypes.DWORD), + ("dwYSize", wintypes.DWORD), + ("dwXCountChars", wintypes.DWORD), + ("dwYCountChars", wintypes.DWORD), + ("dwFillAttribute", wintypes.DWORD), + ("dwFlags", wintypes.DWORD), + ("wShowWindow", wintypes.WORD), + ("cbReserved2", wintypes.WORD), + ("lpReserved2", ctypes.c_void_p), + ("hStdInput", wintypes.HANDLE), + ("hStdOutput", wintypes.HANDLE), + ("hStdError", wintypes.HANDLE), + ] + + class STARTUPINFOEXW(ctypes.Structure): + _fields_ = [ + ("StartupInfo", STARTUPINFOW), + ("lpAttributeList", ctypes.c_void_p), + ] + + class PROCESS_INFORMATION(ctypes.Structure): + _fields_ = [ + ("hProcess", wintypes.HANDLE), + ("hThread", wintypes.HANDLE), + ("dwProcessId", wintypes.DWORD), + ("dwThreadId", wintypes.DWORD), + ] + + kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) + advapi32 = ctypes.WinDLL('advapi32', use_last_error=True) + try: + userenv = ctypes.WinDLL('userenv', use_last_error=True) + except OSError: + self.skipTest("userenv.dll not available") + + userenv.CreateAppContainerProfile.restype = ctypes.c_long + userenv.DeriveAppContainerSidFromAppContainerName.restype = ctypes.c_long + userenv.DeleteAppContainerProfile.restype = ctypes.c_long + + # Create (or reuse existing) AppContainer profile + psid = ctypes.c_void_p() + hr = userenv.CreateAppContainerProfile( + CONTAINER_NAME, CONTAINER_NAME, CONTAINER_NAME, + None, 0, ctypes.byref(psid), + ) + created_profile = (hr >= 0) + if not created_profile: + hr = userenv.DeriveAppContainerSidFromAppContainerName( + CONTAINER_NAME, ctypes.byref(psid), + ) + if hr < 0: + self.skipTest( + f"Cannot create AppContainer: HRESULT {hr:#010x}" + ) + + try: + # Convert SID to string for icacls + sid_ptr = ctypes.c_wchar_p() + if not advapi32.ConvertSidToStringSidW( + psid, ctypes.byref(sid_ptr), + ): + self.skipTest("Cannot convert AppContainer SID") + sid_str = sid_ptr.value + kernel32.LocalFree(sid_ptr) + + work_dir = tempfile.mkdtemp(prefix='_test_ac_') + python_dir = os.path.dirname(os.path.abspath(sys.executable)) + stdlib_dir = os.path.dirname(os.__file__) + + # Directories that need AppContainer read access. + grant_rx = {python_dir, stdlib_dir} + granted = [] + + try: + # Grant AppContainer read+execute to the work directory + # (for the test script) and the Python installation. + subprocess.check_call( + ['icacls', work_dir, '/grant', + f'*{sid_str}:(OI)(CI)RX', '/T', '/Q'], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + granted.append(work_dir) + for d in grant_rx: + subprocess.check_call( + ['icacls', d, '/grant', + f'*{sid_str}:(OI)(CI)RX', '/T', '/Q'], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + granted.append(d) + + # This script is the actual bug scenario: Using mkdtemp under + # an AppContainer, and finding it doesn't work + script = os.path.join(work_dir, '_ac_test.py') + with open(script, 'w') as f: + f.write(textwrap.dedent("""\ + import os + import shutil + import tempfile + + # Test what was reported in gh-134587 + target = tempfile.mkdtemp(prefix='_test_ac_inner_') + try: + fpath = os.path.join(target, 'test.txt') + with open(fpath, 'w') as fp: + fp.write('ok') + with open(fpath) as fp: + assert fp.read() == 'ok', 'content mismatch' + finally: + shutil.rmtree(target, ignore_errors=True) + + # Also test the root cause (mkdir with mode=0o700) + temp_dir = tempfile.gettempdir() + unique_name = next(tempfile._get_candidate_names()) + other_target = os.path.join(temp_dir, '_test_ac_mkdir_' + unique_name) + os.mkdir(other_target, mode=0o700) + try: + fpath = os.path.join(other_target, 'test.txt') + with open(fpath, 'w') as fp: + fp.write('ok') + with open(fpath) as fp: + assert fp.read() == 'ok', 'content mismatch' + finally: + shutil.rmtree(other_target, ignore_errors=True) + """)) + + # Set up proc-thread attribute list for AppContainer + attr_size = ctypes.c_size_t() + kernel32.InitializeProcThreadAttributeList( + None, 1, 0, ctypes.byref(attr_size), + ) + attr_buf = (ctypes.c_byte * attr_size.value)() + if not kernel32.InitializeProcThreadAttributeList( + attr_buf, 1, 0, ctypes.byref(attr_size), + ): + self.skipTest("InitializeProcThreadAttributeList failed") + + try: + sc = SECURITY_CAPABILITIES() + sc.AppContainerSid = psid + if not kernel32.UpdateProcThreadAttribute( + attr_buf, 0, + PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, + ctypes.byref(sc), ctypes.sizeof(sc), + None, None, + ): + self.skipTest("UpdateProcThreadAttribute failed") + + siex = STARTUPINFOEXW() + siex.StartupInfo.cb = ctypes.sizeof(siex) + siex.lpAttributeList = ctypes.addressof(attr_buf) + + pi = PROCESS_INFORMATION() + cmd = ctypes.create_unicode_buffer( + f'"{sys.executable}" -I -S "{script}"' + ) + + if not kernel32.CreateProcessW( + None, cmd, None, None, False, + EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW, + None, None, + ctypes.byref(siex), ctypes.byref(pi), + ): + err = ctypes.get_last_error() + self.skipTest( + f"CreateProcessW failed: error {err}" + ) + + try: + kernel32.WaitForSingleObject( + pi.hProcess, 30_000, + ) + exit_code = wintypes.DWORD() + kernel32.GetExitCodeProcess( + pi.hProcess, ctypes.byref(exit_code), + ) + self.assertEqual( + exit_code.value, 0, + "os.mkdir(mode=0o700) created a directory " + "that is inaccessible from within its " + "AppContainer (gh-134587)" + ) + finally: + kernel32.CloseHandle(pi.hProcess) + kernel32.CloseHandle(pi.hThread) + finally: + kernel32.DeleteProcThreadAttributeList(attr_buf) + finally: + for d in granted: + subprocess.call( + ['icacls', d, '/remove', f'*{sid_str}', + '/T', '/Q'], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + shutil.rmtree(work_dir, ignore_errors=True) + finally: + if created_profile: + userenv.DeleteAppContainerProfile(CONTAINER_NAME) + if psid: + advapi32.FreeSid(psid) + def tearDown(self): path = os.path.join(os_helper.TESTFN, 'dir1', 'dir2', 'dir3', 'dir4', 'dir5', 'dir6') diff --git a/Misc/NEWS.d/next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst b/Misc/NEWS.d/next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst new file mode 100644 index 00000000000000..26c3eb34111dda --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst @@ -0,0 +1 @@ +Fix :func:`os.mkdir` for mode `0o700` when running in Windows AppContainer \ No newline at end of file diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index e5ce487723b25b..4320d56cde6979 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -6140,6 +6140,109 @@ os__path_normpath_impl(PyObject *module, path_t *path) return result; } +#if defined(MS_WINDOWS) && \ + (defined(MS_WINDOWS_APP) || defined(MS_WINDOWS_SYSTEM)) + +// If running under an AppContainer, this will append an ACE to the provided +// SDDL string that grants full control to the AppContainer SID. +static LPCWSTR +sddl_append_for_appcontainer_if_necessary(LPCWSTR base_sddl) { + + // Default to using the "base" SDDL, which is what we want if + // we are not running under an AppContainer + LPCWSTR resolved_sddl = base_sddl; + + HANDLE hToken = NULL; + DWORD isAppContainer = 0; + DWORD returnLength = 0; + void *tokenInfo = NULL; + LPWSTR sidStr = NULL; + + // Start by opening the process token, so we can query it + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) { + goto done; + } + + // Determine if the process is running under an AppContainer + BOOL getTokenResult = GetTokenInformation(hToken, TokenIsAppContainer, + &isAppContainer, sizeof(isAppContainer), + &returnLength); + if (!getTokenResult || !isAppContainer) { + goto done; + } + + // Determine the size of the buffer we need for the AppContainer SID + returnLength = 0; + GetTokenInformation(hToken, TokenAppContainerSid, NULL, 0, &returnLength); + if (!returnLength) { + goto done; + } + + tokenInfo = PyMem_RawMalloc(returnLength); + if (!tokenInfo) { + goto done; + } + + // Get the AppContainer SID + getTokenResult = GetTokenInformation(hToken, TokenAppContainerSid, + tokenInfo, returnLength, &returnLength); + if (!getTokenResult) { + goto done; + } + + TOKEN_APPCONTAINER_INFORMATION *acInfo = + (TOKEN_APPCONTAINER_INFORMATION *)tokenInfo; + if (!acInfo->TokenAppContainer) { + goto done; + } + if (!ConvertSidToStringSidW(acInfo->TokenAppContainer, &sidStr)) { + goto done; + } + + // Now that we know we are running under an AppContainer, and we have + // the AppContainer SID as a string, we can append an ACE to the provided + // SDDL + + // Dynamically allocate the final buffer here. This is expected to be + // called at most once, however in the case it could be called from + // multiple threads, we are dynamically allocating the buffer here rather + // than using a static buffer (which would then require synchronization + // for that static buffer). + LPWSTR sddl_buf = PyMem_RawMalloc(sizeof(WCHAR) * 256); + + int sddl_chars = _snwprintf( + sddl_buf, + 256, + // Append a string that includes inheritable (OICI) entries + // that allow (A) full control (FA) to the AppContainer SID + L"%s(A;OICI;FA;;;%s)", + base_sddl, + sidStr); + + if (sddl_chars >= 0 && (size_t)sddl_chars < 256) { + resolved_sddl = sddl_buf; + } + else { + PyMem_RawFree(sddl_buf); + } + +done: + if (sidStr) { + LocalFree(sidStr); + } + if (tokenInfo) { + PyMem_RawFree(tokenInfo); + } + if (hToken) { + CloseHandle(hToken); + } + + return resolved_sddl; +} + + +#endif + /*[clinic input] os.mkdir @@ -6173,6 +6276,10 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd) int error = 0; SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr) }; SECURITY_ATTRIBUTES *pSecAttr = NULL; +#if defined(MS_WINDOWS_APP) || defined(MS_WINDOWS_SYSTEM) + // Hold the final resolved SDDL as a static, since we only need to resolve it once + static LPCWSTR resolved_sddl = NULL; +#endif #endif #ifdef HAVE_MKDIRAT int mkdirat_unavailable = 0; @@ -6191,11 +6298,15 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd) if (mode == 0700 /* 0o700 */) { ULONG sdSize; pSecAttr = &secAttr; - // 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 (!resolved_sddl) { + // 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). + resolved_sddl = sddl_append_for_appcontainer_if_necessary( + L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)"); + } if (!ConvertStringSecurityDescriptorToSecurityDescriptorW( - L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)", + resolved_sddl, SDDL_REVISION_1, &secAttr.lpSecurityDescriptor, &sdSize From 8d256c92f50a72394650b678c9d859c1e16b98e0 Mon Sep 17 00:00:00 2001 From: Cory Crooks Date: Mon, 20 Apr 2026 14:48:57 -0400 Subject: [PATCH 2/4] Add missing newline --- .../next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst b/Misc/NEWS.d/next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst index 26c3eb34111dda..16e5ec151d78fc 100644 --- a/Misc/NEWS.d/next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst +++ b/Misc/NEWS.d/next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst @@ -1 +1 @@ -Fix :func:`os.mkdir` for mode `0o700` when running in Windows AppContainer \ No newline at end of file +Fix :func:`os.mkdir` for mode `0o700` when running in Windows AppContainer From 277ebc1813ca6944e61f20736618e091c66a3082 Mon Sep 17 00:00:00 2001 From: Cory Crooks Date: Mon, 20 Apr 2026 15:14:11 -0400 Subject: [PATCH 3/4] Remove backquotes --- .../next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst b/Misc/NEWS.d/next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst index 16e5ec151d78fc..f74edae483490d 100644 --- a/Misc/NEWS.d/next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst +++ b/Misc/NEWS.d/next/Windows/2026-04-20-13-04-26.gh-issue-134587.3_IZSd.rst @@ -1 +1 @@ -Fix :func:`os.mkdir` for mode `0o700` when running in Windows AppContainer +Fix :func:`os.mkdir` for mode 0o700 when running in Windows AppContainer From e31766a947e64226197f58d4f09bbb3a4efcd5ae Mon Sep 17 00:00:00 2001 From: Cory Crooks Date: Mon, 20 Apr 2026 15:20:54 -0400 Subject: [PATCH 4/4] Fix trailing spaces --- Lib/test/test_os/test_os.py | 2 +- Modules/posixmodule.c | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 4080305ab06beb..35b970ca2fe08b 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -2338,7 +2338,7 @@ class PROCESS_INFORMATION(ctypes.Structure): import os import shutil import tempfile - + # Test what was reported in gh-134587 target = tempfile.mkdtemp(prefix='_test_ac_inner_') try: diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 4320d56cde6979..b8357bc735efee 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -6143,11 +6143,11 @@ os__path_normpath_impl(PyObject *module, path_t *path) #if defined(MS_WINDOWS) && \ (defined(MS_WINDOWS_APP) || defined(MS_WINDOWS_SYSTEM)) -// If running under an AppContainer, this will append an ACE to the provided +// If running under an AppContainer, this will append an ACE to the provided // SDDL string that grants full control to the AppContainer SID. static LPCWSTR sddl_append_for_appcontainer_if_necessary(LPCWSTR base_sddl) { - + // Default to using the "base" SDDL, which is what we want if // we are not running under an AppContainer LPCWSTR resolved_sddl = base_sddl; @@ -6184,7 +6184,7 @@ sddl_append_for_appcontainer_if_necessary(LPCWSTR base_sddl) { } // Get the AppContainer SID - getTokenResult = GetTokenInformation(hToken, TokenAppContainerSid, + getTokenResult = GetTokenInformation(hToken, TokenAppContainerSid, tokenInfo, returnLength, &returnLength); if (!getTokenResult) { goto done; @@ -6200,20 +6200,20 @@ sddl_append_for_appcontainer_if_necessary(LPCWSTR base_sddl) { } // Now that we know we are running under an AppContainer, and we have - // the AppContainer SID as a string, we can append an ACE to the provided + // the AppContainer SID as a string, we can append an ACE to the provided // SDDL - // Dynamically allocate the final buffer here. This is expected to be - // called at most once, however in the case it could be called from - // multiple threads, we are dynamically allocating the buffer here rather - // than using a static buffer (which would then require synchronization + // Dynamically allocate the final buffer here. This is expected to be + // called at most once, however in the case it could be called from + // multiple threads, we are dynamically allocating the buffer here rather + // than using a static buffer (which would then require synchronization // for that static buffer). LPWSTR sddl_buf = PyMem_RawMalloc(sizeof(WCHAR) * 256); int sddl_chars = _snwprintf( sddl_buf, 256, - // Append a string that includes inheritable (OICI) entries + // Append a string that includes inheritable (OICI) entries // that allow (A) full control (FA) to the AppContainer SID L"%s(A;OICI;FA;;;%s)", base_sddl, @@ -6221,7 +6221,7 @@ sddl_append_for_appcontainer_if_necessary(LPCWSTR base_sddl) { if (sddl_chars >= 0 && (size_t)sddl_chars < 256) { resolved_sddl = sddl_buf; - } + } else { PyMem_RawFree(sddl_buf); }