Skip to content

VirtioFS: host-side unlink leaves a durable phantom dentry in containers (stat-present, readdir-absent, readlink/open→EINVAL up to entry_timeout); breaks Mercurial locks #520

Description

@supersteves

Bug report

Bug description

On Docker Desktop for Mac with VirtioFS file sharing, a host-side unlink() of a file a container has previously looked up is not propagated to the guest. For up to entry_timeout (3600s) the container keeps serving a stale positive dentry, so the path becomes a phantom:

  • lstat(path) succeeds (stale attrs, e.g. symbolic link, nlink=1)
  • readdir(parent) does not list the name
  • readlink(path) / open(path) return EINVAL

Only the macOS→container direction is broken; guest-side changes invalidate synchronously. This is the same cache-coherence family as #7246 (missing host→guest DELETE events), #6989 and #7501, but the EINVAL phantom (with lstat and readdir disagreeing) appears unreported.

The one-hour caching is visible in the VM mount table:

virtiofs0 /run/host_virtiofs/Users virtiofs rw,...,entry_timeout=3600,attr_timeout=3600

Expected: once the host unlinks the path, the container should see it gone (lstat→ENOENT), consistent with readdir; readlink/open must never return EINVAL on a path lstat reports as an existing symlink.

Actual: lstat present + readdir absent + readlink/open EINVAL, for up to 3600s. A container-side rm of the phantom returns ENOENT but flushes the stale entry — the only workaround short of waiting out the TTL.

Real-world impact (Mercurial): hg's lock files (.hg/wlock, .hg/store/lock) are symlinks created and unlinked around every write command — including host-side hg status, which takes the wlock to fix up dirstate. So any host hg plus any container hg within the next hour aborts, with this exact strace signature:

symlinkat(".../.hg/wlock")  = -1 EEXIST   # stale dentry says "lock exists"
readlinkat(".../.hg/wlock") = -1 EINVAL   # phantom
openat(".../.hg/wlock")     = -1 EINVAL   # hg's readlock fallback also fails
abort: Invalid argument: '.../.hg/wlock'

git is unaffected: .git/index.lock is a regular O_CREAT|O_EXCL file and is never readlinked.

Fix requested: propagate host-side unlink/rename invalidations to the guest (as #7246 asks for events), or expose entry_timeout/attr_timeout as a file-sharing setting — they are fixed at first mount inside the VM and not user-tunable today. Docker's Synchronized file shares (mutagen) sidesteps the shared-cache design entirely.

Related: #7246, #6989, #7501. Also related: #7004 (exclusive flock not enforced on bind mounts) — separate evidence added there.

Reproduction steps

mkdir -p ~/vt && docker run -d --name vt -v ~/vt:/ws debian:12 sleep 600

# container primes the lookup cache while the host churns the same path:
docker exec vt bash -c 'for i in $(seq 1 2000); do stat /ws/lnk >/dev/null 2>&1; done' &
for i in $(seq 1 100); do ln -s target ~/vt/lnk; sleep 0.05; rm ~/vt/lnk; sleep 0.05; done
wait

# phantom — persists for minutes (up to entry_timeout=3600s):
docker exec vt bash -c 'stat /ws/lnk; echo "--- ls -a ---"; ls -a /ws/; echo "--- readlink ---"; readlink /ws/lnk || echo EINVAL'

docker rm -f vt

Observed: stat reports /ws/lnk present, ls -a /ws/ omits it, readlink prints EINVAL.

Platform

macOS

Version information

Client:
 Version:           29.4.3
 API version:       1.54
 Go version:        go1.26.2
 Git commit:        055a478
 Built:             Wed May  6 17:06:34 2026
 OS/Arch:           darwin/arm64
 Context:           desktop-linux

Server: Docker Desktop 4.74.0 (227015)
 Engine:
  Version:          29.4.3
  API version:      1.54 (minimum version 1.40)
  Go version:       go1.26.2
  Git commit:       56be731
  Built:            Wed May  6 17:09:05 2026
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          v2.2.3
  GitCommit:        77c84241c7cbdd9b4eca2591793e3d4f4317c590
 runc:
  Version:          1.3.5
  GitCommit:        v1.3.5-0-g488fc13e
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
Client:
 Version:    29.4.3
 Context:    desktop-linux
 Debug Mode: false
 Plugins:
  agent: Docker AI Agent Runner (Docker Inc.)
    Version:  v1.57.0
    Path:     /Users/steve/.docker/cli-plugins/docker-agent
  ai: Docker AI Agent - Ask Gordon (Docker Inc.)
    Version:  v1.20.2
    Path:     /Users/steve/.docker/cli-plugins/docker-ai
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.33.0-desktop.1
    Path:     /Users/steve/.docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v5.1.4
    Path:     /Users/steve/.docker/cli-plugins/docker-compose
  debug: Get a shell into any image or container (Docker Inc.)
    Version:  0.0.47
    Path:     /Users/steve/.docker/cli-plugins/docker-debug
  desktop: Docker Desktop commands (Docker Inc.)
    Version:  v0.3.0
    Path:     /Users/steve/.docker/cli-plugins/docker-desktop
  dhi: CLI for managing Docker Hardened Images (Docker Inc.)
    Version:  v0.0.3
    Path:     /Users/steve/.docker/cli-plugins/docker-dhi
  extension: Manages Docker extensions (Docker Inc.)
    Version:  v0.2.31
    Path:     /Users/steve/.docker/cli-plugins/docker-extension
  init: Creates Docker-related starter files for your project (Docker Inc.)
    Version:  v1.4.0
    Path:     /Users/steve/.docker/cli-plugins/docker-init
  mcp: Docker MCP Plugin (Docker Inc.)
    Version:  v0.42.1
    Path:     /Users/steve/.docker/cli-plugins/docker-mcp
  model: Docker Model Runner (Docker Inc.)
    Version:  v1.1.37
    Path:     /Users/steve/.docker/cli-plugins/docker-model
  offload: Docker Offload (Docker Inc.)
    Version:  v0.5.89
    Path:     /Users/steve/.docker/cli-plugins/docker-offload
  pass: Docker Pass Secrets Manager Plugin (beta) (Docker Inc.)
    Version:  v0.0.27
    Path:     /Users/steve/.docker/cli-plugins/docker-pass
  sandbox: Docker Sandbox (Docker Inc.)
    Version:  v0.12.0
    Path:     /Users/steve/.docker/cli-plugins/docker-sandbox
  sbom: View the packaged-based Software Bill Of Materials (SBOM) for an image (Anchore Inc.)
    Version:  0.6.0
    Path:     /Users/steve/.docker/cli-plugins/docker-sbom
  scout: Docker Scout (Docker Inc.)
    Version:  v1.21.0
    Path:     /Users/steve/.docker/cli-plugins/docker-scout

Server:
 Containers: 14
  Running: 8
  Paused: 0
  Stopped: 6
 Images: 17
 Server Version: 29.4.3
 Storage Driver: overlayfs
  driver-type: io.containerd.snapshotter.v1
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 CDI spec directories:
  /etc/cdi
  /var/run/cdi
 Discovered Devices:
  cdi: docker.com/gpu=webgpu
 Swarm: inactive
 Runtimes: runc io.containerd.runc.v2
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 77c84241c7cbdd9b4eca2591793e3d4f4317c590
 runc version: v1.3.5-0-g488fc13e
 init version: de40ad0
 Security Options:
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 6.12.76-linuxkit
 Operating System: Docker Desktop
 OSType: linux
 Architecture: aarch64
 CPUs: 12
 Total Memory: 7.75GiB
 Name: docker-desktop
 ID: 2b883330-88ce-45da-a307-018bab4cc02b
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 HTTP Proxy: http.docker.internal:3128
 HTTPS Proxy: http.docker.internal:3128
 No Proxy: hubproxy.docker.internal
 Labels:
  com.docker.desktop.address=unix:///Users/steve/Library/Containers/com.docker.docker/Data/docker-cli.sock
 Experimental: false
 Insecure Registries:
  hubproxy.docker.internal:5555
  ::1/128
  127.0.0.0/8
 Live Restore Enabled: false
 Firewall Backend: iptables

Diagnostics ID

7EA2D421-16F4-4197-A23C-EE7F235D836F/20260713062418

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions