Compare the caller's process, not its thread, to the client's - #55
Merged
dragotin merged 1 commit intoAug 24, 2026
Merged
Conversation
openVFSfuse_open() exempts the desktop client from hydration blocking so that it can write the content it just downloaded without that write being treated as a foreign access needing hydration. The check compared fuse_get_context()->pid against the pid the client announced over the socket API's VERSION message. fuse_context::pid is the calling *thread* id, while the client announces its process id. The two are equal only when the client writes from its main thread. A sync client downloads on worker threads, so in practice the exemption never fires: the client's own write is queued as a fresh hydration request, which the client answers with another write, and open() deadlocks against the hydration it is serving. A thread of the client is visible as /proc/<pid>/task/<tid>, so a single stat() answers this with nothing to read or parse. The direct comparison is kept as a fast path in front of it, and is the only check on platforms without /proc, which leaves those on the previous behaviour. Verified against a stub client on a real FUSE mount, on Linux, with the stub serving each hydration on its own thread the way a real client does. Before: eight concurrent opens of dehydrated placeholders wedge the mount until hydrationTimeoutSeconds expires, and not one "bypassing" line is logged. After: eight bypasses and eight hydration requests are logged, and all eight complete with checksums matching the source. macOS has the same bug. The equivalent there is proc_pidinfo() with PROC_PIDTBSDINFO, whose pbi_pid gives the process id for a thread. I have no macOS machine to test that on, so I left the platform on its current behaviour rather than commit an untested path.
dragotin
approved these changes
Aug 22, 2026
dragotin
left a comment
Member
There was a problem hiding this comment.
Looks good to me, thank you!
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.
openVFSfuse_open()exempts the desktop client from hydration blocking so thatthe client can write the content it just downloaded without that write being
treated as a foreign access that needs hydrating. The check is:
if (fuse_get_context()->pid == _jobs.desktopClientPid()) {fuse_context::pidis the calling thread id.desktopClientPid()is theprocess id the client announced in its
VERSIONreply. The two are equalonly when the client happens to do the write from its main thread.
A sync client downloads on worker threads, so in practice the exemption never
fires. The client's own hydration write is queued as a fresh hydration request,
which the client answers with another write, and
open()deadlocks against thehydration it is currently serving.
This is not specific to the client I was testing with: any client that does its
transfers off the main thread hits it.
Reproduction
A stub client on the socket API, driving a real FUSE mount over a tree of
dehydrated placeholders, with the stub serving each
V2/HYDRATE_FILEon its ownthread — which is what a real client does.
Eight concurrent opens of dehydrated placeholders wedged the mount until
hydrationTimeoutSecondsexpired, and not onebypassingline was logged.With this change: eight
bypassinglines and eight hydration requests, alleight opens complete, every checksum matches the source, and the small files no
longer queue behind the 500 MB transfer.
The change
A thread of the client is visible as
/proc/<pid>/task/<tid>, so a singlestat()answers the question with nothing to read or parse. The directcomparison stays as a fast path in front of it.
This check runs on every
open()in the sync root, so its cost matters. It isone
stat()on a path that is already in the dentry cache, and it sits next togetcallername(), which already does areadlink("/proc/<pid>/exe")on the samecode path — so this does not add a
/procaccess where there was none, it addsa cheaper one alongside an existing one. Parsing
/proc/<pid>/statusforTgid:would work too and was my first attempt, but it means opening and reading a file
per
open()where astat()will do.On platforms without
/proconly the direct comparison applies, which leavesthem on exactly the previous behaviour.
What I could not do
macOS has the same bug and this does not fix it there. The equivalent is
proc_pidinfo()withPROC_PIDTBSDINFO, whosepbi_pidgives the process idfor a thread. I have no macOS machine to test that on, so I left the platform on
its current behaviour rather than commit an untested path. Happy to add it if
someone can verify it.
There is also a race I did not try to close: a thread can exit between the FUSE
request and the
stat(), and a pid can in principle be recycled. Both windowsexisted before this change —
getcallername()has the same exposure — andclosing them properly needs pidfds, which felt out of scope for a bug fix.
I also did not add a test. Reproducing this needs a client that writes from a
worker thread and a real mount;
socketthreadtest(from #54) exercises thesocket layer, but the identity check lives on the FUSE side of the boundary and
I did not see a clean way to reach it from there. Suggestions welcome.
Relationship to #54
Independent of it. The line this touches exists on
mainand #54 does notmodify it — its hunks are on either side. This applies cleanly to
mainon itsown, and rebases onto #54 without conflict. I found it while testing #54's
branch, which is the only reason the two are mentioned together.
Environment
Linux 7.1.8 (CachyOS), gcc 16.2.1, libfuse 3.18.2, Btrfs. Built
RelWithDebInfo;ctestgreen.