#693·winfsp

FUSE 2 API: struct fuse_operations has getdir before readlink (libfuse: readlink before getdir) - is this intended?

Author: ThomasWaldmannCreated Sep 19, 2026Updated Sep 19, 2026

Bug Report

Not sure whether this is a bug or intended, thus the question: WinFsp's FUSE 2 struct fuse_operations has the getdir and readlink members in a different order than libfuse 2.x has them.

WinFsp, inc/fuse/fuse.h:

c
struct fuse_operations
{
    /* S - supported by WinFsp */
    /* 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);

libfuse 2.9.9, include/fuse.h:

c
struct fuse_operations {
	int (*getattr) (const char *, struct stat *);
	int (*readlink) (const char *, char *, size_t);
	/* Deprecated, use readdir() instead */
	int (*getdir) (const char *, fuse_dirh_t, fuse_dirfil_t);
	int (*mknod) (const char *, mode_t, dev_t);

All the other members are in the same order as in libfuse (and WinFsp's FUSE 3 struct fuse3_operations has no getdir, so there is no such difference there).

For C / C++ file systems that get compiled against WinFsp's header, this does not matter. But it does for language bindings that describe the struct layout themselves, following libfuse: fusepy / mfusepy (Python, ctypes) have getattr, readlink, getdir. With that:

  • the binding's readlink callback ends up in WinFsp's getdir slot. WinFsp only calls getdir if there is no readdir, so nothing crashes and this easily goes unnoticed.
  • the binding's getdir (NULL) ends up in WinFsp's readlink slot, so 0 != f->ops.readlink in fsp_fuse_loop_start is false, has_symlinks stays false and all S_IFLNK files are reported as regular files (no reparse point).

I have proposed to use WinFsp's order in mfusepy for Windows (mxmlnkn/mfusepy#52), so this is not urgent for us - but I wanted to ask:

  • Is the different order intended (then the bindings just have to follow it, maybe it could be mentioned in the docs / in the header), or is it an accident from the early days (the header has it that way since 2016)?
  • If it is an accident: I guess it can not be changed without breaking the ABI for all existing binaries, so it would rather be something to document?

How to Reproduce

  • a FUSE file system using mfusepy 3.1.1 (or fusepy) that implements getattr, readdir, readlink and reports S_IFLNK for some path. I found this with borg mount (borgbackup/borg#10391) on a backup archive containing symlinks.
  • mount it, look at the symlinks.

Behaviors

Expected: the symlinks show up as symlinks (reparse points), readlink gets called.

Actual: readlink never gets called (not even the readlink("/") probe when the file system starts), the symlinks show up as regular files (FileAttributes=0, ReparseTag=0 in the WinFsp debug log for the Create of a symlink).

With getdir / readlink swapped in the binding's struct definition, everything works as expected (file symlinks, directory symlinks, dangling symlinks).

Environment

  • OS version and build: Windows 11 Pro, 10.0.26200
  • WinFsp version and build: 2.1