1package chroot
2
3import (
4 "errors"
5 "os"
6 "path"
7 "path/filepath"
8 "strings"
9 "syscall"
10
11 "github.com/go-git/go-billy/v5"
12 "github.com/go-git/go-billy/v5/helper/polyfill"
13)
14
15// ChrootHelper is a helper to implement billy.Chroot.
16// It is not a security boundary, callers that need containment should use a
17// filesystem implementation that enforces paths at the OS boundary instead.
18type ChrootHelper struct {
19 underlying billy.Filesystem
20 base string
21}
22
23const maxFollowedSymlinks = 8 // Aligns with POSIX_SYMLOOP_MAX
24
25// New creates a new filesystem wrapping up the given 'fs'.
26// The created filesystem has its base in the given ChrootHelperectory of the
27// underlying filesystem.
28func New(fs billy.Basic, base string) billy.Filesystem {
29 return &ChrootHelper{
30 underlying: polyfill.New(fs),
31 base: base,
32 }
33}
34
35func (fs *ChrootHelper) underlyingPath(filename string) (string, error) {
36 if isCrossBoundaries(filename) {
37 return "", billy.ErrCrossedBoundary
38 }
39
40 return fs.Join(fs.Root(), filename), nil
41}
42
43func (fs *ChrootHelper) followedPath(filename string, followFinal bool, op string) (string, error) {
44 fullpath, err := fs.underlyingPath(filename)
45 if err != nil {
46 return "", err
47 }
48
49 sl, ok := fs.underlying.(billy.Symlink)
50 if !ok {
51 return fullpath, nil
52 }
53
54 rel, err := fs.relativeToRoot(fullpath)
55 if err != nil {
56 return "", err
57 }
58
59 fullpath, err = fs.resolveFollowedPath(rel, followFinal, op, sl)
60 if errors.Is(err, billy.ErrNotSupported) {
61 return fs.underlyingPath(filename)
62 }
63
64 return fullpath, err
65}
66
67func (fs *ChrootHelper) resolveFollowedPath(rel string, followFinal bool, op string, sl billy.Symlink) (string, error) {
68 if rel == "" {
69 return fs.resolveFollowedRoot(followFinal, op, sl)
70 }
71
72 parts := splitRelativePath(rel)
73 resolved := ""
74 followed := 0
75
76 for len(parts) > 0 {
77 part := parts[0]
78 parts = parts[1:]
79
80 currentRel := joinRelativePath(resolved, part)
81 currentPath := fs.Join(fs.Root(), currentRel)
82 if len(parts) == 0 && !followFinal {
83 return currentPath, nil
84 }
85
86 fi, err := sl.Lstat(currentPath)
87 if err != nil {
88 if os.IsNotExist(err) {
89 return fs.Join(fs.Root(), joinRelativePath(append([]string{currentRel}, parts...)...)), nil
90 }
91 return "", err
92 }
93
94 if fi.Mode()&os.ModeSymlink == 0 {
95 resolved = currentRel
96 continue
97 }
98
99 followed++
100 if followed > maxFollowedSymlinks {
101 return "", symlinkLoopError(op, currentPath)
102 }
103
104 target, err := sl.Readlink(currentPath)
105 if err != nil {
106 return "", err
107 }
108
109 targetRel, err := fs.linkTargetRel(currentPath, target)
110 if err != nil {
111 return "", err
112 }
113 if targetRel == currentRel {
114 return "", symlinkLoopError(op, currentPath)
115 }
116
117 parts = append(splitRelativePath(targetRel), parts...)
118 resolved = ""
119 }
120
121 return fs.Join(fs.Root(), resolved), nil
122}
123
124func symlinkLoopError(op, path string) error {
125 return &os.PathError{Op: op, Path: path, Err: syscall.ELOOP}
126}
127
128func (fs *ChrootHelper) resolveFollowedRoot(followFinal bool, op string, sl billy.Symlink) (string, error) {
129 root := fs.Join(fs.Root(), "")
130 if !followFinal {
131 return root, nil
132 }
133
134 fi, err := sl.Lstat(root)
135 if err != nil {
136 if os.IsNotExist(err) {
137 return root, nil
138 }
139 return "", err
140 }
141
142 if fi.Mode()&os.ModeSymlink == 0 {
143 return root, nil
144 }
145
146 target, err := sl.Readlink(root)
147 if err != nil {
148 return "", err
149 }
150
151 targetRel, err := fs.linkTargetRel(root, target)
152 if err != nil {
153 return root, err
154 }
155 if targetRel == "" {
156 return "", symlinkLoopError(op, root)
157 }
158
159 return fs.resolveFollowedPath(targetRel, followFinal, op, sl)
160}
161
162func (fs *ChrootHelper) relativeToRoot(filename string) (string, error) {
163 rel, err := filepath.Rel(filepath.Clean(fs.Root()), filepath.Clean(filename))
164 if err != nil || isCrossBoundaries(rel) {
165 return "", billy.ErrCrossedBoundary
166 }
167
168 if rel == "." {
169 return "", nil
170 }
171 return rel, nil
172}
173
174func (fs *ChrootHelper) linkTargetRel(linkPath, target string) (string, error) {
175 target = filepath.FromSlash(target)
176 if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) {
177 return fs.relativeToRoot(target)
178 }
179
180 return fs.relativeToRoot(fs.Join(filepath.Dir(linkPath), target))
181}
182
183func splitRelativePath(filename string) []string {
184 filename = filepath.Clean(filename)
185 if filename == "" || filename == "." {
186 return nil
187 }
188
189 return strings.Split(filepath.ToSlash(filename), "/")
190}
191
192func joinRelativePath(elem ...string) string {
193 parts := make([]string, 0, len(elem))
194 for _, part := range elem {
195 if part == "" || part == "." {
196 continue
197 }
198 parts = append(parts, part)
199 }
200
201 if len(parts) == 0 {
202 return ""
203 }
204 return filepath.Join(parts...)
205}
206
207func isCreateExclusive(flag int) bool {
208 return flag&os.O_CREATE != 0 && flag&os.O_EXCL != 0
209}
210
211func isCrossBoundaries(name string) bool {
212 name = filepath.ToSlash(name)
213 name = strings.TrimLeft(name, "/")
214 name = path.Clean(name)
215
216 return name == ".." || strings.HasPrefix(name, "../")
217}
218
219func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
220 fullpath, err := fs.followedPath(filename, true, "create")
221 if err != nil {
222 return nil, err
223 }
224
225 f, err := fs.underlying.Create(fullpath)
226 if err != nil {
227 return nil, err
228 }
229
230 return newFile(fs, f, filename), nil
231}
232
233func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
234 fullpath, err := fs.followedPath(filename, true, "open")
235 if err != nil {
236 return nil, err
237 }
238
239 f, err := fs.underlying.Open(fullpath)
240 if err != nil {
241 return nil, err
242 }
243
244 return newFile(fs, f, filename), nil
245}
246
247func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (billy.File, error) {
248 fullpath, err := fs.followedPath(filename, !isCreateExclusive(flag), "open")
249 if err != nil {
250 return nil, err
251 }
252
253 f, err := fs.underlying.OpenFile(fullpath, flag, mode)
254 if err != nil {
255 return nil, err
256 }
257
258 return newFile(fs, f, filename), nil
259}
260
261func (fs *ChrootHelper) Stat(filename string) (os.FileInfo, error) {
262 fullpath, err := fs.followedPath(filename, true, "stat")
263 if err != nil {
264 return nil, err
265 }
266
267 fi, err := fs.underlying.Stat(fullpath)
268 if err != nil {
269 return nil, err
270 }
271 return fileInfo{FileInfo: fi, name: filepath.Base(filename)}, nil
272}
273
274func (fs *ChrootHelper) Rename(from, to string) error {
275 var err error
276 from, err = fs.underlyingPath(from)
277 if err != nil {
278 return err
279 }
280
281 to, err = fs.underlyingPath(to)
282 if err != nil {
283 return err
284 }
285
286 return fs.underlying.Rename(from, to)
287}
288
289func (fs *ChrootHelper) Remove(path string) error {
290 fullpath, err := fs.underlyingPath(path)
291 if err != nil {
292 return err
293 }
294
295 return fs.underlying.Remove(fullpath)
296}
297
298func (fs *ChrootHelper) Join(elem ...string) string {
299 return fs.underlying.Join(elem...)
300}
301
302func (fs *ChrootHelper) TempFile(dir, prefix string) (billy.File, error) {
303 fullpath, err := fs.underlyingPath(dir)
304 if err != nil {
305 return nil, err
306 }
307
308 f, err := fs.underlying.(billy.TempFile).TempFile(fullpath, prefix)
309 if err != nil {
310 return nil, err
311 }
312
313 return newFile(fs, f, fs.Join(dir, filepath.Base(f.Name()))), nil
314}
315
316func (fs *ChrootHelper) ReadDir(path string) ([]os.FileInfo, error) {
317 fullpath, err := fs.followedPath(path, true, "readdir")
318 if err != nil {
319 return nil, err
320 }
321
322 return fs.underlying.(billy.Dir).ReadDir(fullpath)
323}
324
325func (fs *ChrootHelper) MkdirAll(filename string, perm os.FileMode) error {
326 fullpath, err := fs.underlyingPath(filename)
327 if err != nil {
328 return err
329 }
330
331 return fs.underlying.(billy.Dir).MkdirAll(fullpath, perm)
332}
333
334func (fs *ChrootHelper) Lstat(filename string) (os.FileInfo, error) {
335 fullpath, err := fs.underlyingPath(filename)
336 if err != nil {
337 return nil, err
338 }
339
340 return fs.underlying.(billy.Symlink).Lstat(fullpath)
341}
342
343func (fs *ChrootHelper) Symlink(target, link string) error {
344 target = filepath.FromSlash(target)
345
346 // only rewrite target if it's already absolute
347 if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) {
348 target = fs.Join(fs.Root(), target)
349 target = filepath.Clean(filepath.FromSlash(target))
350 }
351
352 link, err := fs.underlyingPath(link)
353 if err != nil {
354 return err
355 }
356
357 return fs.underlying.(billy.Symlink).Symlink(target, link)
358}
359
360func (fs *ChrootHelper) Readlink(link string) (string, error) {
361 fullpath, err := fs.underlyingPath(link)
362 if err != nil {
363 return "", err
364 }
365
366 target, err := fs.underlying.(billy.Symlink).Readlink(fullpath)
367 if err != nil {
368 return "", err
369 }
370
371 if !filepath.IsAbs(target) && !strings.HasPrefix(target, string(filepath.Separator)) {
372 return target, nil
373 }
374
375 target, err = filepath.Rel(fs.base, target)
376 if err != nil {
377 return "", err
378 }
379
380 return string(os.PathSeparator) + target, nil
381}
382
383func (fs *ChrootHelper) Chmod(path string, mode os.FileMode) error {
384 fullpath, err := fs.underlyingPath(path)
385 if err != nil {
386 return err
387 }
388
389 c, ok := fs.underlying.(billy.Chmod)
390 if !ok {
391 return errors.New("underlying fs does not implement billy.Chmod")
392 }
393 return c.Chmod(fullpath, mode)
394}
395
396func (fs *ChrootHelper) Chroot(path string) (billy.Filesystem, error) {
397 fullpath, err := fs.underlyingPath(path)
398 if err != nil {
399 return nil, err
400 }
401
402 return New(fs.underlying, fullpath), nil
403}
404
405func (fs *ChrootHelper) Root() string {
406 return fs.base
407}
408
409func (fs *ChrootHelper) Underlying() billy.Basic {
410 return fs.underlying
411}
412
413// Capabilities implements the Capable interface.
414func (fs *ChrootHelper) Capabilities() billy.Capability {
415 return billy.Capabilities(fs.underlying)
416}
417
418type file struct {
419 billy.File
420 name string
421}
422
423type fileInfo struct {
424 os.FileInfo
425 name string
426}
427
428func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
429 filename = fs.Join(fs.Root(), filename)
430 filename, _ = filepath.Rel(fs.Root(), filename)
431
432 return &file{
433 File: f,
434 name: filename,
435 }
436}
437
438func (f *file) Name() string {
439 return f.name
440}
441
442func (fi fileInfo) Name() string {
443 return fi.name
444}