From e6daf9fc79be25c7e260d5d9cde140dc30258bc8 Mon Sep 17 00:00:00 2001 From: Ebrathul <54629189+Ebrathul@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:13:09 +0200 Subject: [PATCH] Compare the caller's process, not its thread, to the client's 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//task/, 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. --- src/openvfsfuse/openvfsfuse.cpp | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/openvfsfuse/openvfsfuse.cpp b/src/openvfsfuse/openvfsfuse.cpp index a53ae57..032a3aa 100644 --- a/src/openvfsfuse/openvfsfuse.cpp +++ b/src/openvfsfuse/openvfsfuse.cpp @@ -120,6 +120,33 @@ auto getInternalPath(const std::string &path) return VFSFuseContext::instance().getInternalPath(path); } +// FUSE reports the *thread* id of the caller in fuse_context::pid, while the +// desktop client announces its process id over the socket API. A client that +// writes the content it downloaded from a worker thread therefore fails the +// identity check in openVFSfuse_open(), and its own write is treated as a +// foreign access that needs hydrating -- which deadlocks the very hydration it +// is answering. +// +// A thread of the client is visible as /proc//task/, so one stat() +// answers the question without reading or parsing anything. On platforms +// without /proc only the direct comparison applies, which is what was there +// before. +bool callerBelongsToProcess(pid_t callerThreadId, long processId) +{ + if (callerThreadId <= 0 || processId <= 0) { + return false; + } + if (callerThreadId == processId) { + return true; + } +#ifdef __APPLE__ + return false; +#else + std::error_code ignored; + return std::filesystem::exists(std::format("/proc/{}/task/{}", processId, callerThreadId), ignored); +#endif +} + /* * Returns the name of the process which accessed the file system. */ @@ -486,7 +513,7 @@ static int openVFSfuse_open(const char *orig_path, struct fuse_file_info *fi) // The desktop client must not be blocked from accessing the file // to be able to overwrite it. - if (fuse_get_context()->pid == _jobs.desktopClientPid()) { + if (callerBelongsToProcess(fuse_get_context()->pid, _jobs.desktopClientPid())) { openvfsfuse_log(path, "open", res, "Desktop client tries to access, bypassing!"); } else { if (const auto attribs = OpenVFS::PlaceHolderAttributes::fromAttributes(path)) {