@@ -2209,6 +2209,238 @@ def test_win32_mkdir_700(self):
22092209 '"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)"' ,
22102210 )
22112211
2212+ @unittest .skipUnless (os .name == 'nt' , "requires Windows" )
2213+ def test_win32_mkdir_700_appcontainer (self ):
2214+ # gh-134587: os.mkdir(mode=0o700) must include the AppContainer SID
2215+ # in the protected DACL so that the creating process can still access
2216+ # the directory when running inside a Windows AppContainer.
2217+ import ctypes
2218+ from ctypes import wintypes
2219+
2220+ CONTAINER_NAME = "CPythonTestMkdir700"
2221+ PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES = 0x00020009
2222+ EXTENDED_STARTUPINFO_PRESENT = 0x00080000
2223+ CREATE_NO_WINDOW = 0x08000000
2224+
2225+ class SECURITY_CAPABILITIES (ctypes .Structure ):
2226+ _fields_ = [
2227+ ("AppContainerSid" , ctypes .c_void_p ),
2228+ ("Capabilities" , ctypes .c_void_p ),
2229+ ("CapabilityCount" , wintypes .DWORD ),
2230+ ("Reserved" , wintypes .DWORD ),
2231+ ]
2232+
2233+ class STARTUPINFOW (ctypes .Structure ):
2234+ _fields_ = [
2235+ ("cb" , wintypes .DWORD ),
2236+ ("lpReserved" , wintypes .LPWSTR ),
2237+ ("lpDesktop" , wintypes .LPWSTR ),
2238+ ("lpTitle" , wintypes .LPWSTR ),
2239+ ("dwX" , wintypes .DWORD ),
2240+ ("dwY" , wintypes .DWORD ),
2241+ ("dwXSize" , wintypes .DWORD ),
2242+ ("dwYSize" , wintypes .DWORD ),
2243+ ("dwXCountChars" , wintypes .DWORD ),
2244+ ("dwYCountChars" , wintypes .DWORD ),
2245+ ("dwFillAttribute" , wintypes .DWORD ),
2246+ ("dwFlags" , wintypes .DWORD ),
2247+ ("wShowWindow" , wintypes .WORD ),
2248+ ("cbReserved2" , wintypes .WORD ),
2249+ ("lpReserved2" , ctypes .c_void_p ),
2250+ ("hStdInput" , wintypes .HANDLE ),
2251+ ("hStdOutput" , wintypes .HANDLE ),
2252+ ("hStdError" , wintypes .HANDLE ),
2253+ ]
2254+
2255+ class STARTUPINFOEXW (ctypes .Structure ):
2256+ _fields_ = [
2257+ ("StartupInfo" , STARTUPINFOW ),
2258+ ("lpAttributeList" , ctypes .c_void_p ),
2259+ ]
2260+
2261+ class PROCESS_INFORMATION (ctypes .Structure ):
2262+ _fields_ = [
2263+ ("hProcess" , wintypes .HANDLE ),
2264+ ("hThread" , wintypes .HANDLE ),
2265+ ("dwProcessId" , wintypes .DWORD ),
2266+ ("dwThreadId" , wintypes .DWORD ),
2267+ ]
2268+
2269+ kernel32 = ctypes .WinDLL ('kernel32' , use_last_error = True )
2270+ advapi32 = ctypes .WinDLL ('advapi32' , use_last_error = True )
2271+ try :
2272+ userenv = ctypes .WinDLL ('userenv' , use_last_error = True )
2273+ except OSError :
2274+ self .skipTest ("userenv.dll not available" )
2275+
2276+ userenv .CreateAppContainerProfile .restype = ctypes .c_long
2277+ userenv .DeriveAppContainerSidFromAppContainerName .restype = ctypes .c_long
2278+ userenv .DeleteAppContainerProfile .restype = ctypes .c_long
2279+
2280+ # Create (or reuse existing) AppContainer profile
2281+ psid = ctypes .c_void_p ()
2282+ hr = userenv .CreateAppContainerProfile (
2283+ CONTAINER_NAME , CONTAINER_NAME , CONTAINER_NAME ,
2284+ None , 0 , ctypes .byref (psid ),
2285+ )
2286+ created_profile = (hr >= 0 )
2287+ if not created_profile :
2288+ hr = userenv .DeriveAppContainerSidFromAppContainerName (
2289+ CONTAINER_NAME , ctypes .byref (psid ),
2290+ )
2291+ if hr < 0 :
2292+ self .skipTest (
2293+ f"Cannot create AppContainer: HRESULT { hr :#010x} "
2294+ )
2295+
2296+ try :
2297+ # Convert SID to string for icacls
2298+ sid_ptr = ctypes .c_wchar_p ()
2299+ if not advapi32 .ConvertSidToStringSidW (
2300+ psid , ctypes .byref (sid_ptr ),
2301+ ):
2302+ self .skipTest ("Cannot convert AppContainer SID" )
2303+ sid_str = sid_ptr .value
2304+ kernel32 .LocalFree (sid_ptr )
2305+
2306+ work_dir = tempfile .mkdtemp (prefix = '_test_ac_' )
2307+ python_dir = os .path .dirname (os .path .abspath (sys .executable ))
2308+ stdlib_dir = os .path .dirname (os .__file__ )
2309+
2310+ # Directories that need AppContainer read access.
2311+ grant_rx = {python_dir , stdlib_dir }
2312+ granted = []
2313+
2314+ try :
2315+ # Grant AppContainer read+execute to the work directory
2316+ # (for the test script) and the Python installation.
2317+ subprocess .check_call (
2318+ ['icacls' , work_dir , '/grant' ,
2319+ f'*{ sid_str } :(OI)(CI)RX' , '/T' , '/Q' ],
2320+ stdout = subprocess .DEVNULL ,
2321+ stderr = subprocess .DEVNULL ,
2322+ )
2323+ granted .append (work_dir )
2324+ for d in grant_rx :
2325+ subprocess .check_call (
2326+ ['icacls' , d , '/grant' ,
2327+ f'*{ sid_str } :(OI)(CI)RX' , '/T' , '/Q' ],
2328+ stdout = subprocess .DEVNULL ,
2329+ stderr = subprocess .DEVNULL ,
2330+ )
2331+ granted .append (d )
2332+
2333+ # This script is the actual bug scenario: Using mkdtemp under
2334+ # an AppContainer, and finding it doesn't work
2335+ script = os .path .join (work_dir , '_ac_test.py' )
2336+ with open (script , 'w' ) as f :
2337+ f .write (textwrap .dedent ("""\
2338+ import os
2339+ import shutil
2340+ import tempfile
2341+
2342+ # Test what was reported in gh-134587
2343+ target = tempfile.mkdtemp(prefix='_test_ac_inner_')
2344+ try:
2345+ fpath = os.path.join(target, 'test.txt')
2346+ with open(fpath, 'w') as fp:
2347+ fp.write('ok')
2348+ with open(fpath) as fp:
2349+ assert fp.read() == 'ok', 'content mismatch'
2350+ finally:
2351+ shutil.rmtree(target, ignore_errors=True)
2352+
2353+ # Also test the root cause (mkdir with mode=0o700)
2354+ temp_dir = tempfile.gettempdir()
2355+ unique_name = next(tempfile._get_candidate_names())
2356+ other_target = os.path.join(temp_dir, '_test_ac_mkdir_' + unique_name)
2357+ os.mkdir(other_target, mode=0o700)
2358+ try:
2359+ fpath = os.path.join(other_target, 'test.txt')
2360+ with open(fpath, 'w') as fp:
2361+ fp.write('ok')
2362+ with open(fpath) as fp:
2363+ assert fp.read() == 'ok', 'content mismatch'
2364+ finally:
2365+ shutil.rmtree(other_target, ignore_errors=True)
2366+ """ ))
2367+
2368+ # Set up proc-thread attribute list for AppContainer
2369+ attr_size = ctypes .c_size_t ()
2370+ kernel32 .InitializeProcThreadAttributeList (
2371+ None , 1 , 0 , ctypes .byref (attr_size ),
2372+ )
2373+ attr_buf = (ctypes .c_byte * attr_size .value )()
2374+ if not kernel32 .InitializeProcThreadAttributeList (
2375+ attr_buf , 1 , 0 , ctypes .byref (attr_size ),
2376+ ):
2377+ self .skipTest ("InitializeProcThreadAttributeList failed" )
2378+
2379+ try :
2380+ sc = SECURITY_CAPABILITIES ()
2381+ sc .AppContainerSid = psid
2382+ if not kernel32 .UpdateProcThreadAttribute (
2383+ attr_buf , 0 ,
2384+ PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES ,
2385+ ctypes .byref (sc ), ctypes .sizeof (sc ),
2386+ None , None ,
2387+ ):
2388+ self .skipTest ("UpdateProcThreadAttribute failed" )
2389+
2390+ siex = STARTUPINFOEXW ()
2391+ siex .StartupInfo .cb = ctypes .sizeof (siex )
2392+ siex .lpAttributeList = ctypes .addressof (attr_buf )
2393+
2394+ pi = PROCESS_INFORMATION ()
2395+ cmd = ctypes .create_unicode_buffer (
2396+ f'"{ sys .executable } " -I -S "{ script } "'
2397+ )
2398+
2399+ if not kernel32 .CreateProcessW (
2400+ None , cmd , None , None , False ,
2401+ EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW ,
2402+ None , None ,
2403+ ctypes .byref (siex ), ctypes .byref (pi ),
2404+ ):
2405+ err = ctypes .get_last_error ()
2406+ self .skipTest (
2407+ f"CreateProcessW failed: error { err } "
2408+ )
2409+
2410+ try :
2411+ kernel32 .WaitForSingleObject (
2412+ pi .hProcess , 30_000 ,
2413+ )
2414+ exit_code = wintypes .DWORD ()
2415+ kernel32 .GetExitCodeProcess (
2416+ pi .hProcess , ctypes .byref (exit_code ),
2417+ )
2418+ self .assertEqual (
2419+ exit_code .value , 0 ,
2420+ "os.mkdir(mode=0o700) created a directory "
2421+ "that is inaccessible from within its "
2422+ "AppContainer (gh-134587)"
2423+ )
2424+ finally :
2425+ kernel32 .CloseHandle (pi .hProcess )
2426+ kernel32 .CloseHandle (pi .hThread )
2427+ finally :
2428+ kernel32 .DeleteProcThreadAttributeList (attr_buf )
2429+ finally :
2430+ for d in granted :
2431+ subprocess .call (
2432+ ['icacls' , d , '/remove' , f'*{ sid_str } ' ,
2433+ '/T' , '/Q' ],
2434+ stdout = subprocess .DEVNULL ,
2435+ stderr = subprocess .DEVNULL ,
2436+ )
2437+ shutil .rmtree (work_dir , ignore_errors = True )
2438+ finally :
2439+ if created_profile :
2440+ userenv .DeleteAppContainerProfile (CONTAINER_NAME )
2441+ if psid :
2442+ advapi32 .FreeSid (psid )
2443+
22122444 def tearDown (self ):
22132445 path = os .path .join (os_helper .TESTFN , 'dir1' , 'dir2' , 'dir3' ,
22142446 'dir4' , 'dir5' , 'dir6' )
0 commit comments