Skip to content

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

Open
dylan-conway wants to merge 3 commits into
bunfrom
dylan/win-appcontainer
Open

win: support running inside an AppContainer (lowbox token)#5
dylan-conway wants to merge 3 commits into
bunfrom
dylan/win-appcontainer

Conversation

@dylan-conway

Copy link
Copy Markdown
Member

Three small commits that let libuv-based programs work inside a Windows AppContainer (lowbox token). Supersedes #4, which carried a uv_fs_realpath device-mapping fallback (~400 lines) that the consumer can implement on its side instead; this PR is just the parts that have to live in libuv.

What breaks today (verified on Windows 11 26200)

  • An AppContainer process may only create named pipes under \\?\pipe\LOCAL\; anywhere else CreateNamedPipe returns ERROR_ACCESS_DENIED. uv__unique_pipe_name generates \\?\pipe\uv\<ptr>-<pid> and uv__pipe_server treats ERROR_ACCESS_DENIED as a name collision and retries with a new name forever, so any uv_spawn with piped stdio (and uv_pipe()) busy-loops at 100% CPU.
  • GetFinalPathNameByHandleW(VOLUME_NAME_DOS) is denied on every handle (the DOS-name translation opens the mount-manager device, which the sandbox blocks). When the second call in fs__realpath_handle fails, the real error is overwritten with ERROR_INVALID_HANDLE, so the caller reports EBADF regardless of the cause.

Changes

  • win: add uv_os_is_app_container() — returns 1 when the process token is an AppContainer token (GetTokenInformation(TokenIsAppContainer), cached with uv_once); 0 otherwise and on non-Windows. Declared in uv.h, documented in misc.rst, exercised in test-platform-output. Internal callers use uv__is_app_container().
  • win,pipe: make internal pipe names usable inside an AppContaineruv__unique_pipe_name now generates \\?\pipe\LOCAL\uv\<ptr>-<pid>. The names are internal (never surfaced; embed a pointer and the pid) and outside an AppContainer the prefix is just six literal bytes in the name, so this is not observable. uv__pipe_server now allows one ERROR_ACCESS_DENIED retry then fails, so a process denied the pipe namespace for any reason errors instead of looping.
  • win,fs: preserve GetLastError in fs__realpath_handle — keep the real error across the buffer free instead of forcing ERROR_INVALID_HANDLE.

Not in this PR

uv_fs_realpath still returns EPERM inside an AppContainer (correctly now, rather than EBADF). A working fallback requires building a device→drive-letter map, which is large and has no single-call solution; consumers that need it can implement it on top of VOLUME_NAME_NT and FileIdInformation. The uv_pipe_bind2 ERROR_ACCESS_DENIEDUV_EADDRINUSE mapping is also left as-is: gating it on uv__is_app_container() would make a genuine name collision on a LOCAL\ name inside an AppContainer report EACCES instead of EADDRINUSE.

robobun and others 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.
When the second GetFinalPathNameByHandleW call failed, the error was
overwritten with ERROR_INVALID_HANDLE before being read by the caller,
so the caller reported EBADF regardless of the real failure. Preserve
the error across the buffer free instead.
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