Skip to content

Commit c3c111d

Browse files
authored
Merge pull request #4585 from kolyshkin/per-process-properties
Fix process/config properties merging
2 parents 20727c6 + 99f9ed9 commit c3c111d

File tree

10 files changed

+180
-64
lines changed

10 files changed

+180
-64
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
IDs before calling libcontainer; it is recommended to use Go package
1818
github.com/moby/sys/user for that. (#3999)
1919

20+
### Fixed
21+
* `runc exec -p` no longer ignores specified `ioPriority` and `scheduler`
22+
settings. Similarly, libcontainer's `Container.Start` and `Container.Run`
23+
methods no longer ignore `Process.IOPriority` and `Process.Scheduler`
24+
settings. (#4585)
25+
2026
## [1.2.0] - 2024-10-22
2127

2228
> できるときにできることをやるんだ。それが今だ。

libcontainer/capabilities/capabilities.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ func KnownCapabilities() []string {
4747
// printing a warning instead.
4848
func New(capConfig *configs.Capabilities) (*Caps, error) {
4949
var c Caps
50+
if capConfig == nil {
51+
return &c, nil
52+
}
5053

5154
_, err := capMap()
5255
if err != nil {
@@ -103,13 +106,19 @@ type Caps struct {
103106

104107
// ApplyBoundingSet sets the capability bounding set to those specified in the whitelist.
105108
func (c *Caps) ApplyBoundingSet() error {
109+
if c.pid == nil {
110+
return nil
111+
}
106112
c.pid.Clear(capability.BOUNDING)
107113
c.pid.Set(capability.BOUNDING, c.caps[capability.BOUNDING]...)
108114
return c.pid.Apply(capability.BOUNDING)
109115
}
110116

111117
// Apply sets all the capabilities for the current process in the config.
112118
func (c *Caps) ApplyCaps() error {
119+
if c.pid == nil {
120+
return nil
121+
}
113122
c.pid.Clear(capability.CAPS | capability.BOUNDS)
114123
for _, g := range []capability.CapType{
115124
capability.EFFECTIVE,

libcontainer/container_linux.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,9 @@ func (c *Container) newSetnsProcess(p *Process, cmd *exec.Cmd, comm *processComm
689689
}
690690

691691
func (c *Container) newInitConfig(process *Process) *initConfig {
692+
// Set initial properties. For those properties that exist
693+
// both in the container config and the process, use the ones
694+
// from the container config first, and override them later.
692695
cfg := &initConfig{
693696
Config: c.config,
694697
Args: process.Args,
@@ -697,19 +700,25 @@ func (c *Container) newInitConfig(process *Process) *initConfig {
697700
GID: process.GID,
698701
AdditionalGroups: process.AdditionalGroups,
699702
Cwd: process.Cwd,
700-
Capabilities: process.Capabilities,
703+
Capabilities: c.config.Capabilities,
701704
PassedFilesCount: len(process.ExtraFiles),
702705
ContainerID: c.ID(),
703706
NoNewPrivileges: c.config.NoNewPrivileges,
704-
RootlessEUID: c.config.RootlessEUID,
705-
RootlessCgroups: c.config.RootlessCgroups,
706707
AppArmorProfile: c.config.AppArmorProfile,
707708
ProcessLabel: c.config.ProcessLabel,
708709
Rlimits: c.config.Rlimits,
710+
IOPriority: c.config.IOPriority,
711+
Scheduler: c.config.Scheduler,
709712
CreateConsole: process.ConsoleSocket != nil,
710713
ConsoleWidth: process.ConsoleWidth,
711714
ConsoleHeight: process.ConsoleHeight,
712715
}
716+
717+
// Overwrite config properties with ones from process.
718+
719+
if process.Capabilities != nil {
720+
cfg.Capabilities = process.Capabilities
721+
}
713722
if process.NoNewPrivileges != nil {
714723
cfg.NoNewPrivileges = *process.NoNewPrivileges
715724
}
@@ -722,6 +731,15 @@ func (c *Container) newInitConfig(process *Process) *initConfig {
722731
if len(process.Rlimits) > 0 {
723732
cfg.Rlimits = process.Rlimits
724733
}
734+
if process.IOPriority != nil {
735+
cfg.IOPriority = process.IOPriority
736+
}
737+
if process.Scheduler != nil {
738+
cfg.Scheduler = process.Scheduler
739+
}
740+
741+
// Set misc properties.
742+
725743
if cgroups.IsCgroup2UnifiedMode() {
726744
cfg.Cgroup2Path = c.cgroupManager.Path("")
727745
}

libcontainer/init_linux.go

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -47,30 +47,54 @@ type network struct {
4747
TempVethPeerName string `json:"temp_veth_peer_name"`
4848
}
4949

50-
// initConfig is used for transferring parameters from Exec() to Init()
50+
// initConfig is used for transferring parameters from Exec() to Init().
51+
// It contains:
52+
// - original container config;
53+
// - some [Process] properties;
54+
// - set of properties merged from the container config ([configs.Config])
55+
// and the process ([Process]);
56+
// - some properties that come from the container.
57+
//
58+
// When adding new fields, please make sure they go into the relevant section.
5159
type initConfig struct {
52-
Args []string `json:"args"`
53-
Env []string `json:"env"`
54-
Cwd string `json:"cwd"`
55-
Capabilities *configs.Capabilities `json:"capabilities"`
56-
ProcessLabel string `json:"process_label"`
57-
AppArmorProfile string `json:"apparmor_profile"`
58-
NoNewPrivileges bool `json:"no_new_privileges"`
59-
UID int `json:"uid"`
60-
GID int `json:"gid"`
61-
AdditionalGroups []int `json:"additional_groups"`
62-
Config *configs.Config `json:"config"`
63-
Networks []*network `json:"network"`
64-
PassedFilesCount int `json:"passed_files_count"`
65-
ContainerID string `json:"containerid"`
66-
Rlimits []configs.Rlimit `json:"rlimits"`
67-
CreateConsole bool `json:"create_console"`
68-
ConsoleWidth uint16 `json:"console_width"`
69-
ConsoleHeight uint16 `json:"console_height"`
70-
RootlessEUID bool `json:"rootless_euid,omitempty"`
71-
RootlessCgroups bool `json:"rootless_cgroups,omitempty"`
72-
SpecState *specs.State `json:"spec_state,omitempty"`
73-
Cgroup2Path string `json:"cgroup2_path,omitempty"`
60+
// Config is the original container config.
61+
Config *configs.Config `json:"config"`
62+
63+
// Properties that are unique to and come from [Process].
64+
65+
Args []string `json:"args"`
66+
Env []string `json:"env"`
67+
UID int `json:"uid"`
68+
GID int `json:"gid"`
69+
AdditionalGroups []int `json:"additional_groups"`
70+
Cwd string `json:"cwd"`
71+
CreateConsole bool `json:"create_console"`
72+
ConsoleWidth uint16 `json:"console_width"`
73+
ConsoleHeight uint16 `json:"console_height"`
74+
PassedFilesCount int `json:"passed_files_count"`
75+
76+
// Properties that exists both in the container config and the process,
77+
// as merged by [Container.newInitConfig] (process properties has preference).
78+
79+
AppArmorProfile string `json:"apparmor_profile"`
80+
Capabilities *configs.Capabilities `json:"capabilities"`
81+
NoNewPrivileges bool `json:"no_new_privileges"`
82+
ProcessLabel string `json:"process_label"`
83+
Rlimits []configs.Rlimit `json:"rlimits"`
84+
IOPriority *configs.IOPriority `json:"io_priority,omitempty"`
85+
Scheduler *configs.Scheduler `json:"scheduler,omitempty"`
86+
87+
// Miscellaneous properties, filled in by [Container.newInitConfig]
88+
// unless documented otherwise.
89+
90+
ContainerID string `json:"containerid"`
91+
Cgroup2Path string `json:"cgroup2_path,omitempty"`
92+
93+
// Networks is filled in from container config by [initProcess.createNetworkInterfaces].
94+
Networks []*network `json:"network"`
95+
96+
// SpecState is filled in by [initProcess.Start].
97+
SpecState *specs.State `json:"spec_state,omitempty"`
7498
}
7599

76100
// Init is part of "runc init" implementation.
@@ -300,13 +324,7 @@ func finalizeNamespace(config *initConfig) error {
300324
}
301325
}
302326

303-
caps := &configs.Capabilities{}
304-
if config.Capabilities != nil {
305-
caps = config.Capabilities
306-
} else if config.Config.Capabilities != nil {
307-
caps = config.Config.Capabilities
308-
}
309-
w, err := capabilities.New(caps)
327+
w, err := capabilities.New(config.Capabilities)
310328
if err != nil {
311329
return err
312330
}
@@ -456,7 +474,7 @@ func setupUser(config *initConfig) error {
456474
// There's nothing we can do about /etc/group entries, so we silently
457475
// ignore setting groups here (since the user didn't explicitly ask us to
458476
// set the group).
459-
allowSupGroups := !config.RootlessEUID && string(bytes.TrimSpace(setgroups)) != "deny"
477+
allowSupGroups := !config.Config.RootlessEUID && string(bytes.TrimSpace(setgroups)) != "deny"
460478

461479
if allowSupGroups {
462480
if err := unix.Setgroups(config.AdditionalGroups); err != nil {
@@ -590,7 +608,7 @@ func setupRlimits(limits []configs.Rlimit, pid int) error {
590608
return nil
591609
}
592610

593-
func setupScheduler(config *configs.Config) error {
611+
func setupScheduler(config *initConfig) error {
594612
if config.Scheduler == nil {
595613
return nil
596614
}
@@ -599,15 +617,15 @@ func setupScheduler(config *configs.Config) error {
599617
return err
600618
}
601619
if err := unix.SchedSetAttr(0, attr, 0); err != nil {
602-
if errors.Is(err, unix.EPERM) && config.Cgroups.CpusetCpus != "" {
620+
if errors.Is(err, unix.EPERM) && config.Config.Cgroups.CpusetCpus != "" {
603621
return errors.New("process scheduler can't be used together with AllowedCPUs")
604622
}
605623
return fmt.Errorf("error setting scheduler: %w", err)
606624
}
607625
return nil
608626
}
609627

610-
func setupIOPriority(config *configs.Config) error {
628+
func setupIOPriority(config *initConfig) error {
611629
const ioprioWhoPgrp = 1
612630

613631
ioprio := config.IOPriority

libcontainer/process.go

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ type processOperations interface {
1717
pid() int
1818
}
1919

20-
// Process specifies the configuration and IO for a process inside
21-
// a container.
20+
// Process defines the configuration and IO for a process inside a container.
21+
//
22+
// Note that some Process properties are also present in container configuration
23+
// ([configs.Config]). In all such cases, Process properties take precedence
24+
// over container configuration ones.
2225
type Process struct {
2326
// The command to be run followed by any arguments.
2427
Args []string
@@ -34,44 +37,54 @@ type Process struct {
3437
// in addition to those that the user belongs to.
3538
AdditionalGroups []int
3639

37-
// Cwd will change the processes current working directory inside the container's rootfs.
40+
// Cwd will change the process's current working directory inside the container's rootfs.
3841
Cwd string
3942

40-
// Stdin is a pointer to a reader which provides the standard input stream.
43+
// Stdin is a reader which provides the standard input stream.
4144
Stdin io.Reader
4245

43-
// Stdout is a pointer to a writer which receives the standard output stream.
46+
// Stdout is a writer which receives the standard output stream.
4447
Stdout io.Writer
4548

46-
// Stderr is a pointer to a writer which receives the standard error stream.
49+
// Stderr is a writer which receives the standard error stream.
4750
Stderr io.Writer
4851

49-
// ExtraFiles specifies additional open files to be inherited by the container
52+
// ExtraFiles specifies additional open files to be inherited by the process.
5053
ExtraFiles []*os.File
5154

52-
// open handles to cloned binaries -- see dmz.CloneSelfExe for more details
55+
// Open handles to cloned binaries -- see dmz.CloneSelfExe for more details.
5356
clonedExes []*os.File
5457

55-
// Initial sizings for the console
58+
// Initial size for the console.
5659
ConsoleWidth uint16
5760
ConsoleHeight uint16
5861

59-
// Capabilities specify the capabilities to keep when executing the process inside the container
60-
// All capabilities not specified will be dropped from the processes capability mask
62+
// Capabilities specify the capabilities to keep when executing the process.
63+
// All capabilities not specified will be dropped from the processes capability mask.
64+
//
65+
// If not nil, takes precedence over container's [configs.Config.Capabilities].
6166
Capabilities *configs.Capabilities
6267

6368
// AppArmorProfile specifies the profile to apply to the process and is
64-
// changed at the time the process is execed
69+
// changed at the time the process is executed.
70+
//
71+
// If not empty, takes precedence over container's [configs.Config.AppArmorProfile].
6572
AppArmorProfile string
6673

67-
// Label specifies the label to apply to the process. It is commonly used by selinux
74+
// Label specifies the label to apply to the process. It is commonly used by selinux.
75+
//
76+
// If not empty, takes precedence over container's [configs.Config.ProcessLabel].
6877
Label string
6978

7079
// NoNewPrivileges controls whether processes can gain additional privileges.
80+
//
81+
// If not nil, takes precedence over container's [configs.Config.NoNewPrivileges].
7182
NoNewPrivileges *bool
7283

73-
// Rlimits specifies the resource limits, such as max open files, to set in the container
74-
// If Rlimits are not set, the container will inherit rlimits from the parent process
84+
// Rlimits specifies the resource limits, such as max open files, to set for the process.
85+
// If unset, the process will inherit rlimits from the parent process.
86+
//
87+
// If not empty, takes precedence over container's [configs.Config.Rlimit].
7588
Rlimits []configs.Rlimit
7689

7790
// ConsoleSocket provides the masterfd console.
@@ -99,8 +112,14 @@ type Process struct {
99112
// For cgroup v2, the only key allowed is "".
100113
SubCgroupPaths map[string]string
101114

115+
// Scheduler represents the scheduling attributes for a process.
116+
//
117+
// If not empty, takes precedence over container's [configs.Config.Scheduler].
102118
Scheduler *configs.Scheduler
103119

120+
// IOPriority is a process I/O priority.
121+
//
122+
// If not empty, takes precedence over container's [configs.Config.IOPriority].
104123
IOPriority *configs.IOPriority
105124
}
106125

libcontainer/rootfs_linux.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func prepareRootfs(pipe *syncSocket, iConfig *initConfig) (err error) {
106106
root: config.Rootfs,
107107
label: config.MountLabel,
108108
cgroup2Path: iConfig.Cgroup2Path,
109-
rootlessCgroups: iConfig.RootlessCgroups,
109+
rootlessCgroups: config.RootlessCgroups,
110110
cgroupns: config.Namespaces.Contains(configs.NEWCGROUP),
111111
}
112112
for _, m := range config.Mounts {

libcontainer/setns_init_linux.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ func (l *linuxSetnsInit) Init() error {
7171
unix.Umask(int(*l.config.Config.Umask))
7272
}
7373

74-
if err := setupScheduler(l.config.Config); err != nil {
74+
if err := setupScheduler(l.config); err != nil {
7575
return err
7676
}
7777

78-
if err := setupIOPriority(l.config.Config); err != nil {
78+
if err := setupIOPriority(l.config); err != nil {
7979
return err
8080
}
8181
// Tell our parent that we're ready to exec. This must be done before the

libcontainer/standard_init_linux.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,11 @@ func (l *linuxStandardInit) Init() error {
155155
}
156156
}
157157

158-
if err := setupScheduler(l.config.Config); err != nil {
158+
if err := setupScheduler(l.config); err != nil {
159159
return err
160160
}
161161

162-
if err := setupIOPriority(l.config.Config); err != nil {
162+
if err := setupIOPriority(l.config); err != nil {
163163
return err
164164
}
165165

tests/integration/ioprio.bats

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,25 @@ function teardown() {
2020
# Check the init process.
2121
runc exec test_ioprio ionice -p 1
2222
[ "$status" -eq 0 ]
23-
[[ "$output" = *'best-effort: prio 4'* ]]
23+
[ "${lines[0]}" = 'best-effort: prio 4' ]
2424

25-
# Check the process made from the exec command.
25+
# Check an exec process, which should derive ioprio from config.json.
2626
runc exec test_ioprio ionice
2727
[ "$status" -eq 0 ]
28-
29-
[[ "$output" = *'best-effort: prio 4'* ]]
28+
[ "${lines[0]}" = 'best-effort: prio 4' ]
29+
30+
# Check an exec with a priority taken from process.json,
31+
# which should override the ioprio in config.json.
32+
proc='
33+
{
34+
"terminal": false,
35+
"ioPriority": {
36+
"class": "IOPRIO_CLASS_IDLE"
37+
},
38+
"args": [ "/usr/bin/ionice" ],
39+
"cwd": "/"
40+
}'
41+
runc exec --process <(echo "$proc") test_ioprio
42+
[ "$status" -eq 0 ]
43+
[ "${lines[0]}" = 'idle' ]
3044
}

0 commit comments

Comments
 (0)