mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
runc update: implement memory.checkBeforeUpdate
This is aimed at solving the problem of cgroup v2 memory controller behavior which is not compatible with that of cgroup v1. In cgroup v1, if the new memory limit being set is lower than the current usage, setting the new limit fails. In cgroup v2, same operation succeeds, and the container is OOM killed. Introduce a new setting, memory.checkBeforeUpdate, and use it to mimic cgroup v1 behavior. Note that this is not 100% reliable because of TOCTOU, but this is the best we can do. Add some test cases. Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
This commit is contained in:
@@ -12,7 +12,7 @@ require (
|
|||||||
github.com/godbus/dbus/v5 v5.1.0
|
github.com/godbus/dbus/v5 v5.1.0
|
||||||
github.com/moby/sys/mountinfo v0.6.2
|
github.com/moby/sys/mountinfo v0.6.2
|
||||||
github.com/mrunalp/fileutils v0.5.0
|
github.com/mrunalp/fileutils v0.5.0
|
||||||
github.com/opencontainers/runtime-spec v1.0.3-0.20220718201635-a8106e99982b
|
github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78
|
||||||
github.com/opencontainers/selinux v1.10.2
|
github.com/opencontainers/selinux v1.10.2
|
||||||
github.com/seccomp/libseccomp-golang v0.10.0
|
github.com/seccomp/libseccomp-golang v0.10.0
|
||||||
github.com/sirupsen/logrus v1.9.0
|
github.com/sirupsen/logrus v1.9.0
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vyg
|
|||||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
||||||
github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4=
|
github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4=
|
||||||
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
|
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
|
||||||
github.com/opencontainers/runtime-spec v1.0.3-0.20220718201635-a8106e99982b h1:udwtfS44rxYE/ViMLchHQBjfE60GZSB1arY7BFbyxLs=
|
github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78 h1:R5M2qXZiK/mWPMT4VldCOiSL9HIAMuxQZWdG0CSM5+4=
|
||||||
github.com/opencontainers/runtime-spec v1.0.3-0.20220718201635-a8106e99982b/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||||
github.com/opencontainers/selinux v1.10.2 h1:NFy2xCsjn7+WspbfZkUd5zyVeisV7VFbPSP96+8/ha4=
|
github.com/opencontainers/selinux v1.10.2 h1:NFy2xCsjn7+WspbfZkUd5zyVeisV7VFbPSP96+8/ha4=
|
||||||
github.com/opencontainers/selinux v1.10.2/go.mod h1:cARutUbaUrlRClyvxOICCgKixCs6L05aUsohzA3EkHQ=
|
github.com/opencontainers/selinux v1.10.2/go.mod h1:cARutUbaUrlRClyvxOICCgKixCs6L05aUsohzA3EkHQ=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
|||||||
@@ -269,3 +269,35 @@ func (m *Manager) OOMKillCount() (uint64, error) {
|
|||||||
|
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CheckMemoryUsage(dirPath string, r *configs.Resources) error {
|
||||||
|
if !r.MemoryCheckBeforeUpdate {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Memory <= 0 && r.MemorySwap <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
usage, err := fscommon.GetCgroupParamUint(dirPath, "memory.current")
|
||||||
|
if err != nil {
|
||||||
|
// This check is on best-effort basis, so if we can't read the
|
||||||
|
// current usage (cgroup not yet created, or any other error),
|
||||||
|
// we should not fail.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.MemorySwap > 0 {
|
||||||
|
if uint64(r.MemorySwap) <= usage {
|
||||||
|
return fmt.Errorf("rejecting memory+swap limit %d <= usage %d", r.MemorySwap, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Memory > 0 {
|
||||||
|
if uint64(r.Memory) <= usage {
|
||||||
|
return fmt.Errorf("rejecting memory limit %d <= usage %d", r.Memory, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ func setMemory(dirPath string, r *configs.Resources) error {
|
|||||||
if !isMemorySet(r) {
|
if !isMemorySet(r) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := CheckMemoryUsage(dirPath, r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
swap, err := cgroups.ConvertMemorySwapToCgroupV2Value(r.MemorySwap, r.Memory)
|
swap, err := cgroups.ConvertMemorySwapToCgroupV2Value(r.MemorySwap, r.Memory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -174,7 +174,14 @@ func unifiedResToSystemdProps(cm *dbusConnManager, res map[string]string) (props
|
|||||||
return props, nil
|
return props, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func genV2ResourcesProperties(r *configs.Resources, cm *dbusConnManager) ([]systemdDbus.Property, error) {
|
func genV2ResourcesProperties(dirPath string, r *configs.Resources, cm *dbusConnManager) ([]systemdDbus.Property, error) {
|
||||||
|
// We need this check before setting systemd properties, otherwise
|
||||||
|
// the container is OOM-killed and the systemd unit is removed
|
||||||
|
// before we get to fsMgr.Set().
|
||||||
|
if err := fs2.CheckMemoryUsage(dirPath, r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var properties []systemdDbus.Property
|
var properties []systemdDbus.Property
|
||||||
|
|
||||||
// NOTE: This is of questionable correctness because we insert our own
|
// NOTE: This is of questionable correctness because we insert our own
|
||||||
@@ -437,7 +444,7 @@ func (m *UnifiedManager) Set(r *configs.Resources) error {
|
|||||||
if r == nil {
|
if r == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
properties, err := genV2ResourcesProperties(r, m.dbus)
|
properties, err := genV2ResourcesProperties(m.fsMgr.Path(""), r, m.dbus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,4 +155,9 @@ type Resources struct {
|
|||||||
// during Set() to figure out whether the freeze is required. Those
|
// during Set() to figure out whether the freeze is required. Those
|
||||||
// methods may be relatively slow, thus this flag.
|
// methods may be relatively slow, thus this flag.
|
||||||
SkipFreezeOnSet bool `json:"-"`
|
SkipFreezeOnSet bool `json:"-"`
|
||||||
|
|
||||||
|
// MemoryCheckBeforeUpdate is a flag for cgroup v2 managers to check
|
||||||
|
// if the new memory limits (Memory and MemorySwap) being set are lower
|
||||||
|
// than the current memory usage, and reject if so.
|
||||||
|
MemoryCheckBeforeUpdate bool `json:"memory_check_before_update"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -723,6 +723,9 @@ func CreateCgroupConfig(opts *CreateOpts, defaultDevs []*devices.Device) (*confi
|
|||||||
if r.Memory.DisableOOMKiller != nil {
|
if r.Memory.DisableOOMKiller != nil {
|
||||||
c.Resources.OomKillDisable = *r.Memory.DisableOOMKiller
|
c.Resources.OomKillDisable = *r.Memory.DisableOOMKiller
|
||||||
}
|
}
|
||||||
|
if r.Memory.CheckBeforeUpdate != nil {
|
||||||
|
c.Resources.MemoryCheckBeforeUpdate = *r.Memory.CheckBeforeUpdate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if r.CPU != nil {
|
if r.CPU != nil {
|
||||||
if r.CPU.Shares != nil {
|
if r.CPU.Shares != nil {
|
||||||
|
|||||||
@@ -726,3 +726,43 @@ EOF
|
|||||||
runc resume test_update
|
runc resume test_update
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "update memory vs CheckBeforeUpdate" {
|
||||||
|
requires cgroups_v2
|
||||||
|
[ $EUID -ne 0 ] && requires rootless_cgroup
|
||||||
|
|
||||||
|
runc run -d --console-socket "$CONSOLE_SOCKET" test_update
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
|
# Setting memory to low value with checkBeforeUpdate=true should fail.
|
||||||
|
runc update -r - test_update <<EOF
|
||||||
|
{
|
||||||
|
"memory": {
|
||||||
|
"limit": 1024,
|
||||||
|
"checkBeforeUpdate": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[[ "$output" == *"rejecting memory limit"* ]]
|
||||||
|
testcontainer test_update running
|
||||||
|
|
||||||
|
# Setting memory+swap to low value with checkBeforeUpdate=true should fail.
|
||||||
|
runc update -r - test_update <<EOF
|
||||||
|
{
|
||||||
|
"memory": {
|
||||||
|
"limit": 1024,
|
||||||
|
"swap": 2048,
|
||||||
|
"checkBeforeUpdate": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[[ "$output" == *"rejecting memory+swap limit"* ]]
|
||||||
|
testcontainer test_update running
|
||||||
|
|
||||||
|
# The container will be OOM killed, and runc might either succeed
|
||||||
|
# or fail depending on the timing, so we don't check its exit code.
|
||||||
|
runc update test_update --memory 1024
|
||||||
|
testcontainer test_update stopped
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
func i64Ptr(i int64) *int64 { return &i }
|
func i64Ptr(i int64) *int64 { return &i }
|
||||||
func u64Ptr(i uint64) *uint64 { return &i }
|
func u64Ptr(i uint64) *uint64 { return &i }
|
||||||
func u16Ptr(i uint16) *uint16 { return &i }
|
func u16Ptr(i uint16) *uint16 { return &i }
|
||||||
|
func boolPtr(b bool) *bool { return &b }
|
||||||
|
|
||||||
var updateCommand = cli.Command{
|
var updateCommand = cli.Command{
|
||||||
Name: "update",
|
Name: "update",
|
||||||
@@ -37,7 +38,8 @@ The accepted format is as follow (unchanged values can be omitted):
|
|||||||
"memory": {
|
"memory": {
|
||||||
"limit": 0,
|
"limit": 0,
|
||||||
"reservation": 0,
|
"reservation": 0,
|
||||||
"swap": 0
|
"swap": 0,
|
||||||
|
"checkBeforeUpdate": true
|
||||||
},
|
},
|
||||||
"cpu": {
|
"cpu": {
|
||||||
"shares": 0,
|
"shares": 0,
|
||||||
@@ -136,11 +138,12 @@ other options are ignored.
|
|||||||
|
|
||||||
r := specs.LinuxResources{
|
r := specs.LinuxResources{
|
||||||
Memory: &specs.LinuxMemory{
|
Memory: &specs.LinuxMemory{
|
||||||
Limit: i64Ptr(0),
|
Limit: i64Ptr(0),
|
||||||
Reservation: i64Ptr(0),
|
Reservation: i64Ptr(0),
|
||||||
Swap: i64Ptr(0),
|
Swap: i64Ptr(0),
|
||||||
Kernel: i64Ptr(0),
|
Kernel: i64Ptr(0),
|
||||||
KernelTCP: i64Ptr(0),
|
KernelTCP: i64Ptr(0),
|
||||||
|
CheckBeforeUpdate: boolPtr(false),
|
||||||
},
|
},
|
||||||
CPU: &specs.LinuxCPU{
|
CPU: &specs.LinuxCPU{
|
||||||
Shares: u64Ptr(0),
|
Shares: u64Ptr(0),
|
||||||
@@ -293,6 +296,7 @@ other options are ignored.
|
|||||||
config.Cgroups.Resources.Memory = *r.Memory.Limit
|
config.Cgroups.Resources.Memory = *r.Memory.Limit
|
||||||
config.Cgroups.Resources.MemoryReservation = *r.Memory.Reservation
|
config.Cgroups.Resources.MemoryReservation = *r.Memory.Reservation
|
||||||
config.Cgroups.Resources.MemorySwap = *r.Memory.Swap
|
config.Cgroups.Resources.MemorySwap = *r.Memory.Swap
|
||||||
|
config.Cgroups.Resources.MemoryCheckBeforeUpdate = *r.Memory.CheckBeforeUpdate
|
||||||
config.Cgroups.Resources.PidsLimit = r.Pids.Limit
|
config.Cgroups.Resources.PidsLimit = r.Pids.Limit
|
||||||
config.Cgroups.Resources.Unified = r.Unified
|
config.Cgroups.Resources.Unified = r.Unified
|
||||||
|
|
||||||
|
|||||||
+10
@@ -12,6 +12,8 @@ type Spec struct {
|
|||||||
Root *Root `json:"root,omitempty"`
|
Root *Root `json:"root,omitempty"`
|
||||||
// Hostname configures the container's hostname.
|
// Hostname configures the container's hostname.
|
||||||
Hostname string `json:"hostname,omitempty"`
|
Hostname string `json:"hostname,omitempty"`
|
||||||
|
// Domainname configures the container's domainname.
|
||||||
|
Domainname string `json:"domainname,omitempty"`
|
||||||
// Mounts configures additional mounts (on top of Root).
|
// Mounts configures additional mounts (on top of Root).
|
||||||
Mounts []Mount `json:"mounts,omitempty"`
|
Mounts []Mount `json:"mounts,omitempty"`
|
||||||
// Hooks configures callbacks for container lifecycle events.
|
// Hooks configures callbacks for container lifecycle events.
|
||||||
@@ -317,6 +319,10 @@ type LinuxMemory struct {
|
|||||||
DisableOOMKiller *bool `json:"disableOOMKiller,omitempty"`
|
DisableOOMKiller *bool `json:"disableOOMKiller,omitempty"`
|
||||||
// Enables hierarchical memory accounting
|
// Enables hierarchical memory accounting
|
||||||
UseHierarchy *bool `json:"useHierarchy,omitempty"`
|
UseHierarchy *bool `json:"useHierarchy,omitempty"`
|
||||||
|
// CheckBeforeUpdate enables checking if a new memory limit is lower
|
||||||
|
// than the current usage during update, and if so, rejecting the new
|
||||||
|
// limit.
|
||||||
|
CheckBeforeUpdate *bool `json:"checkBeforeUpdate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LinuxCPU for Linux cgroup 'cpu' resource management
|
// LinuxCPU for Linux cgroup 'cpu' resource management
|
||||||
@@ -643,6 +649,10 @@ const (
|
|||||||
// LinuxSeccompFlagSpecAllow can be used to disable Speculative Store
|
// LinuxSeccompFlagSpecAllow can be used to disable Speculative Store
|
||||||
// Bypass mitigation. (since Linux 4.17)
|
// Bypass mitigation. (since Linux 4.17)
|
||||||
LinuxSeccompFlagSpecAllow LinuxSeccompFlag = "SECCOMP_FILTER_FLAG_SPEC_ALLOW"
|
LinuxSeccompFlagSpecAllow LinuxSeccompFlag = "SECCOMP_FILTER_FLAG_SPEC_ALLOW"
|
||||||
|
|
||||||
|
// LinuxSeccompFlagWaitKillableRecv can be used to switch to the wait
|
||||||
|
// killable semantics. (since Linux 5.19)
|
||||||
|
LinuxSeccompFlagWaitKillableRecv LinuxSeccompFlag = "SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Additional architectures permitted to be used for system calls
|
// Additional architectures permitted to be used for system calls
|
||||||
|
|||||||
Vendored
+1
-1
@@ -36,7 +36,7 @@ github.com/moby/sys/mountinfo
|
|||||||
# github.com/mrunalp/fileutils v0.5.0
|
# github.com/mrunalp/fileutils v0.5.0
|
||||||
## explicit; go 1.13
|
## explicit; go 1.13
|
||||||
github.com/mrunalp/fileutils
|
github.com/mrunalp/fileutils
|
||||||
# github.com/opencontainers/runtime-spec v1.0.3-0.20220718201635-a8106e99982b
|
# github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78
|
||||||
## explicit
|
## explicit
|
||||||
github.com/opencontainers/runtime-spec/specs-go
|
github.com/opencontainers/runtime-spec/specs-go
|
||||||
# github.com/opencontainers/selinux v1.10.2
|
# github.com/opencontainers/selinux v1.10.2
|
||||||
|
|||||||
Reference in New Issue
Block a user