detached 2cd66dc

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
136136 return 1
137137 }
138138
139- var dockerPid int
139+ var dockerPid, containerdPid int
140140 _, statErr := os.Stat("/usr/bin/dockerd")
141141 if !cfg.SkipDocker && (statErr == nil || cfg.DockerdArgs != nil) {
142142 _ = os.MkdirAll(sandbox.GuestScratch+"/docker", 0o711)
@@ -144,6 +144,14 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin
144144 if argv == nil {
145145 argv = []string{"/usr/bin/dockerd", "--data-root", sandbox.GuestScratch + "/docker",
146146 "--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+ }
147155 }
148156 pid, err := spawn(argv, nil)
149157 if err != nil {
@@ -183,6 +191,7 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin
183191 switch {
184192 case errors.As(err, &abort):
185193 stopDockerd(dockerPid)
194+ stopDockerd(containerdPid)
186195 return abort.code
187196 case errors.As(err, &exited):
188197 dockerPid = 0 // already reaped
@@ -194,6 +203,7 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin
194203 if err != nil {
195204 log.Error().Err(err).Msg("init: start executor")
196205 stopDockerd(dockerPid)
206+ stopDockerd(containerdPid)
197207 return 1
198208 }
199209 forward := func(sig os.Signal) {
@@ -217,10 +227,43 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin
217227 }()
218228 code := reapUntil(execPid)
219229 stopDockerd(dockerPid)
230+ stopDockerd(containerdPid) // after dockerd, which needs it to stop its containers
220231 return code
221232 }
222233
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
224267 // most 5 s more after the SIGKILL); a zero pid is a no-op.
225268 func stopDockerd(pid int) {
226269 if pid == 0 {
@@ -246,6 +289,11 @@ func stopDockerd(pid int) {
246289 }
247290
248291 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
249297 // dockerSocket is the Docker API socket of the nested dockerd.
250298 dockerSocket = "/var/run/docker.sock"
251299 // 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 1136 return 1
137 }137 }
138 138
139- var dockerPid int139+ 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.code195 return abort.code
187 case errors.As(err, &exited):196 case errors.As(err, &exited):
188 dockerPid = 0 // already reaped197 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 1207 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 code231 return code
221 }232 }
222 233
223-// stopDockerd stops dockerd (SIGTERM, then SIGKILL after 5 s) and reaps it (at234+// 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.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close