Repository navigation
[BUG] npm exec reports ECOMPROMISED when virtiofs changes the lock directory inode #10094
Description
Is there an existing issue for this?
- I have searched the existing issues
Related but distinct: #8710 fixed overlapping stat calls by changing the lock maintenance loop from setInterval to setTimeout. The current implementation contains that fix; this report is an inode-identity failure.
This issue exists in the latest npm version
- I am using the latest npm
Reproduced with npm 12.2.0 immediately before filing.
This is not just a request to bump a dependency for a CVE
- This is not solely a request to bump a dependency for a CVE
Current Behavior
On Docker Desktop for macOS, npx fails with ECOMPROMISED: Lock compromised when npm's cache is a host bind served through virtiofs. Packages whose installation finishes before the first lock check usually pass; a larger installation fails reliably.
libnpmexec/lib/with-lock.js records the lock directory's inode and rejects the lock if a later stat returns a different inode. A preload tracing fs.promises.stat, utimes, mkdir, and rmdir around the lock captured:
...313275 mkdir ok
...313275 stat ok ino=2548552 mtimeMs=1791392313276.2095
...314275 stat ok ino=2548554 mtimeMs=1791392313276.2095
There was no intervening mkdir or rmdir: the same directory returned a new inode with the same mtime while npm's reify workload was writing the package tree beside it. with-lock.js interprets the inode change as compromise and aborts.
Moving only <cache>/_npx to a container-local tmpfs makes the same command pass while the reusable _cacache remains on the virtiofs bind. The downstream investigation and workaround are tracked at filippolmt/toolbox#1167.
Expected Behavior
npm exec should not report a compromised lock merely because a remote/FUSE filesystem returns unstable inode identity for an unchanged lock directory. This is especially surprising because with-lock.js documents that the lock works across machines via NFS.
I am not proposing that inode checks simply be removed; the lock needs a compromise check whose assumptions hold on the remote filesystems it intends to support.
Steps To Reproduce
On macOS with Docker Desktop using virtiofs file sharing:
cache=$(mktemp -d)
docker run --rm \
-v "$cache:/home/toolbox/.npm" \
--entrypoint sh ghcr.io/filippolmt/toolbox:latest \
-lc 'npm install -g npm@latest >/dev/null && /home/toolbox/.npm-global/bin/npx -y @modelcontextprotocol/inspector@2.9.0 --help'The command fails with ECOMPROMISED. The equivalent invocation succeeds when _npx is overlaid with local storage:
docker run --rm \
-v "$cache:/home/toolbox/.npm" \
--tmpfs /home/toolbox/.npm/_npx:rw,exec,uid=1000,gid=1000,mode=0700 \
--entrypoint sh ghcr.io/filippolmt/toolbox:latest \
-lc 'npm install -g npm@latest >/dev/null && /home/toolbox/.npm-global/bin/npx -y @modelcontextprotocol/inspector@2.9.0 --help'A small isolated call to the same withLock with a delayed callback did not reproduce it; the inode change appears tied to the real reify workload.
Environment
- npm: 12.2.0
- Node.js: v24.21.0
- OS Name: macOS host, Debian container under Docker Desktop
- System Model Name: Apple Silicon Mac
- npm cache filesystem:
virtiofs0 on /home/toolbox/.npm, reported asfuseblkbystat -f -c %T - control filesystem: container overlayfs
Activity