diff --git a/.gitignore b/.gitignore index 6991faf08..26914f91d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ vendor/pkg /runc /runc-* -/contrib/cmd/memfd-bind/memfd-bind /tests/cmd/_bin man/man8 release diff --git a/CHANGELOG.md b/CHANGELOG.md index aa47bc838..1cc3328b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### libcontainer API ### +- The following deprecated Go APIs have been removed: + - `CleanPath`, `StripRoot`, and `WithProcfd` from `libcontainer/utils`. Note + that `WithProcfdFile` has not been removed (due to import cycle issues) but + is instead marked as internal in its godoc comment. (#5051) + - All of the cgroup-related types and functions from `libcontainer/configs` + which are now maintained in `github.com/opencontainers/cgroups` (#5141): + - `libcontainer/configs.Cgroup` + - `libcontainer/configs.Resources` + - `libcontainer/configs.FreezerState` + - `libcontainer/configs.LinuxRdma` + - `libcontainer/configs.BlockIODevice` + - `libcontainer/configs.WeightDevice` + - `libcontainer/configs.ThrottleDevice` + - `libcontainer/configs.HugepageLimit` + - `libcontainer/configs.IfPrioMap` + - `libcontainer/configs.Undefined` + - `libcontainer/configs.Frozen` + - `libcontainer/configs.Thawed` + - `libcontainer/configs.NewWeightDevice` + - `libcontainer/configs.NewThrottleDevice` + - `libcontainer/configs.HookList.RunHooks`. (#5141) + - `libcontainer/configs.MPOL_*` (#5414) + - All of the types in `libcontainer/devices` which are now maintained in + `github.com/opencontainers/cgroups/devices/config` (#5141): + - `libcontainer/devices.Wildcard` + - `libcontainer/devices.WildcardDevice` + - `libcontainer/devices.BlockDevice` + - `libcontainer/devices.CharDevice` + - `libcontainer/devices.FifoDevice` + - `libcontainer/devices.Device` + - `libcontainer/devices.Permissions` + - `libcontainer/devices.Type` + - `libcontainer/devices.Rule` + +### Removed ### +- The `memfd-bind` helper binary has been removed, as it has never been + particularly useful and was completely obsoleted by the changes to + `/proc/self/exe` sealing we introduced in runc [1.2.0][]. (#5141) + ## [1.4.0] - 2025-11-27 > 路漫漫其修远兮,吾将上下而求索! diff --git a/Makefile b/Makefile index deaec2566..0a00e6607 100644 --- a/Makefile +++ b/Makefile @@ -77,11 +77,7 @@ runc-bin: $(GO_BUILD) -o runc . .PHONY: all -all: runc memfd-bind - -.PHONY: memfd-bind -memfd-bind: - $(GO_BUILD) -o contrib/cmd/$@/$@ ./contrib/cmd/$@ +all: runc TESTBINDIR := tests/cmd/_bin $(TESTBINDIR): @@ -96,7 +92,6 @@ $(TESTBINS): $(TESTBINDIR) .PHONY: clean clean: rm -f runc runc-* - rm -f contrib/cmd/memfd-bind/memfd-bind rm -fr $(TESTBINDIR) sudo rm -rf release rm -rf man/man8 diff --git a/contrib/cmd/memfd-bind/README.md b/contrib/cmd/memfd-bind/README.md deleted file mode 100644 index 932292502..000000000 --- a/contrib/cmd/memfd-bind/README.md +++ /dev/null @@ -1,69 +0,0 @@ -## memfd-bind ## - -> **NOTE**: Since runc 1.2.0, runc will now use a private overlayfs mount to -> protect the runc binary (if you are on Linux 5.1 or later). This protection -> is far more light-weight than memfd-bind, and for most users this should -> obviate the need for `memfd-bind` entirely. Rootless containers will still -> make a memfd copy (unless you are using `runc` itself inside a user namespace -> -- a-la [`rootlesskit`][rootlesskit] -- and are on Linux 5.11 or later), but -> `memfd-bind` is not particularly useful for rootless container users anyway -> (see [Caveats](#Caveats) for more details). - -`runc` sometimes has to make a binary copy of itself when constructing a -container process in order to defend against certain container runtime attacks -such as CVE-2019-5736. - -This cloned binary only exists until the container process starts (this means -for `runc run` and `runc exec`, it only exists for a few hundred milliseconds --- for `runc create` it exists until `runc start` is called). However, because -the clone is done using a memfd (or by creating files in directories that are -likely to be a `tmpfs`), this can lead to temporary increases in *host* memory -usage. Unless you are running on a cgroupv1 system with the cgroupv1 memory -controller enabled and the (deprecated) `memory.move_charge_at_immigrate` -enabled, there is no effect on the container's memory. - -However, for certain configurations this can still be undesirable. This daemon -allows you to create a sealed memfd copy of the `runc` binary, which will cause -`runc` to skip all binary copying, resulting in no additional memory usage for -each container process (instead there is a single in-memory copy of the -binary). It should be noted that (strictly speaking) this is slightly less -secure if you are concerned about Dirty Cow-like 0-day kernel vulnerabilities, -but for most users the security benefit is identical. - -The provided `memfd-bind@.service` file can be used to get systemd to manage -this daemon. You can supply the path like so: - -```bash -systemctl start memfd-bind@$(systemd-escape -p /usr/bin/runc) -``` - -Thus, there are three ways of protecting against CVE-2019-5736, in order of how -much memory usage they can use: - -* `memfd-bind` only creates a single in-memory copy of the `runc` binary (about - 10MB), regardless of how many containers are running. - -* The classic method of making a copy of the entire `runc` binary during - container process setup takes up about 10MB per process spawned inside the - container by runc (both pid1 and `runc exec`). - -[rootlesskit]: https://github.com/rootless-containers/rootlesskit - -### Caveats ### - -There are several downsides with using `memfd-bind` on the `runc` binary: - -* The `memfd-bind` process needs to continue to run indefinitely in order for - the memfd reference to stay alive. If the process is forcefully killed, the - bind-mount on top of the `runc` binary will become stale and nobody will be - able to execute it (you can use `memfd-bind --cleanup` to clean up the stale - mount). - -* Only root can execute the cloned binary due to permission restrictions on - accessing other process's files. More specifically, only users with ptrace - privileges over the memfd-bind daemon can access the file (but in practice - this is usually only root). - -* When updating `runc`, the daemon needs to be stopped before the update (so - the package manager can access the underlying file) and then restarted after - the update. diff --git a/contrib/cmd/memfd-bind/memfd-bind.go b/contrib/cmd/memfd-bind/memfd-bind.go deleted file mode 100644 index 7bc57b0b3..000000000 --- a/contrib/cmd/memfd-bind/memfd-bind.go +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright (c) 2023 SUSE LLC - * Copyright (c) 2023 Aleksa Sarai - * - * 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. - */ - -// memfd-bind is a command-line tool to construct a persistent -// sealed-memfd-copy of a binary, to allow administrators to amortise the cost -// of memfd cloning for runc. runc will not make its own copy of the binary if -// it detects that the binary is already a sealed-memfd-copy. -// -// Usage of this tool has a lot of caveats -- see this package's README for -// more details on what restrictions apply when using this tool. -// -// Deprecated: runc 1.2 and later use a different mechanism for protecting the -// runc binary that obviates the need for this tool. Unless you are on an old -// kernel or need to use an older runc version, this tool is no longer needed. -package main - -import ( - "errors" - "fmt" - "io" - "os" - "os/signal" - "runtime" - "strings" - "time" - - "github.com/opencontainers/runc/libcontainer/exeseal" - - "github.com/sirupsen/logrus" - "github.com/urfave/cli" - "golang.org/x/sys/unix" -) - -// 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/memfd-bind - -In order to protect against certain container attacks, every runc invocation -that involves creating or joining a container will cause runc to make a copy of -the runc binary in memory (usually to a memfd). While "runc init" is very -short-lived, this extra memory usage can cause problems for containers with -very small memory limits (or containers that have many "runc exec" invocations -applied to them at the same time). - -memfd-bind is a tool to create a persistent memfd-sealed-copy of the runc binary, -which will cause runc to not make its own copy. This means you can get the -benefits of using a sealed memfd as runc's binary (even in a container breakout -attack to get write access to the runc binary, neither the underlying binary -nor the memfd copy can be changed). - -To use memfd-bind, just specify which path you want to create a socket path at -which you want to receive terminals: - - $ sudo memfd-bind /usr/bin/runc - -Note that (due to kernel restrictions on bind-mounts), this program must remain -running on the host in order for the binary to be readable (it is recommended -you use a systemd unit to keep this process around). - -If this program dies, there will be a leftover mountpoint that always returns --EINVAL when attempting to access it. You need to use memfd-bind --cleanup on the -path in order to unmount the path (regular umount(8) will not work): - - $ sudo memfd-bind --cleanup /usr/bin/runc - -Note that (due to restrictions on /proc/$pid/fd/$fd magic-link resolution), -only privileged users (specifically, those that have ptrace privileges over the -memfd-bind daemon) can access the memfd bind-mount. This means that using this -tool to harden your /usr/bin/runc binary would result in unprivileged users -being unable to execute the binary. If this is an issue, you could make all -privileged process use a different copy of runc (by making a copy in somewhere -like /usr/sbin/runc) and only using memfd-bind for the version used by -privileged users. -` -) - -func cleanup(path string) error { - file, err := os.OpenFile(path, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) - if err != nil { - return fmt.Errorf("cleanup: failed to open runc binary path: %w", err) - } - defer file.Close() - fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd()) - - // Keep umounting until we hit a umount error. - for unix.Unmount(fdPath, unix.MNT_DETACH) == nil { - // loop... - logrus.Debugf("memfd-bind: path %q unmount succeeded...", path) - } - logrus.Infof("memfd-bind: path %q has been cleared of all old bind-mounts", path) - return nil -} - -// memfdClone is a memfd-only implementation of exeseal.CloneBinary. -func memfdClone(path string) (*os.File, error) { - binFile, err := os.Open(path) - if err != nil { - return nil, fmt.Errorf("failed to open runc binary path: %w", err) - } - defer binFile.Close() - stat, err := binFile.Stat() - if err != nil { - return nil, fmt.Errorf("checking %s size: %w", path, err) - } - size := stat.Size() - memfd, sealFn, err := exeseal.Memfd("/proc/self/exe") - if err != nil { - return nil, fmt.Errorf("creating memfd failed: %w", err) - } - copied, err := io.Copy(memfd, binFile) - if err != nil { - return nil, fmt.Errorf("copy binary: %w", err) - } else if copied != size { - return nil, fmt.Errorf("copied binary size mismatch: %d != %d", copied, size) - } - if err := sealFn(&memfd); err != nil { - return nil, fmt.Errorf("could not seal fd: %w", err) - } - if !exeseal.IsCloned(memfd) { - return nil, fmt.Errorf("cloned memfd is not properly sealed") - } - return memfd, nil -} - -func mount(path string) error { - memfdFile, err := memfdClone(path) - if err != nil { - return fmt.Errorf("memfd clone: %w", err) - } - defer memfdFile.Close() - memfdPath := fmt.Sprintf("/proc/self/fd/%d", memfdFile.Fd()) - - // We have to open an O_NOFOLLOW|O_PATH to the memfd magic-link because we - // cannot bind-mount the memfd itself (it's in the internal kernel mount - // namespace and cross-mount-namespace bind-mounts are not allowed). This - // also requires that this program stay alive continuously for the - // magic-link to stay alive... - memfdLink, err := os.OpenFile(memfdPath, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) - if err != nil { - return fmt.Errorf("mount: failed to /proc/self/fd magic-link for memfd: %w", err) - } - defer memfdLink.Close() - memfdLinkFdPath := fmt.Sprintf("/proc/self/fd/%d", memfdLink.Fd()) - - exeFile, err := os.OpenFile(path, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) - if err != nil { - return fmt.Errorf("mount: failed to open target runc binary path: %w", err) - } - defer exeFile.Close() - exeFdPath := fmt.Sprintf("/proc/self/fd/%d", exeFile.Fd()) - - err = unix.Mount(memfdLinkFdPath, exeFdPath, "", unix.MS_BIND, "") - if err != nil { - return fmt.Errorf("mount: failed to mount memfd on top of runc binary path target: %w", err) - } - - // If there is a signal we want to do cleanup. - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt, unix.SIGTERM, unix.SIGINT) - go func() { - <-sigCh - logrus.Infof("memfd-bind: exit signal caught! cleaning up the bind-mount on %q...", path) - _ = cleanup(path) - os.Exit(0) - }() - - // Clean up things we don't need... - _ = exeFile.Close() - _ = memfdLink.Close() - - // We now have to stay alive to keep the magic-link alive... - logrus.Infof("memfd-bind: bind-mount of memfd over %q created -- looping forever!", path) - for { - // loop forever... - time.Sleep(time.Duration(1<<63 - 1)) - // make sure the memfd isn't gc'd - runtime.KeepAlive(memfdFile) - } -} - -func main() { - app := cli.NewApp() - app.Name = "memfd-bind" - 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.BoolFlag{ - Name: "cleanup", - Usage: "Do not create a new memfd-sealed file, only clean up an existing one at .", - }, - cli.BoolFlag{ - Name: "debug", - Usage: "Enable debug logging.", - }, - } - - app.Action = func(ctx *cli.Context) error { - args := ctx.Args() - if len(args) != 1 { - return errors.New("need to specify a single path to the runc binary") - } - path := ctx.Args()[0] - - if ctx.Bool("debug") { - logrus.SetLevel(logrus.DebugLevel) - } - - err := cleanup(path) - // We only care about cleanup errors when doing --cleanup. - if ctx.Bool("cleanup") { - return err - } - return mount(path) - } - if err := app.Run(os.Args); err != nil { - fmt.Fprintf(os.Stderr, "memfd-bind: %v\n", err) - os.Exit(1) - } -} diff --git a/contrib/cmd/memfd-bind/memfd-bind@.service b/contrib/cmd/memfd-bind/memfd-bind@.service deleted file mode 100644 index 890869026..000000000 --- a/contrib/cmd/memfd-bind/memfd-bind@.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Manage memfd-bind of %f -Documentation=https://github.com/opencontainers/runc/blob/main/contrib/cmd/memfd-bind/README.md - -[Service] -Type=simple -ExecStart=memfd-bind "%f" -ExecStop=memfd-bind --cleanup "%f" - -[Install] -WantedBy=multi-user.target diff --git a/libcontainer/configs/cgroup_deprecated.go b/libcontainer/configs/cgroup_deprecated.go deleted file mode 100644 index 7836b240a..000000000 --- a/libcontainer/configs/cgroup_deprecated.go +++ /dev/null @@ -1,29 +0,0 @@ -package configs - -import "github.com/opencontainers/cgroups" - -// Deprecated: use [github.com/opencontainers/cgroups]. -type ( - Cgroup = cgroups.Cgroup - Resources = cgroups.Resources - FreezerState = cgroups.FreezerState - LinuxRdma = cgroups.LinuxRdma - BlockIODevice = cgroups.BlockIODevice - WeightDevice = cgroups.WeightDevice - ThrottleDevice = cgroups.ThrottleDevice - HugepageLimit = cgroups.HugepageLimit - IfPrioMap = cgroups.IfPrioMap -) - -// Deprecated: use [github.com/opencontainers/cgroups]. -const ( - Undefined = cgroups.Undefined - Frozen = cgroups.Frozen - Thawed = cgroups.Thawed -) - -// Deprecated: use [github.com/opencontainers/cgroups]. -var ( - NewWeightDevice = cgroups.NewWeightDevice - NewThrottleDevice = cgroups.NewThrottleDevice -) diff --git a/libcontainer/configs/config.go b/libcontainer/configs/config.go index 6141c018f..b8f2321c2 100644 --- a/libcontainer/configs/config.go +++ b/libcontainer/configs/config.go @@ -466,17 +466,6 @@ type Capabilities struct { Ambient []string `json:"Ambient,omitempty"` } -// Deprecated: use [Hooks.Run] instead. -func (hooks HookList) RunHooks(state *specs.State) error { - for i, h := range hooks { - if err := h.Run(state); err != nil { - return fmt.Errorf("error running hook #%d: %w", i, err) - } - } - - return nil -} - func (hooks *Hooks) UnmarshalJSON(b []byte) error { var state map[HookName][]CommandHook diff --git a/libcontainer/configs/memorypolicy.go b/libcontainer/configs/memorypolicy.go index b7b9c6bce..81bff0d10 100644 --- a/libcontainer/configs/memorypolicy.go +++ b/libcontainer/configs/memorypolicy.go @@ -2,25 +2,6 @@ package configs import "golang.org/x/sys/unix" -// Memory policy modes and flags as defined in /usr/include/linux/mempolicy.h -// -// Deprecated: use constants from [unix] instead. -// -//nolint:revive,staticcheck,nolintlint // ignore ALL_CAPS errors in consts from numaif.h, will match unix.* in the future -const ( - MPOL_DEFAULT = unix.MPOL_DEFAULT - MPOL_PREFERRED = unix.MPOL_PREFERRED - MPOL_BIND = unix.MPOL_BIND - MPOL_INTERLEAVE = unix.MPOL_INTERLEAVE - MPOL_LOCAL = unix.MPOL_LOCAL - MPOL_PREFERRED_MANY = unix.MPOL_PREFERRED_MANY - MPOL_WEIGHTED_INTERLEAVE = unix.MPOL_WEIGHTED_INTERLEAVE - - MPOL_F_STATIC_NODES = unix.MPOL_F_STATIC_NODES - MPOL_F_RELATIVE_NODES = unix.MPOL_F_RELATIVE_NODES - MPOL_F_NUMA_BALANCING = unix.MPOL_F_NUMA_BALANCING -) - // LinuxMemoryPolicy contains memory policy configuration. type LinuxMemoryPolicy struct { // Mode specifies memory policy mode without mode flags. See diff --git a/libcontainer/devices/device_deprecated.go b/libcontainer/devices/device_deprecated.go deleted file mode 100644 index 9483f054d..000000000 --- a/libcontainer/devices/device_deprecated.go +++ /dev/null @@ -1,20 +0,0 @@ -package devices - -import "github.com/opencontainers/cgroups/devices/config" - -// Deprecated: use [github.com/opencontainers/cgroups/devices/config]. -const ( - Wildcard = config.Wildcard - WildcardDevice = config.WildcardDevice - BlockDevice = config.BlockDevice - CharDevice = config.CharDevice - FifoDevice = config.FifoDevice -) - -// Deprecated: use [github.com/opencontainers/cgroups/devices/config]. -type ( - Device = config.Device - Permissions = config.Permissions - Type = config.Type - Rule = config.Rule -) diff --git a/libcontainer/devices/device_unix.go b/libcontainer/devices/device_unix.go index 409e58e96..4489663b2 100644 --- a/libcontainer/devices/device_unix.go +++ b/libcontainer/devices/device_unix.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" + "github.com/opencontainers/cgroups/devices/config" "golang.org/x/sys/unix" ) @@ -22,7 +23,7 @@ var ( // DeviceFromPath takes the path to a device and its cgroup_permissions (which // cannot be easily queried) to look up the information about a linux device // and returns that information as a Device struct. -func DeviceFromPath(path, permissions string) (*Device, error) { +func DeviceFromPath(path, permissions string) (*config.Device, error) { var stat unix.Stat_t err := unixLstat(path, &stat) if err != nil { @@ -30,7 +31,7 @@ func DeviceFromPath(path, permissions string) (*Device, error) { } var ( - devType Type + devType config.Type mode = stat.Mode devNumber = uint64(stat.Rdev) //nolint:unconvert // Rdev is uint32 on e.g. MIPS. major = unix.Major(devNumber) @@ -38,20 +39,20 @@ func DeviceFromPath(path, permissions string) (*Device, error) { ) switch mode & unix.S_IFMT { case unix.S_IFBLK: - devType = BlockDevice + devType = config.BlockDevice case unix.S_IFCHR: - devType = CharDevice + devType = config.CharDevice case unix.S_IFIFO: - devType = FifoDevice + devType = config.FifoDevice default: return nil, ErrNotADevice } - return &Device{ - Rule: Rule{ + return &config.Device{ + Rule: config.Rule{ Type: devType, Major: int64(major), Minor: int64(minor), - Permissions: Permissions(permissions), + Permissions: config.Permissions(permissions), }, Path: path, FileMode: os.FileMode(mode &^ unix.S_IFMT), @@ -61,18 +62,18 @@ func DeviceFromPath(path, permissions string) (*Device, error) { } // HostDevices returns all devices that can be found under /dev directory. -func HostDevices() ([]*Device, error) { +func HostDevices() ([]*config.Device, error) { return GetDevices("/dev") } // GetDevices recursively traverses a directory specified by path // and returns all devices found there. -func GetDevices(path string) ([]*Device, error) { +func GetDevices(path string) ([]*config.Device, error) { files, err := osReadDir(path) if err != nil { return nil, err } - var out []*Device + var out []*config.Device for _, f := range files { switch { case f.IsDir(): @@ -103,7 +104,7 @@ func GetDevices(path string) ([]*Device, error) { } return nil, err } - if device.Type == FifoDevice { + if device.Type == config.FifoDevice { continue } out = append(out, device) diff --git a/libcontainer/devices/device_unix_test.go b/libcontainer/devices/device_unix_test.go index 2e00847ac..2b5860532 100644 --- a/libcontainer/devices/device_unix_test.go +++ b/libcontainer/devices/device_unix_test.go @@ -8,6 +8,7 @@ import ( "os" "testing" + "github.com/opencontainers/cgroups/devices/config" "golang.org/x/sys/unix" ) @@ -85,8 +86,8 @@ func TestHostDevicesAllValid(t *testing.T) { t.Errorf("device entry %+v has zero major number", device) } switch device.Type { - case BlockDevice, CharDevice: - case FifoDevice: + case config.BlockDevice, config.CharDevice: + case config.FifoDevice: t.Logf("fifo devices shouldn't show up from HostDevices") fallthrough default: