carlos / platform Public

Merge origin/main (9cf9f18f: FDHolders lstat strictness) into moves-phase1-2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 4d94f7f16727146a610c85e54c948c20b24ca526 parent 350ce62parent 9cf9f18
2 files changed, +40 −3
  • internal/stopevidence/fds_linux.go +13 −3
  • internal/stopevidence/fds_linux_test.go +27 −0
diff --git a/internal/stopevidence/fds_linux.go b/internal/stopevidence/fds_linux.go
index 91624fdef..9cec70976 100644
--- a/internal/stopevidence/fds_linux.go
+++ b/internal/stopevidence/fds_linux.go
@@ -18,9 +18,10 @@ type fdOps struct {
readDir func(string) ([]os.DirEntry, error)
readlink func(string) (string, error)
stat func(string) (os.FileInfo, error)
+ lstat func(string) (os.FileInfo, error)
}
-func realFDOps() fdOps { return fdOps{os.ReadDir, os.Readlink, os.Stat} }
+func realFDOps() fdOps { return fdOps{os.ReadDir, os.Readlink, os.Stat, os.Lstat} }
func FDHolders(procRoot string, paths []string) ([]int, error) {
return fdHolders(context.Background(), procRoot, paths, realFDOps())
@@ -110,9 +111,18 @@ func fdHolders(ctx context.Context, procRoot string, paths []string, ops fdOps)
}
// Stat through the fd link reaches the open file itself, whatever
// name it was opened by. An unreadable entry fails the whole scan.
- fi, err := ops.stat(filepath.Join(fdDir, fd.Name()))
+ fdPath := filepath.Join(fdDir, fd.Name())
+ fi, err := ops.stat(fdPath)
if procGone(err) {
- continue
+ // Closed only if the entry itself is gone. An entry that still
+ // exists but cannot be followed is unreadable, not closed.
+ if _, lerr := ops.lstat(fdPath); procGone(lerr) {
+ continue
+ } else if lerr == nil {
+ err = fmt.Errorf("fd entry exists but its file cannot be reached: %w", err)
+ } else {
+ err = lerr
+ }
}
if err != nil {
return nil, fmt.Errorf("%w: pid %d tid %d fd %s: %w", ErrIncomplete, pid, tid, fd.Name(), err)
diff --git a/internal/stopevidence/fds_linux_test.go b/internal/stopevidence/fds_linux_test.go
index 8a89eafa6..5dfbe591b 100644
--- a/internal/stopevidence/fds_linux_test.go
+++ b/internal/stopevidence/fds_linux_test.go
@@ -309,4 +309,31 @@ func TestFDHoldersMatchesTheInodeNotOnlyThePath(t *testing.T) {
if got, err := fdHolders(t.Context(), root, []string{db}, ops); !errors.Is(err, ErrIncomplete) || got != nil {
t.Fatalf("unreadable wanted path accepted: %v %v", got, err)
}
+ // A live entry whose target cannot be followed is unreadable, not closed;
+ // a vanished entry is closed.
+ for _, gone := range []bool{false, true} {
+ ops = realFDOps()
+ ops.stat = func(p string) (os.FileInfo, error) {
+ if p == filepath.Join(fd, "0") {
+ return nil, os.ErrNotExist
+ }
+ return stat(p)
+ }
+ if gone {
+ lstat := ops.lstat
+ ops.lstat = func(p string) (os.FileInfo, error) {
+ if p == filepath.Join(fd, "0") {
+ return nil, os.ErrNotExist
+ }
+ return lstat(p)
+ }
+ }
+ got, err := fdHolders(t.Context(), root, []string{db}, ops)
+ if gone && (err != nil || len(got) != 0) {
+ t.Fatalf("closed fd: %v %v", got, err)
+ }
+ if !gone && (!errors.Is(err, ErrIncomplete) || got != nil) {
+ t.Fatalf("live entry with an unreachable target counted as closed: %v %v", got, err)
+ }
+ }
}