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}