Skip to content

macOS bind-mount root intermittently reports UID/GID 0:0 via lstat() / newfstatat() #628

Closed

Description

@mjswensen

Bug report

Description

On Docker Desktop for macOS, the root directory of a bind mount intermittently reports UID/GID 0:0 through lstat() / newfstatat(), despite normally being owned by UID/GID 1000:1000.

This reproduces using plain docker run with a stock node image, and reproduces with both VirtioFS and gRPC FUSE file sharing while using Apple Virtualization Framework.

Child directories beneath the bind-mount root continue to report the expected ownership. This causes Git repositories mounted directly at the bind-mount root to intermittently fail with fatal: detected dubious ownership.

Environment

  • macOS: macOS 26.6.2 (build 25G83)
  • Architecture: arm64
  • Docker Desktop: v4.88.0
  • Virtual Machine Manager: Apple Virtualization Framework

Minimal reproduction

On the macOS host, from any Git repository:

docker run --rm -it \
  -v "$PWD:/workspaces/test" \
  -w /workspaces/test \
  -u node \
  node \
  bash

Inside the container, run Git repeatedly as the node user (UID 1000):

for i in $(seq 1 1000); do
  git status --porcelain >/dev/null 2>/tmp/git.err || {
    echo "FAILED on iteration $i"
    cat /tmp/git.err
    break
  }
done

The failure generally occurs within a small number of iterations:

FAILED on iteration 2
fatal: detected dubious ownership in repository at '/workspaces/test'

Syscall evidence

Tracing a failed Git invocation shows the bind-mount root intermittently reported as owned by root:

newfstatat(
    AT_FDCWD,
    "/workspaces/test",
    {
        ...
        st_uid=0,
        st_gid=0,
        ...
    },
    AT_SYMLINK_NOFOLLOW
) = 0

In successful invocations, the same syscall on the same path returns:

st_uid=1000,
st_gid=1000

Meanwhile a directory below the mount root, such as .git, continues to report the correct ownership even during a failing invocation:

/workspaces/test       uid=0    gid=0
/workspaces/test/.git  uid=1000 gid=1000

Not specific to Git

A small C wrapper calling lstat() before executing Git has also observed the incorrect metadata before Git starts:

struct stat st;
lstat("/workspaces/test", &st);

fprintf(stderr, "euid=%u uid=%u gid=%u\n",
        geteuid(), st.st_uid, st.st_gid);

Example failing result:

euid=1000 uid=0 gid=0

Therefore Git itself is not causing the incorrect ownership metadata.

statx() differs

GNU stat uses statx() and consistently reports the expected UID/GID:

1000:1000

Tracing it shows:

statx(
    AT_FDCWD,
    "/workspaces/test",
    AT_STATX_SYNC_AS_STAT|AT_SYMLINK_NOFOLLOW|AT_NO_AUTOMOUNT,
    STATX_UID|STATX_GID,
    {
        ...
        stx_attributes=STATX_ATTR_MOUNT_ROOT,
        ...
    }
) = 0

Tens of thousands of repeated statx()-based lookups did not reproduce the incorrect 0:0 ownership.

Mount-root-specific behavior

The problem appears specific to the bind mount root.

If another Git repository is initialized one level below it:

/workspaces/test            ← bind mount root
└── nested                  ← nested Git worktree
    └── .git

then:

for i in $(seq 1 1000); do
  git -C nested status --porcelain >/dev/null || {
    echo "FAILED $i"
    break
  }
done

does not reproduce the issue.

The repository located directly at /workspaces/test continues to fail within a few iterations.

Expected behavior

Ownership metadata for the bind-mount root should be stable and consistent across metadata APIs.

If the directory is owned by UID/GID 1000:1000, both statx() and newfstatat() / lstat() should consistently report:

1000:1000

Actual behavior

newfstatat() intermittently reports the mount root as:

0:0

while:

  • statx() reports 1000:1000
  • child directories report 1000:1000
  • the effective UID of the process remains 1000

This causes applications that perform ownership-based security checks using lstat() to behave incorrectly (like git).

Workaround

Mounting the parent directory instead:

/workspaces            ← bind mount root
└── test               ← Git repository

avoids the Git symptom because the Git worktree is no longer itself the bind mount root.

Possibly related: #508

Platform

macOS

Version information

Client:
 Version:           29.7.2
 API version:       1.55
 Go version:        go1.26.5
 Git commit:        a7dcaa6
 Built:             Wed Aug  5 18:27:50 2026
 OS/Arch:           darwin/arm64
 Context:           desktop-linux

Server: Docker Desktop 4.88.0 (237115)
 Engine:
  Version:          29.7.2
  API version:      1.55 (minimum version 1.40)
  Go version:       go1.26.5
  Git commit:       6a43e3d
  Built:            Wed Aug  5 18:28:35 2026
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          v2.3.3
  GitCommit:        aad11006b869517fcd3009450b6f82da282e1a9b
 runc:
  Version:          1.4.3
  GitCommit:        v1.4.3-0-gbb14dabe
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
Client:
 Version:    29.7.2
 Context:    desktop-linux
 Debug Mode: false
 Plugins:
  agent: Docker AI Agent Runner (Docker Inc.)
    Version:  v1.124.0
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-agent
  ai: Docker AI Agent - Ask Gordon (Docker Inc.)
    Version:  v1.30.0
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-ai
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.36.1-desktop.1
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v5.4.0
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-compose
  debug: Get a shell into any image or container (Docker Inc.)
    Version:  0.0.47
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-debug
  desktop: Docker Desktop commands (Docker Inc.)
    Version:  v0.4.3
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-desktop
  dhi: CLI for managing Docker Hardened Images (Docker Inc.)
    Version:  v0.0.7
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-dhi
  extension: Manages Docker extensions (Docker Inc.)
    Version:  v0.2.31
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-extension
  init: Creates Docker-related starter files for your project (Docker Inc.)
    Version:  v1.4.0
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-init
  mcp: Docker MCP Plugin (Docker Inc.)
    Version:  v0.43.3
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-mcp
  model: Docker Model Runner (Docker Inc.)
    Version:  v1.2.6
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-model
  offload: Docker Offload (Docker Inc.)
    Version:  v0.6.13
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-offload
  pass: Docker Pass Secrets Manager Plugin (beta) (Docker Inc.)
    Version:  v0.2.1
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-pass
  sandbox: "docker sandbox" is deprecated, use Docker Sandboxes instead (Docker Inc.)
    Version:  v0.13.0
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-sandbox
  scout: Docker Scout (Docker Inc.)
    Version:  v1.24.0
    Path:     /Users/mjswensen/.docker/cli-plugins/docker-scout

Server:
 Containers: 17
  Running: 0
  Paused: 0
  Stopped: 17
 Images: 67
 Server Version: 29.7.2
 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: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: aad11006b869517fcd3009450b6f82da282e1a9b
 runc version: v1.4.3-0-gbb14dabe
 init version: de40ad0
 Security Options:
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 7.0.12-linuxkit
 Operating System: Docker Desktop
 OSType: linux
 Architecture: aarch64
 CPUs: 10
 Total Memory: 7.748GiB
 Name: docker-desktop
 ID: 5ad4520f-9680-40b8-ba0a-1ae2dd24f5d5
 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/mjswensen/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

No response

Activity

  1. added theissue type on Aug 27, 2026
  2. radutopala commented on Aug 27, 2026

    @radutopala

    Independent reproduction on 4.88.1, so this is not fixed in the current release.

    Environment

    • Docker Desktop 4.88.1 (237512), Engine 29.7.2, linux/arm64
    • File sharing: VirtioFS
    • Container user's uid matches the host uid exactly

    Three things that may help narrow it down

    1. It is bursty, not uniformly random. It stays broken for a sustained window, then goes quiet. Two samples minutes apart on the same path:

    window A: 40/40 lstat() calls on the bind-mount root returned st_uid=0
    window B:  0/60 calls returned st_uid=0
    

    So a reproduction attempt can look completely clean for minutes. Anyone testing a fix needs to confirm the flap is live during the measurement, or they'll conclude it's fixed when it isn't.

    2. A uid mismatch is not required. The container user's uid equals the host uid, so ownership matches exactly whenever the flap is inactive. The bogus 0 isn't a uid-mapping problem — it's the attribute itself being wrong intermittently.

    3. Multiple independent bind mounts flap. A directory mount and a separate single-file mount both reported 0:0 within the same window.

    I could not corroborate the bind-mount-root-vs-children distinction: during my sampling the flap was inactive on every path, root and child alike.

    Isolating it without rebuilding anything

    GIT_CONFIG_NOSYSTEM=1 makes git ignore /etc/gitconfig, so you can A/B the workaround against the bug in the same loop iteration on the same path — which matters given how bursty this is:

    for i in $(seq 1 40); do
      uid=$(stat -c %u .)
      GIT_CONFIG_NOSYSTEM=1 git rev-parse --show-toplevel >/dev/null 2>&1 && b=ok || b=FAIL
      git rev-parse --show-toplevel >/dev/null 2>&1 && a=ok || a=FAIL
      echo "uid=$uid before=$b after=$a"
    done

    Workaround for anyone landing here. safe.directory is additive across config scopes and only honoured from system/global, so a system-scope entry baked into the image survives a host ~/.gitconfig bind-mounted over the container's global config:

    RUN git config --system --add safe.directory '*'

    That only stops git consulting ownership — the flap is untouched, so anything else reading uid on a bind mount can still get 0.

    One question: this reproduces under Apple Virtualization Framework per the report — does it also reproduce under Docker VMM? Several recent bind-mount bugs differ between the two, and that would help localize it.

  3. dvg-p4 commented on Sep 1, 2026

    @dvg-p4

    I can reproduce the issue (on docker desktop 4.88.1 and 4.89.0) just by touching any file in a directory -- the directory gets misreported as being owned by root:root (0:0) for almost exactly one second after each touch:

    appuser@2dfb1bc800d5:/src/reveal-data-store$ df -h .
    Filesystem      Size  Used Avail Use% Mounted on
    virtiofs0       461G  355G  106G  78% /src/reveal-data-store
    appuser@2dfb1bc800d5:/src/reveal-data-store$ stat -c %u . && touch anyfile && for i in $(seq 15); do stat -c %u .; sleep 0.1; done
    501
    501
    0
    0
    0
    0
    0
    0
    0
    0
    0
    501
    501
    501
    501
    501
    

    So, the trigger is predictable -- anything that touches a file within the mount -- but can appear sporadic, since there's a whole lot of things that touch files.

  4. dvg-p4 commented on Sep 1, 2026

    @dvg-p4

    The directory that gets its stat messed up seems to be the mount root, when any file within the mount is touched (not the immediate parent as I'd first thought):

    reprex

    appuser@2dfb1bc800d5:/src/reveal-data-store$ ls -dl . ./test && touch anyfile && for i in $(seq 15); do ls -dl . ./test; sleep 0.1; done
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    appuser@2dfb1bc800d5:/src/reveal-data-store$ ls -dl . ./test && touch ./test/anyfile && for i in $(seq 15); do ls -dl . ./test; sleep 0.1; done
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  2 appuser appgroup   64 Sep  1 17:17 ./test
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 root    root     1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    drwxr-xr-x 56 appuser appgroup 1792 Sep  1 17:17 .
    drwxr-xr-x  3 appuser appgroup   96 Sep  1 17:18 ./test
    

  5. dvg-p4 commented on Sep 2, 2026

    @dvg-p4

    I was only able to reproduce this with VirtioFS, not with gRPC FUSE. So, switching to the latter could be a workaround for now, here's where to find it in the settings:

    setting to change

    Image

  6. mjswensen commented on Sep 6, 2026

    @mjswensen
    Author

    For what it's worth, I was also seeing it with gRPC FUSE. 😔

  7. bsousaa commented on Sep 7, 2026

    @bsousaa
    Collaborator

    Can you please test with Desktop 4.90?

  8. idontsov commented on Sep 21, 2026

    @idontsov

    Can you please test with Desktop 4.90?

    I get this issue on v4.91.0

  9. bsousaa commented on Sep 21, 2026

    @bsousaa
    Collaborator

    @idontsov can you please generate a diagnostics id?

  10. idontsov commented on Sep 22, 2026

    @idontsov

    @bsousaa

    Diagnostics id: 3CA66781-E093-4EC3-9BC5-D8191CFB7A22/20260922094345
    Let me know if you need the devcontainer configuration, I can share the reproduction repository as well.

  11. karim-elessawy-arm commented on Oct 3, 2026

    @karim-elessawy-arm

    Hello,
    Just heads up that this was incorrectly marked as fixed in 4.90.0 release notes - last point under 'All platforms' section
    Release notes for 4.90.0 (All platforms)

    It is still reproducible on 4.93.0 with Apple Virtualization with VirtioFS (haven't tried gRPC FUSE)

    It seems also that touching a nested file, causes the nested directory to switch to 0:0 for ~ 1 second

    mkdir nested
    pushd nested
    
    # This runs inside nested dir
    for i in $(seq 30)
      do stat -c %u:%g .
      sleep 0.1
      if [ "$i" -eq 15 ]; then
        printf "Touching file\n"
        touch newfile
      fi
    done
    
    popd
    rm -rf nested/
    

    results in

    ~/test/nested ~/test
    501:20
    501:20
    501:20
    501:20
    501:20
    501:20
    501:20
    501:20
    501:20
    501:20
    501:20
    501:20
    501:20
    501:20
    501:20
    Touching file
    501:20
    0:0
    0:0
    0:0
    0:0
    0:0
    0:0
    0:0
    0:0
    0:0
    501:20
    501:20
    501:20
    501:20
    501:20
    ~/test
    
  12. ab commented on Oct 14, 2026

    @ab

    @bsousaa The bug still exists on Desktop 4.95.0 (242831). Is there anything else that would be helpful to identify a fix?

  13. bsousaa commented on Oct 19, 2026

    @bsousaa
    Collaborator

    This should be fixed with 4.96. Can you please give it a try when it releases?

  14. ab commented on Oct 19, 2026

    @ab

    Looks to be fixed in 4.96, thank you!

  15. karim-elessawy-arm commented on Oct 20, 2026

    @karim-elessawy-arm

    Yes solved for both root mount and nested mount directories - Thank you

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions