cherry-pick: upstream AppContainer support (libuv#5181) - #7
Merged
dylan-conway merged 1 commit intoJul 16, 2026
Merged
dylan-conway merged 1 commit into
dylan-conway merged 1 commit into
Conversation
…v#5181) Inside a Windows AppContainer, pipes must use the \\.\pipe\LOCAL\ prefix. The API expands LOCAL to include the ID of the AppContainer, so that different apps have different pipe name spaces. Outside AppContainer, the \LOCAL\ is permitted and left unmodified by the APIs. This commit changes the construction of unique pipe names to include \LOCAL when running under AppContainer. It leaves apps outside AppContainer behaving exactly as before. In addition to that very small change, this commit adds a program to start and run a command inside AppContainer and then updates the CI to run the Windows tests both outside of and inside an AppContainer. On i686 we need a newer version of mingw, so update CI runner to ubuntu-26.04 (currently in preview). Fixes libuv#5178
This was referenced Jul 16, 2026
dylan-conway
added a commit
to oven-sh/bun
that referenced
this pull request
Jul 20, 2026
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. --------- Co-authored-by: robobun <117481402+robobun@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
robobun
added a commit
to oven-sh/bun
that referenced
this pull request
Jul 20, 2026
oven-sh/libuv#9 landed the high-res poll timeouts on the bun branch, so drop the local patch file and bump LIBUV_COMMIT. This also pulls in the intervening Windows fs correctness fixes on that branch (oven-sh/libuv#7 and #8).
dylan-conway
pushed a commit
to oven-sh/bun
that referenced
this pull request
Jul 20, 2026
## What On an idle Windows box the `GetQueuedCompletionStatusEx` timeout rounds to the ~15.6 ms system clock tick, so `setTimeout(cb, 1)` and `Bun.sleep(1)` fire ~15 ms late unless another process happens to have raised the tick rate (the "works when Spotify is open" heisenbug). Node.js has the same behavior. ## How Bumps libuv to [oven-sh/libuv@9687330](oven-sh/libuv@9687330), which landed [oven-sh/libuv#9](oven-sh/libuv#9) (same approach Go's runtime uses, [golang/go#44343](golang/go#44343)): * At `uv__winapi_init`, dynamically load `NtCreateWaitCompletionPacket` / `NtAssociateWaitCompletionPacket` / `NtCancelWaitCompletionPacket` from ntdll. * Per loop, create a `CREATE_WAITABLE_TIMER_HIGH_RESOLUTION` waitable timer + wait-completion-packet and stash them on `uv__loop_internal_fields_s` (Win10 1803+ / Server 2019+; on older Windows the handles stay NULL and `uv__poll` keeps its original GQCS ms wait, so behavior is unchanged). * In `uv__poll`, when `timeout > 0`: arm the waitable timer for the deadline, associate it with the loop's IOCP, and wait in GQCS with `INFINITE`. When the timer fires, the kernel posts a completion with `lpOverlapped == NULL`, which the existing dequeue loop already treats as a pure wakeup. The bump also pulls in the intervening Windows fs correctness fixes on the `bun` branch ([oven-sh/libuv#7](oven-sh/libuv#7), [#8](oven-sh/libuv#8)). ## Numbers (Windows Server 2019, idle) | | before | after | |--------------------|-----------|----------| | `setTimeout(cb,1)` | 15.52 ms | 1.41 ms | | `Bun.sleep(1)` | 15.62 ms | 1.08 ms | | `setTimeout(cb,5)` | 15.62 ms | 5.24 ms | | `setInterval(16)` | ~28 ms | 16.4 ms | `Bun.serve` hello-world throughput on Windows debug (oha, 10s, 50 concurrent): 11,484 req/s on this branch vs 11,434 req/s on main (noise). The added test in `setTimeout.test.js` measures the median of 50 `setTimeout(1)` samples in a subprocess and asserts it's under 8 ms (before: 15.6 ms; after: ~1.5 ms). Fixes #16714 Fixes #26965 <!-- robobun:evidence:begin --> --- **no test proof** · iteration 1 · Platform-specific test-only change; deferring to CI. <!-- robobun:evidence:end -->
liooil
pushed a commit
to liooil/poly
that referenced
this pull request
Aug 7, 2026
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. --------- Co-authored-by: robobun <117481402+robobun@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
liooil
pushed a commit
to liooil/poly
that referenced
this pull request
Aug 7, 2026
## What On an idle Windows box the `GetQueuedCompletionStatusEx` timeout rounds to the ~15.6 ms system clock tick, so `setTimeout(cb, 1)` and `Bun.sleep(1)` fire ~15 ms late unless another process happens to have raised the tick rate (the "works when Spotify is open" heisenbug). Node.js has the same behavior. ## How Bumps libuv to [oven-sh/libuv@9687330](oven-sh/libuv@9687330), which landed [oven-sh/libuv#9](oven-sh/libuv#9) (same approach Go's runtime uses, [golang/go#44343](golang/go#44343)): * At `uv__winapi_init`, dynamically load `NtCreateWaitCompletionPacket` / `NtAssociateWaitCompletionPacket` / `NtCancelWaitCompletionPacket` from ntdll. * Per loop, create a `CREATE_WAITABLE_TIMER_HIGH_RESOLUTION` waitable timer + wait-completion-packet and stash them on `uv__loop_internal_fields_s` (Win10 1803+ / Server 2019+; on older Windows the handles stay NULL and `uv__poll` keeps its original GQCS ms wait, so behavior is unchanged). * In `uv__poll`, when `timeout > 0`: arm the waitable timer for the deadline, associate it with the loop's IOCP, and wait in GQCS with `INFINITE`. When the timer fires, the kernel posts a completion with `lpOverlapped == NULL`, which the existing dequeue loop already treats as a pure wakeup. The bump also pulls in the intervening Windows fs correctness fixes on the `bun` branch ([oven-sh/libuv#7](oven-sh/libuv#7), [#8](oven-sh/libuv#8)). ## Numbers (Windows Server 2019, idle) | | before | after | |--------------------|-----------|----------| | `setTimeout(cb,1)` | 15.52 ms | 1.41 ms | | `Bun.sleep(1)` | 15.62 ms | 1.08 ms | | `setTimeout(cb,5)` | 15.62 ms | 5.24 ms | | `setInterval(16)` | ~28 ms | 16.4 ms | `Bun.serve` hello-world throughput on Windows debug (oha, 10s, 50 concurrent): 11,484 req/s on this branch vs 11,434 req/s on main (noise). The added test in `setTimeout.test.js` measures the median of 50 `setTimeout(1)` samples in a subprocess and asserts it's under 8 ms (before: 15.6 ms; after: ~1.5 ms). Fixes #16714 Fixes #26965 <!-- robobun:evidence:begin --> --- **no test proof** · iteration 1 · Platform-specific test-only change; deferring to CI. <!-- robobun:evidence:end -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cherry-picks upstream libuv#5181 (
2cadaa40, "win: fix unique named pipes to work inside Windows AppContainer" by Russ Cox) onto thebunbranch.Upstream change: conditionally inserts
LOCAL\intouv__unique_pipe_namewhen the process token is an AppContainer token, adds thetest/appcontainer.clauncher and a CI step that runs the full test suite inside a container, and addsRETURN_SKIP_IN_APPCONTAINERskips for tests that cannot work there (symlinks, junctions, ACL edits).The cherry-pick had two small textual conflicts because
bunis 138 commits behind2cadaa40's parent:test/task.h: upstream moved theUNUSEDmacro earlier in the file and dropped the Windowsnotify_parent_processstub. Resolved by dropping the now-duplicateUNUSEDblock and keeping the Windows stub (the real Windows implementation inrunner-win.cis not part of this commit).test/test-fs.c: upstream's hunk usesTEST_FS_IMPL, a macro from an intermediate commit not present on this base. Resolved withTEST_IMPL+ theRETURN_SKIP_IN_APPCONTAINERline.Verified on Windows Server 2019 x64: all targets build (including the new
uv_run_appcontainer.exe) andctest -C RelWithDebInfopasses 2/2 (uv_test + uv_test_a, 100%). Runninguv_run_appcontainer.exeon that box exits0xC0000142because the non-interactive session lacks a window-station grant; upstream's own GitHub Actions CI exercises it on an interactive runner.Relationship to #5: this covers only the pipe-namespace fix and the AppContainer test harness. The tty console read cancellation,
uv_pipe_bindEACCES/EADDRINUSE disambiguation, fs error fidelity, anduv_os_is_app_container()public API are in #5 and not here.