Skip to content

NVM4305 false-positive: proxy (Zig) cross_user_write_mask includes READ_CONTROL|SYNCHRONIZE, flags every standard ReadAndExecute ACE #1395

Description

@micro-kid

NVM4305 false-positive: proxy (Zig) cross_user_write_mask includes READ_CONTROL|SYNCHRONIZE, flags every standard ReadAndExecute ACE

Summary

On v2.0.1-hotfix.1, when the Node.js install root is on a non-%LOCALAPPDATA% path (e.g. D:\nvm2), running any package manager (npm/yarn/pnpm) triggers NVM4305:

NVM blocked package-manager execution because the Node.js directory is unsafe.
Path: D:\nvm2\installs\v22.12.0
Reason: Directory is writable by other users.
Event code: NVM4305

...even though the version directory's Authenticated Users ACE is a completely standard ReadAndExecute (no write bits, verified via icacls).

Root cause

shim/shared/install_safety.zig defines:

file_generic_write: u32 = 0x00120116;   // <-- contains READ_CONTROL(0x20000) | SYNCHRONIZE(0x100000) = 0x120000
cross_user_write_mask = file_generic_write | generic_write | generic_all | delete_access | write_dac | write_owner;

The value 0x00120116 includes READ_CONTROL (0x20000) and SYNCHRONIZE (0x100000). A standard Windows ReadAndExecute ACE has mask 0x1200A9 (which also contains 0x120000). Therefore:

0x1200A9 & cross_user_write_mask = 0x120000 != 0   -> allowsCrossUserWrite() returns true

so the proxy misclassifies every normal read-only ACE as "writable by other users".

Inconsistency with the Go check

common/fs/acl_windows.go (used by nvm --check-runtime-acls) explicitly excludes those bits — the code comment says:

Write-capable bits only. Do not include READ_CONTROL/SYNCHRONIZE — those overlap FILE_GENERIC_READ/EXECUTE and false-positive AuthUsers RX ACEs.

Its crossUserWriteMask is 0x500D0156:

0x1200A9 & GoMask = 0x0   -> correctly not writable

So nvm --check-runtime-acls returns exit 0 (all version dirs trusted), while the Zig proxy blocks. The two security checks disagree.

Reproduction

  1. Install nvm-windows v2.0.1-hotfix.1.
  2. Set a non-%LOCALAPPDATA% install root, e.g. nvm config set root D:\nvm2\installs (or nvm root D:\nvm2).
  3. nvm install 22.12.0, nvm use 22.12.0.
  4. Confirm Authenticated Users on D:\nvm2\installs\v22.12.0 is only ReadAndExecute (icacls shows (I)(RX)).
  5. In any project dir: yarn --versionNVM4305.

Evidence

ReadAndExecute (0x1200A9) & ZigMask (0x501F0116) = 0x120000   -> proxy blocks
ReadAndExecute (0x1200A9) & GoMask  (0x500D0156) = 0x0        -> Go check passes
nvm --check-runtime-acls  -> exit 0

Expected behavior

The Zig proxy's cross_user_write_mask should exclude READ_CONTROL and SYNCHRONIZE (mirroring common/fs/acl_windows.go), so that standard ReadAndExecute ACEs are not flagged as cross-user writable.

Workaround (documented)

Remove the Authenticated Users / Users ACEs from the nvm install-root tree (only suitable for single-admin machines), then nvm reshim:

$targets = @("D:\nvm2","D:\nvm2\installs") + (Get-ChildItem "D:\nvm2\installs" -Directory).FullName
foreach ($t in $targets) {
  icacls $t /inheritance:r
  icacls $t /remove:g "Authenticated Users" "Users"
  icacls $t /grant:r "SYSTEM:(OI)(CI)(F)" "BUILTIN\Administrators:(OI)(CI)(F)"
}
nvm reshim

Affected versions

  • v2.0.1-hotfix.1 (verified). The install_safety.zig on main still uses file_generic_write = 0x00120116, so likely affected there too.

Files

  • shim/shared/install_safety.zigcross_user_write_mask / file_generic_write
  • common/fs/acl_windows.go — reference (correct) crossUserWriteMask

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions