Support FUSE clients without READDIRPLUS (fall back to plain readdir)#147
Open
zeddybot wants to merge 1 commit into
Open
Support FUSE clients without READDIRPLUS (fall back to plain readdir)#147zeddybot wants to merge 1 commit into
zeddybot wants to merge 1 commit into
Conversation
…ddir pyfuse3 required FUSE_CAP_READDIRPLUS at init and registered only a readdirplus operation. A client that implements plain FUSE_READDIR but not FUSE_READDIRPLUS therefore could not use pyfuse3 at all: fuse_init raised "Kernel too old", leaving the mount unusable, and plain FUSE_READDIR requests hit a NULL operation and were answered with ENOSYS, so directory listings failed. gVisor's sentry is exactly such a client (gVisor issue #3404). Plain FUSE_READDIR is the mandatory baseline; READDIRPLUS (FUSE 7.21 / Linux 3.9) is a negotiated optimization that bundles attributes to save per-entry LOOKUPs. Register a plain readdir operation alongside readdirplus and stop aborting when READDIRPLUS is absent. The two share one async path; a read-only ReaddirToken.plus flag records which the client requested, and readdir_reply emits fuse_add_direntry (plain) or fuse_add_direntry_plus accordingly. Because plain FUSE_READDIR entries take no kernel lookup reference (only readdirplus entries do), filesystems must not increase the lookup count when token.plus is False, or the inode leaks with no balancing FORGET. This is documented on Operations.readdir and applied in the passthroughfs example. On a normal Linux kernel READDIRPLUS is always negotiated, so token.plus is always True and behaviour is unchanged. Signed-off-by: zeddybot <sameernayyar2003@gmail.com>
Contributor
|
Could you please clarify which agent was used for this PR, and to what extent human oversight was applied? |
Author
thank you for the quick response! this pr was authored by claude opus 4.8 in the claude code harness. i've reviewed all the code by hand, and tested it on gvisor + on a linux machine that supports readdir_plus and it seems to work for me. please do let me know if i can do anything else for you here! |
Contributor
|
I can't help but think I'm communicating with an agent here again, rather than a human. Sorry. |
Author
|
hey no this is me writing this by hand, not an agent - i did have claude write the pr but i saw your comment and responded by hand |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
pyfuse3 cannot be used at all under a FUSE client that implements plain
FUSE_READDIRbut notFUSE_READDIRPLUS. Two independent things break:Init aborts.
fuse_initraisesRuntimeError('Kernel too old, pyfuse3 requires kernel 3.9 or newer!')whenever the connection does not advertiseFUSE_CAP_READDIRPLUS, so the mount never comes up.Plain readdir is unimplemented. pyfuse3 registers only
fuse_ops.readdirplus. A client that issues a plainFUSE_READDIRhits aNULL operation and libfuse answers
ENOSYS, so directory listings fail evenif init is bypassed.
The motivating client is gVisor's sentry, whose VFS
implements plain
FUSE_READDIRbut notFUSE_READDIRPLUS(google/gvisor#3404, open since
2020). Running any pyfuse3 filesystem inside a gVisor sandbox is currently
impossible.
Plain
FUSE_READDIR(opcode 28) is the mandatory FUSE baseline;FUSE_READDIRPLUS(opcode 44, FUSE 7.21 / Linux 3.9) is a negotiatedoptimization that bundles each entry's attributes to save a follow-up
LOOKUP.libfuse treats them as independent, optional vtable slots — its own
hello_ll.cimplements only plain readdir, andpassthrough_ll.cimplementsboth and branches on a
plusflag.Fix
fuse_ops.readdiris now registered alongsidefuse_ops.readdirplus; the client's opcode selects which is invoked. No newinit/
Operationsflag is introduced — the client already advertises what itsupports, so auto-detection is the right axis.
fuse_initno longer raises whenFUSE_CAP_READDIRPLUSis absent. (FUSE_CAP_READDIRPLUS_AUTOis stillcleared, so a capable Linux client keeps sending READDIRPLUS.)
fuse_readdirplus_async. A new read-only attributeReaddirToken.plusrecords which request the client made, and
readdir_replyemitsfuse_add_direntry(plain) orfuse_add_direntry_plusaccordingly.Correctness — lookup counts
READDIRPLUS entries take an implicit per-entry kernel lookup reference (balanced
later by
FORGET); plain READDIR entries take none. TheOperations.readdircontract therefore now depends on
token.plus:token.plus is True→ each entry for whichreaddir_replyreturns True takesa lookup reference; the filesystem must increase the inode's lookup count
(unchanged behaviour).
token.plus is False→ the kernel takes no reference; the filesystem mustnot increase the lookup count, or the inode leaks with no balancing
FORGET.This is documented on
Operations.readdirand applied in thepassthroughfsexample (its
_add_pathbookkeeping is now gated ontoken.plus).Compatibility
Inert on Linux. A normal Linux kernel always negotiates READDIRPLUS, so
token.plusis always True, the new plain-readdir callback is never invoked,and byte-for-byte behaviour is unchanged. The only public surface added is one
read-only attribute (
ReaddirToken.plus); no handler signature changes.Testing
No-regression (CI). The full
test/suite passes unchanged.Fs.readdirnow asserts
token.plus is True(a normal kernel never sends plainFUSE_READDIR), and a newtest_readdirforcesopendir+readdirthrough areal listing so the assertion and the new attribute are exercised from Python.
A stock kernel cannot drive the plain-readdir branch, so CI proves only that
nothing regresses — see below for the branch's real coverage.
Plain-readdir branch (under gVisor). Verified by building the patched
wheel and running a pyfuse3 hello filesystem inside a gVisor (
runsc)sandbox. Before/after transcript, same code, same image:
Open question
Happy to add a lower-level
readdir_reply(..)unit test that constructs atokenwithplus=Falseand asserts a plainfuse_direntrecord is emitted,if you'd prefer in-CI coverage of the plain path. It's fragile to build outside
a live request, so I left it out — let me know your preference.