Files
seaweedfs/weed/mount/weedfs_filehandle.go
T
Chris LuandClaude Opus 5.5 6ad4b07598 mount: scope and order the final-release writeback fence (#12651)
* mount: fence a dropped handle only when it holds unpublished content

dirtyMetadata is also set by chmod, xattr and fallocate, so a failed
metadata-only close refused every later write of the inode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: fence the inode before its last handle leaves the handle map

A reopen between the map removal and the fence could publish at the
stale kernel EOF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: fence a failed metadata flush only when the handle holds content

A refused synchronous flush or an abandoned writeback metadata flush of a
handle with only chmod, xattr or fallocate changes fenced the inode, so
later writes were refused until FORGET.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: count a size change on an open handle as unpublished content

A truncate or extend through an open handle changes the entry's size and
chunks, but it set only dirtyMetadata. A failed close then skipped the
final-release fence, so a reopen before FORGET could publish against the
untruncated entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: mark copied content dirty before each page of a streaming copy

A copy that failed or was cancelled after earlier pages were accepted left
dirtyContent false, so the final-release fence skipped that content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: mark copied content dirty only after a page is accepted

A copy whose first page is rejected leaves the destination unchanged and
the kernel EOF where it was, so fencing the inode on release only blocked
later writes until FORGET.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: skip the server-side copy while the destination holds unpublished content

A whole-file server copy clears dirtyContent when it commits, so a page a
cancelled streaming copy left behind would lose its release fence. Fall
back to the streaming copy instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: skip the server-side copy while the source holds unpublished content

The filer copies the stored source, so pages still pending on the source
handle would be missing from the destination. The streaming copy reads
them through the dirty pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: check the publication fence before a server-side whole-file copy

The filer copy publishes the destination without going through the
flush-time guard, so a fenced writeback inode could still take new
content before FORGET. Fall back to the streaming copy, whose flush is
guarded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 10:47:33 -08:00

213 lines
8.4 KiB
Go

package mount
import (
"context"
"fmt"
"syscall"
"github.com/seaweedfs/go-fuse/v2/fuse"
"github.com/seaweedfs/seaweedfs/weed/cluster/lock_manager"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// AcquireHandle returns the shared handle for inode plus this open's own fh
// value — the id handed to the kernel and seen again at RELEASE.
func (wfs *WFS) AcquireHandle(ctx context.Context, inode uint64, flags, uid, gid uint32) (fileHandle *FileHandle, openFh FileHandleId, status fuse.Status) {
var path util.FullPath
for {
// If there is an in-flight async flush for this inode, wait for it to
// complete before reopening. Otherwise the new handle would be built
// from pre-close filer metadata and its next flush could overwrite the
// data that was just written asynchronously.
if !wfs.waitForPendingAsyncFlush(ctx, inode) {
return nil, 0, fuse.EINTR
}
var pathStatus fuse.Status
path, pathStatus = wfs.inodeToPath.GetPath(inode)
if pathStatus != fuse.OK {
return nil, 0, pathStatus
}
// A fresh lookup returns the entry with the filer log position it
// reflects; the handle takes that entry and version as one decision in
// AcquireFileHandle, so a slower concurrent opener cannot
// overwrite a newer install. An existing handle keeps its own entry and
// version — they did not come from this lookup.
var entry *filer_pb.Entry
var entryVersionTsNs entryVersion
freshLookup := false
if existingFh, found := wfs.fhMap.FindFileHandle(inode); found {
entry = existingFh.UpdateEntry(func(entry *filer_pb.Entry) {
if entry != nil && existingFh.entry.Attributes == nil {
entry.Attributes = &filer_pb.FuseAttributes{}
}
})
entryVersionTsNs = entryVersion{tsNs: existingFh.entryVersionTsNs.Load(), signature: existingFh.entryVersionSignature.Load()}
} else {
entry, entryVersionTsNs, status = wfs.maybeLoadEntry(ctx, path)
if status != fuse.OK {
return nil, 0, status
}
freshLookup = true
}
if wormEnforced, _ := wfs.wormEnforcedForEntry(path, entry); wormEnforced && flags&fuse.O_ANYWRITE != 0 {
return nil, 0, fuse.EPERM
}
// Check unix permission bits for the requested access mode. With
// default_permissions the kernel already enforced them before this
// open, so the check (and its supplementary-group lookup) is redundant.
if !wfs.option.DefaultPermissions && entry != nil && entry.Attributes != nil {
fileUid, fileGid := entry.Attributes.Uid, entry.Attributes.Gid
if wfs.option.UidGidMapper != nil {
fileUid, fileGid = wfs.option.UidGidMapper.FilerToLocal(fileUid, fileGid)
}
if mask := openFlagsToAccessMask(flags); mask != 0 && !hasAccess(uid, gid, fileUid, fileGid, entry.Attributes.FileMode, mask) {
return nil, 0, fuse.EACCES
}
}
// need to AcquireFileHandle again to ensure correct handle counter
var existed, draining bool
fileHandle, openFh, existed, draining = wfs.fhMap.AcquireFileHandle(wfs, inode, entry, entryVersionTsNs.tsNs, entryVersionTsNs.signature, flags&syscall.O_APPEND != 0)
if draining {
// A RELEASE slipped between waitForPendingAsyncFlush and this
// attach: the handle's last open closed and its deferred flush
// is draining. The pending-flush flag is registered under the
// same mutex that dropped the counter, so the next iteration
// blocks on the drain (or finds the handle already removed)
// and rebuilds state from post-flush metadata.
continue
}
fileHandle.RememberPath(path)
if existed && freshLookup {
// Another opener created the handle while our lookup was in flight;
// install only state that provably improves on what it holds.
fileHandle.installAckedEntry(entry, entryVersionTsNs.tsNs, entryVersionTsNs.signature)
}
break
}
// Acquire distributed lock for write opens. The lock is held with
// auto-renewal until the file handle is released (close).
// Use the filer path as the lock key since inode numbers are
// assigned per-mount and differ across mount instances.
if wfs.lockClient != nil && flags&fuse.O_ANYWRITE != 0 && fileHandle.dlmLock == nil {
owner := fmt.Sprintf("mount-%d", wfs.signature)
fileHandle.dlmLock = wfs.lockClient.NewBlockingLongLivedLock(
string(path), owner, lock_manager.LiveLockTTL,
)
if fileHandle.dlmLock == nil {
// No lock server is reachable: proceeding would silently drop
// cross-mount write serialization, so fail the open instead.
wfs.ReleaseHandle(openFh)
return nil, 0, fuse.Status(syscall.EAGAIN)
}
glog.V(1).Infof("DLM lock acquired for %s", path)
// The previous holder's flush may not have reached the local cache,
// and the lock orders this writer after it: take the filer's entry.
if entry, version, err := wfs.lookupFilerEntry(ctx, path); err == nil {
fileHandle.installAckedEntry(entry, version.tsNs, version.signature)
}
}
return fileHandle, openFh, fuse.OK
}
// lookupFilerEntry reads path from the filer, bypassing the local cache, in
// local ids and with the log position the answer reflects.
func (wfs *WFS) lookupFilerEntry(ctx context.Context, path util.FullPath) (*filer_pb.Entry, entryVersion, error) {
entry, version, err := wfs.lookupOwnerEntry(ctx, path)
if err != nil {
return nil, entryVersion{}, err
}
if entry.Attributes == nil {
entry.Attributes = &filer_pb.FuseAttributes{}
}
wfs.mapPbIdFromFilerToLocal(entry)
return entry, version, nil
}
// lookupOwnerEntry reads path in filer ids from the filer that checks path's
// write conditions, since another filer may lag it.
func (wfs *WFS) lookupOwnerEntry(ctx context.Context, path util.FullPath) (*filer_pb.Entry, entryVersion, error) {
dir, name := path.DirAndName()
var entry *filer_pb.Entry
var version entryVersion
lookup := func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
resp, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{Directory: dir, Name: name})
if err != nil {
return err
}
entry, version = resp.Entry, entryVersion{tsNs: resp.LogTsNs, signature: resp.LogSignature}
return nil
}
var err error
if owner := wfs.entryWriteOwner(path); owner != "" {
err = wfs.callFiler(ctx, false, wfs.filerRPCWait(), owner.ToGrpcAddress(), lookup)
} else {
err = wfs.withFilerClient(ctx, false, wfs.filerRPCWait(), lookup)
}
if err != nil {
return nil, entryVersion{}, err
}
return entry, version, nil
}
// ReleaseHandle is called from FUSE Release. For handles with a pending
// async flush, the map removal and the pendingAsyncFlush registration are
// done under a single lock hold so that a concurrent AcquireHandle cannot
// slip through the gap between the two (P1-1 TOCTOU fix).
//
// The handle intentionally stays in fhMap during the drain so that rename
// and unlink can still find it via FindFileHandle (P1-2 fix). It is
// removed from fhMap only after the drain completes (RemoveFileHandle).
func (wfs *WFS) ReleaseHandle(handleId FileHandleId) {
// Hold pendingAsyncFlushMu across the counter decrement and the
// pending-flush registration. Lock ordering: pendingAsyncFlushMu → fhMap.
wfs.pendingAsyncFlushMu.Lock()
fhToRelease := wfs.fhMap.ReleaseByHandle(handleId)
if fhToRelease != nil && fhToRelease.asyncFlushPending {
done := make(chan struct{})
wfs.pendingAsyncFlush[fhToRelease.inode] = done
// Add(1) while holding the mutex so WaitForAsyncFlush cannot
// observe a zero counter and close the channel before we send.
wfs.asyncFlushWg.Add(1)
wfs.pendingAsyncFlushMu.Unlock()
// Send after unlock to avoid deadlock — workers acquire the
// same mutex during cleanup.
wfs.asyncFlushCh <- &asyncFlushItem{fh: fhToRelease, done: done}
return
}
wfs.pendingAsyncFlushMu.Unlock()
if fhToRelease != nil {
fhToRelease.ReleaseHandle()
}
}
// waitForPendingAsyncFlush blocks until any in-flight async flush for
// the given inode completes. Called from AcquireHandle before building
// new handle state, so the filer metadata reflects the flushed data.
func (wfs *WFS) waitForPendingAsyncFlush(ctx context.Context, inode uint64) bool {
wfs.pendingAsyncFlushMu.Lock()
done, found := wfs.pendingAsyncFlush[inode]
wfs.pendingAsyncFlushMu.Unlock()
if !found {
return true
}
glog.V(3).Infof("waitForPendingAsyncFlush: waiting for inode %d", inode)
select {
case <-done:
return true
case <-ctx.Done():
return false
}
}
func (wfs *WFS) GetHandle(handleId FileHandleId) *FileHandle {
return wfs.fhMap.GetFileHandle(handleId)
}