Skip to content

cherry-pick: upstream AppContainer support (libuv#5181) - #7

Merged
dylan-conway merged 1 commit into
oven-sh:bunfrom
robobun:farm/a3e228d2/cherry-pick-upstream-appcontainer
Jul 16, 2026
Merged

dylan-conway merged 1 commit into
oven-sh:bunfrom
robobun:farm/a3e228d2/cherry-pick-upstream-appcontainer

Conversation

@robobun

@robobun robobun commented Jul 16, 2026

Copy link
Copy Markdown

Cherry-picks upstream libuv#5181 (2cadaa40, "win: fix unique named pipes to work inside Windows AppContainer" by Russ Cox) onto the bun branch.

Upstream change: conditionally inserts LOCAL\ into uv__unique_pipe_name when the process token is an AppContainer token, adds the test/appcontainer.c launcher and a CI step that runs the full test suite inside a container, and adds RETURN_SKIP_IN_APPCONTAINER skips for tests that cannot work there (symlinks, junctions, ACL edits).

The cherry-pick had two small textual conflicts because bun is 138 commits behind 2cadaa40's parent:

  • test/task.h: upstream moved the UNUSED macro earlier in the file and dropped the Windows notify_parent_process stub. Resolved by dropping the now-duplicate UNUSED block and keeping the Windows stub (the real Windows implementation in runner-win.c is not part of this commit).
  • test/test-fs.c: upstream's hunk uses TEST_FS_IMPL, a macro from an intermediate commit not present on this base. Resolved with TEST_IMPL + the RETURN_SKIP_IN_APPCONTAINER line.

Verified on Windows Server 2019 x64: all targets build (including the new uv_run_appcontainer.exe) and ctest -C RelWithDebInfo passes 2/2 (uv_test + uv_test_a, 100%). Running uv_run_appcontainer.exe on that box exits 0xC0000142 because 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_bind EACCES/EADDRINUSE disambiguation, fs error fidelity, and uv_os_is_app_container() public API are in #5 and not here.

…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
@dylan-conway
dylan-conway merged commit 8eed192 into oven-sh:bun 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 -->
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.

3 participants