Files
seaweedfs/weed/mount/weedfs_filehandle.go
T
10f8b2d5b1 mount: guard peer-truncate gaps, stale append EOF, and terminal writeback refusal (#12630)
* mount: guard append gaps over concurrent content merges

* mount: fence a writeback inode when its async publication is abandoned

A drain that gives up on a non-precondition error left the token clear, so a
reopened writeback handle could publish past the stored EOF.

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

* mount: fence a writeback inode when its async data flush fails

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

* mount: fence the inode when its last handle drops uncommitted content

A synchronous release whose final flush fails destroys the handle's dirty
pages, while under writeback the kernel EOF already counts them.

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

---------

Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 01:13:37 -08:00

216 lines
8.5 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 {
if fhToRelease.dirtyMetadata && !fhToRelease.isDeleted {
(&contentPublication{failure: fhToRelease.writebackContentError}).abandon()
}
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)
}