win: support running inside an AppContainer (lowbox token)
Problem
When a process runs inside a Windows AppContainer (lowbox token), three libuv facilities break (verified on Windows 11 26200):
- Named pipes can only be created under
\\?\pipe\LOCAL\.uv__unique_pipe_namegenerates names outside that prefix, soCreateNamedPipealways fails withERROR_ACCESS_DENIED, whichuv__pipe_servertreats as a name collision and retries with a new name forever: anyuv_spawnwith piped stdio, anduv_pipe(), busy-loop at 100% CPU. GetFinalPathNameByHandleW(VOLUME_NAME_DOS)fails withERROR_ACCESS_DENIEDon every handle (the DOS name translation opens the mount manager device, which the sandbox denies), souv_fs_realpathfails for all paths.uv_pipe_bind2mapsERROR_ACCESS_DENIEDtoUV_EADDRINUSE, which is misleading in a sandbox where the name is off-limits rather than in use.
Changes
Three commits:
-
win: add uv_os_is_app_container(): new API returning 1 when the process token is an AppContainer token (viaGetTokenInformation(TokenIsAppContainer), cached withuv_once), 0 otherwise and on non-Windows platforms. Declared inuv.h, documented inmisc.rst, exercised intest-platform-output. Internal callers useuv__is_app_container(). -
win,pipe: make internal pipe names usable inside an AppContainer:uv__unique_pipe_namenow generates\\?\pipe\LOCAL\uv\%llu-%lu. The names are internal (they embed a pointer value and the pid, and are never surfaced), and outside an AppContainerLOCAL\is just six more bytes of the name, so this is unconditional and not observable.uv__pipe_serverallows one retry onERROR_ACCESS_DENIED(it doubles as a collision error when the existing pipe was created by another user or with different settings), then gives up instead of looping forever.uv_pipe_bind2reportsERROR_ACCESS_DENIEDasUV_EACCESinstead ofUV_EADDRINUSEwhen inside an AppContainer.
-
win,fs: make uv_fs_realpath work inside an AppContainer: when theVOLUME_NAME_DOSsize probe fails withERROR_ACCESS_DENIEDinside an AppContainer, retry withVOLUME_NAME_NT(same kernel-side normalization, not restricted) and translate the NT device prefix back to a drive letter:\Device\Mup\server\share\...is emitted directly as\\?\UNC\server\share\....- Other devices go through a process-global device-to-letter map seeded once from the NT names of well-known process paths (cwd, executable, Windows directory, temp directory), each only recorded when the DOS and NT paths agree below the volume so a reparse point cannot poison the map.
- On a map miss, every logical drive is probed by opening
\\?\X:<relative path>withdwDesiredAccess=0(only the leaf ACL is checked, so this works in the sandbox) and comparingFileIdInformation(volume serial + 128-bit file id) with the original handle. A confirmed match is cached; if no drive matches but exactly one denied the probe, that letter is used without caching. - The non-AppContainer path is unchanged. Also, the second
GetFinalPathNameByHandleWcall no longer overwrites the real error withERROR_INVALID_HANDLE.
FILE_ID_INFORMATION is added to winapi.h (the FileIdInformation enumerator and pNtQueryInformationFile were already there).
Verification
- Linux: full cmake build plus test binary;
uv_os_is_app_container()returns 0; pipe/spawn/stdio tests pass (pipe_bind_error_addrinuseandpipe_getsocknamefail identically on the base commit in this container, environment-related EACCES on/tmp). - Windows: full cross-build of the library and test suite with llvm-mingw (clang 22, ucrt), no warnings in the changed files with
-Wall -Wextra. - The AppContainer behavior itself (pipe namespace restriction, mount manager denial,
dwDesiredAccess=0leaf-ACL opens, workingVOLUME_NAME_NT/FileIdInformation) follows observations collected on Windows 11 26200 with a lowbox token; I could not run a lowbox process from this Linux environment.
A follow-up PR to oven-sh/bun bumps LIBUV_COMMIT and routes Bun's own GetFinalPathNameByHandle callers through the same fallback.