fix(guest): start containerd with umask 022 so docker exec steps get it (#39)
- commit
- 257b319e1940a12b87dc7b7e8310155860df3759
- author
- brice · · 2026-10-17 19:10 UTC
- committer
- bnema · · 2026-10-17 19:10 UTC
- parent
- 122e784
- signature
- SSH Unverified
SHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivAThe key is unknown - refs
- tag: v0.4.2
1 file · +50 -2
M internal/guest/init.go +50 -2
| @@ -136,7 +136,7 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin | |||
| 136 | 136 | return 1 | |
| 137 | 137 | } | |
| 138 | 138 | ||
| 139 | - | var dockerPid int | |
| 139 | + | var dockerPid, containerdPid int | |
| 140 | 140 | _, statErr := os.Stat("/usr/bin/dockerd") | |
| 141 | 141 | if !cfg.SkipDocker && (statErr == nil || cfg.DockerdArgs != nil) { | |
| 142 | 142 | _ = os.MkdirAll(sandbox.GuestScratch+"/docker", 0o711) | |
| @@ -144,6 +144,14 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin | |||
| 144 | 144 | if argv == nil { | |
| 145 | 145 | argv = []string{"/usr/bin/dockerd", "--data-root", sandbox.GuestScratch + "/docker", | |
| 146 | 146 | "--storage-driver", "overlay2", "--host", "unix:///var/run/docker.sock"} | |
| 147 | + | // dockerd sets its umask to 0, and a containerd it starts itself | |
| 148 | + | // inherits it, then every `docker exec` (the steps of a job | |
| 149 | + | // container): their files would be world-writable. Start containerd | |
| 150 | + | // here with our umask instead, as systemd does on GitHub's runners. | |
| 151 | + | if pid, ok := startContainerd(ctx, log); ok { | |
| 152 | + | containerdPid = pid | |
| 153 | + | argv = append(argv, "--containerd", containerdSocket) | |
| 154 | + | } | |
| 147 | 155 | } | |
| 148 | 156 | pid, err := spawn(argv, nil) | |
| 149 | 157 | if err != nil { | |
| @@ -183,6 +191,7 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin | |||
| 183 | 191 | switch { | |
| 184 | 192 | case errors.As(err, &abort): | |
| 185 | 193 | stopDockerd(dockerPid) | |
| 194 | + | stopDockerd(containerdPid) | |
| 186 | 195 | return abort.code | |
| 187 | 196 | case errors.As(err, &exited): | |
| 188 | 197 | dockerPid = 0 // already reaped | |
| @@ -194,6 +203,7 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin | |||
| 194 | 203 | if err != nil { | |
| 195 | 204 | log.Error().Err(err).Msg("init: start executor") | |
| 196 | 205 | stopDockerd(dockerPid) | |
| 206 | + | stopDockerd(containerdPid) | |
| 197 | 207 | return 1 | |
| 198 | 208 | } | |
| 199 | 209 | forward := func(sig os.Signal) { | |
| @@ -217,10 +227,43 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin | |||
| 217 | 227 | }() | |
| 218 | 228 | code := reapUntil(execPid) | |
| 219 | 229 | stopDockerd(dockerPid) | |
| 230 | + | stopDockerd(containerdPid) // after dockerd, which needs it to stop its containers | |
| 220 | 231 | return code | |
| 221 | 232 | } | |
| 222 | 233 | ||
| 223 | - | // stopDockerd stops dockerd (SIGTERM, then SIGKILL after 5 s) and reaps it (at | |
| 234 | + | // startContainerd starts /usr/bin/containerd (when the image has it) and waits, | |
| 235 | + | // bounded, for its socket. false: no containerd, or it did not come up; dockerd | |
| 236 | + | // then starts its own. | |
| 237 | + | func startContainerd(ctx context.Context, log zerowrap.Logger) (int, bool) { | |
| 238 | + | if _, err := os.Stat(containerdBinary); err != nil { | |
| 239 | + | return 0, false | |
| 240 | + | } | |
| 241 | + | // Its data next to dockerd's, on the scratch space: in a container the | |
| 242 | + | // root is an overlay, which the overlayfs snapshotter cannot sit on. | |
| 243 | + | root := sandbox.GuestScratch + "/containerd" | |
| 244 | + | _ = os.MkdirAll(root, 0o711) | |
| 245 | + | pid, err := spawn([]string{containerdBinary, "--address", containerdSocket, "--root", root}, nil) | |
| 246 | + | if err != nil { | |
| 247 | + | log.Warn().Err(err).Msg("init: start containerd (dockerd starts its own)") | |
| 248 | + | return 0, false | |
| 249 | + | } | |
| 250 | + | deadline := time.Now().Add(containerdReadyTimeout) | |
| 251 | + | for time.Now().Before(deadline) && ctx.Err() == nil { | |
| 252 | + | if fi, err := os.Stat(containerdSocket); err == nil && fi.Mode()&os.ModeSocket != 0 { | |
| 253 | + | return pid, true | |
| 254 | + | } | |
| 255 | + | if got, _ := wait4NoHang(pid); got == pid { | |
| 256 | + | log.Warn().Msg("init: containerd exited at start (dockerd starts its own)") | |
| 257 | + | return 0, false | |
| 258 | + | } | |
| 259 | + | time.Sleep(dockerPollInterval) | |
| 260 | + | } | |
| 261 | + | log.Warn().Msg("init: containerd socket not ready (dockerd starts its own)") | |
| 262 | + | stopDockerd(pid) | |
| 263 | + | return 0, false | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | // stopDockerd stops dockerd or containerd (SIGTERM, then SIGKILL after 5 s) and reaps it (at | |
| 224 | 267 | // most 5 s more after the SIGKILL); a zero pid is a no-op. | |
| 225 | 268 | func stopDockerd(pid int) { | |
| 226 | 269 | if pid == 0 { | |
| @@ -246,6 +289,11 @@ func stopDockerd(pid int) { | |||
| 246 | 289 | } | |
| 247 | 290 | ||
| 248 | 291 | const ( | |
| 292 | + | // containerdBinary and containerdSocket are the containerd init starts for | |
| 293 | + | // dockerd; containerdReadyTimeout bounds the wait for its socket. | |
| 294 | + | containerdBinary = "/usr/bin/containerd" | |
| 295 | + | containerdSocket = "/run/containerd/containerd.sock" | |
| 296 | + | containerdReadyTimeout = 20 * time.Second | |
| 249 | 297 | // dockerSocket is the Docker API socket of the nested dockerd. | |
| 250 | 298 | dockerSocket = "/var/run/docker.sock" | |
| 251 | 299 | // dockerReadyTimeout bounds the wait for the Docker API at boot. | |
| @@ -136,7 +136,7 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin | |||||
| 136 | return 1 | 136 | return 1 | ||
| 137 | } | 137 | } | ||
| 138 | 138 | ||||
| 139 | - | var dockerPid int | 139 | + | var dockerPid, containerdPid int |
| 140 | _, statErr := os.Stat("/usr/bin/dockerd") | 140 | _, statErr := os.Stat("/usr/bin/dockerd") | ||
| 141 | if !cfg.SkipDocker && (statErr == nil || cfg.DockerdArgs != nil) { | 141 | if !cfg.SkipDocker && (statErr == nil || cfg.DockerdArgs != nil) { | ||
| 142 | _ = os.MkdirAll(sandbox.GuestScratch+"/docker", 0o711) | 142 | _ = os.MkdirAll(sandbox.GuestScratch+"/docker", 0o711) | ||
| @@ -144,6 +144,14 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin | |||||
| 144 | if argv == nil { | 144 | if argv == nil { | ||
| 145 | argv = []string{"/usr/bin/dockerd", "--data-root", sandbox.GuestScratch + "/docker", | 145 | argv = []string{"/usr/bin/dockerd", "--data-root", sandbox.GuestScratch + "/docker", | ||
| 146 | "--storage-driver", "overlay2", "--host", "unix:///var/run/docker.sock"} | 146 | "--storage-driver", "overlay2", "--host", "unix:///var/run/docker.sock"} | ||
| 147 | + | // dockerd sets its umask to 0, and a containerd it starts itself | |||
| 148 | + | // inherits it, then every `docker exec` (the steps of a job | |||
| 149 | + | // container): their files would be world-writable. Start containerd | |||
| 150 | + | // here with our umask instead, as systemd does on GitHub's runners. | |||
| 151 | + | if pid, ok := startContainerd(ctx, log); ok { | |||
| 152 | + | containerdPid = pid | |||
| 153 | + | argv = append(argv, "--containerd", containerdSocket) | |||
| 154 | + | } | |||
| 147 | } | 155 | } | ||
| 148 | pid, err := spawn(argv, nil) | 156 | pid, err := spawn(argv, nil) | ||
| 149 | if err != nil { | 157 | if err != nil { | ||
| @@ -183,6 +191,7 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin | |||||
| 183 | switch { | 191 | switch { | ||
| 184 | case errors.As(err, &abort): | 192 | case errors.As(err, &abort): | ||
| 185 | stopDockerd(dockerPid) | 193 | stopDockerd(dockerPid) | ||
| 194 | + | stopDockerd(containerdPid) | |||
| 186 | return abort.code | 195 | return abort.code | ||
| 187 | case errors.As(err, &exited): | 196 | case errors.As(err, &exited): | ||
| 188 | dockerPid = 0 // already reaped | 197 | dockerPid = 0 // already reaped | ||
| @@ -194,6 +203,7 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin | |||||
| 194 | if err != nil { | 203 | if err != nil { | ||
| 195 | log.Error().Err(err).Msg("init: start executor") | 204 | log.Error().Err(err).Msg("init: start executor") | ||
| 196 | stopDockerd(dockerPid) | 205 | stopDockerd(dockerPid) | ||
| 206 | + | stopDockerd(containerdPid) | |||
| 197 | return 1 | 207 | return 1 | ||
| 198 | } | 208 | } | ||
| 199 | forward := func(sig os.Signal) { | 209 | forward := func(sig os.Signal) { | ||
| @@ -217,10 +227,43 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin | |||||
| 217 | }() | 227 | }() | ||
| 218 | code := reapUntil(execPid) | 228 | code := reapUntil(execPid) | ||
| 219 | stopDockerd(dockerPid) | 229 | stopDockerd(dockerPid) | ||
| 230 | + | stopDockerd(containerdPid) // after dockerd, which needs it to stop its containers | |||
| 220 | return code | 231 | return code | ||
| 221 | } | 232 | } | ||
| 222 | 233 | ||||
| 223 | - | // stopDockerd stops dockerd (SIGTERM, then SIGKILL after 5 s) and reaps it (at | 234 | + | // startContainerd starts /usr/bin/containerd (when the image has it) and waits, |
| 235 | + | // bounded, for its socket. false: no containerd, or it did not come up; dockerd | |||
| 236 | + | // then starts its own. | |||
| 237 | + | func startContainerd(ctx context.Context, log zerowrap.Logger) (int, bool) { | |||
| 238 | + | if _, err := os.Stat(containerdBinary); err != nil { | |||
| 239 | + | return 0, false | |||
| 240 | + | } | |||
| 241 | + | // Its data next to dockerd's, on the scratch space: in a container the | |||
| 242 | + | // root is an overlay, which the overlayfs snapshotter cannot sit on. | |||
| 243 | + | root := sandbox.GuestScratch + "/containerd" | |||
| 244 | + | _ = os.MkdirAll(root, 0o711) | |||
| 245 | + | pid, err := spawn([]string{containerdBinary, "--address", containerdSocket, "--root", root}, nil) | |||
| 246 | + | if err != nil { | |||
| 247 | + | log.Warn().Err(err).Msg("init: start containerd (dockerd starts its own)") | |||
| 248 | + | return 0, false | |||
| 249 | + | } | |||
| 250 | + | deadline := time.Now().Add(containerdReadyTimeout) | |||
| 251 | + | for time.Now().Before(deadline) && ctx.Err() == nil { | |||
| 252 | + | if fi, err := os.Stat(containerdSocket); err == nil && fi.Mode()&os.ModeSocket != 0 { | |||
| 253 | + | return pid, true | |||
| 254 | + | } | |||
| 255 | + | if got, _ := wait4NoHang(pid); got == pid { | |||
| 256 | + | log.Warn().Msg("init: containerd exited at start (dockerd starts its own)") | |||
| 257 | + | return 0, false | |||
| 258 | + | } | |||
| 259 | + | time.Sleep(dockerPollInterval) | |||
| 260 | + | } | |||
| 261 | + | log.Warn().Msg("init: containerd socket not ready (dockerd starts its own)") | |||
| 262 | + | stopDockerd(pid) | |||
| 263 | + | return 0, false | |||
| 264 | + | } | |||
| 265 | + | ||||
| 266 | + | // stopDockerd stops dockerd or containerd (SIGTERM, then SIGKILL after 5 s) and reaps it (at | |||
| 224 | // most 5 s more after the SIGKILL); a zero pid is a no-op. | 267 | // most 5 s more after the SIGKILL); a zero pid is a no-op. | ||
| 225 | func stopDockerd(pid int) { | 268 | func stopDockerd(pid int) { | ||
| 226 | if pid == 0 { | 269 | if pid == 0 { | ||
| @@ -246,6 +289,11 @@ func stopDockerd(pid int) { | |||||
| 246 | } | 289 | } | ||
| 247 | 290 | ||||
| 248 | const ( | 291 | const ( | ||
| 292 | + | // containerdBinary and containerdSocket are the containerd init starts for | |||
| 293 | + | // dockerd; containerdReadyTimeout bounds the wait for its socket. | |||
| 294 | + | containerdBinary = "/usr/bin/containerd" | |||
| 295 | + | containerdSocket = "/run/containerd/containerd.sock" | |||
| 296 | + | containerdReadyTimeout = 20 * time.Second | |||
| 249 | // dockerSocket is the Docker API socket of the nested dockerd. | 297 | // dockerSocket is the Docker API socket of the nested dockerd. | ||
| 250 | dockerSocket = "/var/run/docker.sock" | 298 | dockerSocket = "/var/run/docker.sock" | ||
| 251 | // dockerReadyTimeout bounds the wait for the Docker API at boot. | 299 | // dockerReadyTimeout bounds the wait for the Docker API at boot. | ||