fix(guest): start containerd with umask 022 so docker exec steps get it
dockerd sets its umask to 0 and the containerd it spawns inherits it, so every docker exec (the steps of a job container) ran with umask 0. Init now starts containerd itself, with its own 022, and points dockerd at it, as systemd does on GitHub's runners. Falls back to dockerd's own containerd.
- commit
- 2cd66dca21ec5232bc519bdb48087920fd7e3544
- author
- brice · · 2026-10-17 19:08 UTC
- parent
- 122e784
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email - refs
- fix/exec-umask
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. | ||