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
- Install
nvm-windows v2.0.1-hotfix.1.
- Set a non-
%LOCALAPPDATA% install root, e.g. nvm config set root D:\nvm2\installs (or nvm root D:\nvm2).
nvm install 22.12.0, nvm use 22.12.0.
- Confirm
Authenticated Users on D:\nvm2\installs\v22.12.0 is only ReadAndExecute (icacls shows (I)(RX)).
- In any project dir:
yarn --version → NVM4305.
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.zig — cross_user_write_mask / file_generic_write
common/fs/acl_windows.go — reference (correct) crossUserWriteMask
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) triggersNVM4305:...even though the version directory's
Authenticated UsersACE is a completely standardReadAndExecute(no write bits, verified viaicacls).Root cause
shim/shared/install_safety.zigdefines:The value
0x00120116includesREAD_CONTROL(0x20000) andSYNCHRONIZE(0x100000). A standard WindowsReadAndExecuteACE has mask0x1200A9(which also contains0x120000). Therefore: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 bynvm --check-runtime-acls) explicitly excludes those bits — the code comment says:Its
crossUserWriteMaskis0x500D0156:So
nvm --check-runtime-aclsreturns exit 0 (all version dirs trusted), while the Zig proxy blocks. The two security checks disagree.Reproduction
nvm-windowsv2.0.1-hotfix.1.%LOCALAPPDATA%install root, e.g.nvm config set root D:\nvm2\installs(ornvm root D:\nvm2).nvm install 22.12.0,nvm use 22.12.0.Authenticated UsersonD:\nvm2\installs\v22.12.0is onlyReadAndExecute(icaclsshows(I)(RX)).yarn --version→NVM4305.Evidence
Expected behavior
The Zig proxy's
cross_user_write_maskshould excludeREAD_CONTROLandSYNCHRONIZE(mirroringcommon/fs/acl_windows.go), so that standardReadAndExecuteACEs are not flagged as cross-user writable.Workaround (documented)
Remove the
Authenticated Users/UsersACEs from the nvm install-root tree (only suitable for single-admin machines), thennvm reshim:Affected versions
v2.0.1-hotfix.1(verified). Theinstall_safety.zigonmainstill usesfile_generic_write = 0x00120116, so likely affected there too.Files
shim/shared/install_safety.zig—cross_user_write_mask/file_generic_writecommon/fs/acl_windows.go— reference (correct)crossUserWriteMask