Merge pull request #5141 from cyphar/remove-deprecated-apis

libct: remove deprecated APIs
This commit is contained in:
Kir Kolyshkin
2026-03-04 11:40:28 -08:00
committed by GitHub
12 changed files with 57 additions and 431 deletions
-1
View File
@@ -1,7 +1,6 @@
vendor/pkg
/runc
/runc-*
/contrib/cmd/memfd-bind/memfd-bind
/tests/cmd/_bin
man/man8
release
+40
View File
@@ -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
> 路漫漫其修远兮,吾将上下而求索!
+1 -6
View File
@@ -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
-69
View File
@@ -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.
-251
View File
@@ -1,251 +0,0 @@
/*
* Copyright (c) 2023 SUSE LLC
* Copyright (c) 2023 Aleksa Sarai <cyphar@cyphar.com>
*
* 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 <path>.",
},
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)
}
}
@@ -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
-29
View File
@@ -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
)
-11
View File
@@ -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
-19
View File
@@ -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
-20
View File
@@ -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
)
+13 -12
View File
@@ -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)
+3 -2
View File
@@ -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: