check / check (push) Waiting to run
A hard link in /data shares its owner with a file that can be anywhere on the same filesystem, so chowning it gave that file to the netwatch user. A file with more than one name now keeps its owner and its path is logged. Owners are now set on the opened directory, file or link after checking it, as chmodDir sets modes, so a hard link swapped in afterwards is caught. Every other file is opened without following it; a symbolic link is given to the netwatch user itself, and devices, named pipes and sockets keep their owner. This is built only for Linux: prepare-data-dir runs only in the Linux image, and macOS has neither O_PATH nor AT_EMPTY_PATH; elsewhere it returns an error. A failed owner change names the file. Model: opus-5-5
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)
|
|
}
|