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): } } }