mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-13 09:38:36 +02:00
* mount: propagate request context through metadata lookups The FUSE interrupt channel never reached the filer RPC: lookupEntry ran its LookupEntry under context.Background(), so a stalled filer parked ops like ListXAttr forever even after the kernel gave up on them. Thread a context from each op's cancel channel through maybeReadEntry, maybeLoadEntry, lookupEntry, ensureDirectoryVisited and waitForPendingAsyncFlush; the existing withFilerClient attempt bound turns it into both an interrupt and a deadline. A caller cancellation now surfaces as EINTR instead of waiting out the RPC. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: reuse ctxFromCancel for the read path's interrupt bridge Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: regression tests for canceled and stalled filer lookups Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: thread ctx through AcquireHandle Open, Create and Mknod all carry the request's cancel channel; let it bound the pending-flush wait and metadata load inside AcquireHandle too. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: keep flush waits and write sequences uninterruptible The cancel-derived context must only cover metadata reads: an interrupted Unlink or Rename that skips the pending-flush wait can race the async write back into existence, and Link/Rename mutations that run under a canceled context abort mid-sequence, strand the rollback, and leave wrong hard-link counts. Restore the blocking flush wait and run each op's write phase on context.Background(). Rmdir and Unlink also propagate EINTR from the parent lookup instead of proceeding past a permission check the interrupt prevented. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: interrupt shared directory builds and directory reads Waiters behind another request's singleflight directory build blocked until it finished even when interrupted; switch the wait to DoChan so the request context can abandon it. ReadDir paths now carry the request context through EnsureListingFresh, cache listings and direct reads, and map its cancellation to EINTR rather than EIO. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: assert exact statuses in filer-stall regression tests Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: let read-side flush waits release on interrupt waitForPendingAsyncFlush reports whether the flush actually finished: lookupEntry and AcquireHandle return EINTR when the request is abandoned mid-wait, while Unlink and Rename keep waiting on context.Background() so the delete/rename still sees the flushed state. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: propagate interrupt through remaining lookup checks Rename's source and destination permission lookups now return EINTR on interrupt instead of skipping checkStickyBit or masking an existing target, and the readdir preload visit maps cancellation to EINTR like the other branches. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: fail closed on errored rename prerequisite lookups * mount: stop rmdir on interrupted target lookup, fail closed on dir lookup errors --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
169 lines
4.5 KiB
Go
169 lines
4.5 KiB
Go
package mount
|
|
|
|
import (
|
|
"os/user"
|
|
"strconv"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
)
|
|
|
|
type cachedGroupIDs struct {
|
|
groups []string
|
|
expiresAt time.Time
|
|
}
|
|
|
|
var (
|
|
supplementaryGroupCache = make(map[uint32]*cachedGroupIDs)
|
|
supplementaryGroupCacheMu sync.RWMutex
|
|
supplementaryGroupCacheTTL = 5 * time.Minute
|
|
|
|
lookupSupplementaryGroupIDs = func(callerUid uint32) ([]string, error) {
|
|
u, err := user.LookupId(strconv.Itoa(int(callerUid)))
|
|
if err != nil {
|
|
glog.Warningf("hasAccess: user.LookupId for uid %d failed: %v", callerUid, err)
|
|
return nil, err
|
|
}
|
|
groupIDs, err := u.GroupIds()
|
|
if err != nil {
|
|
glog.Warningf("hasAccess: u.GroupIds for uid %d failed: %v", callerUid, err)
|
|
return nil, err
|
|
}
|
|
return groupIDs, nil
|
|
}
|
|
)
|
|
|
|
// cachedLookupSupplementaryGroupIDs returns supplementary group IDs for a UID,
|
|
// caching results for 5 minutes to avoid repeated expensive system calls.
|
|
func cachedLookupSupplementaryGroupIDs(callerUid uint32) ([]string, error) {
|
|
now := time.Now()
|
|
|
|
supplementaryGroupCacheMu.RLock()
|
|
cached, ok := supplementaryGroupCache[callerUid]
|
|
supplementaryGroupCacheMu.RUnlock()
|
|
if ok && now.Before(cached.expiresAt) {
|
|
return cached.groups, nil
|
|
}
|
|
|
|
groupIDs, err := lookupSupplementaryGroupIDs(callerUid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
supplementaryGroupCacheMu.Lock()
|
|
supplementaryGroupCache[callerUid] = &cachedGroupIDs{
|
|
groups: groupIDs,
|
|
expiresAt: now.Add(supplementaryGroupCacheTTL),
|
|
}
|
|
supplementaryGroupCacheMu.Unlock()
|
|
|
|
return groupIDs, nil
|
|
}
|
|
|
|
// clearSupplementaryGroupCache wipes the UID->groups cache for test isolation.
|
|
func clearSupplementaryGroupCache() {
|
|
supplementaryGroupCacheMu.Lock()
|
|
defer supplementaryGroupCacheMu.Unlock()
|
|
for k := range supplementaryGroupCache {
|
|
delete(supplementaryGroupCache, k)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check file access permissions
|
|
*
|
|
* This will be called for the access() system call. If the
|
|
* 'default_permissions' mount option is given, this method is not
|
|
* called.
|
|
*
|
|
* This method is not called under Linux kernel versions 2.4.x
|
|
*/
|
|
func (wfs *WFS) Access(cancel <-chan struct{}, input *fuse.AccessIn) (code fuse.Status) {
|
|
ctx, cancelFunc := ctxFromCancel(cancel)
|
|
defer cancelFunc()
|
|
_, _, entry, code := wfs.maybeReadEntry(ctx, input.NodeId)
|
|
if code != fuse.OK {
|
|
return code
|
|
}
|
|
if entry == nil || entry.Attributes == nil {
|
|
return fuse.EIO
|
|
}
|
|
// Map entry uid/gid from filer-space to local-space so the permission
|
|
// check compares like with like (caller uid/gid from FUSE are local).
|
|
fileUid, fileGid := entry.Attributes.Uid, entry.Attributes.Gid
|
|
if wfs.option.UidGidMapper != nil {
|
|
fileUid, fileGid = wfs.option.UidGidMapper.FilerToLocal(fileUid, fileGid)
|
|
}
|
|
if hasAccess(input.Uid, input.Gid, fileUid, fileGid, entry.Attributes.FileMode, input.Mask) {
|
|
return fuse.OK
|
|
}
|
|
return fuse.EACCES
|
|
}
|
|
|
|
func hasAccess(callerUid, callerGid, fileUid, fileGid uint32, perm uint32, mask uint32) bool {
|
|
mask &= fuse.R_OK | fuse.W_OK | fuse.X_OK
|
|
if mask == 0 {
|
|
return true
|
|
}
|
|
if callerUid == 0 {
|
|
return mask&fuse.X_OK == 0 || perm&0o111 != 0
|
|
}
|
|
|
|
if callerUid == fileUid {
|
|
return (perm>>6)&mask == mask
|
|
}
|
|
|
|
isMember := callerGid == fileGid
|
|
if !isMember {
|
|
groupIDs, err := cachedLookupSupplementaryGroupIDs(callerUid)
|
|
if err != nil {
|
|
// Cannot determine supplementary group membership.
|
|
// Fall through to "other" permission check since we already
|
|
// know the caller is not the owner (checked above) and not
|
|
// in the primary group.
|
|
return (perm & mask) == mask
|
|
}
|
|
fileGidStr := strconv.Itoa(int(fileGid))
|
|
for _, gidStr := range groupIDs {
|
|
if gidStr == fileGidStr {
|
|
isMember = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if isMember {
|
|
return (perm>>3)&mask == mask
|
|
}
|
|
|
|
return (perm & mask) == mask
|
|
}
|
|
|
|
// checkStickyBit enforces the POSIX sticky-bit rule: when a directory has the
|
|
// sticky bit set, only the file owner, the directory owner, or root may
|
|
// delete or rename entries within it.
|
|
func checkStickyBit(dirMode, dirUid, targetUid, callerUid uint32) fuse.Status {
|
|
if dirMode&0o1000 == 0 {
|
|
return fuse.OK
|
|
}
|
|
if callerUid == 0 || callerUid == dirUid || callerUid == targetUid {
|
|
return fuse.OK
|
|
}
|
|
return fuse.EPERM
|
|
}
|
|
|
|
// openFlagsToAccessMask converts open(2) flags to an access permission mask.
|
|
func openFlagsToAccessMask(flags uint32) uint32 {
|
|
switch flags & uint32(o_ACCMODE) {
|
|
case syscall.O_WRONLY:
|
|
return fuse.W_OK
|
|
case syscall.O_RDWR:
|
|
return fuse.R_OK | fuse.W_OK
|
|
default: // O_RDONLY
|
|
return fuse.R_OK
|
|
}
|
|
}
|