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 }