From 2d719fb4ea8f51d154818262d17ba5926f27c92e Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Thu, 12 Nov 2026 17:34:03 -0800 Subject: [PATCH] mount: check the creation time when the base has no inode An entry without an inode or hard link id matched any replacement at the same path, so a stale handle could merge into a different file. Without an inode the merge now needs the same creation time. The filer keeps it across updates; only a synthesized base has neither and still merges. Co-Authored-By: Claude Opus 5.5 --- weed/mount/content_flush_condition_test.go | 12 ++++++++++++ weed/mount/weedfs_file_sync.go | 11 ++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/weed/mount/content_flush_condition_test.go b/weed/mount/content_flush_condition_test.go index f6c9a08c70..312d651aa2 100644 --- a/weed/mount/content_flush_condition_test.go +++ b/weed/mount/content_flush_condition_test.go @@ -354,6 +354,18 @@ func TestMergeContentEntry(t *testing.T) { t.Fatal("refused a truncate that shortened an unstamped base chunk") } + noInode := proto.Clone(base).(*filer_pb.Entry) + noInode.Attributes.Inode, noInode.Attributes.Crtime = 0, 5 + sameFile := proto.Clone(noInode).(*filer_pb.Entry) + sameFile.Chunks = append(sameFile.Chunks, stored.Chunks[2]) + if _, ok := mergeContentEntry(sameFile, noInode, local, local, -1); !ok { + t.Fatal("refused an inode-less file with the same creation time") + } + sameFile.Attributes.Crtime = 6 + if _, ok := mergeContentEntry(sameFile, noInode, local, local, -1); ok { + t.Fatal("merged into an inode-less replacement") + } + empty := flushConditionEntry() inline := proto.Clone(empty).(*filer_pb.Entry) inline.Content = []byte("peer") diff --git a/weed/mount/weedfs_file_sync.go b/weed/mount/weedfs_file_sync.go index 0ee2164764..21efd000d4 100644 --- a/weed/mount/weedfs_file_sync.go +++ b/weed/mount/weedfs_file_sync.go @@ -485,13 +485,18 @@ func mergeContentEntry(stored, base, local, sent *filer_pb.Entry, truncatedTo in return merged, true } -// sameContentFile reports whether stored is still the regular file base was. +// sameContentFile reports whether stored is still the regular file base was: +// the same inode, or for an entry without one, the same creation time. A +// synthesized base with neither cannot tell. func sameContentFile(stored, base *filer_pb.Entry) bool { if stored == nil || base == nil || stored.IsDirectory || !bytes.Equal(stored.HardLinkId, base.HardLinkId) { return false } - inode := base.GetAttributes().GetInode() - return inode == 0 || stored.GetAttributes().GetInode() == inode + if inode := base.GetAttributes().GetInode(); inode != 0 { + return stored.GetAttributes().GetInode() == inode + } + crtime := base.GetAttributes().GetCrtime() + return crtime == 0 || stored.GetAttributes().GetCrtime() == crtime } // contentChunkKey identifies a chunk reference: truncation shrinks a chunk in