@@ -14,26 +14,30 @@ import (
14
14
const resolveFlags = unix .RESOLVE_BENEATH | unix .RESOLVE_NO_SYMLINKS | unix .RESOLVE_NO_MAGICLINKS | unix .RESOLVE_NO_XDEV
15
15
16
16
// FS uses a file descriptor for a directory as the base of a fs.FS.
17
- type FS uintptr
17
+ type FS struct {
18
+ file * os.File
19
+ }
18
20
19
21
// DirFS opens the directory dir, and returns an FS rooted at that directory.
20
- // It uses open(2) with O_PATH+O_DIRECTORY+O_CLOEXEC.
21
- func DirFS (dir string ) (FS , error ) {
22
- bd , err := os .OpenFile (dir , unix .O_PATH | unix .O_DIRECTORY | unix .O_CLOEXEC , 0 )
22
+ // It uses open(2) with O_RDONLY+O_DIRECTORY+O_CLOEXEC. Note that this will
23
+ // resolve symlinks in the path, so only use this to open a trusted base path.
24
+ func DirFS (dir string ) (* FS , error ) {
25
+ f , err := os .OpenFile (dir , unix .O_RDONLY | unix .O_DIRECTORY | unix .O_CLOEXEC , 0 )
23
26
if err != nil {
24
- return 0 , err
27
+ return nil , err
25
28
}
26
- return FS ( bd . Fd ()) , nil
29
+ return & FS { file : f } , nil
27
30
}
28
31
29
32
// Close closes the file descriptor.
30
- func (s FS ) Close () error {
31
- return unix . Close (int ( s ) )
33
+ func (s * FS ) Close () error {
34
+ return s . file . Close ()
32
35
}
33
36
34
- // Open wraps openat2(2) with O_RDONLY+O_NOFOLLOW+O_CLOEXEC.
35
- func (s FS ) Open (path string ) (fs.File , error ) {
36
- fd , err := unix .Openat2 (int (s ), path , & unix.OpenHow {
37
+ // Open wraps openat2(2) with O_RDONLY+O_NOFOLLOW+O_CLOEXEC, and prohibits
38
+ // symlinks etc within the path.
39
+ func (s * FS ) Open (path string ) (fs.File , error ) {
40
+ fd , err := unix .Openat2 (int (s .file .Fd ()), path , & unix.OpenHow {
37
41
Flags : unix .O_RDONLY | unix .O_NOFOLLOW | unix .O_CLOEXEC ,
38
42
Mode : 0 ,
39
43
Resolve : resolveFlags ,
@@ -46,19 +50,69 @@ func (s FS) Open(path string) (fs.File, error) {
46
50
}
47
51
48
52
// Lchown wraps fchownat(2) (with AT_SYMLINK_NOFOLLOW).
49
- func (s FS ) Lchown (path string , uid , gid int ) error {
50
- return unix .Fchownat (int (s ), path , uid , gid , unix .AT_SYMLINK_NOFOLLOW )
53
+ func (s * FS ) Lchown (path string , uid , gid int ) error {
54
+ return unix .Fchownat (int (s . file . Fd () ), path , uid , gid , unix .AT_SYMLINK_NOFOLLOW )
51
55
}
52
56
53
- // Sub wraps openat2(2) (with O_PATH+O_DIRECTORY+O_NOFOLLOW+O_CLOEXEC), and returns an FS.
54
- func (s FS ) Sub (dir string ) (FS , error ) {
55
- subFD , err := unix .Openat2 (int (s ), dir , & unix.OpenHow {
56
- Flags : unix .O_PATH | unix .O_DIRECTORY | unix .O_NOFOLLOW | unix .O_CLOEXEC ,
57
+ // Sub wraps openat2(2) (with O_RDONLY+O_DIRECTORY+O_NOFOLLOW+O_CLOEXEC), and
58
+ // returns an FS.
59
+ func (s * FS ) Sub (dir string ) (* FS , error ) {
60
+ fd , err := unix .Openat2 (int (s .file .Fd ()), dir , & unix.OpenHow {
61
+ Flags : unix .O_RDONLY | unix .O_DIRECTORY | unix .O_NOFOLLOW | unix .O_CLOEXEC ,
57
62
Mode : 0 ,
58
63
Resolve : resolveFlags ,
59
64
})
60
65
if err != nil {
61
- return 0 , err
66
+ return nil , err
67
+ }
68
+ return & FS {os .NewFile (uintptr (fd ), dir )}, nil
69
+ }
70
+
71
+ // RecursiveChown lchowns everything within the receiver.
72
+ func (s * FS ) RecursiveChown (uid , gid int ) error {
73
+ // Q: Why not fs.WalkDir(... s.Lchown(path, uid, gid) ... ) ?
74
+ // A: fs.WalkDir gives the callback a subpath to each item. So although
75
+ // fs.WalkDir doesn't traverse symlinks, there's a race between walking
76
+ // each path (no intermediate symlinks), and passing that path to lchown
77
+ // (has possibly changed).
78
+ // Solution: More openat.
79
+
80
+ if err := s .Lchown ("." , uid , gid ); err != nil {
81
+ return err
82
+ }
83
+
84
+ // This closure exists so sd.Close happens before the next loop iteration,
85
+ // rather than at the end of RecursiveChown.
86
+ chownSubdir := func (name string ) error {
87
+ sd , err := s .Sub (name )
88
+ if err != nil {
89
+ return err
90
+ }
91
+ defer sd .Close ()
92
+ return sd .RecursiveChown (uid , gid )
93
+ }
94
+
95
+ // The "file" within an *FS should always be a directory.
96
+ ds , err := s .file .ReadDir (- 1 )
97
+ if err != nil {
98
+ return err
99
+ }
100
+ for _ , d := range ds {
101
+ if ! d .IsDir () {
102
+ if err := s .Lchown (d .Name (), uid , gid ); err != nil {
103
+ return err
104
+ }
105
+ continue
106
+ }
107
+
108
+ // Make sure we're not about to recurse on a symlink.
109
+ if d .Type ()& fs .ModeSymlink != 0 {
110
+ continue
111
+ }
112
+
113
+ if err := chownSubdir (d .Name ()); err != nil {
114
+ return err
115
+ }
62
116
}
63
- return FS ( subFD ), nil
117
+ return nil
64
118
}
0 commit comments