@@ -194,6 +194,20 @@ def move_pid(self, cgname, pid):
194194 cg_pids = self .root .joinpath (f"{ cgname } /cgroup.procs" )
195195 cg_pids .write_text (f"{ pid } \n " , encoding = "ascii" )
196196
197+ def enable_controller_in_subtree (self , cgname , controller ):
198+ """Enable a controller in subtree_control of a cgroup and its ancestors"""
199+ # Enable the controller in all ancestors if not already enabled.
200+ parent_cg = self .root .joinpath (cgname ).parent
201+ parent_subtree_control = parent_cg .joinpath ("cgroup.subtree_control" )
202+ if controller not in parent_subtree_control .read_text (encoding = "ascii" ):
203+ self .enable_controller_in_subtree (
204+ parent_cg .relative_to (self .root ), controller
205+ )
206+
207+ subtree_control = self .root .joinpath (f"{ cgname } /cgroup.subtree_control" )
208+ subtree_control .write_text (f"+{ controller } " , encoding = "ascii" )
209+ assert controller in subtree_control .read_text (encoding = "ascii" )
210+
197211
198212@pytest .fixture (scope = "session" , autouse = True )
199213def cgroups_info ():
@@ -230,11 +244,7 @@ def check_cgroups_v2(vm):
230244 cg_parent = cg .root / parent_cgroup
231245 cg_jail = cg_parent / vm .jailer .jailer_id
232246
233- # if no cgroups were specified, then the jailer should move the FC process
234- # to the parent group
235- if len (vm .jailer .cgroups ) == 0 :
236- procs = cg_parent .joinpath ("cgroup.procs" ).read_text ().splitlines ()
237- assert str (vm .firecracker_pid ) in procs
247+ assert len (vm .jailer .cgroups ) > 0
238248
239249 for cgroup in vm .jailer .cgroups :
240250 controller = cgroup .split ("." )[0 ]
@@ -393,11 +403,23 @@ def test_v1_default_cgroups(uvm_plain, cgroups_info):
393403 check_cgroups_v1 (test_microvm .jailer .cgroups , test_microvm .jailer .jailer_id )
394404
395405
396- def test_cgroups_custom_parent_move (uvm_plain , cgroups_info ):
406+ @pytest .mark .parametrize (
407+ "parent_exists,domain_controller_in_subtree" ,
408+ [(True , False ), (True , True ), (False , None )],
409+ )
410+ def test_cgroups_parent_cgroup_but_no_cgroup (
411+ uvm_plain , cgroups_info , parent_exists , domain_controller_in_subtree
412+ ):
397413 """
398- Test cgroups when a custom parent cgroup is used and no cgroups are specified
414+ Test cgroups when `-- parent- cgroup` is used but no `--cgroup` are specified.
399415
400- In this case we just want to move under the parent cgroup
416+ If the cgroup specified with `--parent-cgroup` exists, the jailer should
417+ move to the specified cgroup instead of creating a new cgroup under it.
418+ However, if the specified cgroup has domain controllers (e.g. `memory`)
419+ enabled in `cgroup.subtree_control`, the move should fail.
420+
421+ If the specified cgroup does not exist, the jailer does not move the process
422+ to any cgroup and proceeds without error.
401423 """
402424 if cgroups_info .version != 2 :
403425 pytest .skip ("cgroupsv2 only" )
@@ -407,9 +429,44 @@ def test_cgroups_custom_parent_move(uvm_plain, cgroups_info):
407429 parent_cgroup = f"custom_cgroup/{ test_microvm .id [:8 ]} "
408430 test_microvm .jailer .parent_cgroup = parent_cgroup
409431
410- cgroups_info .new_cgroup (parent_cgroup )
411- test_microvm .spawn ()
412- check_cgroups_v2 (test_microvm )
432+ if parent_exists :
433+ # Create the parent cgroup.
434+ cgroups_info .new_cgroup (parent_cgroup )
435+ if domain_controller_in_subtree :
436+ # Enable "memory" controller in cgroup.subtree_control of the parent.
437+ cgroups_info .enable_controller_in_subtree (parent_cgroup , "memory" )
438+
439+ # Check no --cgroups are specified just in case.
440+ assert len (test_microvm .jailer .cgroups ) == 0
441+
442+ cg_parent = cgroups_info .root / parent_cgroup
443+
444+ if parent_exists :
445+ if domain_controller_in_subtree :
446+ # The jailer should have failed to move to the `parent_cgroup`
447+ # since it has domain controllers enabled in
448+ # `cgroup.subtree_control` due to the no internal process
449+ # constraint.
450+ # https://docs.kernel.org/admin-guide/cgroup-v2.html#no-internal-process-constraint
451+ with pytest .raises (
452+ ChildProcessError ,
453+ match = (
454+ rf"Failed to move process to cgroup \({ cg_parent } \): "
455+ r"Resource busy \(os error 16\)"
456+ ),
457+ ):
458+ test_microvm .spawn ()
459+ else :
460+ # The jailer should have moved to the `parent_cgroup` instead of
461+ # creating a new cgroup under it and move to the new cgroup.
462+ test_microvm .spawn ()
463+ procs = cg_parent .joinpath ("cgroup.procs" ).read_text ().splitlines ()
464+ assert str (test_microvm .firecracker_pid ) in procs
465+ else :
466+ # The jailer should not have moved to any cgroup and the parent
467+ # still does not exist.
468+ test_microvm .spawn ()
469+ assert not cg_parent .exists ()
413470
414471
415472def test_args_default_resource_limits (uvm_plain ):
0 commit comments