Skip to content

win: support running inside an AppContainer (lowbox token) - #4

Closed
robobun wants to merge 3 commits into
oven-sh:bunfrom
robobun:farm/3eaaaf80/appcontainer
Closed

robobun wants to merge 3 commits into
oven-sh:bunfrom
robobun:farm/3eaaaf80/appcontainer

Conversation

@robobun

@robobun robobun commented Jun 6, 2026

Copy link
Copy Markdown

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.

robobun added 3 commits June 6, 2026 00:08
Returns 1 when the process runs with an AppContainer (lowbox) token,
0 otherwise and on non-Windows platforms. Several Windows facilities
(the named pipe namespace, the mount manager) are restricted inside an
AppContainer; embedders and upcoming libuv code need a cheap way to
detect that situation.
An AppContainer (lowbox token) process may only create named pipes
under \\?\pipe\LOCAL\. uv__unique_pipe_name generated names outside
that prefix, so every CreateNamedPipe call failed with
ERROR_ACCESS_DENIED, which uv__pipe_server treated as a name collision
and retried with a new name forever, spinning at 100% CPU. Any
uv_spawn with piped stdio or uv_pipe() hit this.

- Prefix the generated names with LOCAL\. The names are internal and
  never surfaced (they embed a pointer value and the pid), and outside
  an AppContainer the prefix is just part of the name, so this is not
  observable.
- Cap ERROR_ACCESS_DENIED retries in uv__pipe_server at one so a
  process that is denied access to the pipe namespace for other
  reasons fails instead of looping forever.
- In uv_pipe_bind2, report ERROR_ACCESS_DENIED as UV_EACCES instead of
  UV_EADDRINUSE when running inside an AppContainer: for a sandboxed
  process the name is not in use, it is off-limits.
GetFinalPathNameByHandleW(VOLUME_NAME_DOS) fails with
ERROR_ACCESS_DENIED for every handle inside an AppContainer (lowbox
token) because translating the volume device to a DOS name opens the
mount manager device, which the sandbox denies. That made
uv_fs_realpath fail for all paths.

VOLUME_NAME_NT performs the same kernel-side normalization but names
the volume as an NT device ("\Device\HarddiskVolume4\..."), and is
not restricted. When the DOS variant fails with ERROR_ACCESS_DENIED
inside an AppContainer, fall back to the NT variant and translate the
device prefix back to a drive letter:

- \Device\Mup\server\share\... is emitted directly as
  \\?\UNC\server\share\...
- Other device prefixes are resolved through a process-global
  device-to-letter map seeded from well-known process paths (cwd,
  executable, Windows directory, temp directory) via their NT names.
- On a map miss, probe every logical drive 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) against the
  original handle. A confirmed match is cached; if no drive matches
  but exactly one refused the probe, use it without caching.

Outside an AppContainer nothing changes.

Also stop overwriting the error from the second
GetFinalPathNameByHandleW call with ERROR_INVALID_HANDLE so the real
failure reason reaches the caller.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants