detached 257b319

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 UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The 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
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