From d57ff66506e46927d582ef2fc36c0212b9579823 Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Tue, 7 Jul 2026 17:10:06 -0500 Subject: [PATCH 1/4] fix: IsReadOnly misclassifies read opens as writes when kernel sets extra flag bits --- fsmount/internal/flags/flags.go | 9 +++++++-- fsmount/internal/flags/flags_test.go | 13 +++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/fsmount/internal/flags/flags.go b/fsmount/internal/flags/flags.go index 67321dcc..f44f675c 100644 --- a/fsmount/internal/flags/flags.go +++ b/fsmount/internal/flags/flags.go @@ -26,9 +26,14 @@ func (f FuseFlags) IsEventOnly() bool { return int(f)&O_EVTONLY != 0 } -// IsReadOnly checks if the flag is set to read-only. +// IsReadOnly reports whether the flags carry no write intent: the POSIX +// access mode (low two bits: O_RDONLY=0, O_WRONLY=1, O_RDWR=2) is O_RDONLY +// and no bit implying modification (O_CREAT, O_TRUNC, O_APPEND) is set. +// Kernels set unrelated bits on read opens (e.g. Linux forces +// O_LARGEFILE=0x8000 from 64-bit userspace), so the full flag word must not +// be compared against zero. func (f FuseFlags) IsReadOnly() bool { - return int(f) == 0 + return int(f)&0x3 == 0 && !f.IsCreate() && !f.IsTruncate() && !f.IsAppend() } // IsWriteOnly checks if the flag is set to write-only. diff --git a/fsmount/internal/flags/flags_test.go b/fsmount/internal/flags/flags_test.go index fedb74ae..cbb68315 100644 --- a/fsmount/internal/flags/flags_test.go +++ b/fsmount/internal/flags/flags_test.go @@ -74,6 +74,19 @@ func TestFuseFlags(t *testing.T) { expectedCreateExclusive: true, expectedString: "FuseFlags{O_CREAT|O_EXCL}", }, + { + name: "ReadOnlyWithKernelSetBits", + flags: fuse.O_RDONLY | 0x8000, // Linux forces O_LARGEFILE (0x8000) on every 64-bit open + expectedReadOnly: true, + expectedWriteOnly: false, + expectedReadWrite: false, + expectedCreate: false, + expectedExclusive: false, + expectedTruncate: false, + expectedAppend: false, + expectedCreateExclusive: false, + expectedString: "FuseFlags{O_EVTONLY|O_RDONLY}", + }, { name: "TruncateAppend", flags: fuse.O_TRUNC | fuse.O_APPEND, From 44d1205748cf50aa7148e126dfde072fe12455d5 Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Tue, 7 Jul 2026 17:10:06 -0500 Subject: [PATCH 2/4] fix: upload sweeper cancels live write sessions whose upload has not started --- fsmount/open_handles.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fsmount/open_handles.go b/fsmount/open_handles.go index 69779e9d..0a4e6c58 100644 --- a/fsmount/open_handles.go +++ b/fsmount/open_handles.go @@ -211,6 +211,13 @@ func (h *OpenHandles) startUploadSweeper() { continue } _, bytesWritten, lastActivity := node.uploadStats() + if lastActivity.IsZero() { + // A write session exists but its upload has not started + // (uploads begin at flush/release). Treating the zero + // timestamp as idle time would cancel the live session + // and delete its working copy on the first sweep. + continue + } idle := time.Since(lastActivity) h.log.Debug("Upload sweeper: checking upload for path %s, bytes written: %d, last activity: %v, idle time: %v", node.path, bytesWritten, lastActivity, idle) // Case A: upload opened but never wrote any bytes — allow a long grace period. From 974c89ce38a2cdbe9428dd8f74c3c7fa014a48f9 Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Tue, 7 Jul 2026 17:10:06 -0500 Subject: [PATCH 3/4] fix: Destroy self-deadlocks on held locks and listDir reads lockMap unsynchronized --- fsmount/remotefs.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/fsmount/remotefs.go b/fsmount/remotefs.go index 40729407..8df29d46 100644 --- a/fsmount/remotefs.go +++ b/fsmount/remotefs.go @@ -372,10 +372,16 @@ func (fs *RemoteFs) Init() { func (fs *RemoteFs) Destroy() { fs.log.Debug("RemoteFs: Destroy: removing all file locks") + // Snapshot under the mutex, then release it: unlock re-acquires + // lockMapMutex, so unlocking while holding it self-deadlocks. fs.lockMapMutex.Lock() - defer fs.lockMapMutex.Unlock() - for path, lockInfo := range fs.lockMap { - fs.unlock(path, lockInfo.Fh) + locks := make(map[string]*lockInfo, len(fs.lockMap)) + for path, li := range fs.lockMap { + locks[path] = li + } + fs.lockMapMutex.Unlock() + for path, li := range locks { + fs.unlock(path, li.Fh) } fs.log.Debug("RemoteFs: Destroy: stopping cache maintenance") @@ -2621,7 +2627,10 @@ func (fs *RemoteFs) listDir(path string) (childPaths map[string]struct{}, opErr childPath := path_lib.Join(path, path_lib.Base(lock.Path)) // Ignore paths where the lock is held by this file system. - if _, ok := fs.lockMap[childPath]; ok { + fs.lockMapMutex.Lock() + _, heldByUs := fs.lockMap[childPath] + fs.lockMapMutex.Unlock() + if heldByUs { continue } From d3a879fef8161be8a92441a3e63dcfb5af2c40bf Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Tue, 7 Jul 2026 17:10:06 -0500 Subject: [PATCH 4/4] fix: LocalFs.Getattr stats the wrong path on Linux and reports Go mode bits on Windows --- fsmount/localfs_linux.go | 2 +- fsmount/localfs_windows.go | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/fsmount/localfs_linux.go b/fsmount/localfs_linux.go index 6b19131d..9ad43f6e 100644 --- a/fsmount/localfs_linux.go +++ b/fsmount/localfs_linux.go @@ -14,7 +14,7 @@ import ( func (fs *LocalFs) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) { fq := fs.fqPath(path) stgo := syscall.Stat_t{} - if err := syscall.Lstat(path, &stgo); err != nil { + if err := syscall.Lstat(fq, &stgo); err != nil { if !os.IsNotExist(err) { fs.log.Trace("LocalFs: Getattr: failed to lstat file: path=%v, fh=%v, err=%v", fq, fh, err) return -fuse.EIO diff --git a/fsmount/localfs_windows.go b/fsmount/localfs_windows.go index 9ea2a289..51652996 100644 --- a/fsmount/localfs_windows.go +++ b/fsmount/localfs_windows.go @@ -28,7 +28,13 @@ func (fs *LocalFs) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) return -fuse.ENOENT } - stat.Mode = uint32(info.Mode()) + // Go's FileMode encodes directories in its own high bits; FUSE consumers + // dispatch on the POSIX S_IFMT bits, so translate explicitly. + if info.IsDir() { + stat.Mode = fuse.S_IFDIR | 0755 + } else { + stat.Mode = fuse.S_IFREG | uint32(info.Mode().Perm()) + } stat.Size = int64(info.Size()) node, ok := fs.vfs.fetch(path) if ok && node.info.uid != 0 {