Skip to content

[BUG] npm exec reports ECOMPROMISED when virtiofs changes the lock directory inode #10094

Open

Description

@filippolmt

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 as fuseblk by stat -f -c %T
  • control filesystem: container overlayfs

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions