package localapi import ( "encoding/json" "net/http" "os" "path/filepath" "strings" "testing" ) // R-528: Docker does not always report an OOM kill inside a Felhom LXC guest (OOMKilled stays false, no `oom` event — // measured 2026-09-15/16 on two boxes), but the kernel's own counter does count it, and it is readable from the host: // /sys/fs/cgroup/lxc//ns/system.slice/docker-.scope/memory.events, line `oom_kill N`. GET /guest/oom-kills // serves the asking guest's counters, and only that guest's. func fakeCgroup(t *testing.T, root string, vmid, id, events string) { t.Helper() dir := filepath.Join(root, "lxc", vmid, "ns", "system.slice", "docker-"+id+".scope") if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "memory.events"), []byte(events), 0o444); err != nil { t.Fatal(err) } } const idA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" const idB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" const idOther = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" // RED-PROOF: serve the counters of every guest under lxc/ (drop the vmid from the glob) → the other guest's container // appears → FAILS; parse `oom ` instead of `oom_kill` → idA reads 9 → FAILS. func TestR528_GuestOOMKillsServesThisGuestsCountersOnly(t *testing.T) { root := t.TempDir() fakeCgroup(t, root, "100", idA, "low 0\nhigh 0\nmax 4\noom 9\noom_kill 3\noom_group_kill 0\n") fakeCgroup(t, root, "100", idB, "low 0\nhigh 0\nmax 0\noom 0\noom_kill 0\n") fakeCgroup(t, root, "200", idOther, "oom_kill 7\n") // a scope that is not a container (no 64-hex id) and a cgroupfs-driver layout container if err := os.MkdirAll(filepath.Join(root, "lxc", "100", "ns", "system.slice", "cron.service"), 0o755); err != nil { t.Fatal(err) } dfDir := filepath.Join(root, "lxc", "100", "ns", "docker", idOther[:8]+strings.Repeat("d", 56)) if err := os.MkdirAll(dfDir, 0o755); err != nil { t.Fatal(err) } _ = os.WriteFile(filepath.Join(dfDir, "memory.events"), []byte("oom_kill 2\n"), 0o444) got, avail, err := readGuestOOMKills(root, 100) if err != nil || !avail { t.Fatalf("read: avail=%v err=%v", avail, err) } want := map[string]int64{idA: 3, idB: 0, idOther[:8] + strings.Repeat("d", 56): 2} if len(got) != len(want) { t.Fatalf("got %v, want %v", got, want) } for k, v := range want { if got[k] != v { t.Fatalf("got %v, want %v", got, want) } } if _, leaked := got[idOther]; leaked { t.Fatal("another guest's container leaked into this guest's answer") } // A guest with no cgroup folder (a VM, or not running): not available, no error. if _, avail, err := readGuestOOMKills(root, 300); avail || err != nil { t.Fatalf("a guest without a cgroup folder: avail=%v err=%v", avail, err) } } // Through the route, with the guest's own token. func TestR528_TheRouteAnswersTheTokensGuest(t *testing.T) { root := t.TempDir() fakeCgroup(t, root, "8200", idA, "oom_kill 5\n") fakeCgroup(t, root, "9300", idB, "oom_kill 1\n") // token B's guest: never in A's answer srv := newTestServerS(t, &fakeGuests{}, &fakeBackups{}, &fakeStore{}, nil) srv.cgroupRoot = root w := do(t, srv.Handler(), "GET", "/guest/oom-kills", "A", "") if w.Code != http.StatusOK { t.Fatalf("route: %d %s", w.Code, w.Body.String()) } var env struct { Data struct { VMID int `json:"vmid"` Available bool `json:"available"` Containers map[string]int64 `json:"containers"` } `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &env); err != nil { t.Fatal(err) } if env.Data.VMID != 8200 || !env.Data.Available || len(env.Data.Containers) != 1 || env.Data.Containers[idA] != 5 { t.Fatalf("answer %s — want guest 8200's one container with 5", w.Body.String()) } if w := do(t, srv.Handler(), "GET", "/guest/oom-kills?vmid=9300", "A", ""); w.Code != http.StatusForbidden { t.Fatalf("a cross-guest ask: %d, want 403", w.Code) } if w := do(t, srv.Handler(), "GET", "/guest/oom-kills", "", ""); w.Code != http.StatusUnauthorized { t.Fatalf("no token: %d", w.Code) } }