check / check (push) Waiting to run
main's tree, with next merged on 26 October, as one commit onto prod, on sneak's order. Model: opus-5-5 Co-authored-by: clawbot <clawbot@noreply.example.org>
181 lines
5.7 KiB
Go
181 lines
5.7 KiB
Go
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 the directories, regular files and symbolic links in it to
|
|
// owner, and gives volume and dir the mode the server gives a directory
|
|
// it creates. It leaves a file with a second name (hard link) as it
|
|
// is, as that name could be outside volume, and logs its path; it
|
|
// leaves a device, named pipe or socket as it is too (see chownFile).
|
|
// dir must be volume or a path below it, with no '.', '..', empty part
|
|
// or '/' at the end. On any system but Linux, the system of the image, it
|
|
// fails at the first file in volume that is not a directory (see
|
|
// chownFile).
|
|
//
|
|
// 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. The owners and modes are set on files already opened (see
|
|
// chownAll and 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 = chownAll(root, ".", uid, gid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = chmodDir(root, ".")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return chmodDir(root, rel)
|
|
}
|
|
|
|
// chownAll gives name, a directory inside root, and the directories,
|
|
// regular files and symbolic links in it to uid and gid, but not the
|
|
// files chownFile leaves. It opens each through root and gives the
|
|
// open file to them, as chmodDir sets a mode on the open directory.
|
|
// Root.Lchown would not hold: it acts on whatever is at the name by
|
|
// then, and a hard link swapped in there shares its owner with a file
|
|
// that could be outside root. A symbolic link is given to them itself,
|
|
// never what it points to, because in a directory that anyone can
|
|
// write to and that has the sticky bit set, as /tmp has, the kernel
|
|
// follows a link only for the link's owner or when the link and the
|
|
// directory have the same owner. chownAll goes into an entry only when
|
|
// the read found a directory there, so it follows no link it finds;
|
|
// one swapped in afterwards for a directory is followed only as the
|
|
// os.Root allows, and one swapped in for any other file is not
|
|
// followed at all (see chownFile).
|
|
func chownAll(root *os.Root, name string, uid, gid int) error {
|
|
entries, err := chownDir(root, name, uid, gid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
entryName := filepath.Join(name, entry.Name())
|
|
if entry.IsDir() {
|
|
err = chownAll(root, entryName, uid, gid)
|
|
} else {
|
|
err = chownFile(root, entryName, uid, gid)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// chownDir gives name, a directory inside root, to uid and gid, and
|
|
// returns what it holds. Like chmodDir, it opens name through root and
|
|
// refuses anything but a directory, which has no second name (hard
|
|
// link). It reads the open directory, not root.FS(), which refuses a
|
|
// name that is not valid UTF-8.
|
|
func chownDir(root *os.Root, name string, uid, gid int) ([]fs.DirEntry, error) {
|
|
dir, err := root.Open(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() { _ = dir.Close() }()
|
|
|
|
info, err := dir.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !info.IsDir() {
|
|
return nil, &fs.PathError{Op: "chown", Path: name, Err: syscall.ENOTDIR}
|
|
}
|
|
|
|
err = dir.Chown(uid, gid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return dir.ReadDir(-1)
|
|
}
|
|
|
|
// 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)
|
|
}
|