From 43502cd2e28a523cde4dd148b30cc2266ac3be42 Mon Sep 17 00:00:00 2001 From: dyer-oai Date: Sun, 7 Jun 2026 09:23:07 -0700 Subject: [PATCH] guard invalid file info stat in disk usage Signed-off-by: dyer-oai --- fs/du_unix.go | 23 ++++++++++++++++++--- fs/du_unix_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/fs/du_unix.go b/fs/du_unix.go index fbd4a00..d960b23 100644 --- a/fs/du_unix.go +++ b/fs/du_unix.go @@ -20,6 +20,7 @@ package fs import ( "context" + "fmt" "os" "path/filepath" "syscall" @@ -47,6 +48,16 @@ func newInode(stat *syscall.Stat_t) inode { } } +func fileInfoStat(path string, fi os.FileInfo) (*syscall.Stat_t, error) { + sys := fi.Sys() + stat, ok := sys.(*syscall.Stat_t) + if !ok || stat == nil { + return nil, fmt.Errorf("failed to get stat for %q: expected *syscall.Stat_t, got %T", path, sys) + } + + return stat, nil +} + func diskUsage(ctx context.Context, roots ...string) (Usage, error) { var ( size int64 @@ -65,7 +76,10 @@ func diskUsage(ctx context.Context, roots ...string) (Usage, error) { default: } - stat := fi.Sys().(*syscall.Stat_t) + stat, err := fileInfoStat(path, fi) + if err != nil { + return err + } inoKey := newInode(stat) if _, ok := inodes[inoKey]; !ok { inodes[inoKey] = struct{}{} @@ -90,13 +104,16 @@ func diffUsage(ctx context.Context, a, b string) (Usage, error) { inodes = map[inode]struct{}{} // expensive! ) - if err := Changes(ctx, a, b, func(kind ChangeKind, _ string, fi os.FileInfo, err error) error { + if err := Changes(ctx, a, b, func(kind ChangeKind, path string, fi os.FileInfo, err error) error { if err != nil { return err } if kind == ChangeKindAdd || kind == ChangeKindModify { - stat := fi.Sys().(*syscall.Stat_t) + stat, err := fileInfoStat(path, fi) + if err != nil { + return err + } inoKey := newInode(stat) if _, ok := inodes[inoKey]; !ok { inodes[inoKey] = struct{}{} diff --git a/fs/du_unix_test.go b/fs/du_unix_test.go index 7986734..989ffed 100644 --- a/fs/du_unix_test.go +++ b/fs/du_unix_test.go @@ -26,6 +26,7 @@ import ( "strings" "syscall" "testing" + "time" ) func getBsize(root string) (int64, error) { @@ -91,3 +92,52 @@ func duCheck(root string) (usage int64, err error) { return blocks * blocksUnitSize, nil } + +func TestFileInfoStat(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + sys any + wantErr bool + }{ + { + name: "valid stat", + sys: &syscall.Stat_t{}, + wantErr: false, + }, + { + name: "typed nil stat", + sys: (*syscall.Stat_t)(nil), + wantErr: true, + }, + { + name: "unexpected stat type", + sys: struct{}{}, + wantErr: true, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + _, err := fileInfoStat("/tmp/file", fakeFileInfo{sys: tc.sys}) + if (err != nil) != tc.wantErr { + t.Fatalf("fileInfoStat() error = %v, wantErr %v", err, tc.wantErr) + } + }) + } +} + +type fakeFileInfo struct { + sys any +} + +func (fi fakeFileInfo) Name() string { return "file" } +func (fi fakeFileInfo) Size() int64 { return 0 } +func (fi fakeFileInfo) Mode() os.FileMode { return 0 } +func (fi fakeFileInfo) ModTime() time.Time { return time.Time{} } +func (fi fakeFileInfo) IsDir() bool { return false } +func (fi fakeFileInfo) Sys() any { return fi.sys }