package reportbuf import ( "errors" "io/fs" "os" "os/user" "path/filepath" "strconv" "syscall" ) // ErrDataDirOutsideVolume is returned by PrepareDataDir for a DATA_DIR // that is not the volume or a path below it, written in full. var ErrDataDirOutsideVolume = errors.New( "must be /data or a path below it, with no '.', '..' or extra '/'") // PrepareDataDir gets dir, the server's DATA_DIR, ready for owner, the // user the server runs as, so that a host directory mounted at volume, // /data in the image, needs no preparing: it creates dir, gives volume // and everything in it to owner, and gives volume and dir the mode the // server gives a directory it creates. dir must be volume or a path // below it, with no '.', '..', empty part or '/' at the end. // // bin/entrypoint.sh runs this as root, which would follow a symbolic // link anywhere, so every step goes through an os.Root opened on // volume: it follows a link only when it is written as a relative // path that stays inside volume, and refuses any other. A process on // the host can swap a link onto a path in volume at any moment while // this runs. Even then, the os.Root checks each link as it reaches // it. MkdirAll creates each directory inside a parent it already has // open, never following a link at the name it creates, and follows a // link on the path only as the os.Root allows, so a relative link // inside volume can lead it to create directories elsewhere inside // volume. Lchown never changes what a link points to, and the modes // are set on directories already opened (see chmodDir), so the most // that process can do is make a step fail or wait, or act on // something else inside volume. func PrepareDataDir(volume, dir string, owner *user.User) error { // rel is dir as a path from volume; IsLocal is false for one that // leads out of it. rel, err := filepath.Rel(volume, dir) if err != nil || dir != filepath.Clean(dir) || !filepath.IsLocal(rel) { return ErrDataDirOutsideVolume } uid, err := strconv.Atoi(owner.Uid) if err != nil { return err } gid, err := strconv.Atoi(owner.Gid) if err != nil { return err } root, err := os.OpenRoot(volume) if err != nil { return err } defer func() { _ = root.Close() }() err = root.MkdirAll(rel, dirPerms) if err != nil { return err } err = lchownAll(root, ".", uid, gid) if err != nil { return err } err = chmodDir(root, ".") if err != nil { return err } return chmodDir(root, rel) } // lchownAll gives name, a directory inside root, and everything in it // to uid and gid. It reads each directory opened through root, not // through root.FS(), which refuses a name that is not valid UTF-8, and // calls Lchown on every entry, which gives a symbolic link itself to // them, not what it points to. It goes into an entry only when the // read found a directory there, so it follows no link it finds; one // swapped in for that directory afterwards is followed only as the // os.Root allows. func lchownAll(root *os.Root, name string, uid, gid int) error { err := root.Lchown(name, uid, gid) if err != nil { return err } dir, err := root.Open(name) if err != nil { return err } entries, err := dir.ReadDir(-1) _ = dir.Close() if err != nil { return err } for _, entry := range entries { entryName := filepath.Join(name, entry.Name()) if entry.IsDir() { err = lchownAll(root, entryName, uid, gid) } else { err = root.Lchown(entryName, uid, gid) } if err != nil { return err } } return nil } // chmodDir gives name, a directory inside root, the mode the server // gives a directory it creates. Root.Chmod would not hold: on Linux it // checks that name is not a symbolic link, then sets the mode by name, // following a link swapped in between. So chmodDir opens name through // root and sets the mode on the open directory. It refuses anything // but a directory: a directory has no second name (hard link), so the // one opened is inside root, where any other file could be a hard link // to one outside. func chmodDir(root *os.Root, name string) error { dir, err := root.Open(name) if err != nil { return err } defer func() { _ = dir.Close() }() info, err := dir.Stat() if err != nil { return err } if !info.IsDir() { return &fs.PathError{Op: "chmod", Path: name, Err: syscall.ENOTDIR} } return dir.Chmod(dirPerms) }