2022-07-25 18:19:27 +08:00
|
|
|
//go:build freebsd
|
|
|
|
// +build freebsd
|
|
|
|
|
|
|
|
package chroot
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"os/signal"
|
|
|
|
"path/filepath"
|
|
|
|
"runtime"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"syscall"
|
|
|
|
|
|
|
|
"github.com/containers/buildah/pkg/jail"
|
|
|
|
"github.com/containers/storage/pkg/mount"
|
|
|
|
"github.com/containers/storage/pkg/unshare"
|
|
|
|
"github.com/opencontainers/runtime-spec/specs-go"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
rlimitsMap = map[string]int{
|
|
|
|
"RLIMIT_AS": unix.RLIMIT_AS,
|
|
|
|
"RLIMIT_CORE": unix.RLIMIT_CORE,
|
|
|
|
"RLIMIT_CPU": unix.RLIMIT_CPU,
|
|
|
|
"RLIMIT_DATA": unix.RLIMIT_DATA,
|
|
|
|
"RLIMIT_FSIZE": unix.RLIMIT_FSIZE,
|
|
|
|
"RLIMIT_MEMLOCK": unix.RLIMIT_MEMLOCK,
|
|
|
|
"RLIMIT_NOFILE": unix.RLIMIT_NOFILE,
|
|
|
|
"RLIMIT_NPROC": unix.RLIMIT_NPROC,
|
|
|
|
"RLIMIT_RSS": unix.RLIMIT_RSS,
|
|
|
|
"RLIMIT_STACK": unix.RLIMIT_STACK,
|
|
|
|
}
|
|
|
|
rlimitsReverseMap = map[int]string{}
|
|
|
|
)
|
|
|
|
|
|
|
|
type runUsingChrootSubprocOptions struct {
|
|
|
|
Spec *specs.Spec
|
|
|
|
BundlePath string
|
|
|
|
}
|
|
|
|
|
2022-08-05 21:46:15 +08:00
|
|
|
func setPlatformUnshareOptions(spec *specs.Spec, cmd *unshare.Cmd) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-07-25 18:19:27 +08:00
|
|
|
func createJail(options runUsingChrootExecSubprocOptions) error {
|
|
|
|
path := options.Spec.Root.Path
|
|
|
|
jconf := jail.NewConfig()
|
|
|
|
jconf.Set("name", filepath.Base(path)+"-chroot")
|
|
|
|
jconf.Set("host.hostname", options.Spec.Hostname)
|
|
|
|
jconf.Set("persist", false)
|
|
|
|
jconf.Set("path", path)
|
|
|
|
jconf.Set("ip4", jail.INHERIT)
|
|
|
|
jconf.Set("ip6", jail.INHERIT)
|
|
|
|
jconf.Set("allow.raw_sockets", true)
|
|
|
|
jconf.Set("enforce_statfs", 1)
|
|
|
|
_, err := jail.CreateAndAttach(jconf)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error creating jail: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-08-05 21:26:39 +08:00
|
|
|
// logNamespaceDiagnostics knows which namespaces we want to create.
|
|
|
|
// Output debug messages when that differs from what we're being asked to do.
|
|
|
|
func logNamespaceDiagnostics(spec *specs.Spec) {
|
|
|
|
// Nothing here for FreeBSD
|
|
|
|
}
|
|
|
|
|
2022-07-25 18:19:27 +08:00
|
|
|
// parses the resource limits for ourselves and any processes that
|
|
|
|
// we'll start into a format that's more in line with the kernel APIs
|
|
|
|
func parseRlimits(spec *specs.Spec) (map[int]unix.Rlimit, error) {
|
|
|
|
if spec.Process == nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
parsed := make(map[int]unix.Rlimit)
|
|
|
|
for _, limit := range spec.Process.Rlimits {
|
|
|
|
resource, recognized := rlimitsMap[strings.ToUpper(limit.Type)]
|
|
|
|
if !recognized {
|
|
|
|
return nil, fmt.Errorf("error parsing limit type %q", limit.Type)
|
|
|
|
}
|
|
|
|
parsed[resource] = unix.Rlimit{Cur: int64(limit.Soft), Max: int64(limit.Hard)}
|
|
|
|
}
|
|
|
|
return parsed, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// setRlimits sets any resource limits that we want to apply to processes that
|
|
|
|
// we'll start.
|
|
|
|
func setRlimits(spec *specs.Spec, onlyLower, onlyRaise bool) error {
|
|
|
|
limits, err := parseRlimits(spec)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for resource, desired := range limits {
|
|
|
|
var current unix.Rlimit
|
|
|
|
if err := unix.Getrlimit(resource, ¤t); err != nil {
|
|
|
|
return fmt.Errorf("error reading %q limit: %w", rlimitsReverseMap[resource], err)
|
|
|
|
}
|
|
|
|
if desired.Max > current.Max && onlyLower {
|
|
|
|
// this would raise a hard limit, and we're only here to lower them
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if desired.Max < current.Max && onlyRaise {
|
|
|
|
// this would lower a hard limit, and we're only here to raise them
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if err := unix.Setrlimit(resource, &desired); err != nil {
|
|
|
|
return fmt.Errorf("error setting %q limit to soft=%d,hard=%d (was soft=%d,hard=%d): %w", rlimitsReverseMap[resource], desired.Cur, desired.Max, current.Cur, current.Max, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeReadOnly(mntpoint string, flags uintptr) error {
|
|
|
|
var fs unix.Statfs_t
|
|
|
|
// Make sure it's read-only.
|
|
|
|
if err := unix.Statfs(mntpoint, &fs); err != nil {
|
|
|
|
return fmt.Errorf("error checking if directory %q was bound read-only: %w", mntpoint, err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func isDevNull(dev os.FileInfo) bool {
|
|
|
|
if dev.Mode()&os.ModeCharDevice != 0 {
|
|
|
|
stat, _ := dev.Sys().(*syscall.Stat_t)
|
|
|
|
nullStat := syscall.Stat_t{}
|
|
|
|
if err := syscall.Stat(os.DevNull, &nullStat); err != nil {
|
|
|
|
logrus.Warnf("unable to stat /dev/null: %v", err)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if stat.Rdev == nullStat.Rdev {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func saveDir(spec *specs.Spec, path string) string {
|
|
|
|
id := filepath.Base(spec.Root.Path)
|
|
|
|
return filepath.Join(filepath.Dir(path), ".save-"+id)
|
|
|
|
}
|
|
|
|
|
|
|
|
func copyFile(source, dest string) error {
|
|
|
|
in, err := os.Open(source)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer in.Close()
|
|
|
|
|
|
|
|
out, err := os.Create(dest)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer out.Close()
|
|
|
|
|
|
|
|
_, err = io.Copy(out, in)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return out.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
type rename struct {
|
|
|
|
from, to string
|
|
|
|
}
|
|
|
|
|
|
|
|
// setupChrootBindMounts actually bind mounts things under the rootfs, and returns a
|
|
|
|
// callback that will clean up its work.
|
|
|
|
func setupChrootBindMounts(spec *specs.Spec, bundlePath string) (undoBinds func() error, err error) {
|
|
|
|
renames := []rename{}
|
|
|
|
unmounts := []string{}
|
|
|
|
removes := []string{}
|
|
|
|
undoBinds = func() error {
|
|
|
|
for _, r := range renames {
|
|
|
|
if err2 := os.Rename(r.to, r.from); err2 != nil {
|
|
|
|
logrus.Warnf("pkg/chroot: error renaming %q to %q: %v", r.to, r.from, err2)
|
|
|
|
if err == nil {
|
|
|
|
err = err2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, path := range unmounts {
|
|
|
|
if err2 := mount.Unmount(path); err2 != nil {
|
|
|
|
logrus.Warnf("pkg/chroot: error unmounting %q: %v", spec.Root.Path, err2)
|
|
|
|
if err == nil {
|
|
|
|
err = err2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, path := range removes {
|
|
|
|
if err2 := os.Remove(path); err2 != nil {
|
|
|
|
logrus.Warnf("pkg/chroot: error removing %q: %v", path, err2)
|
|
|
|
if err == nil {
|
|
|
|
err = err2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now mount all of those things to be under the rootfs's location in this
|
|
|
|
// mount namespace.
|
|
|
|
for _, m := range spec.Mounts {
|
|
|
|
// If the target is there, we can just mount it.
|
|
|
|
var srcinfo os.FileInfo
|
|
|
|
switch m.Type {
|
|
|
|
case "nullfs":
|
|
|
|
srcinfo, err = os.Stat(m.Source)
|
|
|
|
if err != nil {
|
|
|
|
return undoBinds, fmt.Errorf("error examining %q for mounting in mount namespace: %w", m.Source, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
target := filepath.Join(spec.Root.Path, m.Destination)
|
|
|
|
if _, err := os.Stat(target); err != nil {
|
|
|
|
// If the target can't be stat()ted, check the error.
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
return undoBinds, fmt.Errorf("error examining %q for mounting in mount namespace: %w", target, err)
|
|
|
|
}
|
|
|
|
// The target isn't there yet, so create it, and make a
|
|
|
|
// note to remove it later.
|
|
|
|
// XXX: This was copied from the linux version which supports bind mounting files.
|
|
|
|
// Leaving it here since I plan to add this to FreeBSD's nullfs.
|
|
|
|
if m.Type != "nullfs" || srcinfo.IsDir() {
|
|
|
|
if err = os.MkdirAll(target, 0111); err != nil {
|
|
|
|
return undoBinds, fmt.Errorf("error creating mountpoint %q in mount namespace: %w", target, err)
|
|
|
|
}
|
|
|
|
removes = append(removes, target)
|
|
|
|
} else {
|
|
|
|
if err = os.MkdirAll(filepath.Dir(target), 0111); err != nil {
|
|
|
|
return undoBinds, fmt.Errorf("error ensuring parent of mountpoint %q (%q) is present in mount namespace: %w", target, filepath.Dir(target), err)
|
|
|
|
}
|
|
|
|
// Don't do this until we can support file mounts in nullfs
|
|
|
|
/*var file *os.File
|
|
|
|
if file, err = os.OpenFile(target, os.O_WRONLY|os.O_CREATE, 0); err != nil {
|
|
|
|
return undoBinds, errors.Wrapf(err, "error creating mountpoint %q in mount namespace", target)
|
|
|
|
}
|
|
|
|
file.Close()
|
|
|
|
removes = append(removes, target)*/
|
|
|
|
}
|
|
|
|
}
|
|
|
|
logrus.Debugf("mount: %v", m)
|
|
|
|
switch m.Type {
|
|
|
|
case "nullfs":
|
|
|
|
// Do the bind mount.
|
|
|
|
if !srcinfo.IsDir() {
|
|
|
|
logrus.Debugf("emulating file mount %q on %q", m.Source, target)
|
|
|
|
_, err := os.Stat(target)
|
|
|
|
if err == nil {
|
|
|
|
save := saveDir(spec, target)
|
|
|
|
if _, err := os.Stat(save); err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
err = os.MkdirAll(save, 0111)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return undoBinds, fmt.Errorf("error creating file mount save directory %q: %w", save, err)
|
|
|
|
}
|
|
|
|
removes = append(removes, save)
|
|
|
|
}
|
|
|
|
savePath := filepath.Join(save, filepath.Base(target))
|
|
|
|
if _, err := os.Stat(target); err == nil {
|
|
|
|
logrus.Debugf("moving %q to %q", target, savePath)
|
|
|
|
if err := os.Rename(target, savePath); err != nil {
|
|
|
|
return undoBinds, fmt.Errorf("error moving %q to %q: %w", target, savePath, err)
|
|
|
|
}
|
|
|
|
renames = append(renames, rename{
|
|
|
|
from: target,
|
|
|
|
to: savePath,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
removes = append(removes, target)
|
|
|
|
}
|
|
|
|
if err := copyFile(m.Source, target); err != nil {
|
|
|
|
return undoBinds, fmt.Errorf("error copying %q to %q: %w", m.Source, target, err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logrus.Debugf("bind mounting %q on %q", m.Destination, filepath.Join(spec.Root.Path, m.Destination))
|
|
|
|
if err := mount.Mount(m.Source, target, "nullfs", strings.Join(m.Options, ",")); err != nil {
|
|
|
|
return undoBinds, fmt.Errorf("error bind mounting %q from host to %q in mount namespace (%q): %w", m.Source, m.Destination, target, err)
|
|
|
|
}
|
|
|
|
logrus.Debugf("bind mounted %q to %q", m.Source, target)
|
|
|
|
unmounts = append(unmounts, target)
|
|
|
|
}
|
|
|
|
case "devfs", "fdescfs", "tmpfs":
|
|
|
|
// Mount /dev, /dev/fd.
|
|
|
|
if err := mount.Mount(m.Source, target, m.Type, strings.Join(m.Options, ",")); err != nil {
|
|
|
|
return undoBinds, fmt.Errorf("error mounting %q to %q in mount namespace (%q, %q): %w", m.Type, m.Destination, target, strings.Join(m.Options, ","), err)
|
|
|
|
}
|
|
|
|
logrus.Debugf("mounted a %q to %q", m.Type, target)
|
|
|
|
unmounts = append(unmounts, target)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return undoBinds, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// setPdeathsig sets a parent-death signal for the process
|
|
|
|
func setPdeathsig(cmd *exec.Cmd) {
|
|
|
|
if cmd.SysProcAttr == nil {
|
|
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
|
|
}
|
|
|
|
cmd.SysProcAttr.Pdeathsig = syscall.SIGKILL
|
|
|
|
}
|