Luigit
repositories / bugabinga.net

bugabinga.net

personal infrastructure for bugabinga!

owned by admin

services/luci/internal/runner/mounts.go

Raw
package runner

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"syscall"
	"time"

	"bugabinga.net/luci/internal/ciconfig"
	"bugabinga.net/luci/internal/podman"
)

func durableMounts(ctx context.Context, dataDir, repo, job string, declarations []ciconfig.Mount) ([]podman.Mount, func(), error) {
	ordered := append([]ciconfig.Mount(nil), declarations...)
	sort.Slice(ordered, func(i, j int) bool { return ordered[i].Type+"/"+ordered[i].Name < ordered[j].Type+"/"+ordered[j].Name })
	var files []*os.File
	cleanup := func() {
		for i := len(files) - 1; i >= 0; i-- {
			_ = syscall.Flock(int(files[i].Fd()), syscall.LOCK_UN)
			_ = files[i].Close()
		}
	}
	mounts := make([]podman.Mount, 0, len(ordered))
	for _, declaration := range ordered {
		source, err := durableMountPath(dataDir, declaration.Type, repo, job, declaration.Name)
		if err != nil {
			cleanup()
			return nil, func() {}, err
		}
		if declaration.Type == "cache" {
			if err := privateMkdirAll(dataDir, source); err != nil {
				cleanup()
				return nil, func() {}, err
			}
		}
		if err := trustedPath(dataDir, source); err != nil {
			cleanup()
			return nil, func() {}, fmt.Errorf("%s mount %q unavailable: %w", declaration.Type, declaration.Name, err)
		}
		if declaration.Type == "auth" {
			info, err := os.Lstat(source)
			if err != nil || !info.IsDir() || info.Mode().Perm()&0o077 != 0 {
				cleanup()
				return nil, func() {}, fmt.Errorf("auth mount %q is not owner-only", declaration.Name)
			}
		}
		lockPath := filepath.Join(dataDir, "mount-locks", declaration.Type, repo, job, declaration.Name)
		if err := privateMkdirAll(dataDir, filepath.Dir(lockPath)); err != nil {
			cleanup()
			return nil, func() {}, err
		}
		file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR|syscall.O_NOFOLLOW, 0o600)
		if err != nil {
			cleanup()
			return nil, func() {}, err
		}
		info, err := file.Stat()
		if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0o077 != 0 {
			_ = file.Close()
			cleanup()
			return nil, func() {}, fmt.Errorf("unsafe mount lock %q", declaration.Name)
		}
		if err := lockContext(ctx, file); err != nil {
			_ = file.Close()
			cleanup()
			return nil, func() {}, err
		}
		files = append(files, file)
		mounts = append(mounts, podman.Mount{Source: source, Target: declaration.Target})
	}
	return mounts, cleanup, nil
}

func durableMountPath(dataDir, kind, repo, job, name string) (string, error) {
	for label, value := range map[string]string{"type": kind, "repo": repo, "job": job, "mount": name} {
		if value == "" || value != filepath.Base(value) || value == "." || value == ".." || strings.Contains(value, string(filepath.Separator)) {
			return "", fmt.Errorf("unsafe %s segment %q", label, value)
		}
	}
	return filepath.Join(dataDir, "mounts", kind, repo, job, name), nil
}

func privateMkdirAll(root, target string) error {
	relative, err := filepath.Rel(root, target)
	if err != nil || relative == "." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
		return fmt.Errorf("outside data directory")
	}
	info, err := os.Lstat(root)
	if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
		return fmt.Errorf("unsafe data directory")
	}
	fd, err := os.OpenRoot(root)
	if err != nil {
		return err
	}
	defer fd.Close()
	current := ""
	for _, part := range strings.Split(relative, string(filepath.Separator)) {
		if current == "" {
			current = part
		} else {
			current = filepath.Join(current, part)
		}
		info, err := fd.Lstat(current)
		if os.IsNotExist(err) {
			if err := fd.Mkdir(current, 0o700); err != nil && !os.IsExist(err) {
				return err
			}
			info, err = fd.Lstat(current)
		}
		if err != nil {
			return err
		}
		if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 || info.Mode().Perm()&0o077 != 0 {
			return fmt.Errorf("not a private directory")
		}
	}
	return nil
}

func trustedPath(root, path string) error {
	relative, err := filepath.Rel(root, path)
	if err != nil || relative == "." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
		return fmt.Errorf("outside data directory")
	}
	current := root
	for _, part := range strings.Split(relative, string(filepath.Separator)) {
		current = filepath.Join(current, part)
		info, err := os.Lstat(current)
		if err != nil {
			return err
		}
		if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
			return fmt.Errorf("not a private directory")
		}
		if info.Mode().Perm()&0o022 != 0 {
			return fmt.Errorf("directory is writable by group or other")
		}
	}
	return nil
}

func lockContext(ctx context.Context, file *os.File) error {
	for {
		err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
		if err == nil {
			return nil
		}
		if err != syscall.EWOULDBLOCK && err != syscall.EAGAIN {
			return err
		}
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(10 * time.Millisecond):
		}
	}
}