mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
mv contrib/cmd tests/cmd (except memfd-bind)
The following commands are moved from `contrib/cmd` to `tests/cmd`: - fs-idmap - pidfd-kill - recvtty - remap-rootfs - sd-helper - seccompagent Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
This commit is contained in:
@@ -1,61 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage:", os.Args[0], "path_to_mount_set_attr")
|
||||
os.Exit(1)
|
||||
}
|
||||
src := os.Args[1]
|
||||
if err := supportsIDMap(src); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "fatal error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func supportsIDMap(src string) error {
|
||||
treeFD, err := unix.OpenTree(unix.AT_FDCWD, src, uint(unix.OPEN_TREE_CLONE|unix.OPEN_TREE_CLOEXEC|unix.AT_EMPTY_PATH))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error calling open_tree %q: %w", src, err)
|
||||
}
|
||||
defer unix.Close(treeFD)
|
||||
|
||||
cmd := exec.Command("sleep", "5")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Cloneflags: syscall.CLONE_NEWUSER,
|
||||
UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: 65536, Size: 65536}},
|
||||
GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: 65536, Size: 65536}},
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to run the helper binary: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = cmd.Process.Kill()
|
||||
_ = cmd.Wait()
|
||||
}()
|
||||
|
||||
path := fmt.Sprintf("/proc/%d/ns/user", cmd.Process.Pid)
|
||||
var userNsFile *os.File
|
||||
if userNsFile, err = os.Open(path); err != nil {
|
||||
return fmt.Errorf("unable to get user ns file descriptor: %w", err)
|
||||
}
|
||||
defer userNsFile.Close()
|
||||
|
||||
attr := unix.MountAttr{
|
||||
Attr_set: unix.MOUNT_ATTR_IDMAP,
|
||||
Userns_fd: uint64(userNsFile.Fd()),
|
||||
}
|
||||
if err := unix.MountSetattr(treeFD, "", unix.AT_EMPTY_PATH, &attr); err != nil {
|
||||
return fmt.Errorf("error calling mount_setattr: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/opencontainers/runc/libcontainer/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
usage = `Open Container Initiative contrib/cmd/pidfd-kill
|
||||
|
||||
pidfd-kill is an implementation of a consumer of runC's --pidfd-socket API.
|
||||
After received SIGTERM, pidfd-kill sends the given signal to init process by
|
||||
pidfd received from --pidfd-socket.
|
||||
|
||||
To use pidfd-kill, just specify a socket path at which you want to receive
|
||||
pidfd:
|
||||
|
||||
$ pidfd-kill [--signal KILL] socket.sock
|
||||
`
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "pidfd-kill"
|
||||
app.Usage = usage
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "signal",
|
||||
Value: "SIGKILL",
|
||||
Usage: "Signal to send to the init process",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pid-file",
|
||||
Value: "",
|
||||
Usage: "Path to write the pidfd-kill process ID to",
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = func(ctx *cli.Context) error {
|
||||
args := ctx.Args()
|
||||
if len(args) != 1 {
|
||||
return errors.New("required a single socket path")
|
||||
}
|
||||
|
||||
socketFile := ctx.Args()[0]
|
||||
|
||||
pidFile := ctx.String("pid-file")
|
||||
if pidFile != "" {
|
||||
pid := fmt.Sprintf("%d\n", os.Getpid())
|
||||
if err := os.WriteFile(pidFile, []byte(pid), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(pidFile)
|
||||
}
|
||||
|
||||
sigStr := ctx.String("signal")
|
||||
if sigStr == "" {
|
||||
sigStr = "SIGKILL"
|
||||
}
|
||||
sig := unix.SignalNum(sigStr)
|
||||
|
||||
pidfdFile, err := recvPidfd(socketFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pidfdFile.Close()
|
||||
|
||||
signalCh := make(chan os.Signal, 16)
|
||||
signal.Notify(signalCh, unix.SIGTERM)
|
||||
<-signalCh
|
||||
|
||||
return unix.PidfdSendSignal(int(pidfdFile.Fd()), sig, nil, 0)
|
||||
}
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "fatal error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func recvPidfd(socketFile string) (*os.File, error) {
|
||||
ln, err := net.Listen("unix", socketFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
unixconn, ok := conn.(*net.UnixConn)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to cast to unixconn")
|
||||
}
|
||||
|
||||
socket, err := unixconn.File()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer socket.Close()
|
||||
|
||||
return utils.RecvFile(socket)
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 SUSE LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/opencontainers/runc/libcontainer/utils"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// version will be populated by the Makefile, read from
|
||||
// VERSION file of the source code.
|
||||
var version = ""
|
||||
|
||||
// gitCommit will be the hash that the binary was built from
|
||||
// and will be populated by the Makefile
|
||||
var gitCommit = ""
|
||||
|
||||
const (
|
||||
usage = `Open Container Initiative contrib/cmd/recvtty
|
||||
|
||||
recvtty is a reference implementation of a consumer of runC's --console-socket
|
||||
API. It has two main modes of operation:
|
||||
|
||||
* single: Only permit one terminal to be sent to the socket, which is
|
||||
then hooked up to the stdio of the recvtty process. This is useful
|
||||
for rudimentary shell management of a container.
|
||||
|
||||
* null: Permit as many terminals to be sent to the socket, but they
|
||||
are read to /dev/null. This is used for testing, and imitates the
|
||||
old runC API's --console=/dev/pts/ptmx hack which would allow for a
|
||||
similar trick. This is probably not what you want to use, unless
|
||||
you're doing something like our bats integration tests.
|
||||
|
||||
To use recvtty, just specify a socket path at which you want to receive
|
||||
terminals:
|
||||
|
||||
$ recvtty [--mode <single|null>] socket.sock
|
||||
`
|
||||
)
|
||||
|
||||
func bail(err error) {
|
||||
fmt.Fprintf(os.Stderr, "[recvtty] fatal error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func handleSingle(path string, noStdin bool) error {
|
||||
// Open a socket.
|
||||
ln, err := net.Listen("unix", path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
// We only accept a single connection, since we can only really have
|
||||
// one reader for os.Stdin. Plus this is all a PoC.
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Close ln, to allow for other instances to take over.
|
||||
ln.Close()
|
||||
|
||||
// Get the fd of the connection.
|
||||
unixconn, ok := conn.(*net.UnixConn)
|
||||
if !ok {
|
||||
return errors.New("failed to cast to unixconn")
|
||||
}
|
||||
|
||||
socket, err := unixconn.File()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer socket.Close()
|
||||
|
||||
// Get the master file descriptor from runC.
|
||||
master, err := utils.RecvFile(socket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := console.ConsoleFromFile(master)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := console.ClearONLCR(c.Fd()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy from our stdio to the master fd.
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
inErr, outErr error
|
||||
)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
_, outErr = io.Copy(os.Stdout, c)
|
||||
wg.Done()
|
||||
}()
|
||||
if !noStdin {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
_, inErr = io.Copy(c, os.Stdin)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
// Only close the master fd once we've stopped copying.
|
||||
wg.Wait()
|
||||
c.Close()
|
||||
|
||||
if outErr != nil {
|
||||
return outErr
|
||||
}
|
||||
|
||||
return inErr
|
||||
}
|
||||
|
||||
func handleNull(path string) error {
|
||||
// Open a socket.
|
||||
ln, err := net.Listen("unix", path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
// As opposed to handleSingle we accept as many connections as we get, but
|
||||
// we don't interact with Stdin at all (and we copy stdout to /dev/null).
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func(conn net.Conn) {
|
||||
// Don't leave references lying around.
|
||||
defer conn.Close()
|
||||
|
||||
// Get the fd of the connection.
|
||||
unixconn, ok := conn.(*net.UnixConn)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
socket, err := unixconn.File()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer socket.Close()
|
||||
|
||||
// Get the master file descriptor from runC.
|
||||
master, err := utils.RecvFile(socket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = io.Copy(io.Discard, master)
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "recvtty"
|
||||
app.Usage = usage
|
||||
|
||||
// Set version to be the same as runC.
|
||||
var v []string
|
||||
if version != "" {
|
||||
v = append(v, version)
|
||||
}
|
||||
if gitCommit != "" {
|
||||
v = append(v, "commit: "+gitCommit)
|
||||
}
|
||||
app.Version = strings.Join(v, "\n")
|
||||
|
||||
// Set the flags.
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "mode, m",
|
||||
Value: "single",
|
||||
Usage: "Mode of operation (single or null)",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pid-file",
|
||||
Value: "",
|
||||
Usage: "Path to write daemon process ID to",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-stdin",
|
||||
Usage: "Disable stdin handling (no-op for null mode)",
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = func(ctx *cli.Context) error {
|
||||
args := ctx.Args()
|
||||
if len(args) != 1 {
|
||||
return errors.New("need to specify a single socket path")
|
||||
}
|
||||
path := ctx.Args()[0]
|
||||
|
||||
pidPath := ctx.String("pid-file")
|
||||
if pidPath != "" {
|
||||
pid := fmt.Sprintf("%d\n", os.Getpid())
|
||||
if err := os.WriteFile(pidPath, []byte(pid), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
noStdin := ctx.Bool("no-stdin")
|
||||
switch ctx.String("mode") {
|
||||
case "single":
|
||||
if err := handleSingle(path, noStdin); err != nil {
|
||||
return err
|
||||
}
|
||||
case "null":
|
||||
if err := handleNull(path); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("need to select a valid mode: %s", ctx.String("mode"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
bail(err)
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
const usage = `contrib/cmd/remap-rootfs
|
||||
|
||||
remap-rootfs is a helper tool to remap the root filesystem of a Open Container
|
||||
Initiative bundle using user namespaces such that the file owners are remapped
|
||||
from "host" mappings to the user namespace's mappings.
|
||||
|
||||
Effectively, this is a slightly more complicated 'chown -R', and is primarily
|
||||
used within runc's integration tests to remap the test filesystem to match the
|
||||
test user namespace. Note that calling remap-rootfs multiple times, or changing
|
||||
the mapping and then calling remap-rootfs will likely produce incorrect results
|
||||
because we do not "un-map" any pre-applied mappings from previous remap-rootfs
|
||||
calls.
|
||||
|
||||
Note that the bundle is assumed to be produced by a trusted source, and thus
|
||||
malicious configuration files will likely not be handled safely.
|
||||
|
||||
To use remap-rootfs, simply pass it the path to an OCI bundle (a directory
|
||||
containing a config.json):
|
||||
|
||||
$ sudo remap-rootfs ./bundle
|
||||
`
|
||||
|
||||
func toHostID(mappings []specs.LinuxIDMapping, id uint32) (int, bool) {
|
||||
for _, m := range mappings {
|
||||
if m.ContainerID <= id && id < m.ContainerID+m.Size {
|
||||
return int(m.HostID + id), true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
type inodeID struct {
|
||||
Dev, Ino uint64
|
||||
}
|
||||
|
||||
func toInodeID(st *syscall.Stat_t) inodeID {
|
||||
return inodeID{Dev: st.Dev, Ino: st.Ino}
|
||||
}
|
||||
|
||||
func remapRootfs(root string, uidMap, gidMap []specs.LinuxIDMapping) error {
|
||||
seenInodes := make(map[inodeID]struct{})
|
||||
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mode := info.Mode()
|
||||
st := info.Sys().(*syscall.Stat_t)
|
||||
|
||||
// Skip symlinks.
|
||||
if mode.Type() == os.ModeSymlink {
|
||||
return nil
|
||||
}
|
||||
// Skip hard-links to files we've already remapped.
|
||||
id := toInodeID(st)
|
||||
if _, seen := seenInodes[id]; seen {
|
||||
return nil
|
||||
}
|
||||
seenInodes[id] = struct{}{}
|
||||
|
||||
// Calculate the new uid:gid.
|
||||
uid := st.Uid
|
||||
newUID, ok1 := toHostID(uidMap, uid)
|
||||
gid := st.Gid
|
||||
newGID, ok2 := toHostID(gidMap, gid)
|
||||
|
||||
// Skip files that cannot be mapped.
|
||||
if !ok1 || !ok2 {
|
||||
niceName := path
|
||||
if relName, err := filepath.Rel(root, path); err == nil {
|
||||
niceName = "/" + relName
|
||||
}
|
||||
fmt.Printf("skipping file %s: cannot remap user %d:%d -> %d:%d\n", niceName, uid, gid, newUID, newGID)
|
||||
return nil
|
||||
}
|
||||
if err := os.Lchown(path, newUID, newGID); err != nil {
|
||||
return err
|
||||
}
|
||||
// Re-apply any setid bits that would be cleared due to chown(2).
|
||||
return os.Chmod(path, mode)
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "remap-rootfs"
|
||||
app.Usage = usage
|
||||
|
||||
app.Action = func(ctx *cli.Context) error {
|
||||
args := ctx.Args()
|
||||
if len(args) != 1 {
|
||||
return errors.New("exactly one bundle argument must be provided")
|
||||
}
|
||||
bundle := args[0]
|
||||
|
||||
configFile, err := os.Open(filepath.Join(bundle, "config.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer configFile.Close()
|
||||
|
||||
var spec specs.Spec
|
||||
if err := json.NewDecoder(configFile).Decode(&spec); err != nil {
|
||||
return fmt.Errorf("parsing config.json: %w", err)
|
||||
}
|
||||
|
||||
if spec.Root == nil {
|
||||
return errors.New("invalid config.json: root section is null")
|
||||
}
|
||||
rootfs := filepath.Join(bundle, spec.Root.Path)
|
||||
|
||||
if spec.Linux == nil {
|
||||
return errors.New("invalid config.json: linux section is null")
|
||||
}
|
||||
uidMap := spec.Linux.UIDMappings
|
||||
gidMap := spec.Linux.GIDMappings
|
||||
if len(uidMap) == 0 && len(gidMap) == 0 {
|
||||
fmt.Println("skipping remapping -- no userns mappings specified")
|
||||
return nil
|
||||
}
|
||||
|
||||
return remapRootfs(rootfs, uidMap, gidMap)
|
||||
}
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups"
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Print(`Open Container Initiative contrib/cmd/sd-helper
|
||||
|
||||
sd-helper is a tool that uses runc/libcontainer/cgroups/systemd package
|
||||
functionality to communicate to systemd in order to perform various operations.
|
||||
Currently this is limited to starting and stopping systemd transient slice
|
||||
units.
|
||||
|
||||
Usage:
|
||||
sd-helper [-debug] [-parent <pname>] {start|stop} <name>
|
||||
|
||||
Example:
|
||||
sd-helper -parent system.slice start system-pod123.slice
|
||||
`)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var (
|
||||
debug = flag.Bool("debug", false, "enable debug output")
|
||||
parent = flag.String("parent", "", "parent unit name")
|
||||
)
|
||||
|
||||
func main() {
|
||||
if !systemd.IsRunningSystemd() {
|
||||
logrus.Fatal("systemd is required")
|
||||
}
|
||||
|
||||
// Set the flags.
|
||||
flag.Parse()
|
||||
if *debug {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
if flag.NArg() != 2 {
|
||||
usage()
|
||||
}
|
||||
|
||||
cmd := flag.Arg(0)
|
||||
unit := flag.Arg(1)
|
||||
|
||||
err := unitCommand(cmd, unit, *parent)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func newManager(config *configs.Cgroup) (cgroups.Manager, error) {
|
||||
if cgroups.IsCgroup2UnifiedMode() {
|
||||
return systemd.NewUnifiedManager(config, "")
|
||||
}
|
||||
return systemd.NewLegacyManager(config, nil)
|
||||
}
|
||||
|
||||
func unitCommand(cmd, name, parent string) error {
|
||||
podConfig := &configs.Cgroup{
|
||||
Name: name,
|
||||
Parent: parent,
|
||||
Resources: &configs.Resources{},
|
||||
}
|
||||
pm, err := newManager(podConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "start":
|
||||
return pm.Apply(-1)
|
||||
case "stop":
|
||||
return pm.Destroy()
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown command: %s", cmd)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
# Seccomp Agent
|
||||
|
||||
## Warning
|
||||
|
||||
Please note this is an example agent, as such it is possible that specially
|
||||
crafted messages can produce bad behaviour. Please use it as an example only.
|
||||
|
||||
Also, this agent is used for integration tests. Be aware that changing the
|
||||
behaviour can break the integration tests.
|
||||
|
||||
## Get started
|
||||
|
||||
Compile runc and seccompagent:
|
||||
```bash
|
||||
make all
|
||||
```
|
||||
|
||||
Run the seccomp agent in the background:
|
||||
```bash
|
||||
sudo ./contrib/cmd/seccompagent/seccompagent &
|
||||
```
|
||||
|
||||
Prepare a container:
|
||||
```bash
|
||||
mkdir container-seccomp-notify
|
||||
cd container-seccomp-notify
|
||||
mkdir rootfs
|
||||
docker export $(docker create busybox) | tar -C rootfs -xvf -
|
||||
```
|
||||
|
||||
Then, generate a config.json by running the script gen-seccomp-example-cfg.sh
|
||||
from the directory where this README.md is in the container directory you
|
||||
prepared earlier (`container-seccomp-notify`).
|
||||
|
||||
Then start the container:
|
||||
```bash
|
||||
runc run mycontainerid
|
||||
```
|
||||
|
||||
The container will output something like this:
|
||||
```bash
|
||||
+ cd /dev/shm
|
||||
+ mkdir test-dir
|
||||
+ touch test-file
|
||||
+ chmod 777 test-file
|
||||
chmod: changing permissions of 'test-file': No medium found
|
||||
+ stat /dev/shm/test-dir-foo
|
||||
File: /dev/shm/test-dir-foo
|
||||
Size: 40 Blocks: 0 IO Block: 4096 directory
|
||||
Device: 3eh/62d Inode: 2 Links: 2
|
||||
Access: (0755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
|
||||
Access: 2021-09-09 15:03:13.043716040 +0000
|
||||
Modify: 2021-09-09 15:03:13.043716040 +0000
|
||||
Change: 2021-09-09 15:03:13.043716040 +0000
|
||||
Birth: -
|
||||
+ ls -l /dev/shm
|
||||
total 0
|
||||
drwxr-xr-x 2 root root 40 Sep 9 15:03 test-dir-foo
|
||||
-rw-r--r-- 1 root root 0 Sep 9 15:03 test-file
|
||||
+ echo Note the agent added a suffix for the directory name and chmod fails
|
||||
Note the agent added a suffix for the directory name and chmod fails
|
||||
```
|
||||
|
||||
This shows a simple example that runs in /dev/shm just because it is a tmpfs in
|
||||
the example config.json.
|
||||
|
||||
The agent makes all chmod calls fail with ENOMEDIUM, as the example output shows.
|
||||
|
||||
For mkdir, the agent adds a "-foo" suffix: the container runs "mkdir test-dir"
|
||||
but the directory created is "test-dir-foo".
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Detect if we are running inside bats (i.e. inside integration tests) or just
|
||||
# called by an end-user
|
||||
# bats-core v1.2.1 defines BATS_RUN_TMPDIR
|
||||
if [ -z "$BATS_RUN_TMPDIR" ]; then
|
||||
# When not running in bats, we create the config.json
|
||||
set -e
|
||||
runc spec
|
||||
fi
|
||||
|
||||
# We can't source $(dirname $0)/../../../tests/integration/helpers.bash as that
|
||||
# exits when not running inside bats. We can do hacks, but just to redefine
|
||||
# update_config() seems clearer. We don't even really need to keep them in sync.
|
||||
function update_config() {
|
||||
jq "$1" "./config.json" | awk 'BEGIN{RS="";getline<"-";print>ARGV[1]}' "./config.json"
|
||||
}
|
||||
|
||||
update_config '.linux.seccomp = {
|
||||
"defaultAction": "SCMP_ACT_ALLOW",
|
||||
"listenerPath": "/run/seccomp-agent.socket",
|
||||
"listenerMetadata": "foo",
|
||||
"architectures": [ "SCMP_ARCH_X86", "SCMP_ARCH_X32", "SCMP_ARCH_X86_64" ],
|
||||
"syscalls": [
|
||||
{
|
||||
"names": [ "chmod", "fchmod", "fchmodat", "mkdir" ],
|
||||
"action": "SCMP_ACT_NOTIFY"
|
||||
}
|
||||
]
|
||||
}'
|
||||
|
||||
update_config '.process.args = [
|
||||
"sh",
|
||||
"-c",
|
||||
"set -x; cd /dev/shm; mkdir test-dir; touch test-file; chmod 777 test-file; stat /dev/shm/test-dir-foo && ls -l /dev/shm && echo \"Note the agent added a suffix for the directory name and chmod fails\" "
|
||||
]'
|
||||
@@ -1,290 +0,0 @@
|
||||
//go:build linux && seccomp
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
libseccomp "github.com/seccomp/libseccomp-golang"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var (
|
||||
socketFile string
|
||||
pidFile string
|
||||
)
|
||||
|
||||
func closeStateFds(recvFds []int) {
|
||||
for i := range recvFds {
|
||||
unix.Close(i)
|
||||
}
|
||||
}
|
||||
|
||||
// parseStateFds returns the seccomp-fd and closes the rest of the fds in recvFds.
|
||||
// In case of error, no fd is closed.
|
||||
// StateFds is assumed to be formatted as specs.ContainerProcessState.Fds and
|
||||
// recvFds the corresponding list of received fds in the same SCM_RIGHT message.
|
||||
func parseStateFds(stateFds []string, recvFds []int) (uintptr, error) {
|
||||
// Let's find the index in stateFds of the seccomp-fd.
|
||||
idx := -1
|
||||
err := false
|
||||
|
||||
for i, name := range stateFds {
|
||||
if name == specs.SeccompFdName && idx == -1 {
|
||||
idx = i
|
||||
continue
|
||||
}
|
||||
|
||||
// We found the seccompFdName twice. Error out!
|
||||
if name == specs.SeccompFdName && idx != -1 {
|
||||
err = true
|
||||
}
|
||||
}
|
||||
|
||||
if idx == -1 || err {
|
||||
return 0, errors.New("seccomp fd not found or malformed containerProcessState.Fds")
|
||||
}
|
||||
|
||||
if idx >= len(recvFds) || idx < 0 {
|
||||
return 0, errors.New("seccomp fd index out of range")
|
||||
}
|
||||
|
||||
fd := uintptr(recvFds[idx])
|
||||
|
||||
for i := range recvFds {
|
||||
if i == idx {
|
||||
continue
|
||||
}
|
||||
|
||||
unix.Close(recvFds[i])
|
||||
}
|
||||
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
func handleNewMessage(sockfd int) (uintptr, string, error) {
|
||||
const maxNameLen = 4096
|
||||
stateBuf := make([]byte, maxNameLen)
|
||||
oobSpace := unix.CmsgSpace(4)
|
||||
oob := make([]byte, oobSpace)
|
||||
|
||||
n, oobn, _, _, err := unix.Recvmsg(sockfd, stateBuf, oob, 0)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if n >= maxNameLen || oobn != oobSpace {
|
||||
return 0, "", fmt.Errorf("recvfd: incorrect number of bytes read (n=%d oobn=%d)", n, oobn)
|
||||
}
|
||||
|
||||
// Truncate.
|
||||
stateBuf = stateBuf[:n]
|
||||
oob = oob[:oobn]
|
||||
|
||||
scms, err := unix.ParseSocketControlMessage(oob)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if len(scms) != 1 {
|
||||
return 0, "", fmt.Errorf("recvfd: number of SCMs is not 1: %d", len(scms))
|
||||
}
|
||||
scm := scms[0]
|
||||
|
||||
fds, err := unix.ParseUnixRights(&scm)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
containerProcessState := &specs.ContainerProcessState{}
|
||||
err = json.Unmarshal(stateBuf, containerProcessState)
|
||||
if err != nil {
|
||||
closeStateFds(fds)
|
||||
return 0, "", fmt.Errorf("cannot parse OCI state: %w", err)
|
||||
}
|
||||
|
||||
fd, err := parseStateFds(containerProcessState.Fds, fds)
|
||||
if err != nil {
|
||||
closeStateFds(fds)
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
return fd, containerProcessState.Metadata, nil
|
||||
}
|
||||
|
||||
func readArgString(pid uint32, offset int64) (string, error) {
|
||||
buffer := make([]byte, 4096) // PATH_MAX
|
||||
|
||||
memfd, err := unix.Open(fmt.Sprintf("/proc/%d/mem", pid), unix.O_RDONLY, 0o777)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer unix.Close(memfd)
|
||||
|
||||
_, err = unix.Pread(memfd, buffer, offset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buffer[len(buffer)-1] = 0
|
||||
s := buffer[:bytes.IndexByte(buffer, 0)]
|
||||
return string(s), nil
|
||||
}
|
||||
|
||||
func runMkdirForContainer(pid uint32, fileName string, mode uint32, metadata string) error {
|
||||
// We validated before that metadata is not a string that can make
|
||||
// newFile a file in a different location other than root.
|
||||
newFile := fmt.Sprintf("%s-%s", fileName, metadata)
|
||||
root := fmt.Sprintf("/proc/%d/cwd/", pid)
|
||||
|
||||
if strings.HasPrefix(fileName, "/") {
|
||||
// If it starts with /, use the rootfs as base
|
||||
root = fmt.Sprintf("/proc/%d/root/", pid)
|
||||
}
|
||||
|
||||
path, err := securejoin.SecureJoin(root, newFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return unix.Mkdir(path, mode)
|
||||
}
|
||||
|
||||
// notifHandler handles seccomp notifications and responses
|
||||
func notifHandler(fd libseccomp.ScmpFd, metadata string) {
|
||||
defer unix.Close(int(fd))
|
||||
for {
|
||||
req, err := libseccomp.NotifReceive(fd)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error in NotifReceive(): %s", err)
|
||||
continue
|
||||
}
|
||||
syscallName, err := req.Data.Syscall.GetName()
|
||||
if err != nil {
|
||||
logrus.Errorf("Error decoding syscall %v(): %s", req.Data.Syscall, err)
|
||||
continue
|
||||
}
|
||||
logrus.Debugf("Received syscall %q, pid %v, arch %q, args %+v", syscallName, req.Pid, req.Data.Arch, req.Data.Args)
|
||||
|
||||
resp := &libseccomp.ScmpNotifResp{
|
||||
ID: req.ID,
|
||||
Error: 0,
|
||||
Val: 0,
|
||||
Flags: libseccomp.NotifRespFlagContinue,
|
||||
}
|
||||
|
||||
// TOCTOU check
|
||||
if err := libseccomp.NotifIDValid(fd, req.ID); err != nil {
|
||||
logrus.Errorf("TOCTOU check failed: req.ID is no longer valid: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
switch syscallName {
|
||||
case "mkdir":
|
||||
fileName, err := readArgString(req.Pid, int64(req.Data.Args[0]))
|
||||
if err != nil {
|
||||
logrus.Errorf("Cannot read argument: %s", err)
|
||||
resp.Error = int32(unix.ENOSYS)
|
||||
resp.Val = ^uint64(0) // -1
|
||||
goto sendResponse
|
||||
}
|
||||
|
||||
logrus.Debugf("mkdir: %q", fileName)
|
||||
|
||||
// TOCTOU check
|
||||
if err := libseccomp.NotifIDValid(fd, req.ID); err != nil {
|
||||
logrus.Errorf("TOCTOU check failed: req.ID is no longer valid: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = runMkdirForContainer(req.Pid, fileName, uint32(req.Data.Args[1]), metadata)
|
||||
if err != nil {
|
||||
resp.Error = int32(unix.ENOSYS)
|
||||
resp.Val = ^uint64(0) // -1
|
||||
}
|
||||
resp.Flags = 0
|
||||
case "chmod", "fchmod", "fchmodat":
|
||||
resp.Error = int32(unix.ENOMEDIUM)
|
||||
resp.Val = ^uint64(0) // -1
|
||||
resp.Flags = 0
|
||||
}
|
||||
|
||||
sendResponse:
|
||||
if err = libseccomp.NotifRespond(fd, resp); err != nil {
|
||||
logrus.Errorf("Error in notification response: %s", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&socketFile, "socketfile", "/run/seccomp-agent.socket", "Socket file")
|
||||
flag.StringVar(&pidFile, "pid-file", "", "Pid file")
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
|
||||
// Parse arguments
|
||||
flag.Parse()
|
||||
if flag.NArg() > 0 {
|
||||
flag.PrintDefaults()
|
||||
logrus.Fatal("Invalid command")
|
||||
}
|
||||
|
||||
if err := os.Remove(socketFile); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
logrus.Fatalf("Cannot cleanup socket file: %v", err)
|
||||
}
|
||||
|
||||
if pidFile != "" {
|
||||
pid := fmt.Sprintf("%d", os.Getpid())
|
||||
if err := os.WriteFile(pidFile, []byte(pid), 0o644); err != nil {
|
||||
logrus.Fatalf("Cannot write pid file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Info("Waiting for seccomp file descriptors")
|
||||
l, err := net.Listen("unix", socketFile)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Cannot listen: %s", err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
logrus.Errorf("Cannot accept connection: %s", err)
|
||||
continue
|
||||
}
|
||||
socket, err := conn.(*net.UnixConn).File()
|
||||
conn.Close()
|
||||
if err != nil {
|
||||
logrus.Errorf("Cannot get socket: %v", err)
|
||||
continue
|
||||
}
|
||||
newFd, metadata, err := handleNewMessage(int(socket.Fd()))
|
||||
socket.Close()
|
||||
if err != nil {
|
||||
logrus.Errorf("Error receiving seccomp file descriptor: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Make sure we don't allow strings like "/../p", as that means
|
||||
// a file in a different location than expected. We just want
|
||||
// safe things to use as a suffix for a file name.
|
||||
metadata = filepath.Base(metadata)
|
||||
if strings.Contains(metadata, "/") {
|
||||
// Fallback to a safe string.
|
||||
metadata = "agent-generated-suffix"
|
||||
}
|
||||
|
||||
logrus.Infof("Received new seccomp fd: %v", newFd)
|
||||
go notifHandler(libseccomp.ScmpFd(newFd), metadata)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build !linux || !seccomp
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Not supported, to use this compile with build tag: seccomp.")
|
||||
}
|
||||
Reference in New Issue
Block a user