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, meta_cache.DefaultListingCacheEntries, ) 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(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") } }