Skip to content

[fix] Windows: WinFsp has getdir before readlink in fuse_operations - #52

Open
ThomasWaldmann wants to merge 3 commits into
mxmlnkn:masterfrom
ThomasWaldmann:winfsp-readlink-order
Open

ThomasWaldmann wants to merge 3 commits into
mxmlnkn:masterfrom
ThomasWaldmann:winfsp-readlink-order

Conversation

@ThomasWaldmann

Copy link
Copy Markdown

On Windows (WinFsp), symlinks of a mfusepy file system show up as regular files and readlink of the operations class is never called.

Cause

libfuse 2 starts struct fuse_operations with getattr, readlink, getdir, but WinFsp's inc/fuse/fuse.h has:

struct fuse_operations
{
    /* S */ int (*getattr)(const char *path, struct fuse_stat *stbuf);
    /* S */ int (*getdir)(const char *path, fuse_dirh_t h, fuse_dirfil_t filler);
    /* S */ int (*readlink)(const char *path, char *buf, size_t size);
    /* S */ int (*mknod)(const char *path, fuse_mode_t mode, fuse_dev_t dev);
    ...

All other members are in the libfuse order. So with mfusepy's (libfuse) order:

  • our readlink callback lands in WinFsp's getdir slot. WinFsp only calls getdir if there is no readdir, so this went unnoticed (it would crash otherwise).
  • our getdir (NULL) lands in WinFsp's readlink slot. WinFsp checks 0 != f->ops.readlink when starting the file system (fuse_loop.c, has_symlinks) and, as it is NULL, reports S_IFLNK files without the reparse point attribute, i.e. as regular files.

I guess fusepy's WinFsp port always had this.

Fix

Use the WinFsp order of these two fields for Windows / Cygwin (cygfuse is built with the same WinFsp header).

Testing

Windows 11, WinFsp 2.1, MSYS2 UCRT64 Python 3.14, using borg mount (borgbackup/borg#10391) on an archive with a file symlink, a directory symlink and a dangling symlink:

before:

link_dangling 0o100666 islink: False attrs: 0x80 tag: 0x0
link_dir 0o100666 islink: False attrs: 0x80 tag: 0x0
link_file 0o100666 islink: False attrs: 0x80 tag: 0x0

after:

link_dangling 0o120666 islink: True attrs: 0x400 tag: 0xa000000c
   readlink: somewhere
link_dir 0o120777 islink: True attrs: 0x410 tag: 0xa000000c
   readlink: dir
link_file 0o120666 islink: True attrs: 0x400 tag: 0xa000000c
   readlink: file.txt

Not tested on Cygwin. Everything else I tried via WinFsp worked fine with mfusepy 3.1.1 (getattr, readdir, open / read / release, statfs, init with fuse_conn_info.want, timestamps incl. birthtime).

Note: once WinFsp sees a readlink, it calls readlink("/") when starting the file system and expects that to fail with ENOSYS or EINVAL, so a readlink implementation should raise FuseOSError(errno.EINVAL) for a non-symlink rather than a random exception (which mfusepy logs with a traceback).

🤖 Generated with Claude Code

@ThomasWaldmann

Copy link
Copy Markdown
Author

Force-pushed: the first version failed the mypy check (list-item error for the inline conditional lists), it now uses a separately defined, typed _fuse_operations_fields_readlink_getdir list that gets reversed for WinFsp. Re-tested on Windows, same result as in the PR description.

About the other red jobs of the first run: Tests (macos-14, 3.12) died at python3 -c 'import mfusepy' with a dyld assertion of the runner (dyld: Assertion failed: (_usedCount < _allocCount), function push_back, file Array.h), the NetBSD job also fails for master (b929e3e), all others were cancelled by fail-fast.

@ThomasWaldmann

Copy link
Copy Markdown
Author

CI status of the 2nd run: Static-Code-Checks pass now. The two jobs that really fail are not related to this change (everything else red was cancelled by fail-fast):

  • VM-Tests (netbsd, 10.1): also fails for master (b929e3e).
  • Tests (macos-14, 3.12), step "Test Import": python3 -c 'import mfusepy' aborts inside dyld (Assertion failed: (_usedCount < _allocCount), function push_back, file Array.h, line 64), same in both runs. brew installs macFUSE 5.3.3 there now (runner image macos-14-arm64 20260831), the last green master run was on 2026-08-10. In the 1st run, macos-15, 3.14 got past that step and passed, so it looks specific to macos-14 + the new macFUSE. This PR only changes the field order for Windows / Cygwin, the struct is unchanged elsewhere.

ThomasWaldmann and others added 2 commits September 19, 2026 04:13
…failure

perfuse is part of the NetBSD base system, there is no such package. pkgin
now fails when asked to install a package that does not exist:

    perfuse is not available in the repository

fail-fast: false, so one can see which platforms a failure affects.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
mfusepy looked up the macfuse_version symbol in libfuse to detect the
legacy MacFuse (and then used an old 32 bit inode struct stat layout).
macFUSE does not export that symbol, so this was a lookup of a missing
symbol on every import.

With macFUSE 5.3.x, libfuse depends on a lot of system frameworks (dyld
loads about 500 images for it) and on macOS 14, that lookup makes dyld abort:

    dyld[29263]: Assertion failed: (_usedCount < _allocCount), function push_back, file Array.h, line 64.

So "import mfusepy" crashed the Python interpreter there (macOS 15 is fine).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@ThomasWaldmann

Copy link
Copy Markdown
Author

The unrelated CI failures (macos-14 dyld abort at import, NetBSD perfuse package) are dealt with in #53. Once that is in, I'll rebase this one.

libfuse 2 has getattr, readlink, getdir at the start of struct
fuse_operations, but WinFsp has getattr, getdir, readlink, see:
https://github.com/winfsp/winfsp/blob/master/inc/fuse/fuse.h

With the libfuse order, the readlink callback ended up in the getdir slot
(which WinFsp never calls if there is a readdir) and readlink was NULL for
WinFsp. WinFsp then decides that the file system does not support symlinks
and shows all symlinks as regular files.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@ThomasWaldmann

Copy link
Copy Markdown
Author

Rebased onto #53 (the CI fixes), so CI can be green here. Until #53 is merged, this PR shows its 2 commits as well - the only commit that belongs to this PR is the last one ([fix] Windows: WinFsp has getdir before readlink in fuse_operations).

About the struct order: the WinFsp author confirmed in winfsp/winfsp#693 that it "was an early accident that unfortunately no longer can be fixed due to backwards compatibility", so the bindings have to follow WinFsp's order.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant