From 70d663915507a907ed34393ce6d3b6383e9d2914 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 13 Nov 2026 02:48:16 -0800 Subject: [PATCH] mount: record a hard link's looked-up names on its inode Lookup mapped a second name of a hard-linked file to the existing inode without adding it to the inode's names, so GetAllPaths missed it and the final forget left it mapped to a released inode. Co-Authored-By: Claude Opus 5.5 --- weed/mount/inode_to_path.go | 8 ++++++-- weed/mount/inode_to_path_test.go | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/weed/mount/inode_to_path.go b/weed/mount/inode_to_path.go index 5ac35bbc42..055a82cc14 100644 --- a/weed/mount/inode_to_path.go +++ b/weed/mount/inode_to_path.go @@ -201,9 +201,13 @@ func (i *InodeToPath) Lookup(path util.FullPath, unixTime int64, isDirectory boo } i.path2inode[path] = inode - if _, found := i.inode2path[inode]; found { + if ie, known := i.inode2path[inode]; known { + // Another name of a hard link the table already holds. + if !found { + ie.addPath(path) + } if isLookup { - i.inode2path[inode].nlookup++ + ie.nlookup++ } } else { nlookup := uint64(0) diff --git a/weed/mount/inode_to_path_test.go b/weed/mount/inode_to_path_test.go index 2625ed20aa..c502838b46 100644 --- a/weed/mount/inode_to_path_test.go +++ b/weed/mount/inode_to_path_test.go @@ -208,3 +208,25 @@ func TestMovePathWithNoSourceLeavesTheTarget(t *testing.T) { t.Errorf("/a/g.txt resolves to %d (found %v), want %d", got, found, inode) } } + +// A second hard link name found by a lookup belongs to the inode, so it is +// reported with the others and released by the final forget. +func TestLookupRecordsHardLinkNames(t *testing.T) { + table := NewInodeToPath(util.FullPath("/"), 0) + inode := table.Lookup("/a", 1, false, true, 42, true) + if got := table.Lookup("/b", 1, false, true, 42, true); got != inode { + t.Fatalf("second name got inode %d, want %d", got, inode) + } + table.Lookup("/b", 1, false, true, 42, true) + if got := table.GetAllPaths(inode); len(got) != 2 || got[0] != "/a" || got[1] != "/b" { + t.Fatalf("paths %v, want [/a /b]", got) + } + table.RemovePath("/a", nil) + if got, status := table.GetPath(inode); status != fuse.OK || got != "/b" { + t.Fatalf("after removing /a the inode answers to %q (%v), want /b", got, status) + } + table.Forget(inode, 3, nil, nil) + if _, found := table.GetInode("/b"); found { + t.Fatal("the final forget left /b mapped to a released inode") + } +}