Commit 7acfcebdf5
7acfcebdf5445e4c73e674ee6be54f499f243c98
parent: 2258972d20
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-29 03:45 UTC
runner: refuse to start without build cgroups when taking untrusted builds or polling over loopback
Ref #260
Layout: unified · split
cmd/gitbay-runner/cgroup.go
+19
| @@ -31,6 +31,25 @@ func buildCgroupDir(builds string, id int64, trusted bool) string { |
| 31 | 31 | return filepath.Join(builds, class, fmt.Sprintf("build-%d", id)) |
| 32 | 32 | } |
| 33 | 33 | |
| 34 | // buildCgroupsRequired names what makes build cgroups mandatory, or "" |
| 35 | // when a podman runner may run its builds in its own service cgroup. |
| 36 | // Limits that cannot be applied are refused, not dropped: a runner that |
| 37 | // accepted -memory and ran uncapped is what #188 was. A runner taking |
| 38 | // untrusted builds, or polling over loopback on the daemon's host, is |
| 39 | // the shape the builds nftables table guards, and that table matches |
| 40 | // builds by these cgroups; without them it matches nothing (#260). |
| 41 | func buildCgroupsRequired(memory, cpus string, untrusted, loopback bool) string { |
| 42 | switch { |
| 43 | case memory != "" || cpus != "": |
| 44 | return "-memory/-cpus" |
| 45 | case untrusted: |
| 46 | return "-untrusted" |
| 47 | case loopback: |
| 48 | return "a loopback -remote" |
| 49 | } |
| 50 | return "" |
| 51 | } |
| 52 | |
| 34 | 53 | // memoryBytes parses podman's memory units — a whole number with an |
| 35 | 54 | // optional b, k, m or g suffix — into bytes. |
| 36 | 55 | func memoryBytes(s string) (int64, error) { |
cmd/gitbay-runner/cgroup_test.go
+23
| @@ -134,3 +134,26 @@ func TestBuildsTableNamesTheClassCgroups(t *testing.T) { |
| 134 | 134 | t.Error("the drop-in does not load the builds table") |
| 135 | 135 | } |
| 136 | 136 | } |
| 137 | |
| 138 | // Without build cgroups a podman runner may carry on only where nothing |
| 139 | // depends on them: no limits, no untrusted builds, and not on the |
| 140 | // daemon's host, where the builds table matches by cgroup (#260). |
| 141 | func TestBuildCgroupsRequired(t *testing.T) { |
| 142 | cases := []struct { |
| 143 | memory, cpus string |
| 144 | untrusted, loopback bool |
| 145 | want string |
| 146 | }{ |
| 147 | {"", "", false, false, ""}, |
| 148 | {"6g", "", false, false, "-memory/-cpus"}, |
| 149 | {"", "3", false, false, "-memory/-cpus"}, |
| 150 | {"", "", true, false, "-untrusted"}, |
| 151 | {"", "", false, true, "a loopback -remote"}, |
| 152 | {"", "", true, true, "-untrusted"}, |
| 153 | } |
| 154 | for _, c := range cases { |
| 155 | if got := buildCgroupsRequired(c.memory, c.cpus, c.untrusted, c.loopback); got != c.want { |
| 156 | t.Errorf("buildCgroupsRequired(%q, %q, %v, %v) = %q, want %q", c.memory, c.cpus, c.untrusted, c.loopback, got, c.want) |
| 157 | } |
| 158 | } |
| 159 | } |
cmd/gitbay-runner/main.go
+3 −6
| @@ -132,14 +132,11 @@ func main() { |
| 132 | 132 | // cgroup of the first invocation, and that must be the runner's |
| 133 | 133 | // leaf, not a build's. |
| 134 | 134 | cg, err := prepareBuildCgroups() |
| 135 | | switch { |
| 135 | switch why := buildCgroupsRequired(r.memory, r.cpus, *untrusted, r.loopbackRemote()); { |
| 136 | 136 | case err == nil: |
| 137 | 137 | r.cgroups = cg |
| 138 | | case r.memory != "" || r.cpus != "": |
| 139 | | // Limits that cannot be applied are refused, not dropped: |
| 140 | | // a runner that accepted -memory and ran uncapped is what |
| 141 | | // #188 was. |
| 142 | | log.Fatalf("-memory/-cpus: build cgroups unavailable: %v", err) |
| 138 | case why != "": |
| 139 | log.Fatalf("%s: build cgroups unavailable: %v", why, err) |
| 143 | 140 | default: |
| 144 | 141 | log.Printf("build cgroups unavailable (%v); builds run unconfined in the service cgroup", err) |
| 145 | 142 | } |