Skip to content

windows: recognize reserved DOS device names before NtCreateFile - #34770

Open
robobun wants to merge 21 commits into
mainfrom
farm/620f5a19/windows-dos-device-names
Open

windows: recognize reserved DOS device names before NtCreateFile#34770
robobun wants to merge 21 commits into
mainfrom
farm/620f5a19/windows-dos-device-names

Conversation

@robobun

@robobun robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

On Windows, fs.writeFileSync("nul", data) created a literal file named nul in the current directory instead of writing to the null device. The same happened for Nul, con, aux, prn, com1, lpt1, and relative paths like sub\nul. The resulting files cannot be opened or deleted from Explorer or cmd without a \\?\ prefix.

Cause

writeFile/appendFile open through bun_sys::openat -> normalize_path_windows -> NtCreateFile, which resolves names in the NT object namespace and knows nothing about DOS device names. The only existing special case matched an absolute path whose last four code units were \nul or \NUL, so a bare nul, mixed-case Nul, forward-slash sub/nul, and every other reserved name reached NtCreateFile verbatim.

Fix

normalize_path_windows_opts (src/sys/lib.rs) now classifies the input the way RtlGetFullPathName_U does and, for Relative / Rooted / DriveRelative / DriveAbsolute inputs (plus \\?\X:, since slice_z_with_force_copy hands every drive-absolute node:fs path through with that prefix), checks whether the final path component is a reserved DOS device name (NUL, CON, PRN, AUX, COM1-COM9, LPT1-LPT9; case-insensitive, trailing ./ ignored). If so the whole path resolves to \??\DEVICE. This replaces the old \nul tail match.

UNC (\\server\...), LocalDevice (\\.\..., \\?\UNC\..., \\?\Volume{...}) and NT-object (\??\...) inputs are exempt, because Win32 never applies DOS translation there: a named pipe whose last component happens to be com1 or nul, or a file literally named aux on an SMB share, must keep opening the pipe or the share file. The old \nul tail check did not make that distinction, so \\.\pipe\foo\nul used to be redirected to the NUL device; that case now reaches the pipe.

\\.\... and bare \\?\<name> inputs now emit \??\... for NtCreateFile (both spell the DosDevices directory; NtCreateFile only accepts the NT form, with / normalised to \), so fs.writeFileSync(os.devNull, ...) keeps working even though the DOS-name check no longer special-cases it. The now-unreachable \\.\ -> CreateFileW fallback in open_dir_at_windows_nt_path (and its open_windows_device_path helper) has been removed.

The recogniser lives in bun_paths::windows_reserved_device_name_t (backed by a comptime_string_map), testable on every platform. nul.txt and similar names with an extension are not treated as devices.

Trailing-dot/space stripping of ordinary filenames is intentionally left out of this PR: applying it only in normalize_path_windows_opts would make writeFileSync(path.join(tmpdir, "x.")) and statSync(...) disagree on the target file, which is a regression from current behaviour. That, along with applying the device check to libuv-routed ops for drive-absolute paths, belongs in PathLike::slice_z_with_force_copy (see #33034).

Tests

  • bun_paths::windows_reserved_device_name_tests: unit coverage for case, trailing characters, numbered devices, and near-misses (run on every platform).
  • bun_sys::normalize_path_windows_tests::dos_device_names_resolve_to_nt_device: Windows-only unit tests covering the path-type gate, including \\.\pipe\com1, \\.\pipe/name, \\?\nul, \\server\share\aux, \\?\UNC\...\nul, \??\C:\a\nul.
  • test/js/node/fs/fs-windows-dos-device-names.test.ts: end-to-end on Windows for writeFileSync, readFileSync, statSync, Bun.write, relative sub\nul, near-misses, and os.devNull. 4/4 pass with the fix; the device-name test fails on the unpatched build.

Note on Node compatibility: Node v26 on Windows also creates literal nul files from fs.writeFileSync("nul", ...) because it passes every path through path.toNamespacedPath(). This change intentionally matches cmd/Explorer rather than Node there, since the alternative leaves users with files they cannot remove through normal tools.


no test proof · iteration 3 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/fs/fs-windows-dos-device-names.test.ts

@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:01 AM PT - Jul 20th, 2026

@robobun, your commit de663fc466304f6fb7effad8ee69605d53c4bf79 passed in Build #76227! 🎉


🧪   To try this PR locally:

bunx bun-pr 34770

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

bun-34770 --bun

@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Verified on Windows Server 2019 x64 (debug build at 53a19fb):

test/js/node/fs/fs-windows-dos-device-names.test.ts  4 pass / 0 fail
test/js/bun/import-attributes/import-attributes.test.ts  12 pass / 0 fail
test/js/node/fs/fs.test.ts -t "/dev/null"  1 pass / 0 fail
test/js/node/fs/fs-mkdir.test.ts  24 pass / 0 fail

The fs.writeFileSync to a reserved DOS device name test fails on the unpatched build.

Diff is now 3 files: src/paths/lib.rs, src/sys/lib.rs, and the new test. The trailing-dot strip and the bundler output-path trim were both dropped after review showed the strip makes writeFileSync(path.join(tmpdir, "x.")) and statSync(...) disagree; that work belongs in PathLike::slice_z_with_force_copy (see #33034). The bundler no longer needs touching because the bundler output and import agreed without the strip.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 45200d42-c0ae-41b7-89b7-f9804f30c4c0

📥 Commits

Reviewing files that changed from the base of the PR and between 2b44001 and de663fc.

📒 Files selected for processing (3)
  • src/paths/lib.rs
  • src/sys/lib.rs
  • test/js/node/fs/fs-windows-dos-device-names.test.ts

Walkthrough

Adds Windows reserved DOS device-name recognition, integrates it into Win32 path normalization and NT device resolution, removes the directory-opening fast path, and adds Rust and JavaScript coverage.

Changes

Windows path handling

Layer / File(s) Summary
Reserved device-name recognition
src/paths/lib.rs
Adds case-insensitive recognition for canonical DOS device names, trims trailing dots and spaces, and tests valid, invalid, ASCII, and UTF-16 inputs.
Windows normalization and NT opening
src/sys/lib.rs
Translates reserved final path components to NT or Win32 device prefixes, applies trailing-character handling, removes the \\.\ directory-opening branch, and updates related Rust tests.
Filesystem API coverage
test/js/node/fs/fs-windows-dos-device-names.test.ts
Tests reserved-name behavior across fs.writeFileSync, fs.readFileSync, fs.statSync, Bun.write, os.devNull, explicit \\.\NUL paths, relative components, and near-miss filenames.

Suggested reviewers: jarred-sumner, dylan-conway

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: Windows reserved DOS device names are recognized before NtCreateFile.
Description check ✅ Passed The description is thorough and covers what changed and how it was verified, though it does not use the exact template headings.
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.

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

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. [win][fs] Some functions do not support tailing dot "." and space " " in a filename #8836 - PR strips trailing dots and spaces from path components before NtCreateFile, directly fixing the ENOENT errors reported when reading/stating/unlinking files with trailing dots or spaces on Windows

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #8836

🤖 Generated with Claude Code

Comment thread src/sys/lib.rs Outdated
Comment thread src/sys/lib.rs Outdated
Comment thread src/sys/lib.rs Outdated
Comment thread src/sys/lib.rs
Comment thread test/js/node/fs/fs-windows-dos-device-names.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bundler/bundle_v2.rs`:
- Around line 4210-4216: Extend the trailing-character stripping in the
bundle_v2 path normalization loop at src/bundler/bundle_v2.rs:4210-4216 to
remove both dots and spaces, matching normalize_path_windows_opts. Apply the
same change to rel_path handling before POSIX conversion at
src/bundler/linker_context/generateChunksInParallel.rs:369-378, preserving
parity between written paths and recorded output/import paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a2c39e14-a785-4bf3-96af-a79a1c1cee08

📥 Commits

Reviewing files that changed from the base of the PR and between b82a43f and 30981ef.

📒 Files selected for processing (5)
  • src/bundler/bundle_v2.rs
  • src/bundler/linker_context/generateChunksInParallel.rs
  • src/paths/lib.rs
  • src/sys/lib.rs
  • test/js/node/fs/fs-windows-dos-device-names.test.ts

Comment thread src/bundler/bundle_v2.rs Outdated
Comment thread src/sys/lib.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/paths/lib.rs`:
- Around line 108-111: Condense the comments at src/paths/lib.rs lines 108-111,
src/bundler/bundle_v2.rs lines 4210-4215, and
src/bundler/linker_context/generateChunksInParallel.rs lines 372-376 so each is
no more than three lines, preserving the existing rationale and behavior without
changing code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9e294bdf-623e-4642-8d04-7f423f2328e5

📥 Commits

Reviewing files that changed from the base of the PR and between 30981ef and 2b44001.

📒 Files selected for processing (5)
  • src/bundler/bundle_v2.rs
  • src/bundler/linker_context/generateChunksInParallel.rs
  • src/paths/lib.rs
  • src/sys/lib.rs
  • test/js/node/fs/fs-windows-dos-device-names.test.ts

Comment thread src/paths/lib.rs Outdated
Comment thread src/sys/lib.rs Outdated
Comment thread src/sys/lib.rs
Comment thread src/sys/lib.rs Outdated
…t consistency); handle bare \\?\<name> and /->\ in \\.\ output
@robobun robobun changed the title windows: recognize reserved DOS device names and strip trailing dots/spaces before NtCreateFile windows: recognize reserved DOS device names before NtCreateFile Jul 20, 2026
Comment thread src/sys/lib.rs Outdated
Comment thread src/sys/lib.rs
Comment thread src/bundler/bundle_v2.rs Outdated
Comment thread src/sys/lib.rs
Comment thread src/bundler/bundle_v2.rs Outdated
Comment thread test/js/node/fs/fs-windows-dos-device-names.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new findings after de663fc — all earlier inline comments are addressed or explicitly deferred to #33034. Leaving the merge call to a human since this rewrites the normalize_path_windows_opts prefix classification that every Windows openat goes through, removes the \\.\ → CreateFileW fallback in open_dir_at_windows_nt_path, and intentionally diverges from Node's behaviour.

What was reviewed:

  • win32_normalizes gate traced for each path shape (Relative/Rooted/Drive*, \??\, UNC, \\.\, \\?\X:, \\?\UNC\, \\?\Volume{…}, bare \\?\<name>) against the unit-test matrix.
  • Callers of open_dir_at_windows_nt_path — all reach it via normalize_path_windows or to_nt_path, neither of which emits \\.\ now, so the removed CreateFileW arm is dead.
  • windows_reserved_device_name_t recogniser: trailing-./ handling, near-misses, non-ASCII rejection, u8/u16 parity.
  • Remaining known gaps (libuv-routed ops for drive-absolute device paths; nul\.) are pre-existing and called out for #33034.
Extended reasoning...

Overview

Three files: src/paths/lib.rs adds windows_reserved_device_name_t (a comptime_string_map-backed recogniser generic over u8/u16) with cross-platform unit tests; src/sys/lib.rs rewrites the DOS-device / \\.\ / \\?\ handling at the top of normalize_path_windows_opts, deletes open_windows_device_path and the \\.\ → CreateFileW fallback in open_dir_at_windows_nt_path, and adds a Windows-only unit-test module; test/js/node/fs/fs-windows-dos-device-names.test.ts is new end-to-end coverage. Earlier iterations also touched the bundler and added trailing-dot/space stripping — both were reverted after review, so the diff is now device-names only.

Security risks

None identified. The change narrows the set of inputs that reach NtCreateFile as literal filenames (fewer ways to create Explorer-undeletable files), and the exemption for UNC/LocalDevice/NT-object paths means named pipes and SMB shares are not silently redirected. No user input flows into a new allocation or index computation without a bounds check (buf.len() <= total guards the device-prefix copy).

Level of scrutiny

High. normalize_path_windows_opts sits on every Windows file-open path, the new win32_normalizes classification has to correctly distinguish six Win32 path types, and the PR removes a fallback (CreateFileW for \\.\) whose absence would break os.devNull if the new \??\ emission were wrong. It also deliberately diverges from Node, which the description justifies but which is a maintainer-level call.

Other factors

This PR has been through five prior review rounds; every substantive finding (write/stat inconsistency from trailing-dot stripping, untested bundler output change, stale comments, over-length comment blocks) was either fixed or reverted, and the two acknowledged non-blocking gaps are documented in the description with a follow-up issue. The author reports the Windows integration tests and fs-mkdir pass on a debug build. Windows CI for the current head was still building at review time, and I cannot execute the Windows-gated unit tests locally, so I'm deferring rather than approving.

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