Skip to content

Commit 3023e6c

Browse files
authored
Merge pull request opencontainers#3166 from kolyshkin/fix-freeze-before-set-alt-2
libct/cg/sd/v1: fix freezeBeforeSet (alt 2)
2 parents 926a9a0 + 9a095e4 commit 3023e6c

File tree

5 files changed

+274
-9
lines changed

5 files changed

+274
-9
lines changed

libcontainer/cgroups/systemd/common.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,14 @@ func getUnitName(c *configs.Cgroup) string {
311311
return c.Name
312312
}
313313

314+
// This code should be in sync with getUnitName.
315+
func getUnitType(unitName string) string {
316+
if strings.HasSuffix(unitName, ".slice") {
317+
return "Slice"
318+
}
319+
return "Scope"
320+
}
321+
314322
// isDbusError returns true if the error is a specific dbus error.
315323
func isDbusError(err error, name string) bool {
316324
if err != nil {
@@ -389,10 +397,10 @@ func resetFailedUnit(cm *dbusConnManager, name string) {
389397
}
390398
}
391399

392-
func getUnitProperty(cm *dbusConnManager, unitName string, propertyName string) (*systemdDbus.Property, error) {
400+
func getUnitTypeProperty(cm *dbusConnManager, unitName string, unitType string, propertyName string) (*systemdDbus.Property, error) {
393401
var prop *systemdDbus.Property
394402
err := cm.retryOnDisconnect(func(c *systemdDbus.Conn) (Err error) {
395-
prop, Err = c.GetUnitPropertyContext(context.TODO(), unitName, propertyName)
403+
prop, Err = c.GetUnitTypePropertyContext(context.TODO(), unitName, unitType, propertyName)
396404
return Err
397405
})
398406
return prop, err

libcontainer/cgroups/systemd/systemd_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ func TestSystemdVersion(t *testing.T) {
4040
}
4141
}
4242

43+
func TestValidUnitTypes(t *testing.T) {
44+
testCases := []struct {
45+
unitName string
46+
expectedUnitType string
47+
}{
48+
{"system.slice", "Slice"},
49+
{"kubepods.slice", "Slice"},
50+
{"testing-container:ab.scope", "Scope"},
51+
}
52+
for _, sdTest := range testCases {
53+
unitType := getUnitType(sdTest.unitName)
54+
if unitType != sdTest.expectedUnitType {
55+
t.Errorf("getUnitType(%s); want %q; got %q", sdTest.unitName, sdTest.expectedUnitType, unitType)
56+
}
57+
}
58+
}
59+
4360
func newManager(config *configs.Cgroup) cgroups.Manager {
4461
if cgroups.IsCgroup2UnifiedMode() {
4562
return NewUnifiedManager(config, "", false)

libcontainer/cgroups/systemd/v1.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"os"
88
"path/filepath"
9+
"reflect"
910
"strings"
1011
"sync"
1112

@@ -345,6 +346,11 @@ func (m *legacyManager) freezeBeforeSet(unitName string, r *configs.Resources) (
345346
// Special case for SkipDevices, as used by Kubernetes to create pod
346347
// cgroups with allow-all device policy).
347348
if r.SkipDevices {
349+
if r.SkipFreezeOnSet {
350+
// Both needsFreeze and needsThaw are false.
351+
return
352+
}
353+
348354
// No need to freeze if SkipDevices is set, and either
349355
// (1) systemd unit does not (yet) exist, or
350356
// (2) it has DevicePolicy=auto and empty DeviceAllow list.
@@ -353,15 +359,20 @@ func (m *legacyManager) freezeBeforeSet(unitName string, r *configs.Resources) (
353359
// a non-existent unit returns default properties,
354360
// and settings in (2) are the defaults.
355361
//
356-
// Do not return errors from getUnitProperty, as they alone
362+
// Do not return errors from getUnitTypeProperty, as they alone
357363
// should not prevent Set from working.
358-
devPolicy, e := getUnitProperty(m.dbus, unitName, "DevicePolicy")
364+
365+
unitType := getUnitType(unitName)
366+
367+
devPolicy, e := getUnitTypeProperty(m.dbus, unitName, unitType, "DevicePolicy")
359368
if e == nil && devPolicy.Value == dbus.MakeVariant("auto") {
360-
devAllow, e := getUnitProperty(m.dbus, unitName, "DeviceAllow")
361-
if e == nil && devAllow.Value == dbus.MakeVariant([]deviceAllowEntry{}) {
362-
needsFreeze = false
363-
needsThaw = false
364-
return
369+
devAllow, e := getUnitTypeProperty(m.dbus, unitName, unitType, "DeviceAllow")
370+
if e == nil {
371+
if rv := reflect.ValueOf(devAllow.Value.Value()); rv.Kind() == reflect.Slice && rv.Len() == 0 {
372+
needsFreeze = false
373+
needsThaw = false
374+
return
375+
}
365376
}
366377
}
367378
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package systemd
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"strings"
7+
"testing"
8+
9+
"golang.org/x/sys/unix"
10+
11+
"github.com/opencontainers/runc/libcontainer/cgroups"
12+
"github.com/opencontainers/runc/libcontainer/configs"
13+
)
14+
15+
func TestFreezeBeforeSet(t *testing.T) {
16+
requireV1(t)
17+
18+
testCases := []struct {
19+
desc string
20+
// Test input.
21+
cg *configs.Cgroup
22+
preFreeze bool
23+
// Expected values.
24+
// Before unit creation (Apply).
25+
freeze0, thaw0 bool
26+
// After unit creation.
27+
freeze1, thaw1 bool
28+
}{
29+
{
30+
// A slice with SkipDevices.
31+
desc: "slice,skip-devices",
32+
cg: &configs.Cgroup{
33+
Name: "system-runc_test_freeze_1.slice",
34+
Parent: "system.slice",
35+
Resources: &configs.Resources{
36+
SkipDevices: true,
37+
},
38+
},
39+
// Expected.
40+
freeze0: false,
41+
thaw0: false,
42+
freeze1: false,
43+
thaw1: false,
44+
},
45+
{
46+
// A scope with SkipDevices. Not a realistic scenario with runc
47+
// (as container can't have SkipDevices == true), but possible
48+
// for a standalone cgroup manager.
49+
desc: "scope,skip-devices",
50+
cg: &configs.Cgroup{
51+
ScopePrefix: "test",
52+
Name: "testFreeze2",
53+
Parent: "system.slice",
54+
Resources: &configs.Resources{
55+
SkipDevices: true,
56+
},
57+
},
58+
// Expected.
59+
freeze0: false,
60+
thaw0: false,
61+
freeze1: false,
62+
thaw1: false,
63+
},
64+
{
65+
// A slice that is about to be frozen in Set.
66+
desc: "slice,will-freeze",
67+
cg: &configs.Cgroup{
68+
Name: "system-runc_test_freeze_3.slice",
69+
Parent: "system.slice",
70+
Resources: &configs.Resources{
71+
Freezer: configs.Frozen,
72+
},
73+
},
74+
// Expected.
75+
freeze0: true,
76+
thaw0: false,
77+
freeze1: true,
78+
thaw1: false,
79+
},
80+
{
81+
// A pre-frozen slice that should stay frozen.
82+
desc: "slice,pre-frozen,will-freeze",
83+
cg: &configs.Cgroup{
84+
Name: "system-runc_test_freeze_4.slice",
85+
Parent: "system.slice",
86+
Resources: &configs.Resources{
87+
Freezer: configs.Frozen,
88+
},
89+
},
90+
preFreeze: true,
91+
// Expected.
92+
freeze0: true, // not actually frozen yet.
93+
thaw0: false,
94+
freeze1: false,
95+
thaw1: false,
96+
},
97+
{
98+
// A pre-frozen scope with skip devices set.
99+
desc: "scope,pre-frozen,skip-devices",
100+
cg: &configs.Cgroup{
101+
ScopePrefix: "test",
102+
Name: "testFreeze5",
103+
Parent: "system.slice",
104+
Resources: &configs.Resources{
105+
SkipDevices: true,
106+
},
107+
},
108+
preFreeze: true,
109+
// Expected.
110+
freeze0: false,
111+
thaw0: false,
112+
freeze1: false,
113+
thaw1: false,
114+
},
115+
{
116+
// A pre-frozen scope which will be thawed.
117+
desc: "scope,pre-frozen",
118+
cg: &configs.Cgroup{
119+
ScopePrefix: "test",
120+
Name: "testFreeze6",
121+
Parent: "system.slice",
122+
Resources: &configs.Resources{},
123+
},
124+
preFreeze: true,
125+
// Expected.
126+
freeze0: true, // not actually frozen yet.
127+
thaw0: true,
128+
freeze1: false,
129+
thaw1: false,
130+
},
131+
}
132+
133+
for _, tc := range testCases {
134+
tc := tc
135+
t.Run(tc.desc, func(t *testing.T) {
136+
m := NewLegacyManager(tc.cg, nil)
137+
defer m.Destroy() //nolint:errcheck
138+
lm := m.(*legacyManager)
139+
140+
// Checks for a non-existent unit.
141+
freeze, thaw, err := lm.freezeBeforeSet(getUnitName(tc.cg), tc.cg.Resources)
142+
if err != nil {
143+
t.Fatal(err)
144+
}
145+
if freeze != tc.freeze0 || thaw != tc.thaw0 {
146+
t.Errorf("before Apply (non-existent unit): expected freeze: %v, thaw: %v, got freeze: %v, thaw: %v",
147+
tc.freeze0, tc.thaw0, freeze, thaw)
148+
}
149+
150+
// Create systemd unit.
151+
pid := -1
152+
if strings.HasSuffix(getUnitName(tc.cg), ".scope") {
153+
// Scopes require a process inside.
154+
cmd := exec.Command("bash", "-c", "sleep 1m")
155+
if err := cmd.Start(); err != nil {
156+
t.Fatal(err)
157+
}
158+
pid = cmd.Process.Pid
159+
// Make sure to not leave a zombie.
160+
defer func() {
161+
// These may fail, we don't care.
162+
_ = cmd.Process.Kill()
163+
_ = cmd.Wait()
164+
}()
165+
}
166+
if err := m.Apply(pid); err != nil {
167+
t.Fatal(err)
168+
}
169+
if tc.preFreeze {
170+
if err := m.Freeze(configs.Frozen); err != nil {
171+
t.Error(err)
172+
return // no more checks
173+
}
174+
}
175+
freeze, thaw, err = lm.freezeBeforeSet(getUnitName(tc.cg), tc.cg.Resources)
176+
if err != nil {
177+
t.Error(err)
178+
return // no more checks
179+
}
180+
if freeze != tc.freeze1 || thaw != tc.thaw1 {
181+
t.Errorf("expected freeze: %v, thaw: %v, got freeze: %v, thaw: %v",
182+
tc.freeze1, tc.thaw1, freeze, thaw)
183+
}
184+
// Destroy() timeouts on a frozen container, so we need to thaw it.
185+
if tc.preFreeze {
186+
if err := m.Freeze(configs.Thawed); err != nil {
187+
t.Error(err)
188+
}
189+
}
190+
// Destroy() does not kill processes in cgroup, so we should.
191+
if pid != -1 {
192+
if err = unix.Kill(pid, unix.SIGKILL); err != nil {
193+
t.Errorf("unable to kill pid %d: %s", pid, err)
194+
}
195+
}
196+
// Not really needed, but may help catch some bugs.
197+
if err := m.Destroy(); err != nil {
198+
t.Errorf("destroy: %s", err)
199+
}
200+
})
201+
}
202+
}
203+
204+
// requireV1 skips the test unless a set of requirements (cgroup v1,
205+
// systemd, root) is met.
206+
func requireV1(t *testing.T) {
207+
t.Helper()
208+
if cgroups.IsCgroup2UnifiedMode() {
209+
t.Skip("Test requires cgroup v1.")
210+
}
211+
if !IsRunningSystemd() {
212+
t.Skip("Test requires systemd.")
213+
}
214+
if os.Geteuid() != 0 {
215+
t.Skip("Test requires root.")
216+
}
217+
}

libcontainer/configs/cgroup_linux.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,16 @@ type Resources struct {
131131
//
132132
// NOTE it is impossible to start a container which has this flag set.
133133
SkipDevices bool `json:"-"`
134+
135+
// SkipFreezeOnSet is a flag for cgroup manager to skip the cgroup
136+
// freeze when setting resources. Only applicable to systemd legacy
137+
// (i.e. cgroup v1) manager (which uses freeze by default to avoid
138+
// spurious permission errors caused by systemd inability to update
139+
// device rules in a non-disruptive manner).
140+
//
141+
// If not set, a few methods (such as looking into cgroup's
142+
// devices.list and querying the systemd unit properties) are used
143+
// during Set() to figure out whether the freeze is required. Those
144+
// methods may be relatively slow, thus this flag.
145+
SkipFreezeOnSet bool `json:"-"`
134146
}

0 commit comments

Comments
 (0)