diff --git a/weed/command/mount_std.go b/weed/command/mount_std.go index c133d82e7..eabd561b4 100644 --- a/weed/command/mount_std.go +++ b/weed/command/mount_std.go @@ -359,6 +359,7 @@ func RunMount(option *MountOptions, umask os.FileMode) bool { Cipher: cipher, UidGidMapper: uidGidMapper, IncludeSystemEntries: *option.includeSystemEntries, + DefaultPermissions: *option.defaultPermissions, DisableXAttr: *option.disableXAttr, IsMacOs: runtime.GOOS == "darwin", MetadataFlushSeconds: *option.metadataFlushSeconds, diff --git a/weed/mount/weedfs.go b/weed/mount/weedfs.go index 9b7665183..fd2ef7cc9 100644 --- a/weed/mount/weedfs.go +++ b/weed/mount/weedfs.go @@ -71,6 +71,13 @@ type Option struct { UidGidMapper *meta_cache.UidGidMapper IncludeSystemEntries bool + // DefaultPermissions mirrors the FUSE default_permissions mount option. + // When set, the kernel enforces unix permission bits from the getattr/ + // lookup attributes before it ever calls Open/Create/Mknod, so the mount + // skips its own redundant permission checks (and the group lookups behind + // them) on those hot paths. + DefaultPermissions bool + // Periodic metadata flush interval in seconds (0 to disable) // This protects chunks from being purged by volume.fsck for long-running writes MetadataFlushSeconds int diff --git a/weed/mount/weedfs_file_mkrm.go b/weed/mount/weedfs_file_mkrm.go index d7d4b7043..57fda67ab 100644 --- a/weed/mount/weedfs_file_mkrm.go +++ b/weed/mount/weedfs_file_mkrm.go @@ -320,7 +320,10 @@ func (wfs *WFS) createRegularFile(dirFullPath util.FullPath, name string, mode u return 0, nil, fuse.Status(syscall.ENOSPC) } - // Verify write+search permission on the parent directory. + // Load the parent directory to validate it exists (the create RPC sets + // SkipCheckParentDirectory, so this is the only parent check). With + // default_permissions the kernel already verified write+search on it before + // Create/Mknod, so skip only the mode-bit check and its group lookup. parentEntry, parentStatus := wfs.maybeLoadEntry(dirFullPath) if parentStatus != fuse.OK { return 0, nil, parentStatus @@ -328,14 +331,16 @@ func (wfs *WFS) createRegularFile(dirFullPath util.FullPath, name string, mode u if parentEntry == nil || parentEntry.Attributes == nil { return 0, nil, fuse.EIO } - // Map parent dir uid/gid from filer-space to local-space so the - // permission check compares like with like (caller uid/gid are local). - parentUid, parentGid := parentEntry.Attributes.Uid, parentEntry.Attributes.Gid - if wfs.option.UidGidMapper != nil { - parentUid, parentGid = wfs.option.UidGidMapper.FilerToLocal(parentUid, parentGid) - } - if !hasAccess(uid, gid, parentUid, parentGid, parentEntry.Attributes.FileMode, fuse.W_OK|fuse.X_OK) { - return 0, nil, fuse.Status(syscall.EACCES) + if !wfs.option.DefaultPermissions { + // Map parent dir uid/gid from filer-space to local-space so the + // permission check compares like with like (caller uid/gid are local). + parentUid, parentGid := parentEntry.Attributes.Uid, parentEntry.Attributes.Gid + if wfs.option.UidGidMapper != nil { + parentUid, parentGid = wfs.option.UidGidMapper.FilerToLocal(parentUid, parentGid) + } + if !hasAccess(uid, gid, parentUid, parentGid, parentEntry.Attributes.FileMode, fuse.W_OK|fuse.X_OK) { + return 0, nil, fuse.Status(syscall.EACCES) + } } entryFullPath := dirFullPath.Child(name) diff --git a/weed/mount/weedfs_file_mkrm_test.go b/weed/mount/weedfs_file_mkrm_test.go index 8511f9473..ba3828a9c 100644 --- a/weed/mount/weedfs_file_mkrm_test.go +++ b/weed/mount/weedfs_file_mkrm_test.go @@ -487,3 +487,69 @@ func TestCreateExistingFileIgnoresQuotaPreflight(t *testing.T) { t.Fatalf("Create status = %v, want EEXIST", status) } } + +// With default_permissions the kernel enforces unix bits before it calls +// Open, so AcquireHandle must skip its own check (and the group lookup behind +// it). Without it, AcquireHandle stays the enforcer. +func TestAcquireHandleHonorsDefaultPermissions(t *testing.T) { + for _, tc := range []struct { + name string + defaultPermissions bool + want fuse.Status + }{ + {"kernel enforces", true, fuse.OK}, + {"mount enforces", false, fuse.EACCES}, + } { + t.Run(tc.name, func(t *testing.T) { + wfs, _ := newCreateTestWFS(t) + wfs.option.DefaultPermissions = tc.defaultPermissions + + oldLookup := lookupSupplementaryGroupIDs + lookupSupplementaryGroupIDs = func(uint32) ([]string, error) { return nil, nil } + clearSupplementaryGroupCache() + t.Cleanup(func() { + lookupSupplementaryGroupIDs = oldLookup + clearSupplementaryGroupCache() + }) + + entry := &filer_pb.Entry{ + Name: "secret.txt", + Attributes: &filer_pb.FuseAttributes{ + FileMode: 0o600, // owner-only: an "other" uid has no read + Inode: 202, + Crtime: 1, + Mtime: 1, + Uid: 123, + Gid: 456, + }, + } + if err := wfs.metaCache.InsertEntry(context.Background(), filer.FromPbEntry("/", entry)); err != nil { + t.Fatalf("InsertEntry: %v", err) + } + inode := wfs.inodeToPath.Lookup(util.FullPath("/secret.txt"), entry.Attributes.Crtime, false, false, entry.Attributes.Inode, true) + + fh, status := wfs.AcquireHandle(inode, syscall.O_RDONLY, 999, 999) + if status != tc.want { + t.Fatalf("AcquireHandle status = %v, want %v", status, tc.want) + } + if status == fuse.OK { + if fh == nil { + t.Fatal("AcquireHandle returned nil handle on OK") + } + wfs.ReleaseHandle(fh.fh) + } + }) + } +} + +// default_permissions skips only the mode-bit check, not parent-existence +// validation: the create RPC sets SkipCheckParentDirectory, so the mount's +// own parent lookup is the only thing guarding against an orphaned entry. +func TestCreateRegularFileValidatesParentUnderDefaultPermissions(t *testing.T) { + wfs, _ := newCreateTestWFS(t) + wfs.option.DefaultPermissions = true + + if _, _, code := wfs.createRegularFile(util.FullPath("/ghost"), "f.txt", 0o644, 99, 100, 0, false, false); code == fuse.OK { + t.Fatal("createRegularFile returned OK for a missing parent directory") + } +} diff --git a/weed/mount/weedfs_filehandle.go b/weed/mount/weedfs_filehandle.go index 6f9cd376e..7fe61d1db 100644 --- a/weed/mount/weedfs_filehandle.go +++ b/weed/mount/weedfs_filehandle.go @@ -24,8 +24,10 @@ func (wfs *WFS) AcquireHandle(inode uint64, flags, uid, gid uint32) (fileHandle if wormEnforced, _ := wfs.wormEnforcedForEntry(path, entry); wormEnforced && flags&fuse.O_ANYWRITE != 0 { return nil, fuse.EPERM } - // Check unix permission bits for the requested access mode. - if entry != nil && entry.Attributes != nil { + // Check unix permission bits for the requested access mode. With + // default_permissions the kernel already enforced them before this + // open, so the check (and its supplementary-group lookup) is redundant. + if !wfs.option.DefaultPermissions && entry != nil && entry.Attributes != nil { fileUid, fileGid := entry.Attributes.Uid, entry.Attributes.Gid if wfs.option.UidGidMapper != nil { fileUid, fileGid = wfs.option.UidGidMapper.FilerToLocal(fileUid, fileGid)