Skip to content

Commit 37789f5

Browse files
committed
libcontainer: cgroups: add pids controller support
Add support for the pids cgroup controller to libcontainer, a recent feature that is available in Linux 4.3+. Unfortunately, due to the init process being written in Go, it can spawn an an unknown number of threads due to blocked syscalls. This results in the init process being unable to run properly, and thus small pids.max configs won't work properly. Signed-off-by: Aleksa Sarai <[email protected]>
1 parent 9d6ce71 commit 37789f5

File tree

8 files changed

+238
-1
lines changed

8 files changed

+238
-1
lines changed

libcontainer/cgroups/fs/apply_raw.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var (
2323
&MemoryGroup{},
2424
&CpuGroup{},
2525
&CpuacctGroup{},
26+
&PidsGroup{},
2627
&BlkioGroup{},
2728
&HugetlbGroup{},
2829
&NetClsGroup{},

libcontainer/cgroups/fs/pids.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// +build linux
2+
3+
package fs
4+
5+
import (
6+
"fmt"
7+
"strconv"
8+
9+
"github.com/opencontainers/runc/libcontainer/cgroups"
10+
"github.com/opencontainers/runc/libcontainer/configs"
11+
)
12+
13+
type PidsGroup struct {
14+
}
15+
16+
func (s *PidsGroup) Name() string {
17+
return "pids"
18+
}
19+
20+
func (s *PidsGroup) Apply(d *cgroupData) error {
21+
dir, err := d.join("pids")
22+
if err != nil && !cgroups.IsNotFound(err) {
23+
return err
24+
}
25+
26+
if err := s.Set(dir, d.config); err != nil {
27+
return err
28+
}
29+
30+
return nil
31+
}
32+
33+
func (s *PidsGroup) Set(path string, cgroup *configs.Cgroup) error {
34+
if cgroup.Resources.PidsLimit != 0 {
35+
// "max" is the fallback value.
36+
limit := "max"
37+
38+
if cgroup.Resources.PidsLimit > 0 {
39+
limit = strconv.FormatInt(cgroup.Resources.PidsLimit, 10)
40+
}
41+
42+
if err := writeFile(path, "pids.max", limit); err != nil {
43+
return err
44+
}
45+
}
46+
47+
return nil
48+
}
49+
50+
func (s *PidsGroup) Remove(d *cgroupData) error {
51+
return removePath(d.path("pids"))
52+
}
53+
54+
func (s *PidsGroup) GetStats(path string, stats *cgroups.Stats) error {
55+
value, err := getCgroupParamUint(path, "pids.current")
56+
if err != nil {
57+
return fmt.Errorf("failed to parse pids.current - %s", err)
58+
}
59+
60+
stats.PidsStats.Current = value
61+
return nil
62+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// +build linux
2+
3+
package fs
4+
5+
import (
6+
"strconv"
7+
"testing"
8+
9+
"github.com/opencontainers/runc/libcontainer/cgroups"
10+
)
11+
12+
const (
13+
maxUnlimited = -1
14+
maxLimited = 1024
15+
)
16+
17+
func TestPidsSetMax(t *testing.T) {
18+
helper := NewCgroupTestUtil("pids", t)
19+
defer helper.cleanup()
20+
21+
helper.writeFileContents(map[string]string{
22+
"pids.max": "max",
23+
})
24+
25+
helper.CgroupData.config.Resources.PidsLimit = maxLimited
26+
pids := &PidsGroup{}
27+
if err := pids.Set(helper.CgroupPath, helper.CgroupData.config); err != nil {
28+
t.Fatal(err)
29+
}
30+
31+
value, err := getCgroupParamUint(helper.CgroupPath, "pids.max")
32+
if err != nil {
33+
t.Fatalf("Failed to parse pids.max - %s", err)
34+
}
35+
36+
if value != maxLimited {
37+
t.Fatalf("Expected %d, got %d for setting pids.max - limited", maxLimited, value)
38+
}
39+
}
40+
41+
func TestPidsSetUnlimited(t *testing.T) {
42+
helper := NewCgroupTestUtil("pids", t)
43+
defer helper.cleanup()
44+
45+
helper.writeFileContents(map[string]string{
46+
"pids.max": strconv.Itoa(maxLimited),
47+
})
48+
49+
helper.CgroupData.config.Resources.PidsLimit = maxUnlimited
50+
pids := &PidsGroup{}
51+
if err := pids.Set(helper.CgroupPath, helper.CgroupData.config); err != nil {
52+
t.Fatal(err)
53+
}
54+
55+
value, err := getCgroupParamString(helper.CgroupPath, "pids.max")
56+
if err != nil {
57+
t.Fatalf("Failed to parse pids.max - %s", err)
58+
}
59+
60+
if value != "max" {
61+
t.Fatalf("Expected %s, got %s for setting pids.max - unlimited", "max", value)
62+
}
63+
}
64+
65+
func TestPidsStats(t *testing.T) {
66+
helper := NewCgroupTestUtil("pids", t)
67+
defer helper.cleanup()
68+
69+
helper.writeFileContents(map[string]string{
70+
"pids.current": strconv.Itoa(1337),
71+
"pids.max": strconv.Itoa(maxLimited),
72+
})
73+
74+
pids := &PidsGroup{}
75+
stats := *cgroups.NewStats()
76+
if err := pids.GetStats(helper.CgroupPath, &stats); err != nil {
77+
t.Fatal(err)
78+
}
79+
80+
if stats.PidsStats.Current != 1337 {
81+
t.Fatalf("Expected %d, got %d for pids.current", 1337, stats.PidsStats.Current)
82+
}
83+
}

libcontainer/cgroups/stats.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ type MemoryStats struct {
4949
Stats map[string]uint64 `json:"stats,omitempty"`
5050
}
5151

52+
type PidsStats struct {
53+
// number of pids in the cgroup
54+
Current uint64 `json:"current,omitempty"`
55+
}
56+
5257
type BlkioStatEntry struct {
5358
Major uint64 `json:"major,omitempty"`
5459
Minor uint64 `json:"minor,omitempty"`
@@ -80,6 +85,7 @@ type HugetlbStats struct {
8085
type Stats struct {
8186
CpuStats CpuStats `json:"cpu_stats,omitempty"`
8287
MemoryStats MemoryStats `json:"memory_stats,omitempty"`
88+
PidsStats PidsStats `json:"pids_stats,omitempty"`
8389
BlkioStats BlkioStats `json:"blkio_stats,omitempty"`
8490
// the map is in the format "size of hugepage: stats of the hugepage"
8591
HugetlbStats map[string]HugetlbStats `json:"hugetlb_stats,omitempty"`

libcontainer/cgroups/systemd/apply_systemd.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ var subsystems = subsystemSet{
5555
&fs.MemoryGroup{},
5656
&fs.CpuGroup{},
5757
&fs.CpuacctGroup{},
58+
&fs.PidsGroup{},
5859
&fs.BlkioGroup{},
5960
&fs.HugetlbGroup{},
6061
&fs.PerfEventGroup{},
@@ -233,7 +234,7 @@ func (m *Manager) Apply(pid int) error {
233234
return err
234235
}
235236

236-
// we need to manually join the freezer, net_cls, net_prio and cpuset cgroup in systemd
237+
// we need to manually join the freezer, net_cls, net_prio, pids and cpuset cgroup in systemd
237238
// because it does not currently support it via the dbus api.
238239
if err := joinFreezer(c, pid); err != nil {
239240
return err
@@ -246,6 +247,10 @@ func (m *Manager) Apply(pid int) error {
246247
return err
247248
}
248249

250+
if err := joinPids(c, pid); err != nil {
251+
return err
252+
}
253+
249254
if err := joinCpuset(c, pid); err != nil {
250255
return err
251256
}
@@ -394,6 +399,18 @@ func joinNetCls(c *configs.Cgroup, pid int) error {
394399
return netcls.Set(path, c)
395400
}
396401

402+
func joinPids(c *configs.Cgroup, pid int) error {
403+
path, err := join(c, "pids", pid)
404+
if err != nil && !cgroups.IsNotFound(err) {
405+
return err
406+
}
407+
pids, err := subsystems.Get("pids")
408+
if err != nil {
409+
return err
410+
}
411+
return pids.Set(path, c)
412+
}
413+
397414
func getSubsystemPath(c *configs.Cgroup, subsystem string) (string, error) {
398415
mountpoint, err := cgroups.FindCgroupMountpoint(subsystem)
399416
if err != nil {

libcontainer/configs/cgroup_unix.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ type Resources struct {
6464
// MEM to use
6565
CpusetMems string `json:"cpuset_mems"`
6666

67+
// Process limit; set <= `0' to disable limit.
68+
PidsLimit int64 `json:"pids_limit"`
69+
6770
// Specifies per cgroup weight, range is from 10 to 1000.
6871
BlkioWeight uint16 `json:"blkio_weight"`
6972

libcontainer/integration/exec_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,70 @@ func testCpuShares(t *testing.T, systemd bool) {
525525
}
526526
}
527527

528+
func TestPids(t *testing.T) {
529+
testPids(t, false)
530+
}
531+
532+
func TestPidsSystemd(t *testing.T) {
533+
if !systemd.UseSystemd() {
534+
t.Skip("Systemd is unsupported")
535+
}
536+
testPids(t, true)
537+
}
538+
539+
func testPids(t *testing.T, systemd bool) {
540+
if testing.Short() {
541+
return
542+
}
543+
544+
rootfs, err := newRootfs()
545+
ok(t, err)
546+
defer remove(rootfs)
547+
548+
config := newTemplateConfig(rootfs)
549+
if systemd {
550+
config.Cgroups.Parent = "system.slice"
551+
}
552+
config.Cgroups.Resources.PidsLimit = -1
553+
554+
// Running multiple processes.
555+
_, ret, err := runContainer(config, "", "/bin/sh", "-c", "/bin/true | /bin/true | /bin/true | /bin/true")
556+
if err != nil && strings.Contains(err.Error(), "no such directory for pids.max") {
557+
t.Skip("PIDs cgroup is unsupported")
558+
}
559+
ok(t, err)
560+
561+
if ret != 0 {
562+
t.Fatalf("expected fork() to succeed with no pids limit")
563+
}
564+
565+
// Enforce a permissive limit (shell + 4 * true).
566+
config.Cgroups.Resources.PidsLimit = 5
567+
_, ret, err = runContainer(config, "", "/bin/sh", "-c", "/bin/true | /bin/true | /bin/true | /bin/true")
568+
if err != nil && strings.Contains(err.Error(), "no such directory for pids.max") {
569+
t.Skip("PIDs cgroup is unsupported")
570+
}
571+
ok(t, err)
572+
573+
if ret != 0 {
574+
t.Fatalf("expected fork() to succeed with permissive pids limit")
575+
}
576+
577+
// Enforce a restrictive limit limit (shell + 5 * true).
578+
config.Cgroups.Resources.PidsLimit = 5
579+
out, ret, err := runContainer(config, "", "/bin/sh", "-c", "/bin/true | /bin/true | /bin/true | /bin/true | /bin/true")
580+
if err != nil && strings.Contains(err.Error(), "no such directory for pids.max") {
581+
t.Skip("PIDs cgroup is unsupported")
582+
}
583+
if err != nil && !strings.Contains(out.String(), "sh: can't fork") {
584+
ok(t, err)
585+
}
586+
587+
if err == nil {
588+
t.Fatalf("expected fork() to fail with restrictive pids limit")
589+
}
590+
}
591+
528592
func TestRunWithKernelMemory(t *testing.T) {
529593
testRunWithKernelMemory(t, false)
530594
}

spec.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ func createCgroupConfig(name string, spec *specs.LinuxRuntimeSpec, devices []*co
456456
c.Resources.CpuRtPeriod = r.CPU.RealtimePeriod
457457
c.Resources.CpusetCpus = r.CPU.Cpus
458458
c.Resources.CpusetMems = r.CPU.Mems
459+
c.Resources.PidsLimit = r.Pids.Limit
459460
c.Resources.BlkioWeight = r.BlockIO.Weight
460461
c.Resources.BlkioLeafWeight = r.BlockIO.LeafWeight
461462
for _, wd := range r.BlockIO.WeightDevice {

0 commit comments

Comments
 (0)