Merge origin/main (9cf9f18f: FDHolders lstat strictness) into moves-phase1-2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+40
−3
internal/stopevidence/fds_linux.go+13 −3internal/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) |
| + } |
| + } |
| } |