Skip to content

sys(windows): map unknown NTSTATUS through RtlNtStatusToDosError for fs.rm - #32538

Merged
Jarred-Sumner merged 7 commits into
mainfrom
farm/98c3010f/fix-rm-ntstatus-windows
Jun 21, 2026
Merged

Jarred-Sumner merged 7 commits into
mainfrom
farm/98c3010f/fix-rm-ntstatus-windows

Conversation

@robobun

@robobun robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

What

Sentry BUN-2V67 reports a steady stream of Panic: reached unreachable code crashes from async fs.rm(..., { recursive: true }) on Windows (1057 lifetime events, 304 on 1.3.14 alone, 100% Windows). The stack bottoms out in:

zigDeleteTree                src/runtime/node/node_fs.zig:6998
deleteFile                   vendor/zig/lib/std/fs/Dir.zig:1699
deleteFileW                  vendor/zig/lib/std/fs/Dir.zig:1732
unlinkatW                    vendor/zig/lib/std/posix.zig:2608
DeleteFile                   vendor/zig/lib/std/os/windows.zig:1013   <- unreachable

Cause

Recursive fs.rm on Windows deletes each entry via NtCreateFile + NtSetInformationFile(FileDispositionInformation[Ex]). Real-world Windows filesystems (and especially filter drivers, AV hooks, and cloud-sync placeholder providers like OneDrive/Dropbox) return NTSTATUS codes from those calls that were not enumerated in the status mapping: STATUS_CANNOT_DELETE (readonly attribute / memory-mapped section), STATUS_IO_REPARSE_TAG_NOT_HANDLED, various network-redirector statuses, and so on.

The shipped Zig implementation called std.fs.Dir.deleteFile whose NTSTATUS switch ends in an unreachable for any status it does not recognise, crashing the worker-pool thread and the whole process. On main the Rust port routes the same path through DeleteFileBun / translate_ntstatus_to_errno, which no longer panics, but its fallthrough arm returned E::UNKNOWN. The fs.rm error-name table has no entry for that and surfaces it to JS as EFAULT, so a "this file is readonly" or "AV is holding this file" condition is reported as a bad-pointer error.

Separately, STATUS_ACCESS_DENIED was already mapped to E::PERM, but dt_err(EPERM) produces the tag "PermissionDenied" which neither rm/rmdir recursive error table handles, so a plain access-denied during a recursive rm also surfaced as EFAULT on Windows.

Fix

  • translate_ntstatus_to_errno: for any NTSTATUS not in the explicit table, call RtlNtStatusToDosError and run the resulting Win32 error through the existing libuv-derived Win32->errno table (the same mapping Node.js uses). This turns STATUS_CANNOT_DELETE into EPERM, STATUS_DISK_FULL into ENOSPC, STATUS_MEDIA_WRITE_PROTECTED into EROFS, and so on, instead of UNKNOWN. Codes that RtlNtStatusToDosError cannot map still fall back to UNKNOWN, never a panic.
  • Add an explicit STATUS_CANNOT_DELETE -> E::PERM arm so the intent is visible without reading the ntdll mapping.
  • DeleteFileBun: treat STATUS_DELETE_PENDING / STATUS_FILE_DELETED from NtSetInformationFile as success, matching the existing NtCreateFile handling (another handle already marked the file for deletion; the caller's intent is satisfied).
  • map_anyerror_to_errno / map_anyerror_to_errno_rm_tree: add "PermissionDenied" -> EPERM so STATUS_ACCESS_DENIED and STATUS_CANNOT_DELETE reach JS as EPERM rather than falling through to EFAULT.

Verification

There is no portable way to force NtSetInformationFile to return an arbitrary NTSTATUS from userspace, so this follows the precedent of translateUVErrorToE / readdir-windows-ntstatus.test.ts: translateNtStatusToE is exposed through bun:internal-for-testing and test/js/node/fs/rm-windows-ntstatus.test.ts pins the mapping directly on Windows CI:

  • STATUS_CANNOT_DELETE (0xC0000121) -> PERM (was UNKNOWN)
  • STATUS_DISK_FULL, STATUS_NO_SUCH_FILE, STATUS_TOO_MANY_OPENED_FILES, STATUS_NOT_SUPPORTED, STATUS_MEDIA_WRITE_PROTECTED -> their libuv errnos via RtlNtStatusToDosError (all were UNKNOWN)
  • 0xCFFFFFFF (junk) -> UNKNOWN, no crash
  • all existing explicit mappings unchanged

An additional Windows-only integration test denies DELETE access on a file via icacls and asserts that both fs.rmSync and fs.promises.rm with { recursive: true } throw a catchable EPERM/EACCES/EBUSY (never EFAULT) and exit 0, covering the full zig_delete_tree -> unlinkat -> DeleteFileBun -> errno_sys path and both rm error tables.

On the unfixed build the test file fails at import (translateNtStatusToE is not exported). With this change the non-Windows sanity test passes and the Windows assertions exercise the fix.

cargo check -p bun_runtime is clean on host, x86_64-pc-windows-msvc, and aarch64-pc-windows-msvc. Existing fs.test.ts -t rm and test-fs-rm*.js pass unchanged.

…fs.rm

On Windows, fs.rm({recursive: true}) deletes each entry via NtCreateFile +
NtSetInformationFile. Filter drivers, AV hooks and cloud-sync placeholder
providers (OneDrive, Dropbox) return NTSTATUS codes from those calls that
were not in the explicit mapping table. The shipped Zig implementation hit
an unreachable and crashed; the Rust port returned E::UNKNOWN which the
fs.rm error table then surfaced as the misleading EFAULT.

Route the fallthrough through RtlNtStatusToDosError and the existing
libuv-derived Win32->errno table so these statuses produce the same errno
Node.js would (STATUS_CANNOT_DELETE -> ERROR_ACCESS_DENIED -> EPERM,
STATUS_DISK_FULL -> ENOSPC, etc). Also:

- add an explicit STATUS_CANNOT_DELETE -> EPERM entry
- treat STATUS_DELETE_PENDING/STATUS_FILE_DELETED from NtSetInformationFile
  as success, matching the NtCreateFile handling
- add PermissionDenied -> EPERM in the rm/rmdir recursive error tables so
  STATUS_ACCESS_DENIED reaches JS as EPERM instead of EFAULT
- expose translateNtStatusToE via bun:internal-for-testing so the mapping
  can be asserted directly on Windows CI
@robobun

robobun commented Jun 20, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:36 AM PT - Jun 20th, 2026

❌ @robobun, your commit 098a84e has 1 failures in Build #63660 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32538

That installs a local version of the PR into your bun-32538 executable, so you can run:

bun-32538 --bun

@coderabbitai

coderabbitai Bot commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds NTSTATUS::CANNOT_DELETE constant and maps it to E::PERM in translate_ntstatus_to_errno, replacing the prior E::UNKNOWN catch-all with a Win32Error→SystemErrno→E fallback chain. DeleteFileBun now treats DELETE_PENDING and FILE_DELETED as success. node_fs.rs error-name matching gains "PermissionDenied" arms. A new translateNtStatusToE JSC testing API exposes the translation, covered by unit and integration tests.

Changes

Windows fs.rm NTSTATUS error mapping

Layer / File(s) Summary
NTSTATUS constant, errno mapping, and DeleteFileBun success cases
src/windows_sys/externs.rs, src/windows_sys/lib.rs, src/errno/windows_errno.rs, src/sys/windows/mod.rs, src/sys/lib.rs
Adds NTSTATUS::CANNOT_DELETE (0xC000_0121) and mirrors it at crate root. Updates translate_ntstatus_to_errno to map CANNOT_DELETE→E::PERM and replaces the E::UNKNOWN fallthrough with a Win32Error→SystemErrno→E pipeline. Treats DELETE_PENDING and FILE_DELETED as success in DeleteFileBun's NtSetInformationFile result handling. Documentation comments clarify the fallback behavior in Windows error paths.
node_fs PermissionDenied error name mapping
src/runtime/node/node_fs.rs
Adds "PermissionDenied" match arms in map_anyerror_to_errno and map_anyerror_to_errno_rm_tree, mapping the name to E::EPERM alongside the existing "AccessDenied" arms.
translateNtStatusToE JSC testing API wiring
src/sys_jsc/error_jsc.rs, src/runtime/dispatch_js2native.rs, src/js/internal-for-testing.ts
Adds TestingAPIs::translate_nt_status_to_e as a JSC host function that validates a numeric argument, returns undefined on non-Windows, and on Windows converts to NTSTATUS and returns the errno name string. Re-exported in dispatch_js2native.rs and surfaced as translateNtStatusToE in internal-for-testing.ts.
NTSTATUS mapping and fs.rm integration tests
test/js/node/fs/rm-windows-ntstatus.test.ts
Windows-only tests verify explicit NTSTATUS→errno mappings, fallback translations, and unknown NTSTATUS degrading to UNKNOWN. A non-Windows test confirms translateNtStatusToE returns undefined. A Windows integration test denies DELETE via ACLs, runs recursive fs.rm sync and async in a child process, and asserts error codes are in {EPERM, EACCES, EBUSY}, never EFAULT, with exit code 0.

Suggested reviewers

  • RiskyMH
  • dylan-conway
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: mapping unknown NTSTATUS codes through RtlNtStatusToDosError for fs.rm on Windows, which is the core fix addressing the crashes.
Description check ✅ Passed The description comprehensively addresses both required template sections: 'What' explains the root cause and fix in detail, and 'How' covers verification through unit and integration tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread test/js/node/fs/rm-windows-ntstatus.test.ts Outdated
Comment thread test/js/node/fs/rm-windows-ntstatus.test.ts Outdated
Comment thread src/errno/windows_errno.rs Outdated
robobun added 2 commits June 20, 2026 16:49
With bun -e, process.argv is [bunExe, arg1, ...] so argv[2] was undefined
and fs.rmSync threw ERR_INVALID_ARG_TYPE instead of exercising the
DeleteFileBun path.
Comment thread test/js/node/fs/rm-windows-ntstatus.test.ts Outdated
Comment thread test/js/node/fs/rm-windows-ntstatus.test.ts
Comment thread src/sys/windows/mod.rs Outdated
…heck

Windows grants DELETE on a file if the parent directory grants
FILE_DELETE_CHILD, so denying DELETE on the file alone does not make
NtCreateFile(DELETE) fail. Deny DC on the parent as well, matching the
approach in test-fs-mkdir-recursive-eaccess.js.

Also move the DELETE_PENDING/FILE_DELETED -> success short-circuit to
after both NtSetInformationFile calls so it covers the legacy
FileDispositionInformation fallback as well as the Ex path.
Comment thread src/errno/windows_errno.rs Outdated
Comment thread src/runtime/node/node_fs.rs
…lthrough

RtlNtStatusToDosError maps STATUS_NOT_IMPLEMENTED,
STATUS_INVALID_DEVICE_REQUEST and STATUS_ILLEGAL_FUNCTION all to
ERROR_INVALID_FUNCTION, which the libuv Win32 table maps to EISDIR
(for the DeleteFileW-on-a-directory case). At the NTSTATUS layer the
is-a-directory case has its own status (STATUS_FILE_IS_A_DIRECTORY,
handled explicitly); anything else that funnels into
ERROR_INVALID_FUNCTION means the driver did not implement the
request. Returning ISDIR from the fallthrough would make recursive
fs.rm flip treat_as_dir forever when a filter driver returns one of
these, so override it to NOTSUP.
@robobun

robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

CI status at 098a84e (build 63660): 281 jobs passed, 1 failed. The new test/js/node/fs/rm-windows-ntstatus.test.ts passes on all Windows lanes (2019 x64, 2019 x64-baseline, 11 aarch64), verifying both the NTSTATUS->errno mapping table and the end-to-end fs.rm -> DeleteFileBun -> EPERM path.

The only error-level failure is test/js/node/test/parallel/test-tls-client-destroy-soon.js on darwin 14 aarch64 (TLS bytesRead mismatch, 2097152 vs 2048000), which is unrelated to this diff: the changes here are Windows-only NTSTATUS handling plus two additive match arms in the cross-platform rm error-name tables, and do not touch TLS, streams, or macOS code paths. The remaining flaky-warning items (hot.test.ts, terminal-platform-gaps.test.ts, update_interactive_install.test.ts, bun-install-registry.test.ts) passed on retry.

All eight review threads have been addressed and resolved. Ready for a maintainer to merge.

@Jarred-Sumner
Jarred-Sumner merged commit b0fb1f7 into main Jun 21, 2026
78 of 79 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/98c3010f/fix-rm-ntstatus-windows branch June 21, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants