windows: run Bun inside an AppContainer (lowbox token)

Merged
DY
4mo ago
+901-73

Makes Bun usable inside a Windows AppContainer (lowbox token), the sandbox used by packaged apps and embedders that launch worker processes with CreateAppContainerProfile + PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES. Depends on the libuv-side fixes in oven-sh/libuv#7 (cherry-pick of upstream libuv/libuv#5181's AppContainer pipe-namespace fix) and oven-sh/libuv#8 (fs stat/realpath bounds and tty exact-fill correctness fixes), both merged into the bun branch and pinned here.

The four unconditional changes below apply everywhere; the two AppContainer-only changes are gated on bun_sys::windows::is_app_container() (a cached GetTokenInformation(TokenIsAppContainer) probe) and are no-ops outside a container.

Changes

Resolver ancestor-directory tolerance (cross-platform, unconditional). bun run <script> failed with error loading current directory when any ancestor directory on the path to cwd was unreadable: the resolver builds a DirInfo for every ancestor starting at the drive root, and a sandboxed token (or an execute-only 0o111 unix directory, Android /data, etc.) denies that listing. A permission-denied ancestor is now treated as an opaque empty directory, the same treatment the existing ENOTDIR tolerance applies; errors on the requested directory itself stay fatal. Also fixes #28220 and #30859.

Windows O_RDONLY open no longer requests FILE_WRITE_ATTRIBUTES (Windows, unconditional). The openat base access mask unconditionally included FILE_WRITE_ATTRIBUTES, so opening a file O_RDONLY on a tree with an RX-only ACL grant (Program Files, read-only shares, the normal sandbox project-tree shape) failed EPERM. The mask now matches libuv's fs__open (O_RDONLY -> GENERIC_READ only; write modes already include it via GENERIC_WRITE); fs.futimes continues to work via libuv's ReOpenFile(FILE_WRITE_ATTRIBUTES) at futimes time. Unskips test-module-readonly.js.

Windows directory opens no longer request FILE_ADD_FILE | FILE_ADD_SUBDIRECTORY (Windows, unconditional). NtCreateFile with a RootDirectory handle checks the target directory's ACL for child creates and renames, not the handle's access mask, so these bits grant nothing and only narrow where the open is admitted. Dropping them lets Bun.Glob/recursive readdir/fs.opendir descend RX-only directories (Program Files, read-only shares, sandboxed project trees); creating children through the handle still works where the ACL allows it. Removes the now-vestigial WindowsOpenDirOptions.read_only field.

Named-pipe listen failures surface as Node-shaped errors (Windows, unconditional). They were a codeless ERR_INVALID_ARG_TYPE TypeError; now an Error with code/errno/syscall/path set, matching the POSIX unix-socket listen path. Fixes #30265.

AppContainer-only (gated on is_app_container(); no-ops outside):

  • GetFinalPathNameByHandleW(VOLUME_NAME_DOS) is denied on every handle inside an AppContainer because the DOS-name translation opens the mount manager. For handles on the system volume, reconstruct the DOS name as <system-drive>: + the VOLUME_NAME_NT tail (the system directory carries an ALL APPLICATION PACKAGES:(RX) ACE by Windows default, so its device name is resolvable from any lowbox); handles on any other volume surface the original denial. Applies to both the typed bun_sys wrapper and a raw-ABI drop-in. This is what keeps Bun's resolver and bun install working inside a container; user-facing fs.realpath goes through libuv and is left at Node parity (fails EPERM).
  • Bun.Terminal ConPTY internal pipe names: insert LOCAL\ into the \\.\pipe\... name inside a container (the only namespace an AppContainer may create server pipes under), matching libuv's conditional insert.

deps: pins oven-sh/libuv f6e75a7e (= bun branch after #7 and #8). Behavioural delta at this pin: libuv's internal pipe names gain LOCAL\ inside a container (upstream #5181); uv_fs_stat of files the OS holds exclusively at a drive root (the C:\pagefile.sys class) reports the real stats instead of ENOENT; uv_fs_realpath preserves the real error instead of masking as EBADF; console line reads don't tear characters on an exact-fill allocation and report UV_ENOBUFS for allocations too small to convert into.

Known limitations

  • "ignore" stdio opens the NUL device, whose default ACL denies AppContainer tokens; grant the device ACL to the container SIDs from an elevated context per boot, or use "inherit" stdin.
  • fs.realpath (all variants) fails EPERM inside a container, as it does under Node.js; Bun's own module resolution does not go through it.
  • The isolated linker is unsupported in sandboxes (its junctions are quarantined by the kernel); use the default hoisted linker.
  • A package cache primed outside the container is currently re-validated as a miss inside it; prefer letting the sandboxed process populate its own cache.

Tests

test/js/bun/windows/appcontainer.test.ts launches bun inside a real AppContainer in the regular Windows CI lanes (bun:ffi lowbox launcher, no admin needed) and asserts the sandbox-only behaviours (piped-stdio spawn, LOCAL\ pipe namespace, fs.realpath denial, fork + IPC); hosts that cannot run sandboxed children skip visibly. resolver-permission-denied-ancestor.test.ts covers the ancestor tolerance on unix with an execute-only directory. The glob scan.test.ts RX-only case and named-pipe-listen-error.test.ts error-shape assertions cover the unconditional Windows changes.