mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-31 09:20:50 +01:00
* mount: refuse to attach opens to a draining file handle With the writeback cache, releasing the last open of a file leaves the shared FileHandle in the map at counter zero while an async flush drains it; the worker removes the map entries once the flush commits. AcquireHandle waits for a pending flush and then attaches, but the two steps are not atomic: a release can slip in between, drop the counter to zero, and queue the drain while the opener goes on to mint a new file-handle id on the draining handle. When the drain completes, RemoveFileHandle deletes every fh id for the inode -- including the one just minted. The kernel still holds that id, so its next FUSE_READ gets ENOENT from GetHandle; the failed read leaves the folio not uptodate and the kernel surfaces EIO to userspace. This was the flaky TestWritebackCacheConcurrentMixedOps failure. AcquireFileHandle now refuses to attach when the shared handle has reached a zero counter and reports it as draining. AcquireHandle and the Create path retry the whole wait-resolve-attach sequence; the next pass blocks on the pending flush (flag registration and the counter drop share pendingAsyncFlushMu) and builds a fresh handle from the post-flush entry. The DLM-failure release in AcquireHandle also goes through ReleaseHandle so it cannot strand a counter-zero handle outside the drain bookkeeping. Removes the refreshEntryForRead retry from #12206, which worked around the symptom by refetching the entry after the fh id was already gone. Test: TestAcquireFileHandleRefusesDrainingHandle covers the refusal and fresh-handle path; TestWritebackCacheConcurrentMixedOps and an amplified concurrent-overwrite repro run clean. * mount: keep refresh-on-error retry for stale-chunk reads
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")
|
|
}
|
|
}
|