//go:build linux package cgroup import ( "context" "errors" "os" "os/exec" "path/filepath" "strconv" "syscall" "testing" "time" ) // genParentFixture is strictFixture turned into a generation parent: the // tier on its files, memory.oom.group=0, the record matching. func genParentFixture(t *testing.T) *Prepared { t.Helper() p := strictFixture(t) p.genParent, p.tier = true, TierDefault for name, value := range tierExpected(TierDefault, "0") { if err := strictWrite(p.limits[name], value); err != nil { t.Fatal(err) } p.expected[name] = value } return p } func makeGenFixture(t *testing.T, p *Prepared, n uint64, events string) string { t.Helper() d := filepath.Join(p.dir.Name(), GenChild(n)) if err := os.Mkdir(d, 0o700); err != nil { t.Fatal(err) } for name, v := range map[string]string{"cgroup.events": events, "cgroup.kill": "", "memory.max": "", "memory.high": "", "memory.swap.max": "", "memory.oom.group": ""} { if err := os.WriteFile(filepath.Join(d, name), []byte(v), 0o600); err != nil { t.Fatal(err) } } return d } func TestPrepareGenWritesTheFullTier(t *testing.T) { p := genParentFixture(t) d := makeGenFixture(t, p, 1, "populated 0\n") child, err := p.PrepareGen(1, TierDefault) if err != nil { t.Fatal(err) } defer child.Close() want := tierExpected(TierDefault, "1") for name, value := range want { if got := readFixture(t, filepath.Join(d, name)); got != value { t.Errorf("g1/%s = %q, want %q", name, got, value) } } if want["memory.max"] != "536870912" || want["memory.oom.group"] != "1" { t.Fatalf("tierExpected = %v", want) } } func TestPrepareGenReservesPerChild(t *testing.T) { p := genParentFixture(t) makeGenFixture(t, p, 1, "populated 0\n") makeGenFixture(t, p, 2, "populated 0\n") g1, err := p.PrepareGen(1, TierDefault) if err != nil { t.Fatal(err) } defer g1.Close() if again, err := p.PrepareGen(1, TierDefault); !errors.Is(err, ErrStrictState) || again != nil { t.Fatalf("second reservation of g1: %v", err) } g2, err := p.PrepareGen(2, TierDefault) if err != nil { t.Fatalf("g2 beside a live g1: %v", err) } defer g2.Close() if _, err := p.PrepareGen(0, TierDefault); !errors.Is(err, ErrStrictRefused) { t.Errorf("generation 0: %v", err) } if _, err := p.PrepareGen(3, Limit{MemoryMax: 64 << 20}); !errors.Is(err, ErrStrictRefused) { t.Errorf("a limit that is not a tier: %v", err) } } func TestPrepareGenAndSetParentLimitRefuseAPlainHostGroup(t *testing.T) { p := strictFixture(t) if _, err := p.PrepareGen(1, TierDefault); !errors.Is(err, ErrStrictState) { t.Errorf("PrepareGen on a leaf host group: %v", err) } if err := p.SetParentLimit(TierDefault); !errors.Is(err, ErrStrictState) { t.Errorf("SetParentLimit on a leaf host group: %v", err) } } func TestGenerationParentRefusesAProcess(t *testing.T) { p := genParentFixture(t) if err := p.Start(exec.Command("/bin/true")); !errors.Is(err, ErrStrictState) { t.Fatalf("a generation parent accepted a process: %v", err) } } // The parent's record moves with the limit it raises, so a child's Start // passes limitsOK after a raise; a limit set by anyone else fails it. func TestSetParentLimitRecordsWhatItWrites(t *testing.T) { p := genParentFixture(t) makeGenFixture(t, p, 1, "populated 0\n") makeGenFixture(t, p, 2, "populated 0\n") child, err := p.PrepareGen(1, TierDefault) if err != nil { t.Fatal(err) } defer child.Close() if err := p.SetParentLimit(OverlapLimit(TierDefault, TierDefault)); err != nil { t.Fatal(err) } if got := readFixture(t, filepath.Join(p.dir.Name(), "memory.max")); got != "1073741824" { t.Fatalf("parent memory.max = %q", got) } // The fixture's start answers ENOSYS: reaching it means every check passed. child.start = func(*exec.Cmd) error { return syscall.ENOSYS } if err := child.Start(exec.Command("/bin/true")); !errors.Is(err, ErrStrictUnsupported) { t.Fatalf("child start after an API raise: %v, want it to reach the spawn", err) } // strictWrite does not truncate a fixture file: "9" over "1073741824" // leaves "9073741824", a limit this API never set. if err := strictWrite(p.limits["memory.max"], "9"); err != nil { t.Fatal(err) } other, err := p.PrepareGen(2, TierDefault) if err != nil { t.Fatal(err) } defer other.Close() if err := other.Start(exec.Command("/bin/true")); !errors.Is(err, ErrStrictRefused) { t.Fatalf("child start under a limit the API did not set: %v", err) } if err := p.SetParentLimit(TierDefault); !errors.Is(err, ErrStrictRefused) { t.Fatalf("SetParentLimit papered over a foreign limit: %v", err) } } // Review focus 4: a release that changes tier. func TestSetParentLimitAcceptsATierChange(t *testing.T) { p := genParentFixture(t) if err := p.SetParentLimit(OverlapLimit(TierDefault, TierChildren)); err != nil { t.Fatalf("512M + 1G: %v", err) } if got := readFixture(t, filepath.Join(p.dir.Name(), "memory.max")); got != strconv.FormatInt(1536<<20, 10) { t.Fatalf("parent memory.max = %q", got) } if err := p.SetParentLimit(Limit{MemoryMax: 64 << 20}); !errors.Is(err, ErrStrictRefused) { t.Fatalf("a limit that is no tier sum: %v", err) } } func TestGenerationKillAndEmptyTouchesOnlyItsChild(t *testing.T) { p := genParentFixture(t) d1 := makeGenFixture(t, p, 1, "populated 1\n") d2 := makeGenFixture(t, p, 2, "populated 0\n") g1, err := p.PrepareGen(1, TierDefault) if err == nil { t.Fatal("PrepareGen pinned a populated group") } _ = g1 if err := os.WriteFile(filepath.Join(d1, "cgroup.events"), []byte("populated 0\n"), 0o600); err != nil { t.Fatal(err) } if g1, err = p.PrepareGen(1, TierDefault); err != nil { t.Fatal(err) } defer g1.Close() g2, err := p.PrepareGen(2, TierDefault) if err != nil { t.Fatal(err) } ctx, cancel := context.WithTimeout(t.Context(), time.Second) defer cancel() if err := g2.KillAndEmpty(ctx); err != nil { t.Fatal(err) } for dir, want := range map[string]string{d2: "1", d1: "", p.dir.Name(): ""} { if got := readFixture(t, filepath.Join(dir, "cgroup.kill")); got != want { t.Errorf("%s/cgroup.kill = %q, want %q", dir, got, want) } } // CloseRemove: refused while populated; once empty it closes and frees // the reservation even when rmdir fails (a fixture dir holds files). if err := os.WriteFile(filepath.Join(d2, "cgroup.events"), []byte("populated 1\n"), 0o600); err != nil { t.Fatal(err) } if err := g2.CloseRemove(); !errors.Is(err, ErrStrictRefused) { t.Fatalf("CloseRemove of a populated child: %v", err) } if err := os.WriteFile(filepath.Join(d2, "cgroup.events"), []byte("populated 0\n"), 0o600); err != nil { t.Fatal(err) } if err := g2.CloseRemove(); err == nil { t.Fatal("rmdir of a fixture dir holding files reported success") } if _, err := g2.Populated(); !errors.Is(err, ErrStrictState) { t.Fatalf("handle still open after CloseRemove: %v", err) } strictReservations.Lock() held := strictReservations.held[g2.identity] strictReservations.Unlock() if held { t.Fatal("reservation not released by CloseRemove") } // The directory survived its failed rmdir, but it took cgroup.kill, so // it is never pinned for a spawn again (amadan #272). if again, err := p.PrepareGen(2, TierDefault); !errors.Is(err, ErrStrictRefused) { if again != nil { again.Close() } t.Fatalf("re-pin of a killed generation child: %v, want ErrStrictRefused", err) } }