diff --git a/go.mod b/go.mod index 60aa3f032..65663e221 100644 --- a/go.mod +++ b/go.mod @@ -149,7 +149,7 @@ require ( github.com/rdleal/intervalst v1.5.0 github.com/redis/go-redis/v9 v9.18.0 github.com/schollz/progressbar/v3 v3.19.0 - github.com/seaweedfs/go-fuse/v2 v2.9.1 + github.com/seaweedfs/go-fuse/v2 v2.9.2 github.com/shirou/gopsutil/v4 v4.26.2 github.com/tarantool/go-tarantool/v2 v2.4.2 github.com/testcontainers/testcontainers-go v0.40.0 diff --git a/go.sum b/go.sum index a56204a96..ddcab0f1f 100644 --- a/go.sum +++ b/go.sum @@ -1840,6 +1840,8 @@ github.com/seaweedfs/cockroachdb-parser v0.0.0-20260225204133-2f342c5ea564 h1:Tg github.com/seaweedfs/cockroachdb-parser v0.0.0-20260225204133-2f342c5ea564/go.mod h1:JSKCh6uCHBz91lQYFYHCyTrSVIPge4SUFVn28iwMNB0= github.com/seaweedfs/go-fuse/v2 v2.9.1 h1:gnKmfrKreCRGJmekGz5WMnNZqXEf9s9+V2hdWQdvx88= github.com/seaweedfs/go-fuse/v2 v2.9.1/go.mod h1:zABdmWEa6A0bwaBeEOBUeUkGIZlxUhcdv+V1Dcc/U/I= +github.com/seaweedfs/go-fuse/v2 v2.9.2 h1:IfP/yFjLGO4rALcJY2Gb39PlebHxLnj7dkIiQAjFres= +github.com/seaweedfs/go-fuse/v2 v2.9.2/go.mod h1:zABdmWEa6A0bwaBeEOBUeUkGIZlxUhcdv+V1Dcc/U/I= github.com/seaweedfs/goexif v1.0.3 h1:ve/OjI7dxPW8X9YQsv3JuVMaxEyF9Rvfd04ouL+Bz30= github.com/seaweedfs/goexif v1.0.3/go.mod h1:Oni780Z236sXpIQzk1XoJlTwqrJ02smEin9zQeff7Fk= github.com/seaweedfs/raft v1.1.7 h1:3mVJZ2p4rdvBtbbrHROPjYKtH+q5qjMBc56G6VRu1kA= diff --git a/weed/mount/filehandle.go b/weed/mount/filehandle.go index 135b1807e..6ac75f532 100644 --- a/weed/mount/filehandle.go +++ b/weed/mount/filehandle.go @@ -112,6 +112,13 @@ func (fh *FileHandle) SetEntry(entry *filer_pb.Entry) { fh.invalidateChunkCache() } +func (fh *FileHandle) ResetDirtyPages() { + fh.dirtyPages.Destroy() + fh.dirtyPages = newPageWriter(fh, fh.wfs.option.ChunkSizeLimit) + fh.dirtyMetadata = false + fh.contentType = "" +} + func (fh *FileHandle) UpdateEntry(fn func(entry *filer_pb.Entry)) *filer_pb.Entry { result := fh.entry.UpdateEntry(fn) diff --git a/weed/mount/weedfs_access.go b/weed/mount/weedfs_access.go new file mode 100644 index 000000000..fede73f4e --- /dev/null +++ b/weed/mount/weedfs_access.go @@ -0,0 +1,99 @@ +package mount + +import ( + "os/user" + "strconv" + "syscall" + + "github.com/seaweedfs/go-fuse/v2/fuse" + "github.com/seaweedfs/seaweedfs/weed/glog" +) + +var 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 +} + +/** + * 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) { + _, _, entry, code := wfs.maybeReadEntry(input.NodeId) + if code != fuse.OK { + return code + } + if entry == nil || entry.Attributes == nil { + return fuse.EIO + } + if hasAccess(input.Uid, input.Gid, entry.Attributes.Uid, entry.Attributes.Gid, 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 := lookupSupplementaryGroupIDs(callerUid) + if err != nil { + // Cannot determine group membership; require both group and + // other permission classes to satisfy the mask so we never + // overgrant when the lookup fails. + groupMatch := ((perm >> 3) & mask) == mask + otherMatch := (perm & mask) == mask + return groupMatch && otherMatch + } + 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 +} + +// openFlagsToAccessMask converts open(2) flags to an access permission mask. +func openFlagsToAccessMask(flags uint32) uint32 { + switch flags & uint32(syscall.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 + } +} diff --git a/weed/mount/weedfs_file_io.go b/weed/mount/weedfs_file_io.go index 47a9d6df7..cd63e6805 100644 --- a/weed/mount/weedfs_file_io.go +++ b/weed/mount/weedfs_file_io.go @@ -1,9 +1,6 @@ package mount -import ( - "github.com/seaweedfs/go-fuse/v2/fuse" - "github.com/seaweedfs/seaweedfs/weed/glog" -) +import "github.com/seaweedfs/go-fuse/v2/fuse" /** * Open a file @@ -66,15 +63,7 @@ func (wfs *WFS) Open(cancel <-chan struct{}, in *fuse.OpenIn, out *fuse.OpenOut) fileHandle, status = wfs.AcquireHandle(in.NodeId, in.Flags, in.Uid, in.Gid) if status == fuse.OK { out.Fh = uint64(fileHandle.fh) - out.OpenFlags = in.Flags - if wfs.option.IsMacOs { - // remove the direct_io flag, as it is not well-supported on macOS - // https://code.google.com/archive/p/macfuse/wikis/OPTIONS.wiki recommended to avoid the direct_io flag - if in.Flags&fuse.FOPEN_DIRECT_IO != 0 { - glog.V(4).Infof("macfuse direct_io mode %v => false\n", in.Flags&fuse.FOPEN_DIRECT_IO != 0) - out.OpenFlags &^= fuse.FOPEN_DIRECT_IO - } - } + out.OpenFlags = 0 // TODO https://github.com/libfuse/libfuse/blob/master/include/fuse_common.h#L64 } return status diff --git a/weed/mount/weedfs_file_mkrm.go b/weed/mount/weedfs_file_mkrm.go index 17f4e1ac8..122834573 100644 --- a/weed/mount/weedfs_file_mkrm.go +++ b/weed/mount/weedfs_file_mkrm.go @@ -8,6 +8,7 @@ import ( "github.com/seaweedfs/go-fuse/v2/fuse" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/util" ) /** @@ -21,92 +22,117 @@ import ( * will be called instead. */ func (wfs *WFS) Create(cancel <-chan struct{}, in *fuse.CreateIn, name string, out *fuse.CreateOut) (code fuse.Status) { - // if implemented, need to use - // inode := wfs.inodeToPath.Lookup(entryFullPath) - // to ensure nlookup counter - return fuse.ENOSYS -} - -/** Create a file node - * - * This is called for creation of all non-directory, non-symlink - * nodes. If the filesystem defines a create() method, then for - * regular files that will be called instead. - */ -func (wfs *WFS) Mknod(cancel <-chan struct{}, in *fuse.MknodIn, name string, out *fuse.EntryOut) (code fuse.Status) { - - if wfs.IsOverQuotaWithUncommitted() { - return fuse.Status(syscall.ENOSPC) - } - if s := checkName(name); s != fuse.OK { return s } dirFullPath, code := wfs.inodeToPath.GetPath(in.NodeId) if code != fuse.OK { - return + return code } entryFullPath := dirFullPath.Child(name) - fileMode := toOsFileMode(in.Mode) - now := time.Now().Unix() - inode := wfs.inodeToPath.AllocateInode(entryFullPath, now) - - newEntry := &filer_pb.Entry{ - Name: name, - IsDirectory: false, - Attributes: &filer_pb.FuseAttributes{ - Mtime: now, - Crtime: now, - FileMode: uint32(fileMode), - Uid: in.Uid, - Gid: in.Gid, - TtlSec: wfs.option.TtlSec, - Rdev: in.Rdev, - Inode: inode, - }, - } + var inode uint64 - err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { + newEntry, code := wfs.maybeLoadEntry(entryFullPath) + if code == fuse.OK { + if newEntry == nil || newEntry.Attributes == nil { + return fuse.EIO + } + if in.Flags&syscall.O_EXCL != 0 { + return fuse.Status(syscall.EEXIST) + } + inode = wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, false, len(newEntry.HardLinkId) > 0, newEntry.Attributes.Inode, true) + fileHandle, status := wfs.AcquireHandle(inode, in.Flags, in.Uid, in.Gid) + if status != fuse.OK { + return status + } + if in.Flags&syscall.O_TRUNC != 0 && in.Flags&fuse.O_ANYWRITE != 0 { + if code = wfs.truncateEntry(entryFullPath, newEntry); code != fuse.OK { + wfs.ReleaseHandle(fileHandle.fh) + return code + } + newEntry = fileHandle.GetEntry().GetEntry() + } - wfs.mapPbIdFromLocalToFiler(newEntry) - defer wfs.mapPbIdFromFilerToLocal(newEntry) + wfs.outputPbEntry(&out.EntryOut, inode, newEntry) + out.Fh = uint64(fileHandle.fh) + out.OpenFlags = 0 + return fuse.OK + } + if code != fuse.ENOENT { + return code + } - request := &filer_pb.CreateEntryRequest{ - Directory: string(dirFullPath), - Entry: newEntry, - Signatures: []int32{wfs.signature}, - SkipCheckParentDirectory: true, + inode, newEntry, code = wfs.createRegularFile(dirFullPath, name, in.Mode, in.Uid, in.Gid, 0) + if code == fuse.Status(syscall.EEXIST) && in.Flags&syscall.O_EXCL == 0 { + // Race: another process created the file between our check and create. + // Reopen the winner's entry. + newEntry, code = wfs.maybeLoadEntry(entryFullPath) + if code != fuse.OK { + return code } - - glog.V(1).Infof("mknod: %v", request) - resp, err := filer_pb.CreateEntryWithResponse(context.Background(), client, request) - if err != nil { - glog.V(0).Infof("mknod %s: %v", entryFullPath, err) - return err + if newEntry == nil || newEntry.Attributes == nil { + return fuse.EIO } - - event := resp.GetMetadataEvent() - if event == nil { - event = metadataCreateEvent(string(dirFullPath), newEntry) + inode = wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, false, len(newEntry.HardLinkId) > 0, newEntry.Attributes.Inode, true) + fileHandle, status := wfs.AcquireHandle(inode, in.Flags, in.Uid, in.Gid) + if status != fuse.OK { + return status } - if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil { - glog.Warningf("mknod %s: best-effort metadata apply failed: %v", entryFullPath, applyErr) - wfs.inodeToPath.InvalidateChildrenCache(dirFullPath) + if in.Flags&syscall.O_TRUNC != 0 && in.Flags&fuse.O_ANYWRITE != 0 { + if code = wfs.truncateEntry(entryFullPath, newEntry); code != fuse.OK { + wfs.ReleaseHandle(fileHandle.fh) + return code + } + newEntry = fileHandle.GetEntry().GetEntry() } - wfs.inodeToPath.TouchDirectory(dirFullPath) + wfs.outputPbEntry(&out.EntryOut, inode, newEntry) + out.Fh = uint64(fileHandle.fh) + out.OpenFlags = 0 + return fuse.OK + } else if code != fuse.OK { + return code + } else { + inode = wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, false, false, inode, true) + } - return nil - }) + wfs.outputPbEntry(&out.EntryOut, inode, newEntry) - glog.V(3).Infof("mknod %s: %v", entryFullPath, err) + fileHandle, status := wfs.AcquireHandle(inode, in.Flags, in.Uid, in.Gid) + if status != fuse.OK { + return status + } + out.Fh = uint64(fileHandle.fh) + out.OpenFlags = 0 - if err != nil { - return fuse.EIO + return fuse.OK +} + +/** Create a file node + * + * This is called for creation of all non-directory, non-symlink + * nodes. If the filesystem defines a create() method, then for + * regular files that will be called instead. + */ +func (wfs *WFS) Mknod(cancel <-chan struct{}, in *fuse.MknodIn, name string, out *fuse.EntryOut) (code fuse.Status) { + + if s := checkName(name); s != fuse.OK { + return s + } + + dirFullPath, code := wfs.inodeToPath.GetPath(in.NodeId) + if code != fuse.OK { + return + } + + inode, newEntry, code := wfs.createRegularFile(dirFullPath, name, in.Mode, in.Uid, in.Gid, in.Rdev) + if code != fuse.OK { + return code } // this is to increase nlookup counter + entryFullPath := dirFullPath.Child(name) inode = wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, false, false, inode, true) wfs.outputPbEntry(out, inode, newEntry) @@ -175,3 +201,114 @@ func (wfs *WFS) Unlink(cancel <-chan struct{}, header *fuse.InHeader, name strin return fuse.OK } + +func (wfs *WFS) createRegularFile(dirFullPath util.FullPath, name string, mode uint32, uid, gid, rdev uint32) (inode uint64, newEntry *filer_pb.Entry, code fuse.Status) { + if wfs.IsOverQuotaWithUncommitted() { + return 0, nil, fuse.Status(syscall.ENOSPC) + } + + // Verify write+search permission on the parent directory. + parentEntry, parentStatus := wfs.maybeLoadEntry(dirFullPath) + if parentStatus != fuse.OK { + return 0, nil, parentStatus + } + if parentEntry == nil || parentEntry.Attributes == nil { + return 0, nil, fuse.EIO + } + if !hasAccess(uid, gid, parentEntry.Attributes.Uid, parentEntry.Attributes.Gid, parentEntry.Attributes.FileMode, fuse.W_OK|fuse.X_OK) { + return 0, nil, fuse.Status(syscall.EACCES) + } + + entryFullPath := dirFullPath.Child(name) + if _, status := wfs.maybeLoadEntry(entryFullPath); status == fuse.OK { + return 0, nil, fuse.Status(syscall.EEXIST) + } else if status != fuse.ENOENT { + return 0, nil, status + } + fileMode := toOsFileMode(mode) + now := time.Now().Unix() + inode = wfs.inodeToPath.AllocateInode(entryFullPath, now) + + newEntry = &filer_pb.Entry{ + Name: name, + IsDirectory: false, + Attributes: &filer_pb.FuseAttributes{ + Mtime: now, + Crtime: now, + FileMode: uint32(fileMode), + Uid: uid, + Gid: gid, + TtlSec: wfs.option.TtlSec, + Rdev: rdev, + Inode: inode, + }, + } + + err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { + wfs.mapPbIdFromLocalToFiler(newEntry) + defer wfs.mapPbIdFromFilerToLocal(newEntry) + + request := &filer_pb.CreateEntryRequest{ + Directory: string(dirFullPath), + Entry: newEntry, + Signatures: []int32{wfs.signature}, + SkipCheckParentDirectory: true, + } + + glog.V(1).Infof("createFile: %v", request) + resp, err := filer_pb.CreateEntryWithResponse(context.Background(), client, request) + if err != nil { + glog.V(0).Infof("createFile %s: %v", entryFullPath, err) + return err + } + + event := resp.GetMetadataEvent() + if event == nil { + event = metadataCreateEvent(string(dirFullPath), newEntry) + } + if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil { + glog.Warningf("createFile %s: best-effort metadata apply failed: %v", entryFullPath, applyErr) + wfs.inodeToPath.InvalidateChildrenCache(dirFullPath) + } + wfs.inodeToPath.TouchDirectory(dirFullPath) + + return nil + }) + + glog.V(3).Infof("createFile %s: %v", entryFullPath, err) + + if err != nil { + return 0, nil, grpcErrorToFuseStatus(err) + } + + return inode, newEntry, fuse.OK +} + +func (wfs *WFS) truncateEntry(entryFullPath util.FullPath, entry *filer_pb.Entry) fuse.Status { + if entry == nil { + return fuse.EIO + } + if entry.Attributes == nil { + entry.Attributes = &filer_pb.FuseAttributes{} + } + + entry.Content = nil + entry.Chunks = nil + entry.Attributes.FileSize = 0 + entry.Attributes.Mtime = time.Now().Unix() + + if code := wfs.saveEntry(entryFullPath, entry); code != fuse.OK { + return code + } + + if inode, found := wfs.inodeToPath.GetInode(entryFullPath); found { + if fh, fhFound := wfs.fhMap.FindFileHandle(inode); fhFound { + fhActiveLock := fh.wfs.fhLockTable.AcquireLock("truncateEntry", fh.fh, util.ExclusiveLock) + fh.ResetDirtyPages() + fh.SetEntry(entry) + fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock) + } + } + + return fuse.OK +} diff --git a/weed/mount/weedfs_file_mkrm_test.go b/weed/mount/weedfs_file_mkrm_test.go new file mode 100644 index 000000000..36a7cdf0a --- /dev/null +++ b/weed/mount/weedfs_file_mkrm_test.go @@ -0,0 +1,370 @@ +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](), + } + wfs.metaCache = meta_cache.NewMetaCache( + filepath.Join(t.TempDir(), "meta"), + uidGidMapper, + root, + func(path util.FullPath) { + wfs.inodeToPath.MarkChildrenCached(path) + }, + func(path util.FullPath) bool { + return wfs.inodeToPath.IsChildrenCached(path) + }, + func(util.FullPath, *filer_pb.Entry) {}, + 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") + } + + 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 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) + 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 + } + t.Cleanup(func() { + lookupSupplementaryGroupIDs = oldLookupSupplementaryGroupIDs + }) + + 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, + }, + }) + 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 + } + t.Cleanup(func() { + lookupSupplementaryGroupIDs = oldLookupSupplementaryGroupIDs + }) + + 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 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)); 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) + } +} diff --git a/weed/mount/weedfs_filehandle.go b/weed/mount/weedfs_filehandle.go index 47700eb1e..155a97cd8 100644 --- a/weed/mount/weedfs_filehandle.go +++ b/weed/mount/weedfs_filehandle.go @@ -21,6 +21,12 @@ 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 { + if mask := openFlagsToAccessMask(flags); mask != 0 && !hasAccess(uid, gid, entry.Attributes.Uid, entry.Attributes.Gid, entry.Attributes.FileMode, mask) { + return nil, fuse.EACCES + } + } // need to AcquireFileHandle again to ensure correct handle counter fileHandle = wfs.fhMap.AcquireFileHandle(wfs, inode, entry) fileHandle.RememberPath(path) diff --git a/weed/mount/weedfs_unsupported.go b/weed/mount/weedfs_unsupported.go index eb9afbf04..7a8c0af37 100644 --- a/weed/mount/weedfs_unsupported.go +++ b/weed/mount/weedfs_unsupported.go @@ -15,16 +15,3 @@ import "github.com/seaweedfs/go-fuse/v2/fuse" func (wfs *WFS) Fallocate(cancel <-chan struct{}, in *fuse.FallocateIn) (code fuse.Status) { return fuse.ENOSYS } - -/** - * 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) { - return fuse.ENOSYS -}