github.com/mgoltzsche/ctnr@v0.7.1-alpha/pkg/fs/writer/dirwriter.go (about)

     1  package writer
     2  
     3  import (
     4  	"io"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  	"syscall"
     9  	"time"
    10  
    11  	"github.com/cyphar/filepath-securejoin"
    12  	exterrors "github.com/mgoltzsche/ctnr/pkg/errors"
    13  	"github.com/mgoltzsche/ctnr/pkg/fs"
    14  	"github.com/mgoltzsche/ctnr/pkg/fs/source"
    15  	"github.com/mgoltzsche/ctnr/pkg/log"
    16  	"github.com/openSUSE/umoci/pkg/fseval"
    17  	"github.com/pkg/errors"
    18  	"golang.org/x/sys/unix"
    19  )
    20  
    21  var _ fs.Writer = &DirWriter{}
    22  
    23  // A mapping file system writer that secures root directory boundaries.
    24  // Derived from umoci's tar_extract.go to allow separate source/dest interfaces
    25  // and filter archive contents on extraction
    26  type DirWriter struct {
    27  	dir        string
    28  	dirTimes   map[string]fs.FileTimes
    29  	attrMapper fs.AttrMapper
    30  	fsEval     fseval.FsEval
    31  	rootless   bool
    32  	now        time.Time
    33  	warn       log.Logger
    34  }
    35  
    36  func NewDirWriter(dir string, opts fs.FSOptions, warn log.Logger) (w *DirWriter) {
    37  	var attrMapper fs.AttrMapper
    38  	if opts.Rootless {
    39  		attrMapper = fs.NewRootlessAttrMapper(opts.IdMappings)
    40  	} else {
    41  		attrMapper = fs.NewAttrMapper(opts.IdMappings)
    42  	}
    43  	return &DirWriter{
    44  		dir:        dir,
    45  		dirTimes:   map[string]fs.FileTimes{},
    46  		attrMapper: attrMapper,
    47  		fsEval:     opts.FsEval,
    48  		rootless:   opts.Rootless,
    49  		now:        time.Now(),
    50  		warn:       warn,
    51  	}
    52  }
    53  
    54  func (w *DirWriter) Close() (err error) {
    55  	// Apply dir time metadata
    56  	for dir, a := range w.dirTimes {
    57  		if err = w.writeTimeMetadata(dir, a); err != nil {
    58  			break
    59  		}
    60  	}
    61  	return
    62  }
    63  
    64  func (w *DirWriter) Parent() error {
    65  	return nil
    66  }
    67  
    68  func (w *DirWriter) LowerNode(path, name string, a *fs.NodeAttrs) (err error) {
    69  	return errors.Errorf("dirwriter: operation not supported: write node (%s) from parsed fs spec. hint: load FsSpec from dir", path)
    70  }
    71  
    72  func (w *DirWriter) LowerLink(path, target string, a *fs.NodeAttrs) error {
    73  	return errors.Errorf("dirwriter: operation not supported: write link (%s) from parsed fs spec. hint: load FsSpec from dir", path)
    74  }
    75  
    76  func (w *DirWriter) Lazy(path, name string, src fs.LazySource, written map[fs.Source]string) (err error) {
    77  	return src.Expand(path, w, written)
    78  }
    79  
    80  func (w *DirWriter) mkdirAll(dir string) (err error) {
    81  	if err = w.fsEval.MkdirAll(dir, 0755); err != nil {
    82  		err = errors.Wrap(err, "dir writer")
    83  	}
    84  	return
    85  }
    86  
    87  func (w *DirWriter) File(file string, src fs.FileSource) (r fs.Source, err error) {
    88  	if file, err = w.resolveParentDir(file); err != nil {
    89  		return
    90  	}
    91  	a := src.Attrs()
    92  	f, err := src.Reader()
    93  	if err != nil {
    94  		return
    95  	}
    96  	defer func() {
    97  		err = exterrors.Append(err, errors.WithMessage(f.Close(), "write file"))
    98  	}()
    99  	if err = w.remove(file); err != nil {
   100  		return
   101  	}
   102  	if err = w.mkdirAll(filepath.Dir(file)); err != nil {
   103  		return
   104  	}
   105  	destFile, err := w.fsEval.Create(file)
   106  	if err != nil {
   107  		return
   108  	}
   109  	defer destFile.Close()
   110  	n, err := io.Copy(destFile, f)
   111  	if err != nil {
   112  		return nil, errors.Errorf("write to %s: %s", file, err)
   113  	}
   114  	if n != a.Size {
   115  		return nil, errors.Wrap(io.ErrShortWrite, "copy file")
   116  	}
   117  	a.Size = n
   118  	err = w.writeMetadataChmod(file, a.FileAttrs)
   119  	return source.NewSourceFileHashed(fs.NewFileReader(file, fseval.RootlessFsEval), a.FileAttrs, src.HashIfAvailable()), errors.Wrap(err, "copy file")
   120  }
   121  
   122  func (w *DirWriter) Link(file, target string) (err error) {
   123  	if !filepath.IsAbs(target) {
   124  		target = filepath.Join(filepath.Dir(file), target)
   125  	}
   126  	if file, err = w.resolveParentDir(file); err != nil {
   127  		return
   128  	}
   129  	linkName, err := w.resolveParentDir(target)
   130  	if err != nil {
   131  		return
   132  	}
   133  	if err = w.remove(file); err != nil {
   134  		return
   135  	}
   136  	if err = w.mkdirAll(filepath.Dir(file)); err != nil {
   137  		return
   138  	}
   139  	return w.fsEval.Link(linkName, file)
   140  }
   141  
   142  func (w *DirWriter) Symlink(path string, a fs.FileAttrs) (err error) {
   143  	file, err := w.resolveParentDir(path)
   144  	if err != nil {
   145  		return
   146  	}
   147  	a.Symlink, err = normalizeLinkDest(path, a.Symlink)
   148  	if err != nil {
   149  		return
   150  	}
   151  	if err = w.remove(file); err != nil {
   152  		return
   153  	}
   154  	if err = w.mkdirAll(filepath.Dir(file)); err != nil {
   155  		return
   156  	}
   157  	if err = w.fsEval.Symlink(a.Symlink, file); err != nil {
   158  		return
   159  	}
   160  	if err = w.writeMetadata(file, a); err != nil {
   161  		return
   162  	}
   163  	return w.writeTimeMetadata(file, a.FileTimes)
   164  }
   165  
   166  func normalizeLinkDest(path, target string) (r string, err error) {
   167  	target = filepath.Clean(target)
   168  	r = target
   169  	basePath := filepath.Dir(string(os.PathSeparator) + path)
   170  	basePath, _ = filepath.Rel(string(os.PathSeparator), basePath)
   171  	abs := filepath.IsAbs(r)
   172  	if !abs {
   173  		r = filepath.Join(basePath, r)
   174  	}
   175  	if abs || !isValidPath(r) {
   176  		r, err = normalize(r)
   177  		if err == nil {
   178  			r = filepath.Clean(string(os.PathSeparator) + r)
   179  			if !abs {
   180  				r, err = filepath.Rel(string(os.PathSeparator)+basePath, r)
   181  			}
   182  		}
   183  		return r, errors.Wrapf(err, "normalize link %s dest", path)
   184  	}
   185  	return target, nil
   186  }
   187  
   188  func (w *DirWriter) Fifo(file string, a fs.DeviceAttrs) (err error) {
   189  	a.Mode = syscall.S_IFIFO | a.Mode.Perm()
   190  	return w.device(file, a)
   191  }
   192  
   193  func (w *DirWriter) device(file string, a fs.DeviceAttrs) (err error) {
   194  	if file, err = w.resolveParentDir(file); err != nil {
   195  		return
   196  	}
   197  	if err = w.remove(file); err != nil {
   198  		return
   199  	}
   200  	if err = w.mkdirAll(filepath.Dir(file)); err != nil {
   201  		return
   202  	}
   203  	dev := unix.Mkdev(uint32(a.Devmajor), uint32(a.Devminor))
   204  	if err := w.fsEval.Mknod(file, a.Mode, dev); err != nil {
   205  		return errors.Wrap(err, "mknod")
   206  	}
   207  	return w.writeMetadataChmod(file, a.FileAttrs)
   208  }
   209  
   210  func (w *DirWriter) Device(path string, a fs.DeviceAttrs) (err error) {
   211  	if w.rootless {
   212  		w.warn.Println("dirwriter: faking device in rootless mode: " + path)
   213  		_, err = w.File(path, source.NewSourceFile(fs.NewReadableBytes([]byte{}), a.FileAttrs))
   214  	} else {
   215  		err = w.device(path, a)
   216  	}
   217  	return
   218  }
   219  
   220  func (w *DirWriter) Mkdir(dir string) (err error) {
   221  	return w.mkdirAll(dir)
   222  }
   223  
   224  func (w *DirWriter) Dir(dir, base string, a fs.FileAttrs) (err error) {
   225  	if dir, err = w.resolveParentDir(dir); err != nil {
   226  		return
   227  	}
   228  
   229  	st, err := w.fsEval.Lstat(dir)
   230  	exists := false
   231  	if err == nil {
   232  		if st.IsDir() {
   233  			exists = true
   234  		} else if err = w.fsEval.Remove(dir); err != nil {
   235  			return
   236  		}
   237  	} else if !os.IsNotExist(errors.Cause(err)) {
   238  		return errors.Wrap(err, "write dir")
   239  	}
   240  	if !exists {
   241  		if err = w.mkdirAll(filepath.Dir(dir)); err != nil {
   242  			return
   243  		}
   244  		if err = w.fsEval.Mkdir(dir, a.Mode); err != nil {
   245  			return
   246  		}
   247  	}
   248  	// write metadata
   249  	if err = w.fsEval.Chmod(dir, a.Mode); err != nil {
   250  		return errors.Wrap(err, "chmod")
   251  	}
   252  	if err = w.writeMetadata(dir, a); err != nil {
   253  		return
   254  	}
   255  	w.dirTimes[dir] = a.FileTimes
   256  	return
   257  }
   258  
   259  func (w *DirWriter) Remove(file string) (err error) {
   260  	if file, err = w.resolveParentDir(file); err != nil {
   261  		return
   262  	}
   263  	return w.remove(file)
   264  }
   265  
   266  func (w *DirWriter) remove(realFile string) (err error) {
   267  	if err = w.fsEval.RemoveAll(realFile); err != nil {
   268  		return errors.Wrap(err, "write dir")
   269  	}
   270  	delete(w.dirTimes, realFile)
   271  	return
   272  }
   273  
   274  func (w *DirWriter) writeMetadataChmod(file string, a fs.FileAttrs) (err error) {
   275  	// chmod
   276  	if err = w.fsEval.Chmod(file, a.Mode); err != nil {
   277  		return errors.Wrap(err, "chmod")
   278  	}
   279  	if err = w.writeMetadata(file, a); err != nil {
   280  		return
   281  	}
   282  	return w.writeTimeMetadata(file, a.FileTimes)
   283  }
   284  
   285  func (w *DirWriter) writeMetadata(file string, a fs.FileAttrs) (err error) {
   286  	// chown
   287  	if err = w.attrMapper.ToHost(&a); err != nil {
   288  		return errors.Wrapf(err, "write file metadata: %s", file)
   289  	}
   290  	if !w.rootless {
   291  		// TODO: use fseval method if available
   292  		if err = errors.Wrap(os.Lchown(file, int(a.Uid), int(a.Gid)), "chown"); err != nil {
   293  			return
   294  		}
   295  	}
   296  
   297  	// xattrs
   298  	if err = w.fsEval.Lclearxattrs(file); err != nil {
   299  		return errors.Wrapf(err, "clear xattrs: %s", file)
   300  	}
   301  	for k, v := range a.Xattrs {
   302  		if e := w.fsEval.Lsetxattr(file, k, []byte(v), 0); e != nil {
   303  			// In rootless mode, some xattrs will fail (security.capability).
   304  			// This is _fine_ as long as not run as root
   305  			if w.rootless && os.IsPermission(errors.Cause(e)) {
   306  				w.warn.Printf("write file metadata: %s: ignoring EPERM on setxattr %s: %v", file[len(w.dir):], k, e)
   307  				continue
   308  			}
   309  			return errors.Wrapf(e, "set xattr: %s", file)
   310  		}
   311  	}
   312  	return
   313  }
   314  
   315  func (w *DirWriter) writeTimeMetadata(file string, t fs.FileTimes) error {
   316  	if t.Mtime.IsZero() {
   317  		t.Mtime = w.now.UTC()
   318  	}
   319  	if t.Atime.IsZero() {
   320  		t.Atime = t.Mtime
   321  	}
   322  	if err := w.fsEval.Lutimes(file, t.Atime, t.Mtime); !os.IsNotExist(errors.Cause(err)) {
   323  		return errors.Wrapf(err, "write file times: %s", file)
   324  	}
   325  	return nil
   326  }
   327  
   328  func (w *DirWriter) validateLink(path, file, target string) (err error) {
   329  	dest := target
   330  	if filepath.IsAbs(dest) {
   331  		dest = filepath.Join(w.dir, dest)
   332  	} else {
   333  		dest = filepath.Join(filepath.Dir(file), dest)
   334  	}
   335  	if !within(dest, w.dir) {
   336  		err = errors.Errorf("refused to write link %s with destination outside rootfs: %s", path, target)
   337  	}
   338  	return
   339  }
   340  
   341  // true if file is within rootDir
   342  func within(file, rootDir string) bool {
   343  	a := string(filepath.Separator)
   344  	return strings.Index(file+a, rootDir+a) == 0
   345  }
   346  
   347  func (w *DirWriter) resolveFile(path string) (r string, err error) {
   348  	r, err = securejoin.SecureJoinVFS(w.dir, path, w.fsEval)
   349  	err = errors.Wrap(err, "sanitise symlinks in rootfs")
   350  	return
   351  }
   352  
   353  func (w *DirWriter) resolveParentDir(path string) (r string, err error) {
   354  	dir, file := filepath.Split(path)
   355  	r, err = w.resolveFile(dir)
   356  	r = filepath.Join(r, file)
   357  	return
   358  }