main
1//go:build !js
2// +build !js
3
4/*
5 Copyright 2022 The Flux authors.
6
7 Licensed under the Apache License, Version 2.0 (the "License");
8 you may not use this file except in compliance with the License.
9 You may obtain a copy of the License at
10
11 http://www.apache.org/licenses/LICENSE-2.0
12
13 Unless required by applicable law or agreed to in writing, software
14 distributed under the License is distributed on an "AS IS" BASIS,
15 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 See the License for the specific language governing permissions and
17 limitations under the License.
18*/
19
20package osfs
21
22import (
23 "fmt"
24 "os"
25 "path/filepath"
26 "strings"
27
28 securejoin "github.com/cyphar/filepath-securejoin"
29 "github.com/go-git/go-billy/v5"
30)
31
32// BoundOS is a fs implementation based on the OS filesystem which is bound to
33// a base dir.
34// Prefer this fs implementation over ChrootOS.
35//
36// Behaviours of note:
37// 1. Read and write operations can only be directed to files which descends
38// from the base dir.
39// 2. Symlinks don't have their targets modified, and therefore can point
40// to locations outside the base dir or to non-existent paths.
41// 3. Readlink and Lstat ensures that the link file is located within the base
42// dir, evaluating any symlinks that file or base dir may contain.
43type BoundOS struct {
44 baseDir string
45 deduplicatePath bool
46}
47
48func newBoundOS(d string, deduplicatePath bool) billy.Filesystem {
49 return &BoundOS{baseDir: d, deduplicatePath: deduplicatePath}
50}
51
52func (fs *BoundOS) Create(filename string) (billy.File, error) {
53 return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultCreateMode)
54}
55
56func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
57 fn, err := fs.abs(filename)
58 if err != nil {
59 return nil, err
60 }
61 return openFile(fn, flag, perm, fs.createDir)
62}
63
64func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) {
65 dir, err := fs.abs(path)
66 if err != nil {
67 return nil, err
68 }
69
70 return readDir(dir)
71}
72
73func (fs *BoundOS) Rename(from, to string) error {
74 f, err := fs.abs(from)
75 if err != nil {
76 return err
77 }
78 t, err := fs.abs(to)
79 if err != nil {
80 return err
81 }
82
83 // MkdirAll for target name.
84 if err := fs.createDir(t); err != nil {
85 return err
86 }
87
88 return os.Rename(f, t)
89}
90
91func (fs *BoundOS) MkdirAll(path string, perm os.FileMode) error {
92 dir, err := fs.abs(path)
93 if err != nil {
94 return err
95 }
96 return os.MkdirAll(dir, perm)
97}
98
99func (fs *BoundOS) Open(filename string) (billy.File, error) {
100 return fs.OpenFile(filename, os.O_RDONLY, 0)
101}
102
103func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) {
104 filename, err := fs.abs(filename)
105 if err != nil {
106 return nil, err
107 }
108 return os.Stat(filename)
109}
110
111func (fs *BoundOS) Remove(filename string) error {
112 fn, err := fs.abs(filename)
113 if err != nil {
114 return err
115 }
116 return os.Remove(fn)
117}
118
119// TempFile creates a temporary file. If dir is empty, the file
120// will be created within the OS Temporary dir. If dir is provided
121// it must descend from the current base dir.
122func (fs *BoundOS) TempFile(dir, prefix string) (billy.File, error) {
123 if dir != "" {
124 var err error
125 dir, err = fs.abs(dir)
126 if err != nil {
127 return nil, err
128 }
129 }
130
131 return tempFile(dir, prefix)
132}
133
134func (fs *BoundOS) Join(elem ...string) string {
135 return filepath.Join(elem...)
136}
137
138func (fs *BoundOS) RemoveAll(path string) error {
139 dir, err := fs.abs(path)
140 if err != nil {
141 return err
142 }
143 return os.RemoveAll(dir)
144}
145
146func (fs *BoundOS) Symlink(target, link string) error {
147 ln, err := fs.abs(link)
148 if err != nil {
149 return err
150 }
151 // MkdirAll for containing dir.
152 if err := fs.createDir(ln); err != nil {
153 return err
154 }
155 return os.Symlink(target, ln)
156}
157
158func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) {
159 filename = filepath.Clean(filename)
160 if !filepath.IsAbs(filename) {
161 filename = filepath.Join(fs.baseDir, filename)
162 }
163 if ok, err := fs.insideBaseDirEval(filename); !ok {
164 return nil, err
165 }
166 return os.Lstat(filename)
167}
168
169func (fs *BoundOS) Readlink(link string) (string, error) {
170 if !filepath.IsAbs(link) {
171 link = filepath.Clean(filepath.Join(fs.baseDir, link))
172 }
173 if ok, err := fs.insideBaseDirEval(link); !ok {
174 return "", err
175 }
176 return os.Readlink(link)
177}
178
179// Chroot returns a new OS filesystem, with the base dir set to the
180// result of joining the provided path with the underlying base dir.
181func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) {
182 joined, err := securejoin.SecureJoin(fs.baseDir, path)
183 if err != nil {
184 return nil, err
185 }
186 return New(joined), nil
187}
188
189// Root returns the current base dir of the billy.Filesystem.
190// This is required in order for this implementation to be a drop-in
191// replacement for other upstream implementations (e.g. memory and osfs).
192func (fs *BoundOS) Root() string {
193 return fs.baseDir
194}
195
196func (fs *BoundOS) createDir(fullpath string) error {
197 dir := filepath.Dir(fullpath)
198 if dir != "." {
199 if err := os.MkdirAll(dir, defaultDirectoryMode); err != nil {
200 return err
201 }
202 }
203
204 return nil
205}
206
207// abs transforms filename to an absolute path, taking into account the base dir.
208// Relative paths won't be allowed to ascend the base dir, so `../file` will become
209// `/working-dir/file`.
210//
211// Note that if filename is a symlink, the returned address will be the target of the
212// symlink.
213func (fs *BoundOS) abs(filename string) (string, error) {
214 if filename == fs.baseDir {
215 filename = string(filepath.Separator)
216 }
217
218 path, err := securejoin.SecureJoin(fs.baseDir, filename)
219 if err != nil {
220 return "", nil
221 }
222
223 if fs.deduplicatePath {
224 vol := filepath.VolumeName(fs.baseDir)
225 dup := filepath.Join(fs.baseDir, fs.baseDir[len(vol):])
226 if strings.HasPrefix(path, dup+string(filepath.Separator)) {
227 return fs.abs(path[len(dup):])
228 }
229 }
230 return path, nil
231}
232
233// insideBaseDir checks whether filename is located within
234// the fs.baseDir.
235func (fs *BoundOS) insideBaseDir(filename string) (bool, error) {
236 if filename == fs.baseDir {
237 return true, nil
238 }
239 if !strings.HasPrefix(filename, fs.baseDir+string(filepath.Separator)) {
240 return false, fmt.Errorf("path outside base dir")
241 }
242 return true, nil
243}
244
245// insideBaseDirEval checks whether filename is contained within
246// a dir that is within the fs.baseDir, by first evaluating any symlinks
247// that either filename or fs.baseDir may contain.
248func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) {
249 // "/" contains all others.
250 if fs.baseDir == "/" {
251 return true, nil
252 }
253 dir, err := filepath.EvalSymlinks(filepath.Dir(filename))
254 if dir == "" || os.IsNotExist(err) {
255 dir = filepath.Dir(filename)
256 }
257 wd, err := filepath.EvalSymlinks(fs.baseDir)
258 if wd == "" || os.IsNotExist(err) {
259 wd = fs.baseDir
260 }
261 if filename != wd && dir != wd && !strings.HasPrefix(dir, wd+string(filepath.Separator)) {
262 return false, fmt.Errorf("%q: path outside base dir %q: %w", filename, fs.baseDir, os.ErrNotExist)
263 }
264 return true, nil
265}