Skip to content

VirtioFS: statx() returns ENOENT for a just-created path when STATX_BTIME is requested #7882

Description

@lparker-rti
  • I have tried with the latest version of Docker Desktop
  • I have tried disabling enabled experimental features
  • I have uploaded Diagnostics
  • Diagnostics ID: none — I'm not able to share a bundle from this machine. The repro below is self-contained and reproduces on a stock python:3-slim image against any bind-mounted host directory, so it shouldn't need one. Happy to run specific commands and report the output instead.

Expected behavior

After mkdir(path) returns, statx(path, STATX_BTIME) should describe that path, as statx(path, STATX_BASIC_STATS) and stat(path) both already do.

Actual behavior

On a VirtioFS bind mount, statx() returns ENOENT for a just-created path whenever STATX_BTIME (0x800) is in the requested mask. Removing that one bit makes the identical call succeed on the identical path. The entry is really on disk throughout — readdir lists it, and open/access succeed.

It is create-scoped and time-bounded, not a permanent negative cache: the same masked call succeeds once the path is a few milliseconds old.

arch=aarch64  __NR_statx=291
mkdir(), then statx() the same path:
  mask=STATX_BASIC_STATS (0x7ff)                0/20 ENOENT
  mask=STATX_BTIME       (0x800)               20/20 ENOENT
  mask=basic|btime       (0xfff)               20/20 ENOENT
  mask=STATX_BTIME, after 5ms sleep             0/20 ENOENT
os.stat() (newfstatat, no btime) for comparison:
  os.stat()                                     0/20 ENOENT

Reproduce

Stock python:3-slim, no Node, no project code. Any bind-mounted host directory:

mkdir -p /tmp/statxbug && cd /tmp/statxbug
cat > probe.py <<'EOF'
import ctypes, ctypes.util, os, platform, time

NR = {"aarch64": 291, "x86_64": 332}[platform.machine()]
AT_FDCWD, ENOENT = -100, 2
BASIC, BTIME = 0x7FF, 0x800

libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
buf = ctypes.create_string_buffer(256)

def statx(path, mask):
    ctypes.set_errno(0)
    rc = libc.syscall(
        ctypes.c_long(NR), ctypes.c_int(AT_FDCWD), ctypes.c_char_p(path.encode()),
        ctypes.c_int(0), ctypes.c_uint(mask), ctypes.byref(buf),
    )
    if rc != 0:
        raise OSError(ctypes.get_errno(), "statx")

def trial(label, mask, delay=0.0):
    fail = 0
    for i in range(20):
        p = "/vol/probe-%x-%d" % (mask, i)
        os.mkdir(p)
        if delay:
            time.sleep(delay)
        try:
            statx(p, mask)
        except OSError as e:
            fail += e.errno == ENOENT
        os.rmdir(p)
    print("  %-44s %2d/20 ENOENT" % (label, fail))

print("arch=%s  __NR_statx=%d" % (platform.machine(), NR))
print("mkdir(), then statx() the same path:")
trial("mask=STATX_BASIC_STATS (0x7ff)", BASIC)
trial("mask=STATX_BTIME       (0x800)", BTIME)
trial("mask=basic|btime       (0xfff)", BASIC | BTIME)
trial("mask=STATX_BTIME, after 5ms sleep", BTIME, 0.005)
print("os.stat() (newfstatat, no btime) for comparison:")
fail = 0
for i in range(20):
    p = "/vol/plain-%d" % i
    os.mkdir(p)
    try:
        os.stat(p)
    except FileNotFoundError:
        fail += 1
    os.rmdir(p)
print("  %-44s %2d/20 ENOENT" % ("os.stat()", fail))
EOF
docker run --rm -v "$PWD:/vol" python:3-slim python3 /vol/probe.py

The mount under test:

3515 3506 0:44 /private/tmp/statxbug /vol rw,nosuid,nodev,relatime - virtiofs host rw,ignore_atime,no_xattr

Why this reaches ordinary users

The kernel picks the FUSE opcode from the requested mask. In fuse_update_get_attr:

if (!fc->no_statx && (request_mask & ~STATX_BASIC_STATS))
        err = fuse_do_statx(idmap, inode, file, stat);   /* FUSE_STATX */
else
        err = fuse_do_getattr(idmap, inode, stat, file); /* FUSE_GETATTR */

So asking for btime is exactly what routes a request to FUSE_STATX, which is the opcode that answers ENOENT here. Everything that stays within STATX_BASIC_STATS takes FUSE_GETATTR and is fine.

libuv requests btime, so every fs.stat() in Node on a VirtioFS bind hits this path. In one Node process on one mount, fresh unique path per iteration:

Node call result
statSync / lstatSync 10/10 ENOENT
accessSync, existsSync 0/10
readdirSync, opendirSync 0/10
openSync, readFileSync 0/10

Downstream this reads as tooling being broken rather than as a filesystem fault. Two we hit: Storybook's mkdirp stats a directory it just created and dies with ENOTDIR: not a directory, mkdir …/node_modules/.cache/storybook/…/public, and Vitest writes .vite-temp/vitest.config.ts.timestamp-*.mjs and then has the ESM resolver stat it, dying with ERR_MODULE_NOT_FOUND before a single test runs. Both look like broken configs and are not.

A shell mkdir d && stat d does not reproduce it, because process spawn costs more than the window — which makes this awkward to catch by hand.

Information

  • Reproduces on every run, 20/20, on a fresh unique path each iteration.
  • Recovery time varies with system load — a repeat run showed 4/20 rather than 0/20 for the 5 ms case. The mask result itself is stable across every run: 0/20 for 0x7ff, 20/20 for 0x800.
  • Not observed on an ext4 named volume or on the container overlay — 0/20 on both, same probe.
  • No workaround from userland that we can see: Node exposes no way to ask libuv to drop btime.
  • Possibly the undiagnosed cause behind pnpm fails with spurious ENOENT in shared volume using gRPC FUSE or VirtioFS #6787, though the open/copyfile failures reported there do not reproduce for us, and that issue predates FUSE statx support (Linux 6.6), so it may well be unrelated.

docker version

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.87.0 (236836)
 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.2.5
  GitCommit:        e53c7c1516c3b2bff98eb76f1f4117477e6f4e66
 runc:
  Version:          1.3.6
  GitCommit:        v1.3.6-0-g491b69ba
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

Host

  • macOS 26.5.2 (25F84), arm64
  • Guest kernel 7.0.12-linuxkit #1 SMP PREEMPT Wed Aug 12 20:18:49 UTC 2026 aarch64
  • File sharing implementation: VirtioFS

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