win: support running inside an AppContainer (lowbox token)

Closed
RO
5mo ago
+490-16

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_name generates names outside that prefix, so CreateNamedPipe always fails with ERROR_ACCESS_DENIED, which uv__pipe_server treats as a name collision and retries with a new name forever: any uv_spawn with piped stdio, and uv_pipe(), busy-loop at 100% CPU.
  • GetFinalPathNameByHandleW(VOLUME_NAME_DOS) fails with ERROR_ACCESS_DENIED on every handle (the DOS name translation opens the mount manager device, which the sandbox denies), so uv_fs_realpath fails for all paths.
  • uv_pipe_bind2 maps ERROR_ACCESS_DENIED to UV_EADDRINUSE, which is misleading in a sandbox where the name is off-limits rather than in use.

Changes

Three commits:

  1. win: add uv_os_is_app_container(): new API returning 1 when the process token is an AppContainer token (via GetTokenInformation(TokenIsAppContainer), cached with uv_once), 0 otherwise and on non-Windows platforms. Declared in uv.h, documented in misc.rst, exercised in test-platform-output. Internal callers use uv__is_app_container().

  2. win,pipe: make internal pipe names usable inside an AppContainer:

    • uv__unique_pipe_name now 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 AppContainer LOCAL\ is just six more bytes of the name, so this is unconditional and not observable.
    • uv__pipe_server allows one retry on ERROR_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_bind2 reports ERROR_ACCESS_DENIED as UV_EACCES instead of UV_EADDRINUSE when inside an AppContainer.
  3. win,fs: make uv_fs_realpath work inside an AppContainer: when the VOLUME_NAME_DOS size probe fails with ERROR_ACCESS_DENIED inside an AppContainer, retry with VOLUME_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> with dwDesiredAccess=0 (only the leaf ACL is checked, so this works in the sandbox) and comparing FileIdInformation (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 GetFinalPathNameByHandleW call no longer overwrites the real error with ERROR_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_addrinuse and pipe_getsockname fail 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=0 leaf-ACL opens, working VOLUME_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.