Skip to content

Commit cbd725e

Browse files
authored
Merge pull request #3223 from kolyshkin/refuse-bad-cgroup
run create/run/exec: refuse bad cgroup
2 parents 5948764 + 68c2b6a commit cbd725e

File tree

5 files changed

+155
-5
lines changed

5 files changed

+155
-5
lines changed

contrib/completions/bash/runc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ _runc_exec() {
173173
--apparmor
174174
--cap, -c
175175
--preserve-fds
176+
--ignore-paused
176177
"
177178

178179
local all_options="$options_with_args $boolean_options"

exec.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ following will output a list of processes running in the container:
9191
Name: "cgroup",
9292
Usage: "run the process in an (existing) sub-cgroup(s). Format is [<controller>:]<cgroup>.",
9393
},
94+
cli.BoolFlag{
95+
Name: "ignore-paused",
96+
Usage: "allow exec in a paused container",
97+
},
9498
},
9599
Action: func(context *cli.Context) error {
96100
if err := checkArgs(context, 1, minArgs); err != nil {
@@ -145,7 +149,10 @@ func execProcess(context *cli.Context) (int, error) {
145149
return -1, err
146150
}
147151
if status == libcontainer.Stopped {
148-
return -1, errors.New("cannot exec a container that has stopped")
152+
return -1, errors.New("cannot exec in a stopped container")
153+
}
154+
if status == libcontainer.Paused && !context.Bool("ignore-paused") {
155+
return -1, errors.New("cannot exec in a paused container (use --ignore-paused to override)")
149156
}
150157
path := context.String("process")
151158
if path == "" && len(context.Args()) == 1 {

libcontainer/factory_linux.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,45 @@ func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, err
160160
} else if !os.IsNotExist(err) {
161161
return nil, err
162162
}
163-
if err := os.MkdirAll(containerRoot, 0o711); err != nil {
163+
164+
cm, err := manager.New(config.Cgroups)
165+
if err != nil {
164166
return nil, err
165167
}
166-
if err := os.Chown(containerRoot, unix.Geteuid(), unix.Getegid()); err != nil {
167-
return nil, err
168+
169+
// Check that cgroup does not exist or empty (no processes).
170+
// Note for cgroup v1 this check is not thorough, as there are multiple
171+
// separate hierarchies, while both Exists() and GetAllPids() only use
172+
// one for "devices" controller (assuming others are the same, which is
173+
// probably true in almost all scenarios). Checking all the hierarchies
174+
// would be too expensive.
175+
if cm.Exists() {
176+
pids, err := cm.GetAllPids()
177+
// Reading PIDs can race with cgroups removal, so ignore ENOENT and ENODEV.
178+
if err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, unix.ENODEV) {
179+
return nil, fmt.Errorf("unable to get cgroup PIDs: %w", err)
180+
}
181+
if len(pids) != 0 {
182+
// TODO: return an error.
183+
logrus.Warnf("container's cgroup is not empty: %d process(es) found", len(pids))
184+
logrus.Warn("DEPRECATED: running container in a non-empty cgroup won't be supported in runc 1.2; https://github.com/opencontainers/runc/issues/3132")
185+
}
168186
}
169-
cm, err := manager.New(config.Cgroups)
187+
188+
// Check that cgroup is not frozen. Do not use Exists() here
189+
// since in cgroup v1 it only checks "devices" controller.
190+
st, err := cm.GetFreezerState()
170191
if err != nil {
192+
return nil, fmt.Errorf("unable to get cgroup freezer state: %w", err)
193+
}
194+
if st == configs.Frozen {
195+
return nil, errors.New("container's cgroup unexpectedly frozen")
196+
}
197+
198+
if err := os.MkdirAll(containerRoot, 0o711); err != nil {
199+
return nil, err
200+
}
201+
if err := os.Chown(containerRoot, unix.Geteuid(), unix.Getegid()); err != nil {
171202
return nil, err
172203
}
173204
c := &linuxContainer{

man/runc-exec.8.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ multiple times.
5959
: Pass _N_ additional file descriptors to the container (**stdio** +
6060
**$LISTEN_FDS** + _N_ in total). Default is **0**.
6161

62+
**--ignore-paused**
63+
: Allow exec in a paused container. By default, if a container is paused,
64+
**runc exec** errors out; this option can be used to override it.
65+
A paused container needs to be resumed for the exec to complete.
66+
6267
**--cgroup** _path_ | _controller_[,_controller_...]:_path_
6368
: Execute a process in a sub-cgroup. If the specified cgroup does not exist, an
6469
error is returned. Default is empty path, which means to use container's top

tests/integration/cgroups.bats

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,109 @@ function setup() {
303303
# check that the cgroups v2 path is the same for both processes
304304
[[ "$run_cgroup" == "$exec_cgroup" ]]
305305
}
306+
307+
@test "runc exec should refuse a paused container" {
308+
if [[ "$ROOTLESS" -ne 0 ]]; then
309+
requires rootless_cgroup
310+
fi
311+
requires cgroups_freezer
312+
313+
set_cgroups_path
314+
315+
runc run -d --console-socket "$CONSOLE_SOCKET" ct1
316+
[ "$status" -eq 0 ]
317+
runc pause ct1
318+
[ "$status" -eq 0 ]
319+
320+
# Exec should not timeout or succeed.
321+
runc exec ct1 echo ok
322+
[ "$status" -eq 255 ]
323+
[[ "$output" == *"cannot exec in a paused container"* ]]
324+
}
325+
326+
@test "runc exec --ignore-paused" {
327+
if [[ "$ROOTLESS" -ne 0 ]]; then
328+
requires rootless_cgroup
329+
fi
330+
requires cgroups_freezer
331+
332+
set_cgroups_path
333+
334+
runc run -d --console-socket "$CONSOLE_SOCKET" ct1
335+
[ "$status" -eq 0 ]
336+
runc pause ct1
337+
[ "$status" -eq 0 ]
338+
339+
# Resume the container a bit later.
340+
(
341+
sleep 2
342+
runc resume ct1
343+
) &
344+
345+
# Exec should not timeout or succeed.
346+
runc exec --ignore-paused ct1 echo ok
347+
[ "$status" -eq 0 ]
348+
[ "$output" = "ok" ]
349+
}
350+
351+
@test "runc run/create should warn about a non-empty cgroup" {
352+
if [[ "$ROOTLESS" -ne 0 ]]; then
353+
requires rootless_cgroup
354+
fi
355+
356+
set_cgroups_path
357+
358+
runc run -d --console-socket "$CONSOLE_SOCKET" ct1
359+
[ "$status" -eq 0 ]
360+
361+
# Run a second container sharing the cgroup with the first one.
362+
runc --debug run -d --console-socket "$CONSOLE_SOCKET" ct2
363+
[ "$status" -eq 0 ]
364+
[[ "$output" == *"container's cgroup is not empty"* ]]
365+
366+
# Same but using runc create.
367+
runc create --console-socket "$CONSOLE_SOCKET" ct3
368+
[ "$status" -eq 0 ]
369+
[[ "$output" == *"container's cgroup is not empty"* ]]
370+
}
371+
372+
@test "runc run/create should refuse pre-existing frozen cgroup" {
373+
requires cgroups_freezer
374+
if [[ "$ROOTLESS" -ne 0 ]]; then
375+
requires rootless_cgroup
376+
fi
377+
378+
set_cgroups_path
379+
380+
case $CGROUP_UNIFIED in
381+
no)
382+
FREEZER_DIR="${CGROUP_FREEZER_BASE_PATH}/${REL_CGROUPS_PATH}"
383+
FREEZER="${FREEZER_DIR}/freezer.state"
384+
STATE="FROZEN"
385+
;;
386+
yes)
387+
FREEZER_DIR="${CGROUP_PATH}"
388+
FREEZER="${FREEZER_DIR}/cgroup.freeze"
389+
STATE="1"
390+
;;
391+
esac
392+
393+
# Create and freeze the cgroup.
394+
mkdir -p "$FREEZER_DIR"
395+
echo "$STATE" >"$FREEZER"
396+
397+
# Start a container.
398+
runc run -d --console-socket "$CONSOLE_SOCKET" ct1
399+
[ "$status" -eq 1 ]
400+
# A warning should be printed.
401+
[[ "$output" == *"container's cgroup unexpectedly frozen"* ]]
402+
403+
# Same check for runc create.
404+
runc create --console-socket "$CONSOLE_SOCKET" ct2
405+
[ "$status" -eq 1 ]
406+
# A warning should be printed.
407+
[[ "$output" == *"container's cgroup unexpectedly frozen"* ]]
408+
409+
# Cleanup.
410+
rmdir "$FREEZER_DIR"
411+
}

0 commit comments

Comments
 (0)