mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-19 04:24:28 +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>
556 lines
16 KiB
Go
556 lines
16 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"path/filepath"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
type createEntryTestServer struct {
|
|
filer_pb.UnimplementedSeaweedFilerServer
|
|
mu sync.Mutex
|
|
lastDirectory string
|
|
lastName string
|
|
lastUID uint32
|
|
lastGID uint32
|
|
lastMode uint32
|
|
}
|
|
|
|
type createEntrySnapshot struct {
|
|
directory string
|
|
name string
|
|
uid uint32
|
|
gid uint32
|
|
mode uint32
|
|
}
|
|
|
|
func (s *createEntryTestServer) CreateEntry(ctx context.Context, req *filer_pb.CreateEntryRequest) (*filer_pb.CreateEntryResponse, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.lastDirectory = req.GetDirectory()
|
|
if req.GetEntry() != nil {
|
|
s.lastName = req.GetEntry().GetName()
|
|
if req.GetEntry().GetAttributes() != nil {
|
|
s.lastUID = req.GetEntry().GetAttributes().GetUid()
|
|
s.lastGID = req.GetEntry().GetAttributes().GetGid()
|
|
s.lastMode = req.GetEntry().GetAttributes().GetFileMode()
|
|
}
|
|
}
|
|
return &filer_pb.CreateEntryResponse{}, nil
|
|
}
|
|
|
|
func (s *createEntryTestServer) UpdateEntry(ctx context.Context, req *filer_pb.UpdateEntryRequest) (*filer_pb.UpdateEntryResponse, error) {
|
|
return &filer_pb.UpdateEntryResponse{}, nil
|
|
}
|
|
|
|
func (s *createEntryTestServer) snapshot() createEntrySnapshot {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return createEntrySnapshot{
|
|
directory: s.lastDirectory,
|
|
name: s.lastName,
|
|
uid: s.lastUID,
|
|
gid: s.lastGID,
|
|
mode: s.lastMode,
|
|
}
|
|
}
|
|
|
|
func newCreateTestWFS(t *testing.T) (*WFS, *createEntryTestServer) {
|
|
t.Helper()
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = listener.Close()
|
|
})
|
|
|
|
server := pb.NewGrpcServer()
|
|
testServer := &createEntryTestServer{}
|
|
filer_pb.RegisterSeaweedFilerServer(server, testServer)
|
|
go server.Serve(listener)
|
|
t.Cleanup(server.Stop)
|
|
|
|
uidGidMapper, err := meta_cache.NewUidGidMapper("", "")
|
|
if err != nil {
|
|
t.Fatalf("create uid/gid mapper: %v", err)
|
|
}
|
|
|
|
root := util.FullPath("/")
|
|
option := &Option{
|
|
ChunkSizeLimit: 1024,
|
|
ConcurrentReaders: 1,
|
|
VolumeServerAccess: "filerProxy",
|
|
FilerAddresses: []pb.ServerAddress{
|
|
pb.NewServerAddressWithGrpcPort("127.0.0.1:1", listener.Addr().(*net.TCPAddr).Port),
|
|
},
|
|
GrpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
FilerMountRootPath: "/",
|
|
MountUid: 99,
|
|
MountGid: 100,
|
|
MountMode: 0o777,
|
|
MountMtime: time.Now(),
|
|
MountCtime: time.Now(),
|
|
UidGidMapper: uidGidMapper,
|
|
uniqueCacheDirForWrite: t.TempDir(),
|
|
}
|
|
|
|
wfs := &WFS{
|
|
option: option,
|
|
signature: 1,
|
|
inodeToPath: NewInodeToPath(root, 0),
|
|
fhMap: NewFileHandleToInode(),
|
|
fhLockTable: util.NewLockTable[FileHandleId](),
|
|
hardLinkLockTable: util.NewLockTable[string](),
|
|
}
|
|
wfs.metaCache = meta_cache.NewMetaCache(
|
|
filepath.Join(t.TempDir(), "meta"),
|
|
uidGidMapper,
|
|
root,
|
|
false,
|
|
func(path util.FullPath) {
|
|
wfs.inodeToPath.MarkChildrenCached(path)
|
|
},
|
|
func(path util.FullPath) bool {
|
|
return wfs.inodeToPath.IsChildrenCached(path)
|
|
},
|
|
func(meta_cache.EntryInvalidation) {},
|
|
nil,
|
|
)
|
|
wfs.inodeToPath.MarkChildrenCached(root)
|
|
t.Cleanup(func() {
|
|
wfs.metaCache.Shutdown()
|
|
})
|
|
|
|
return wfs, testServer
|
|
}
|
|
|
|
func TestCreateCreatesAndOpensFile(t *testing.T) {
|
|
wfs, testServer := newCreateTestWFS(t)
|
|
|
|
out := &fuse.CreateOut{}
|
|
status := wfs.Create(make(chan struct{}), &fuse.CreateIn{
|
|
InHeader: fuse.InHeader{
|
|
NodeId: 1,
|
|
Caller: fuse.Caller{
|
|
Owner: fuse.Owner{
|
|
Uid: 123,
|
|
Gid: 456,
|
|
},
|
|
},
|
|
},
|
|
Flags: syscall.O_WRONLY | syscall.O_CREAT,
|
|
Mode: 0o640,
|
|
}, "hello.txt", out)
|
|
if status != fuse.OK {
|
|
t.Fatalf("Create status = %v, want OK", status)
|
|
}
|
|
if out.NodeId == 0 {
|
|
t.Fatal("Create returned zero inode")
|
|
}
|
|
if out.Fh == 0 {
|
|
t.Fatal("Create returned zero file handle")
|
|
}
|
|
if out.OpenFlags != 0 {
|
|
t.Fatalf("Create returned OpenFlags = %#x, want 0", out.OpenFlags)
|
|
}
|
|
|
|
fileHandle := wfs.GetHandle(FileHandleId(out.Fh))
|
|
if fileHandle == nil {
|
|
t.Fatal("Create did not register an open file handle")
|
|
}
|
|
if got := fileHandle.FullPath(); got != "/hello.txt" {
|
|
t.Fatalf("FullPath = %q, want %q", got, "/hello.txt")
|
|
}
|
|
|
|
// File creation is deferred to flush time. Trigger a synchronous flush
|
|
// so the CreateEntry gRPC call is sent to the test server.
|
|
if flushStatus := wfs.Flush(make(chan struct{}), &fuse.FlushIn{
|
|
InHeader: fuse.InHeader{
|
|
NodeId: out.NodeId,
|
|
Caller: fuse.Caller{Owner: fuse.Owner{Uid: 123, Gid: 456}},
|
|
},
|
|
Fh: out.Fh,
|
|
}); flushStatus != fuse.OK {
|
|
t.Fatalf("Flush status = %v, want OK", flushStatus)
|
|
}
|
|
|
|
snapshot := testServer.snapshot()
|
|
if snapshot.directory != "/" {
|
|
t.Fatalf("CreateEntry directory = %q, want %q", snapshot.directory, "/")
|
|
}
|
|
if snapshot.name != "hello.txt" {
|
|
t.Fatalf("CreateEntry name = %q, want %q", snapshot.name, "hello.txt")
|
|
}
|
|
if snapshot.uid != 123 || snapshot.gid != 456 {
|
|
t.Fatalf("CreateEntry uid/gid = %d/%d, want 123/456", snapshot.uid, snapshot.gid)
|
|
}
|
|
if snapshot.mode != 0o640 {
|
|
t.Fatalf("CreateEntry mode = %o, want %o", snapshot.mode, 0o640)
|
|
}
|
|
}
|
|
|
|
func TestReleaseFlushesDirtyCreateIfFlushWasSkipped(t *testing.T) {
|
|
wfs, testServer := newCreateTestWFS(t)
|
|
|
|
out := &fuse.CreateOut{}
|
|
status := wfs.Create(make(chan struct{}), &fuse.CreateIn{
|
|
InHeader: fuse.InHeader{
|
|
NodeId: 1,
|
|
Caller: fuse.Caller{
|
|
Owner: fuse.Owner{
|
|
Uid: 123,
|
|
Gid: 456,
|
|
},
|
|
},
|
|
},
|
|
Flags: syscall.O_WRONLY | syscall.O_CREAT,
|
|
Mode: 0o640,
|
|
}, "release_flush.txt", out)
|
|
if status != fuse.OK {
|
|
t.Fatalf("Create status = %v, want OK", status)
|
|
}
|
|
|
|
wfs.Release(make(chan struct{}), &fuse.ReleaseIn{
|
|
InHeader: fuse.InHeader{
|
|
NodeId: out.NodeId,
|
|
Caller: fuse.Caller{Owner: fuse.Owner{Uid: 123, Gid: 456}},
|
|
},
|
|
Fh: out.Fh,
|
|
})
|
|
|
|
snapshot := testServer.snapshot()
|
|
if snapshot.directory != "/" {
|
|
t.Fatalf("CreateEntry directory = %q, want %q", snapshot.directory, "/")
|
|
}
|
|
if snapshot.name != "release_flush.txt" {
|
|
t.Fatalf("CreateEntry name = %q, want %q", snapshot.name, "release_flush.txt")
|
|
}
|
|
if snapshot.uid != 123 || snapshot.gid != 456 {
|
|
t.Fatalf("CreateEntry uid/gid = %d/%d, want 123/456", snapshot.uid, snapshot.gid)
|
|
}
|
|
if snapshot.mode != 0o640 {
|
|
t.Fatalf("CreateEntry mode = %o, want %o", snapshot.mode, 0o640)
|
|
}
|
|
if fh := wfs.GetHandle(FileHandleId(out.Fh)); fh != nil {
|
|
t.Fatal("Release should remove the file handle after fallback flush")
|
|
}
|
|
}
|
|
|
|
func TestTruncateEntryClearsDirtyPagesForOpenHandle(t *testing.T) {
|
|
wfs, _ := newCreateTestWFS(t)
|
|
|
|
fullPath := util.FullPath("/truncate.txt")
|
|
inode := wfs.inodeToPath.Lookup(fullPath, 1, false, false, 0, true)
|
|
entry := &filer_pb.Entry{
|
|
Name: "truncate.txt",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
FileMode: 0o644,
|
|
FileSize: 5,
|
|
Inode: inode,
|
|
Crtime: 1,
|
|
Mtime: 1,
|
|
},
|
|
}
|
|
|
|
fh, _ := wfs.fhMap.AcquireFileHandle(wfs, inode, entry, 0, 0)
|
|
fh.RememberPath(fullPath)
|
|
|
|
if err := fh.dirtyPages.AddPage(0, []byte("hello"), true, time.Now().UnixNano()); err != nil {
|
|
t.Fatalf("AddPage: %v", err)
|
|
}
|
|
oldDirtyPages := fh.dirtyPages
|
|
|
|
truncatedEntry := &filer_pb.Entry{
|
|
Name: "truncate.txt",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
FileMode: 0o644,
|
|
FileSize: 5,
|
|
Inode: inode,
|
|
Crtime: 1,
|
|
Mtime: 1,
|
|
},
|
|
}
|
|
|
|
if status := wfs.truncateEntry(fullPath, truncatedEntry); status != fuse.OK {
|
|
t.Fatalf("truncateEntry status = %v, want OK", status)
|
|
}
|
|
if fh.dirtyPages == oldDirtyPages {
|
|
t.Fatal("truncateEntry should replace the dirtyPages writer for an open handle")
|
|
}
|
|
if got := fh.GetEntry().GetEntry().GetAttributes().GetFileSize(); got != 0 {
|
|
t.Fatalf("file handle size = %d, want 0", got)
|
|
}
|
|
buf := make([]byte, 5)
|
|
if maxStop := fh.dirtyPages.ReadDirtyDataAt(buf, 0, time.Now().UnixNano()); maxStop != 0 {
|
|
t.Fatalf("dirty pages maxStop = %d, want 0 after truncate", maxStop)
|
|
}
|
|
}
|
|
|
|
func TestAccessChecksPermissions(t *testing.T) {
|
|
wfs := newCopyRangeTestWFS()
|
|
oldLookupSupplementaryGroupIDs := lookupSupplementaryGroupIDs
|
|
lookupSupplementaryGroupIDs = func(uint32) ([]string, error) {
|
|
return nil, nil
|
|
}
|
|
clearSupplementaryGroupCache()
|
|
t.Cleanup(func() {
|
|
lookupSupplementaryGroupIDs = oldLookupSupplementaryGroupIDs
|
|
clearSupplementaryGroupCache()
|
|
})
|
|
|
|
fullPath := util.FullPath("/visible.txt")
|
|
inode := wfs.inodeToPath.Lookup(fullPath, 1, false, false, 0, true)
|
|
handle, _ := wfs.fhMap.AcquireFileHandle(wfs, inode, &filer_pb.Entry{
|
|
Name: "visible.txt",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
FileMode: 0o640,
|
|
Uid: 123,
|
|
Gid: 456,
|
|
Inode: inode,
|
|
},
|
|
}, 0, 0)
|
|
handle.RememberPath(fullPath)
|
|
|
|
if status := wfs.Access(make(chan struct{}), &fuse.AccessIn{
|
|
InHeader: fuse.InHeader{
|
|
NodeId: inode,
|
|
Caller: fuse.Caller{
|
|
Owner: fuse.Owner{
|
|
Uid: 123,
|
|
Gid: 999,
|
|
},
|
|
},
|
|
},
|
|
Mask: fuse.R_OK | fuse.W_OK,
|
|
}); status != fuse.OK {
|
|
t.Fatalf("owner Access status = %v, want OK", status)
|
|
}
|
|
|
|
if status := wfs.Access(make(chan struct{}), &fuse.AccessIn{
|
|
InHeader: fuse.InHeader{
|
|
NodeId: inode,
|
|
Caller: fuse.Caller{
|
|
Owner: fuse.Owner{
|
|
Uid: 999,
|
|
Gid: 999,
|
|
},
|
|
},
|
|
},
|
|
Mask: fuse.W_OK,
|
|
}); status != fuse.EACCES {
|
|
t.Fatalf("other-user Access status = %v, want EACCES", status)
|
|
}
|
|
|
|
if got := hasAccess(123, 999, 123, 456, 0o400, fuse.R_OK|fuse.W_OK); got {
|
|
t.Fatal("owner should not get write access from a read-only owner mode")
|
|
}
|
|
|
|
if got := hasAccess(999, 456, 123, 456, 0o040, fuse.R_OK|fuse.W_OK); got {
|
|
t.Fatal("group member should not get write access from a read-only group mode")
|
|
}
|
|
|
|
if got := hasAccess(999, 999, 123, 456, 0o004, fuse.R_OK|fuse.W_OK); got {
|
|
t.Fatal("other users should not get write access from a read-only other mode")
|
|
}
|
|
|
|
if got := hasAccess(0, 0, 123, 456, 0o644, fuse.X_OK); got {
|
|
t.Fatal("root should not get execute access when no execute bit is set")
|
|
}
|
|
|
|
if got := hasAccess(0, 0, 123, 456, 0o755, fuse.R_OK|fuse.X_OK); !got {
|
|
t.Fatal("root should get execute access when at least one execute bit is set")
|
|
}
|
|
}
|
|
|
|
func TestHasAccessUsesSupplementaryGroups(t *testing.T) {
|
|
oldLookupSupplementaryGroupIDs := lookupSupplementaryGroupIDs
|
|
lookupSupplementaryGroupIDs = func(uint32) ([]string, error) {
|
|
return []string{"456"}, nil
|
|
}
|
|
clearSupplementaryGroupCache()
|
|
t.Cleanup(func() {
|
|
lookupSupplementaryGroupIDs = oldLookupSupplementaryGroupIDs
|
|
clearSupplementaryGroupCache()
|
|
})
|
|
|
|
if got := hasAccess(999, 999, 123, 456, 0o060, fuse.R_OK|fuse.W_OK); !got {
|
|
t.Fatal("supplementary group membership should grant matching group permissions")
|
|
}
|
|
}
|
|
|
|
func TestSupplementaryGroupCaching(t *testing.T) {
|
|
callCount := 0
|
|
oldLookupSupplementaryGroupIDs := lookupSupplementaryGroupIDs
|
|
lookupSupplementaryGroupIDs = func(uid uint32) ([]string, error) {
|
|
callCount++
|
|
return []string{"456"}, nil
|
|
}
|
|
clearSupplementaryGroupCache()
|
|
t.Cleanup(func() {
|
|
lookupSupplementaryGroupIDs = oldLookupSupplementaryGroupIDs
|
|
clearSupplementaryGroupCache()
|
|
})
|
|
|
|
cachedLookupSupplementaryGroupIDs(999)
|
|
cachedLookupSupplementaryGroupIDs(999)
|
|
cachedLookupSupplementaryGroupIDs(999)
|
|
|
|
if callCount != 1 {
|
|
t.Fatalf("lookupSupplementaryGroupIDs called %d times, expected 1 (cache should prevent repeated calls)", callCount)
|
|
}
|
|
|
|
cachedLookupSupplementaryGroupIDs(1000)
|
|
if callCount != 2 {
|
|
t.Fatalf("lookupSupplementaryGroupIDs called %d times after different UID, expected 2", callCount)
|
|
}
|
|
}
|
|
|
|
func TestSupplementaryGroupCacheExpiry(t *testing.T) {
|
|
callCount := 0
|
|
oldLookupSupplementaryGroupIDs := lookupSupplementaryGroupIDs
|
|
oldTTL := supplementaryGroupCacheTTL
|
|
supplementaryGroupCacheTTL = 0
|
|
lookupSupplementaryGroupIDs = func(uid uint32) ([]string, error) {
|
|
callCount++
|
|
return []string{"456"}, nil
|
|
}
|
|
clearSupplementaryGroupCache()
|
|
t.Cleanup(func() {
|
|
lookupSupplementaryGroupIDs = oldLookupSupplementaryGroupIDs
|
|
supplementaryGroupCacheTTL = oldTTL
|
|
clearSupplementaryGroupCache()
|
|
})
|
|
|
|
cachedLookupSupplementaryGroupIDs(999)
|
|
if callCount != 1 {
|
|
t.Fatalf("Expected 1 lookup on first call, got %d", callCount)
|
|
}
|
|
|
|
cachedLookupSupplementaryGroupIDs(999)
|
|
if callCount != 2 {
|
|
t.Fatalf("Expected 2 lookups after TTL expiry, got %d", callCount)
|
|
}
|
|
}
|
|
|
|
func TestCreateExistingFileIgnoresQuotaPreflight(t *testing.T) {
|
|
wfs, _ := newCreateTestWFS(t)
|
|
wfs.option.Quota = 1
|
|
wfs.IsOverQuota = true
|
|
|
|
entry := &filer_pb.Entry{
|
|
Name: "existing.txt",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
FileMode: 0o644,
|
|
FileSize: 7,
|
|
Inode: 101,
|
|
Crtime: 1,
|
|
Mtime: 1,
|
|
Uid: 123,
|
|
Gid: 456,
|
|
},
|
|
}
|
|
if err := wfs.metaCache.InsertEntry(context.Background(), filer.FromPbEntry("/", entry), 0); err != nil {
|
|
t.Fatalf("InsertEntry: %v", err)
|
|
}
|
|
wfs.inodeToPath.Lookup(util.FullPath("/existing.txt"), entry.Attributes.Crtime, false, false, entry.Attributes.Inode, true)
|
|
|
|
out := &fuse.CreateOut{}
|
|
status := wfs.Create(make(chan struct{}), &fuse.CreateIn{
|
|
InHeader: fuse.InHeader{
|
|
NodeId: 1,
|
|
Caller: fuse.Caller{
|
|
Owner: fuse.Owner{
|
|
Uid: 123,
|
|
Gid: 456,
|
|
},
|
|
},
|
|
},
|
|
Flags: syscall.O_WRONLY | syscall.O_CREAT | syscall.O_EXCL,
|
|
Mode: 0o644,
|
|
}, "existing.txt", out)
|
|
if status != fuse.Status(syscall.EEXIST) {
|
|
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), 0); 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(context.Background(), 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(context.Background(), util.FullPath("/ghost"), "f.txt", 0o644, 99, 100, 0, false, false); code == fuse.OK {
|
|
t.Fatal("createRegularFile returned OK for a missing parent directory")
|
|
}
|
|
}
|