Merge pull request #6201 from nalind/relabel-binds-1.40

[release-1.40] run: handle relabeling bind mounts ourselves
This commit is contained in:
openshift-merge-bot[bot] 2025-06-04 09:01:26 +00:00 committed by GitHub
commit f98fb7ce19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 776 additions and 64 deletions

View File

@ -6,7 +6,7 @@ env:
#### Global variables used for all tasks
####
# Name of the ultimate destination branch for this CI run, PR or post-merge.
DEST_BRANCH: "main"
DEST_BRANCH: "release-1.40"
GOPATH: "/var/tmp/go"
GOSRC: "${GOPATH}/src/github.com/containers/buildah"
GOCACHE: "/tmp/go-build"
@ -22,18 +22,20 @@ env:
IN_PODMAN: 'false'
# root or rootless
PRIV_NAME: root
# default "mention the $BUILDAH_RUNTIME in the task alias, with initial whitespace" value
RUNTIME_N: ""
####
#### Cache-image names to test with
####
# GCE project where images live
IMAGE_PROJECT: "libpod-218412"
FEDORA_NAME: "fedora-41"
PRIOR_FEDORA_NAME: "fedora-40"
FEDORA_NAME: "fedora-42"
PRIOR_FEDORA_NAME: "fedora-41"
DEBIAN_NAME: "debian-13"
# Image identifiers
IMAGE_SUFFIX: "c20250324t111922z-f41f40d13"
IMAGE_SUFFIX: "c20250422t130822z-f42f41d13"
FEDORA_CACHE_IMAGE_NAME: "fedora-${IMAGE_SUFFIX}"
PRIOR_FEDORA_CACHE_IMAGE_NAME: "prior-fedora-${IMAGE_SUFFIX}"
DEBIAN_CACHE_IMAGE_NAME: "debian-${IMAGE_SUFFIX}"
@ -122,7 +124,7 @@ vendor_task:
# Runs within Cirrus's "community cluster"
container:
image: docker.io/library/golang:1.23
image: docker.io/library/golang:1.23.3
cpu: 1
memory: 1
@ -196,7 +198,7 @@ conformance_task:
integration_task:
name: "Integration $DISTRO_NV w/ $STORAGE_DRIVER"
name: "Integration $DISTRO_NV$RUNTIME_N w/ $STORAGE_DRIVER"
alias: integration
skip: *not_build_docs
depends_on: *smoke_vendor
@ -207,11 +209,26 @@ integration_task:
DISTRO_NV: "${FEDORA_NAME}"
IMAGE_NAME: "${FEDORA_CACHE_IMAGE_NAME}"
STORAGE_DRIVER: 'vfs'
# Disabled until we update to f41/42 as f40 does not have go 1.22
# - env:
# DISTRO_NV: "${PRIOR_FEDORA_NAME}"
# IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}"
# STORAGE_DRIVER: 'vfs'
BUILDAH_RUNTIME: crun
RUNTIME_N: " using crun"
- env:
DISTRO_NV: "${FEDORA_NAME}"
IMAGE_NAME: "${FEDORA_CACHE_IMAGE_NAME}"
STORAGE_DRIVER: 'vfs'
BUILDAH_RUNTIME: runc
RUNTIME_N: " using runc"
- env:
DISTRO_NV: "${PRIOR_FEDORA_NAME}"
IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}"
STORAGE_DRIVER: 'vfs'
BUILDAH_RUNTIME: crun
RUNTIME_N: " using crun"
- env:
DISTRO_NV: "${PRIOR_FEDORA_NAME}"
IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}"
STORAGE_DRIVER: 'vfs'
BUILDAH_RUNTIME: runc
RUNTIME_N: " using runc"
- env:
DISTRO_NV: "${DEBIAN_NAME}"
IMAGE_NAME: "${DEBIAN_CACHE_IMAGE_NAME}"
@ -221,11 +238,26 @@ integration_task:
DISTRO_NV: "${FEDORA_NAME}"
IMAGE_NAME: "${FEDORA_CACHE_IMAGE_NAME}"
STORAGE_DRIVER: 'overlay'
# Disabled until we update to f41/42 as f40 does not have go 1.22
# - env:
# DISTRO_NV: "${PRIOR_FEDORA_NAME}"
# IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}"
# STORAGE_DRIVER: 'overlay'
BUILDAH_RUNTIME: crun
RUNTIME_N: " using crun"
- env:
DISTRO_NV: "${FEDORA_NAME}"
IMAGE_NAME: "${FEDORA_CACHE_IMAGE_NAME}"
STORAGE_DRIVER: 'overlay'
BUILDAH_RUNTIME: runc
RUNTIME_N: " using runc"
- env:
DISTRO_NV: "${PRIOR_FEDORA_NAME}"
IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}"
STORAGE_DRIVER: 'overlay'
BUILDAH_RUNTIME: crun
RUNTIME_N: " using crun"
- env:
DISTRO_NV: "${PRIOR_FEDORA_NAME}"
IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}"
STORAGE_DRIVER: 'overlay'
BUILDAH_RUNTIME: runc
RUNTIME_N: " using runc"
- env:
DISTRO_NV: "${DEBIAN_NAME}"
IMAGE_NAME: "${DEBIAN_CACHE_IMAGE_NAME}"
@ -255,7 +287,7 @@ integration_task:
golang_version_script: '$GOSRC/$SCRIPT_BASE/logcollector.sh golang'
integration_rootless_task:
name: "Integration rootless $DISTRO_NV w/ $STORAGE_DRIVER"
name: "Integration rootless $DISTRO_NV$RUNTIME_N w/ $STORAGE_DRIVER"
alias: integration_rootless
skip: *not_build_docs
depends_on: *smoke_vendor
@ -268,12 +300,29 @@ integration_rootless_task:
IMAGE_NAME: "${FEDORA_CACHE_IMAGE_NAME}"
STORAGE_DRIVER: 'overlay'
PRIV_NAME: rootless
# Disabled until we update to f40/41 as f39 does not have go 1.22
# - env:
# DISTRO_NV: "${PRIOR_FEDORA_NAME}"
# IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}"
# STORAGE_DRIVER: 'overlay'
# PRIV_NAME: rootless
BUILDAH_RUNTIME: runc
RUNTIME_N: " using runc"
- env:
DISTRO_NV: "${FEDORA_NAME}"
IMAGE_NAME: "${FEDORA_CACHE_IMAGE_NAME}"
STORAGE_DRIVER: 'overlay'
PRIV_NAME: rootless
BUILDAH_RUNTIME: crun
RUNTIME_N: " using crun"
- env:
DISTRO_NV: "${PRIOR_FEDORA_NAME}"
IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}"
STORAGE_DRIVER: 'overlay'
PRIV_NAME: rootless
BUILDAH_RUNTIME: runc
RUNTIME_N: " using runc"
- env:
DISTRO_NV: "${PRIOR_FEDORA_NAME}"
IMAGE_NAME: "${PRIOR_FEDORA_CACHE_IMAGE_NAME}"
STORAGE_DRIVER: 'overlay'
PRIV_NAME: rootless
BUILDAH_RUNTIME: crun
RUNTIME_N: " using crun"
- env:
DISTRO_NV: "${DEBIAN_NAME}"
IMAGE_NAME: "${DEBIAN_CACHE_IMAGE_NAME}"

View File

@ -59,7 +59,7 @@ export GOLANGCI_LINT_VERSION := 2.1.0
# Note: Uses the -N -l go compiler options to disable compiler optimizations
# and inlining. Using these build options allows you to subsequently
# use source debugging tools like delve.
all: bin/buildah bin/imgtype bin/copy bin/inet bin/tutorial docs
all: bin/buildah bin/imgtype bin/copy bin/inet bin/tutorial bin/dumpspec docs
# Update nix/nixpkgs.json its latest stable commit
.PHONY: nixpkgs
@ -107,6 +107,9 @@ bin/buildah.%: $(SOURCES)
mkdir -p ./bin
GOOS=$(word 2,$(subst ., ,$@)) GOARCH=$(word 3,$(subst ., ,$@)) $(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ -tags "containers_image_openpgp" ./cmd/buildah
bin/dumpspec: $(SOURCES)
$(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/dumpspec
bin/imgtype: $(SOURCES)
$(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/imgtype/imgtype.go

View File

@ -1,11 +0,0 @@
//go:build !linux && !(freebsd && cgo)
package chroot
import (
"errors"
)
func getPtyDescriptors() (int, int, error) {
return -1, -1, errors.New("getPtyDescriptors not supported on this platform")
}

View File

@ -18,6 +18,7 @@ import (
"syscall"
"github.com/containers/buildah/bind"
"github.com/containers/buildah/internal/pty"
"github.com/containers/buildah/util"
"github.com/containers/storage/pkg/ioutils"
"github.com/containers/storage/pkg/reexec"
@ -217,7 +218,7 @@ func runUsingChrootMain() {
var stderr io.Writer
fdDesc := make(map[int]string)
if options.Spec.Process.Terminal {
ptyMasterFd, ptyFd, err := getPtyDescriptors()
ptyMasterFd, ptyFd, err := pty.GetPtyDescriptors()
if err != nil {
logrus.Errorf("error opening PTY descriptors: %v", err)
os.Exit(1)

View File

@ -189,7 +189,7 @@ The default certificates directory is _/etc/containers/certs.d_.
**--cgroup-parent**=""
Path to cgroups under which the cgroup for the container will be created. If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. Cgroups will be created if they do not already exist.
Path to cgroups under which the cgroup for RUN instructions will be created. If the path is not absolute, the path is considered to be relative to the cgroups path of the init process. Cgroups will be created if they do not already exist.
**--cgroupns** *how*

View File

@ -1,6 +1,6 @@
//go:build freebsd && cgo
package chroot
package pty
// #include <fcntl.h>
// #include <stdlib.h>
@ -37,7 +37,9 @@ func unlockpt(fd int) error {
return nil
}
func getPtyDescriptors() (int, int, error) {
// GetPtyDescriptors allocates a new pseudoterminal and returns the control and
// pseudoterminal file descriptors.
func GetPtyDescriptors() (int, int, error) {
// Create a pseudo-terminal and open the control side
controlFd, err := openpt()
if err != nil {

View File

@ -1,6 +1,6 @@
//go:build linux
package chroot
package pty
import (
"fmt"
@ -11,9 +11,11 @@ import (
"golang.org/x/sys/unix"
)
// Open a PTY using the /dev/ptmx device. The main advantage of using
// this instead of posix_openpt is that it avoids cgo.
func getPtyDescriptors() (int, int, error) {
// GetPtyDescriptors allocates a new pseudoterminal and returns the control and
// pseudoterminal file descriptors. This implementation uses the /dev/ptmx
// device. The main advantage of using this instead of posix_openpt is that it
// avoids cgo.
func GetPtyDescriptors() (int, int, error) {
// Create a pseudo-terminal -- open a copy of the master side.
controlFd, err := unix.Open("/dev/ptmx", os.O_RDWR, 0o600)
if err != nil {

View File

@ -0,0 +1,13 @@
//go:build !linux && !(freebsd && cgo)
package pty
import (
"errors"
)
// GetPtyDescriptors would allocate a new pseudoterminal and return the control and
// pseudoterminal file descriptors, if only it could.
func GetPtyDescriptors() (int, int, error) {
return -1, -1, errors.New("GetPtyDescriptors not supported on this platform")
}

View File

@ -137,6 +137,7 @@ export BUILDTAGS+=" libtrust_openssl"
%gobuild -o bin/copy ./tests/copy
%gobuild -o bin/tutorial ./tests/tutorial
%gobuild -o bin/inet ./tests/inet
%gobuild -o bin/dumpspec ./tests/dumpspec
%{__make} docs
%install
@ -148,6 +149,7 @@ cp bin/imgtype %{buildroot}/%{_bindir}/%{name}-imgtype
cp bin/copy %{buildroot}/%{_bindir}/%{name}-copy
cp bin/tutorial %{buildroot}/%{_bindir}/%{name}-tutorial
cp bin/inet %{buildroot}/%{_bindir}/%{name}-inet
cp bin/dumpspec %{buildroot}/%{_bindir}/%{name}-dumpspec
rm %{buildroot}%{_datadir}/%{name}/test/system/tools/build/*
@ -172,6 +174,7 @@ rm %{buildroot}%{_datadir}/%{name}/test/system/tools/build/*
%{_bindir}/%{name}-copy
%{_bindir}/%{name}-tutorial
%{_bindir}/%{name}-inet
%{_bindir}/%{name}-dumpspec
%{_datadir}/%{name}/test
%changelog

View File

@ -696,8 +696,9 @@ func runUsingRuntime(options RunOptions, configureNetwork bool, moreCreateArgs [
return 1, fmt.Errorf("parsing container state %q from %s: %w", string(stateOutput), runtime, err)
}
switch state.Status {
case "running":
case "stopped":
case specs.StateCreating, specs.StateCreated, specs.StateRunning:
// all fine
case specs.StateStopped:
atomic.StoreUint32(&stopped, 1)
default:
return 1, fmt.Errorf("container status unexpectedly changed to %q", state.Status)

View File

@ -543,6 +543,33 @@ rootless=%d
defer b.cleanupTempVolumes()
// Handle mount flags that request that the source locations for "bind" mountpoints be
// relabeled, and filter those flags out of the list of mount options we pass to the
// runtime.
for i := range spec.Mounts {
switch spec.Mounts[i].Type {
default:
continue
case "bind", "rbind":
// all good, keep going
}
zflag := ""
for _, opt := range spec.Mounts[i].Options {
if opt == "z" || opt == "Z" {
zflag = opt
}
}
if zflag == "" {
continue
}
spec.Mounts[i].Options = slices.DeleteFunc(spec.Mounts[i].Options, func(opt string) bool {
return opt == "z" || opt == "Z"
})
if err := relabel(spec.Mounts[i].Source, b.MountLabel, zflag == "z"); err != nil {
return fmt.Errorf("setting file label %q on %q: %w", b.MountLabel, spec.Mounts[i].Source, err)
}
}
switch isolation {
case define.IsolationOCI:
var moreCreateArgs []string
@ -1139,16 +1166,19 @@ func (b *Builder) runSetupVolumeMounts(mountLabel string, volumeMounts []string,
if err := relabel(host, mountLabel, true); err != nil {
return specs.Mount{}, err
}
options = slices.DeleteFunc(options, func(o string) bool { return o == "z" })
}
if foundZ {
if err := relabel(host, mountLabel, false); err != nil {
return specs.Mount{}, err
}
options = slices.DeleteFunc(options, func(o string) bool { return o == "Z" })
}
if foundU {
if err := chown.ChangeHostPathOwnership(host, true, idMaps.processUID, idMaps.processGID); err != nil {
return specs.Mount{}, err
}
options = slices.DeleteFunc(options, func(o string) bool { return o == "U" })
}
if foundO {
if (upperDir != "" && workDir == "") || (workDir != "" && upperDir == "") {

View File

@ -6065,7 +6065,6 @@ _EOF
@test "bud with --cgroup-parent" {
skip_if_rootless_environment
skip_if_no_runtime
skip_if_chroot
_prefetch alpine
@ -6073,24 +6072,18 @@ _EOF
mytmpdir=${TEST_SCRATCH_DIR}/my-dir
mkdir -p ${mytmpdir}
cat > $mytmpdir/Containerfile << _EOF
from alpine
run cat /proc/self/cgroup
FROM alpine
RUN .linux.cgroupsPath
_EOF
# with cgroup-parent
run_buildah --cgroup-manager cgroupfs build --cgroupns=host --cgroup-parent test-cgroup -t with-flag \
$WITH_POLICY_JSON --file ${mytmpdir}/Containerfile .
if is_cgroupsv2; then
expect_output --from="${lines[2]}" "0::/test-cgroup"
else
expect_output --substring "/test-cgroup"
fi
--runtime ${DUMPSPEC_BINARY} $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile .
expect_output --substring "test-cgroup"
# without cgroup-parent
run_buildah --cgroup-manager cgroupfs build -t without-flag \
$WITH_POLICY_JSON --file ${mytmpdir}/Containerfile .
if [ -n "$(grep "test-cgroup" <<< "$output")" ]; then
die "Unexpected cgroup."
fi
--runtime ${DUMPSPEC_BINARY} $WITH_POLICY_JSON --file ${mytmpdir}/Containerfile .
assert "$output" !~ test-cgroup
}
@test "bud with --cpu-period and --cpu-quota" {

475
tests/dumpspec/dumpspec.go Normal file
View File

@ -0,0 +1,475 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"unicode"
"github.com/containers/storage/pkg/ioutils"
"github.com/containers/storage/pkg/reexec"
rspec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// use defined names for our various commands. we absolutely don't support
// everything that an actual functional runtime would, and have no intention of
// expanding to do so
type modeType string
const (
modeCreate = modeType("create")
modeStart = modeType("start")
modeState = modeType("state")
modeKill = modeType("kill")
modeDelete = modeType("delete")
subprocName = "dumpspec-subproc"
)
// signalsByName is a guess at which signals we'd be asked to send to a child
// process, currently restricted to the subset defined across all of our
// targets
var signalsByName = map[string]syscall.Signal{
"SIGABRT": syscall.SIGABRT,
"SIGALRM": syscall.SIGALRM,
"SIGBUS": syscall.SIGBUS,
"SIGFPE": syscall.SIGFPE,
"SIGHUP": syscall.SIGHUP,
"SIGILL": syscall.SIGILL,
"SIGINT": syscall.SIGINT,
"SIGKILL": syscall.SIGKILL,
"SIGPIPE": syscall.SIGPIPE,
"SIGQUIT": syscall.SIGQUIT,
"SIGSEGV": syscall.SIGSEGV,
"SIGTERM": syscall.SIGTERM,
"SIGTRAP": syscall.SIGTRAP,
}
var (
globalArgs struct {
debug bool
cgroupManager string
log string
logFormat string
logLevel string
root string
systemdCgroup bool
rootless bool
}
createArgs struct {
bundleDir string
configFile string
consoleSocket string
pidFile string
noPivot bool
noNewKeyring bool
preserveFds int
}
stateArgs struct {
all bool
pid int
regex string
}
killArgs struct {
all bool
pid int
regex string
signal int
}
deleteArgs struct {
force bool
regex string
}
)
func main() {
if reexec.Init() {
return
}
if len(os.Args) < 2 {
return
}
var container, containerID, containerDir string
mainCommand := cobra.Command{
Use: "dumpspec",
Short: "fake OCI runtime",
PersistentPreRunE: func(_ *cobra.Command, args []string) error {
tmpdir, ok := os.LookupEnv("XDG_RUNTIME_DIR")
if !ok {
tmpdir = filepath.Join(os.TempDir(), strconv.Itoa(os.Getuid()))
}
if globalArgs.root != "" {
tmpdir = globalArgs.root
}
tmpdir = filepath.Join(tmpdir, "dumpspec")
if err := os.MkdirAll(tmpdir, 0o700); err != nil && !errors.Is(err, os.ErrExist) {
return fmt.Errorf("ensuring that %q exists: %w", tmpdir, err)
}
if len(args) > 0 {
// this is the first arg for all of the commands that we care about
container = args[0]
}
containerID = mapToContainerID(container)
containerDir = filepath.Join(tmpdir, containerID)
return nil
},
}
mainFlags := mainCommand.PersistentFlags()
mainFlags.BoolVar(&globalArgs.debug, "debug", false, "log for debugging")
mainFlags.BoolVar(&globalArgs.systemdCgroup, "systemd-cgroup", false, "use systemd for handling cgroups")
mainFlags.BoolVar(&globalArgs.rootless, "rootless", false, "ignore some settings to that conflict with rootless operation")
mainFlags.StringVar(&globalArgs.cgroupManager, "cgroup-manager", "cgroupfs", "method for managing cgroups")
mainFlags.StringVar(&globalArgs.log, "log", "", "logging destination")
mainFlags.StringVar(&globalArgs.logFormat, "log-format", "", "logging format specifier")
mainFlags.StringVar(&globalArgs.logLevel, "log-level", "", "logging level")
rootUsage := "root `directory` of runtime data"
rootDefault := ""
if xdgRuntimeDir, ok := os.LookupEnv("XDG_RUNTIME_DIR"); ok {
rootUsage += " (default $XDG_RUNTIME_DIR)"
rootDefault = xdgRuntimeDir
}
mainFlags.StringVar(&globalArgs.root, "root", rootDefault, rootUsage)
createCommand := &cobra.Command{
Use: string(modeCreate),
Args: cobra.ExactArgs(1),
Short: "create a ready-to-start container process",
RunE: func(_ *cobra.Command, _ []string) error {
if err := os.MkdirAll(containerDir, 0o700); err != nil {
return fmt.Errorf("creating container directory: %w", err)
}
configFile := createArgs.configFile
if configFile == "" {
configFile = filepath.Join(createArgs.bundleDir, "config.json")
}
config, err := os.ReadFile(configFile)
if err != nil {
return fmt.Errorf("reading runtime configuration: %w", err)
}
var spec rspec.Spec
if err := json.Unmarshal(config, &spec); err != nil {
return fmt.Errorf("parsing runtime configuration: %w", err)
}
if err := os.WriteFile(filepath.Join(containerDir, "config.json"), config, 0o600); err != nil {
return fmt.Errorf("saving copy of runtime configuration: %w", err)
}
state := rspec.State{
Version: rspec.Version,
ID: container,
Bundle: createArgs.bundleDir,
}
stateBytes, err := json.Marshal(state)
if err != nil {
return fmt.Errorf("encoding initial runtime state: %w", err)
}
if err := os.WriteFile(filepath.Join(containerDir, "state"), stateBytes, 0o600); err != nil {
return fmt.Errorf("writing initial runtime state: %w", err)
}
pr, pw, err := os.Pipe()
if err != nil {
return fmt.Errorf("internal error: %w", err)
}
defer pr.Close()
cmd := getStarter(containerDir, createArgs.consoleSocket, createArgs.pidFile, spec, pw)
if err := cmd.Start(); err != nil {
return fmt.Errorf("internal error: %w", err)
}
pw.Close()
ready, err := io.ReadAll(pr)
if err != nil {
return fmt.Errorf("waiting for child to start: %w", err)
}
if strings.TrimSpace(string(ready)) != "OK" {
return fmt.Errorf("unexpected child status %q", string(ready))
}
return nil
},
}
createFlags := createCommand.Flags()
createFlags.StringVarP(&createArgs.bundleDir, "bundle", "b", "", "`directory` containing config.json")
createFlags.StringVarP(&createArgs.configFile, "config", "f", "", "`path` to config.json")
createFlags.StringVar(&createArgs.consoleSocket, "console-socket", "", "socket `path` for passing PTY")
createFlags.StringVar(&createArgs.pidFile, "pid-file", "", "`path` in which to store child PID")
createFlags.BoolVar(&createArgs.noPivot, "no-pivot", false, "use chroot() instead of pivot_root()")
createFlags.BoolVar(&createArgs.noNewKeyring, "no-new-keyring", false, "don't create a new keyring")
mainCommand.AddCommand(createCommand)
startCommand := &cobra.Command{
Use: string(modeStart),
Args: cobra.ExactArgs(1),
Short: "start a previously-created container process",
RunE: func(_ *cobra.Command, _ []string) error {
if err := ioutils.AtomicWriteFile(filepath.Join(containerDir, "start"), []byte("start"), 0o600); err != nil {
return fmt.Errorf("writing start file: %w", err)
}
return nil
},
}
mainCommand.AddCommand(startCommand)
stateCommand := &cobra.Command{
Use: string(modeState),
Args: cobra.ExactArgs(1),
Short: "poll the state of a container process",
RunE: func(_ *cobra.Command, _ []string) error {
stateFile, err := os.Open(filepath.Join(containerDir, "state"))
if err != nil {
return err
}
defer stateFile.Close()
if _, err := io.Copy(os.Stdout, stateFile); err != nil {
return fmt.Errorf("copying state file: %w", err)
}
return nil
},
}
stateFlags := stateCommand.Flags()
stateFlags.BoolVarP(&stateArgs.all, "all", "a", false, "start all containers")
stateFlags.IntVar(&stateArgs.pid, "pid", 0, "start container by `pid`")
stateFlags.StringVarP(&stateArgs.regex, "regex", "r", "", "start containers with IDs matching a `regex`")
mainCommand.AddCommand(stateCommand)
killCommand := &cobra.Command{
Use: string(modeKill),
Args: cobra.RangeArgs(1, 2),
Short: "signal/kill a container process",
RunE: func(_ *cobra.Command, args []string) error {
if len(args) > 1 {
signalString := args[1]
signalNumber, err := strconv.Atoi(signalString)
if err != nil {
n, ok := signalsByName[signalString]
if !ok {
n, ok = signalsByName["SIG"+signalString]
if !ok {
return fmt.Errorf("%v: unrecognized signal %q", os.Args, signalString)
}
}
signalNumber = int(n)
}
killArgs.signal = signalNumber
}
if err := ioutils.AtomicWriteFile(filepath.Join(containerDir, "kill"), []byte(strconv.Itoa(killArgs.signal)), 0o600); err != nil {
return fmt.Errorf("writing exit status file: %w", err)
}
return nil
},
}
killFlags := killCommand.Flags()
killFlags.BoolVarP(&killArgs.all, "all", "a", false, "signal/kill all containers")
killFlags.IntVar(&killArgs.pid, "pid", 0, "signal/kill container by `pid`")
killFlags.StringVarP(&killArgs.regex, "regex", "r", "", "signal/kill containers with IDs matching a `regex`")
mainCommand.AddCommand(killCommand)
deleteCommand := &cobra.Command{
Use: string(modeDelete),
Args: cobra.ExactArgs(1),
Short: "delete a container process",
RunE: func(_ *cobra.Command, _ []string) error {
if err := os.RemoveAll(containerDir); err != nil {
return fmt.Errorf("removing container directory: %w", err)
}
return nil
},
}
deleteFlags := deleteCommand.Flags()
deleteFlags.StringVarP(&deleteArgs.regex, "regex", "r", "", "delete containers with IDs matching a `regex`")
deleteFlags.BoolVarP(&deleteArgs.force, "force", "f", false, "forcibly stop containers which are not stopped")
mainCommand.AddCommand(deleteCommand)
err := mainCommand.Execute()
if err != nil {
logrus.Fatal(err)
os.Exit(1)
}
os.Exit(0)
}
func mapToContainerID(container string) string {
var encoder strings.Builder
for _, c := range container {
if unicode.IsLetter(c) || unicode.IsNumber(c) {
if _, err := encoder.WriteRune(c); err != nil {
logrus.Fatalf("%v: encoding container ID: %q: %v", os.Args, c, err)
}
} else {
if _, err := encoder.WriteString(strconv.Itoa(int(c))); err != nil {
logrus.Fatalf("%v: encoding container ID: %q: %v", os.Args, c, err)
}
}
}
return encoder.String()
}
func waitForFile(dirname, basename string) string {
waitedFile := filepath.Join(dirname, basename)
for {
if _, err := os.Stat(dirname); err != nil {
logrus.Fatalf("%v: %v", os.Args, err)
}
st, err := os.Stat(waitedFile)
if err != nil && !errors.Is(err, os.ErrNotExist) {
logrus.Fatalf("%v: %v", os.Args, err)
}
if err != nil || st.Size() == 0 {
time.Sleep(100 * time.Millisecond)
continue
}
contents, err := os.ReadFile(waitedFile)
if err != nil {
logrus.Fatalf("%v: %v", os.Args, err)
}
text := strings.TrimSpace(string(contents))
return text
}
}
func init() {
reexec.Register(subprocName, subproc)
}
func subproc() {
mainCommand := cobra.Command{
Use: "dumpspec",
Short: "fake OCI runtime",
Long: "dumpspec containerDir consoleSocket pidFile [spec ...]",
Args: cobra.ExactArgs(3),
RunE: func(_ *cobra.Command, args []string) error {
dir := args[0]
consoleSocket := args[1]
pidFile := args[2]
config, err := os.ReadFile(filepath.Join(dir, "config.json"))
if err != nil {
return fmt.Errorf("reading runtime configuration: %w", err)
}
var spec rspec.Spec
if err := json.Unmarshal(config, &spec); err != nil {
return fmt.Errorf("parsing runtime configuration: %w", err)
}
stateBytes, err := os.ReadFile(filepath.Join(dir, "state"))
if err != nil {
return fmt.Errorf("reading initial state : %w", err)
}
var state rspec.State
if err := json.Unmarshal(stateBytes, &state); err != nil {
return fmt.Errorf("parsing initial state: %w", err)
}
saveState := func() error {
stateBytes, err := json.Marshal(state)
if err != nil {
return fmt.Errorf("encoding updated state: %w", err)
}
err = ioutils.AtomicWriteFile(filepath.Join(dir, "state"), stateBytes, 0o600)
if err != nil {
return fmt.Errorf("writing updated state: %w", err)
}
return nil
}
output := io.Writer(os.Stdout)
if pidFile != "" {
if err := ioutils.AtomicWriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())), 0o600); err != nil {
return fmt.Errorf("writing pid file %q: %w", pidFile, err)
}
}
state.Pid = os.Getpid()
state.Status = rspec.StateCreated
if err := saveState(); err != nil {
return err
}
if consoleSocket != "" {
if output, err = sendConsoleDescriptor(consoleSocket); err != nil {
return fmt.Errorf("sending terminal control fd to parent process: %w", err)
}
}
ok := os.NewFile(3, "startup status pipe")
fmt.Fprintf(ok, "OK")
ok.Close()
start := waitForFile(dir, "start")
if start != "start" {
return fmt.Errorf("unexpected start indicator %q", start)
}
state.Status = rspec.StateRunning
if err := saveState(); err != nil {
return err
}
if spec.Process == nil || len(spec.Process.Args) == 0 {
if _, err := io.Copy(output, bytes.NewReader(config)); err != nil {
return fmt.Errorf("writing configuration: %w", err)
}
} else {
for _, query := range spec.Process.Args {
var data any
if err := json.Unmarshal(config, &data); err != nil {
return fmt.Errorf("parsing runtime configuration: %w", err)
}
path := strings.Split(query, ".")
for i, component := range path {
if component == "" {
continue
}
pathSoFar := strings.Join(path[:i], ".")
if data == nil {
return fmt.Errorf("unable to descend into %q after %q", component, pathSoFar)
}
if m, ok := data.(map[string]any); ok {
data = m[component]
} else if s, ok := data.([]any); ok {
i, err := strconv.Atoi(component)
if err != nil {
return fmt.Errorf("%q is not numeric while indexing slice at %q", component, pathSoFar)
}
data = s[i]
} else {
return fmt.Errorf("unable to descend into %q after %q", component, pathSoFar)
}
}
final, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("encoding query result: %w", err)
}
if len(final) == 0 || final[len(final)-1] != '\n' {
final = append(final, byte('\n'))
}
if _, err := io.Copy(output, bytes.NewReader(final)); err != nil {
return fmt.Errorf("writing configuration subset %q: %w", query, err)
}
}
}
state.Status = rspec.StateStopped
if err := saveState(); err != nil {
return err
}
return nil
},
}
err := mainCommand.Execute()
if err != nil {
logrus.Fatal(err)
os.Exit(1)
}
os.Exit(0)
}

View File

@ -0,0 +1,41 @@
package main
import (
"os"
"slices"
"syscall"
"github.com/containers/storage/pkg/unshare"
rspec "github.com/opencontainers/runtime-spec/specs-go"
)
func getStarter(containerDir, consoleSocket, pidFile string, spec rspec.Spec, extraFile *os.File) interface{ Start() error } {
cmd := unshare.Command(subprocName, containerDir, consoleSocket, pidFile)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if spec.Linux != nil {
for _, ns := range spec.Linux.Namespaces {
switch ns.Type {
case rspec.UserNamespace:
cmd.UnshareFlags |= syscall.CLONE_NEWUSER
case rspec.NetworkNamespace: // caller is expecting to configure networking for this process's network namespace
cmd.UnshareFlags |= syscall.CLONE_NEWNET
case rspec.MountNamespace:
cmd.UnshareFlags |= syscall.CLONE_NEWNS
case rspec.IPCNamespace:
cmd.UnshareFlags |= syscall.CLONE_NEWIPC
case rspec.UTSNamespace:
cmd.UnshareFlags |= syscall.CLONE_NEWUTS
case rspec.CgroupNamespace:
cmd.UnshareFlags |= syscall.CLONE_NEWCGROUP
}
}
cmd.UidMappings = slices.Clone(spec.Linux.UIDMappings)
cmd.GidMappings = slices.Clone(spec.Linux.GIDMappings)
}
if extraFile != nil {
cmd.ExtraFiles = append([]*os.File{extraFile}, cmd.ExtraFiles...)
}
return cmd
}

View File

@ -0,0 +1,21 @@
//go:build !linux
package main
import (
"os"
"os/exec"
rspec "github.com/opencontainers/runtime-spec/specs-go"
)
func getStarter(containerDir, consoleSocket, pidFile string, _ rspec.Spec, extraFile *os.File) interface{ Start() error } {
cmd := exec.Command(subprocName, containerDir, consoleSocket, pidFile)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if extraFile != nil {
cmd.ExtraFiles = append([]*os.File{extraFile}, cmd.ExtraFiles...)
}
return cmd
}

View File

@ -0,0 +1,12 @@
//go:build windows
package main
import (
"errors"
"os"
)
func sendConsoleDescriptor(consoleSocket string) (*os.File, error) {
return nil, errors.New("unable to transport pseudoterminal descriptors")
}

View File

@ -0,0 +1,41 @@
//go:build !windows
package main
import (
"fmt"
"net"
"os"
"github.com/containers/buildah/internal/pty"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
func sendConsoleDescriptor(consoleSocket string) (*os.File, error) {
closePty := true
control, pty, err := pty.GetPtyDescriptors()
if err != nil {
return nil, fmt.Errorf("allocating pseudo-terminal: %w", err)
}
defer unix.Close(control)
defer func() {
if closePty {
if err := unix.Close(pty); err != nil {
logrus.Errorf("closing pty descriptor %d: %v", pty, err)
}
}
}()
socketReceiver, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: consoleSocket, Net: "unix"})
if err != nil {
return nil, fmt.Errorf("allocating pseudo-terminal: %w", err)
}
defer socketReceiver.Close()
rights := unix.UnixRights(control)
_, _, err = socketReceiver.WriteMsgUnix(nil, rights, nil)
if err != nil {
return nil, fmt.Errorf("sending terminal control fd to parent process: %w", err)
}
closePty = false
return os.NewFile(uintptr(pty), "controlling terminal"), nil
}

34
tests/dumpspec/notes.md Normal file
View File

@ -0,0 +1,34 @@
Global flags:
crun: --cgroup-manager=MANAGER --debug --log=FILE --log-format={text|json} --log-level --root=DIR --rootless={true|false|auto} --systemd-cgroup
runc: --debug --log=FILE --log-format={text|json} --root=DIR --systemd-cgroup --rootless={true|false|auto}
create [-b|--bundle dir] [--console-socket[=]path] [--pid-file[=]path] [--no-pivot] [--preserve-fds[=]N] containerID
runc: [--pidfd-socket=path] [--no-new-keyring]
crun: [-f|--config file] [--no-subreaper (ignored)] [--no-new-keyring]
runsc: [--pidfd-socket=path]
* Start keeping track of containerID under --root or $XDG_RUNTIME_DIR/$runtimeName
* If console socket given, allocate a pseudoterminal, connect to it, and pass a TTY descriptor.
* If not, pass stdio down directly.
* Prepare, but have babysitter wait before starting process.
start containerID
* Start process connected to stdio or terminal.
state containerID
crun: [-a|--all] [-r|--regex regex]
runsc: [-all|--all] [-pid int (in parent pid namespace)]
* Output a JSON-encoded github.com/opencontainers/runtime-spec/specs-go.State value on stdout.
kill containerID [signal]
crun: [-a|--all] [-r|--regex regex]
runsc: [-all|--all] [-pid int (in parent pid namespace)]
* Send signal to process tree.
delete containerID
runc: [-f|--force (SIGKILL first if need be)]
crun: [-f|--force (SIGKILL first if need be)] [-r|--regex regex]
runsc: [-force|--force]
runc: checkpoint events exec features list pause ps resume restore run spec state update
crun: checkpoint exec features list pause ps resume restore run spec state update
runsc: checkpoint do events exec flags list pause port-forward ps restore resume run spec state wait

View File

@ -8,6 +8,7 @@ IMGTYPE_BINARY=${IMGTYPE_BINARY:-$TEST_SOURCES/../bin/imgtype}
COPY_BINARY=${COPY_BINARY:-$TEST_SOURCES/../bin/copy}
TUTORIAL_BINARY=${TUTORIAL_BINARY:-$TEST_SOURCES/../bin/tutorial}
INET_BINARY=${INET_BINARY:-$TEST_SOURCES/../bin/inet}
DUMPSPEC_BINARY=${DUMPSPEC_BINARY:-$TEST_SOURCES/../bin/dumpspec}
STORAGE_DRIVER=${STORAGE_DRIVER:-vfs}
PATH=$(dirname ${BASH_SOURCE})/../bin:${PATH}
OCI=${CI_DESIRED_RUNTIME:-$(${BUILDAH_BINARY} info --format '{{.host.OCIRuntime}}' || command -v runc || command -v crun)}

View File

@ -417,7 +417,7 @@ function configure_and_check_user() {
zflag=
if which selinuxenabled > /dev/null 2> /dev/null ; then
if selinuxenabled ; then
zflag=z
zflag=,z
fi
fi
${OCI} --version
@ -426,23 +426,23 @@ function configure_and_check_user() {
cid=$output
mkdir -p ${TEST_SCRATCH_DIR}/was:empty
# As a baseline, this should succeed.
run_buildah run --mount type=tmpfs,dst=/var/tmpfs-not-empty $cid touch /var/tmpfs-not-empty/testfile
run_buildah run --mount type=tmpfs,dst=/var/tmpfs-not-empty $cid touch /var/tmpfs-not-empty/testfile
# This should succeed, but the writes should effectively be discarded
run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/not-empty,rw${zflag:+,${zflag}} $cid touch /var/not-empty/testfile
run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/not-empty,rw${zflag} $cid touch /var/not-empty/testfile
if test -r ${TEST_SCRATCH_DIR}/was:empty/testfile ; then
die write to mounted type=bind was not discarded, ${TEST_SCRATCH_DIR}/was:empty/testfile exists
fi
# If we're parsing the options at all, this should be read-only, so it should fail.
run_buildah 1 run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/not-empty,ro${zflag:+,${zflag}} $cid touch /var/not-empty/testfile
run_buildah 1 run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/not-empty,ro${zflag} $cid touch /var/not-empty/testfile
# Even if the parent directory doesn't exist yet, this should succeed, but again the write should be discarded.
run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/multi-level/subdirectory,rw${zflag:+,${zflag}} $cid touch /var/multi-level/subdirectory/testfile
run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty,dst=/var/multi-level/subdirectory,rw${zflag} $cid touch /var/multi-level/subdirectory/testfile
if test -r ${TEST_SCRATCH_DIR}/was:empty/testfile ; then
die write to mounted type=bind was not discarded, ${TEST_SCRATCH_DIR}/was:empty/testfile exists
fi
# And check the same for file volumes, which make life harder because the kernel's overlay
# filesystem really only wants to be dealing with directories.
: > ${TEST_SCRATCH_DIR}/was:empty/testfile
run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty/testfile,dst=/var/different-multi-level/subdirectory/testfile,rw${zflag:+,${zflag}} $cid sh -c 'echo wrote > /var/different-multi-level/subdirectory/testfile'
run_buildah run --mount type=bind,src=${TEST_SCRATCH_DIR}/was:empty/testfile,dst=/var/different-multi-level/subdirectory/testfile,rw${zflag} $cid sh -c 'echo wrote > /var/different-multi-level/subdirectory/testfile'
if test -s ${TEST_SCRATCH_DIR}/was:empty/testfile ; then
die write to mounted type=bind was not discarded, ${TEST_SCRATCH_DIR}/was:empty/testfile was written to
fi

View File

@ -9,6 +9,7 @@ environment:
INET_BINARY: /usr/bin/buildah-inet
COPY_BINARY: /usr/bin/buildah-copy
TUTORIAL_BINARY: /usr/bin/buildah-tutorial
DUMPSPEC_BINARY: /usr/bin/buildah-dumpspec
TMPDIR: /var/tmp
/local/root: