Skip to content

Commit 4120935

Browse files
authored
Merge pull request #4325 from kolyshkin/opt-env
libct: speedup process.Env handling
2 parents e5b777f + 06f1e07 commit 4120935

File tree

14 files changed

+290
-78
lines changed

14 files changed

+290
-78
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### libcontainer API
10+
* `configs.CommandHook` struct has changed, Command is now a pointer.
11+
Also, `configs.NewCommandHook` now accepts a `*Command`. (#4325)
12+
913
## [1.2.0] - 2024-10-22
1014

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

libcontainer/configs/config.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,16 @@ func (hooks Hooks) Run(name HookName, state *specs.State) error {
434434
return nil
435435
}
436436

437+
// SetDefaultEnv sets the environment for those CommandHook entries
438+
// that do not have one set.
439+
func (hooks HookList) SetDefaultEnv(env []string) {
440+
for _, h := range hooks {
441+
if ch, ok := h.(CommandHook); ok && len(ch.Env) == 0 {
442+
ch.Env = env
443+
}
444+
}
445+
}
446+
437447
type Hook interface {
438448
// Run executes the hook with the provided state.
439449
Run(*specs.State) error
@@ -463,17 +473,17 @@ type Command struct {
463473
}
464474

465475
// NewCommandHook will execute the provided command when the hook is run.
466-
func NewCommandHook(cmd Command) CommandHook {
476+
func NewCommandHook(cmd *Command) CommandHook {
467477
return CommandHook{
468478
Command: cmd,
469479
}
470480
}
471481

472482
type CommandHook struct {
473-
Command
483+
*Command
474484
}
475485

476-
func (c Command) Run(s *specs.State) error {
486+
func (c *Command) Run(s *specs.State) error {
477487
b, err := json.Marshal(s)
478488
if err != nil {
479489
return err

libcontainer/configs/config_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
func TestUnmarshalHooks(t *testing.T) {
1616
timeout := time.Second
1717

18-
hookCmd := configs.NewCommandHook(configs.Command{
18+
hookCmd := configs.NewCommandHook(&configs.Command{
1919
Path: "/var/vcap/hooks/hook",
2020
Args: []string{"--pid=123"},
2121
Env: []string{"FOO=BAR"},
@@ -52,7 +52,7 @@ func TestUnmarshalHooksWithInvalidData(t *testing.T) {
5252
func TestMarshalHooks(t *testing.T) {
5353
timeout := time.Second
5454

55-
hookCmd := configs.NewCommandHook(configs.Command{
55+
hookCmd := configs.NewCommandHook(&configs.Command{
5656
Path: "/var/vcap/hooks/hook",
5757
Args: []string{"--pid=123"},
5858
Env: []string{"FOO=BAR"},
@@ -84,7 +84,7 @@ func TestMarshalHooks(t *testing.T) {
8484
func TestMarshalUnmarshalHooks(t *testing.T) {
8585
timeout := time.Second
8686

87-
hookCmd := configs.NewCommandHook(configs.Command{
87+
hookCmd := configs.NewCommandHook(&configs.Command{
8888
Path: "/var/vcap/hooks/hook",
8989
Args: []string{"--pid=123"},
9090
Env: []string{"FOO=BAR"},
@@ -194,7 +194,7 @@ exit 0
194194
}
195195
defer os.Remove(filename)
196196

197-
cmdHook := configs.NewCommandHook(configs.Command{
197+
cmdHook := configs.NewCommandHook(&configs.Command{
198198
Path: filename,
199199
Args: []string{filename, "testarg"},
200200
Env: []string{"FOO=BAR"},
@@ -216,7 +216,7 @@ func TestCommandHookRunTimeout(t *testing.T) {
216216
}
217217
timeout := 100 * time.Millisecond
218218

219-
cmdHook := configs.NewCommandHook(configs.Command{
219+
cmdHook := configs.NewCommandHook(&configs.Command{
220220
Path: "/bin/sleep",
221221
Args: []string{"/bin/sleep", "1"},
222222
Timeout: &timeout,

libcontainer/env.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package libcontainer
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"slices"
8+
"strings"
9+
)
10+
11+
// prepareEnv processes a list of environment variables, preparing it
12+
// for direct consumption by unix.Exec. In particular, it:
13+
// - validates each variable is in the NAME=VALUE format and
14+
// contains no \0 (nil) bytes;
15+
// - removes any duplicates (keeping only the last value for each key)
16+
// - sets PATH for the current process, if found in the list.
17+
//
18+
// It returns the deduplicated environment, a flag telling whether HOME
19+
// is present in the input, and an error.
20+
func prepareEnv(env []string) ([]string, bool, error) {
21+
if env == nil {
22+
return nil, false, nil
23+
}
24+
// Deduplication code based on dedupEnv from Go 1.22 os/exec.
25+
26+
// Construct the output in reverse order, to preserve the
27+
// last occurrence of each key.
28+
out := make([]string, 0, len(env))
29+
saw := make(map[string]bool, len(env))
30+
for n := len(env); n > 0; n-- {
31+
kv := env[n-1]
32+
i := strings.IndexByte(kv, '=')
33+
if i == -1 {
34+
return nil, false, errors.New("invalid environment variable: missing '='")
35+
}
36+
if i == 0 {
37+
return nil, false, errors.New("invalid environment variable: name cannot be empty")
38+
}
39+
key := kv[:i]
40+
if saw[key] { // Duplicate.
41+
continue
42+
}
43+
saw[key] = true
44+
if strings.IndexByte(kv, 0) >= 0 {
45+
return nil, false, fmt.Errorf("invalid environment variable %q: contains nul byte (\\x00)", key)
46+
}
47+
if key == "PATH" {
48+
// Needs to be set as it is used for binary lookup.
49+
if err := os.Setenv("PATH", kv[i+1:]); err != nil {
50+
return nil, false, err
51+
}
52+
}
53+
out = append(out, kv)
54+
}
55+
// Restore the original order.
56+
slices.Reverse(out)
57+
58+
return out, saw["HOME"], nil
59+
}

libcontainer/env_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package libcontainer
2+
3+
import (
4+
"slices"
5+
"testing"
6+
)
7+
8+
func TestPrepareEnvDedup(t *testing.T) {
9+
tests := []struct {
10+
env, wantEnv []string
11+
}{
12+
{
13+
env: []string{},
14+
wantEnv: []string{},
15+
},
16+
{
17+
env: []string{"HOME=/root", "FOO=bar"},
18+
wantEnv: []string{"HOME=/root", "FOO=bar"},
19+
},
20+
{
21+
env: []string{"A=a", "A=b", "A=c"},
22+
wantEnv: []string{"A=c"},
23+
},
24+
{
25+
env: []string{"TERM=vt100", "HOME=/home/one", "HOME=/home/two", "TERM=xterm", "HOME=/home/three", "FOO=bar"},
26+
wantEnv: []string{"TERM=xterm", "HOME=/home/three", "FOO=bar"},
27+
},
28+
}
29+
30+
for _, tc := range tests {
31+
env, _, err := prepareEnv(tc.env)
32+
if err != nil {
33+
t.Error(err)
34+
continue
35+
}
36+
if !slices.Equal(env, tc.wantEnv) {
37+
t.Errorf("want %v, got %v", tc.wantEnv, env)
38+
}
39+
}
40+
}

libcontainer/factory_linux_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ func TestFactoryLoadContainer(t *testing.T) {
3131
id = "1"
3232
expectedHooks = configs.Hooks{
3333
configs.Prestart: configs.HookList{
34-
configs.CommandHook{Command: configs.Command{Path: "prestart-hook"}},
34+
configs.CommandHook{Command: &configs.Command{Path: "prestart-hook"}},
3535
},
3636
configs.Poststart: configs.HookList{
37-
configs.CommandHook{Command: configs.Command{Path: "poststart-hook"}},
37+
configs.CommandHook{Command: &configs.Command{Path: "poststart-hook"}},
3838
},
3939
configs.Poststop: configs.HookList{
4040
unserializableHook{},
41-
configs.CommandHook{Command: configs.Command{Path: "poststop-hook"}},
41+
configs.CommandHook{Command: &configs.Command{Path: "poststop-hook"}},
4242
},
4343
}
4444
expectedConfig = &configs.Config{

libcontainer/init_linux.go

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"runtime"
1212
"runtime/debug"
1313
"strconv"
14-
"strings"
1514
"syscall"
1615

1716
"github.com/containerd/console"
@@ -185,8 +184,8 @@ func startInitialization() (retErr error) {
185184
defer pidfdSocket.Close()
186185
}
187186

188-
// clear the current process's environment to clean any libcontainer
189-
// specific env vars.
187+
// From here on, we don't need current process environment. It is not
188+
// used directly anywhere below this point, but let's clear it anyway.
190189
os.Clearenv()
191190

192191
defer func() {
@@ -209,9 +208,11 @@ func startInitialization() (retErr error) {
209208
}
210209

211210
func containerInit(t initType, config *initConfig, pipe *syncSocket, consoleSocket, pidfdSocket, fifoFile, logPipe *os.File) error {
212-
if err := populateProcessEnvironment(config.Env); err != nil {
211+
env, homeSet, err := prepareEnv(config.Env)
212+
if err != nil {
213213
return err
214214
}
215+
config.Env = env
215216

216217
// Clean the RLIMIT_NOFILE cache in go runtime.
217218
// Issue: https://github.com/opencontainers/runc/issues/4195
@@ -225,6 +226,7 @@ func containerInit(t initType, config *initConfig, pipe *syncSocket, consoleSock
225226
pidfdSocket: pidfdSocket,
226227
config: config,
227228
logPipe: logPipe,
229+
addHome: !homeSet,
228230
}
229231
return i.Init()
230232
case initStandard:
@@ -236,36 +238,13 @@ func containerInit(t initType, config *initConfig, pipe *syncSocket, consoleSock
236238
config: config,
237239
fifoFile: fifoFile,
238240
logPipe: logPipe,
241+
addHome: !homeSet,
239242
}
240243
return i.Init()
241244
}
242245
return fmt.Errorf("unknown init type %q", t)
243246
}
244247

245-
// populateProcessEnvironment loads the provided environment variables into the
246-
// current processes's environment.
247-
func populateProcessEnvironment(env []string) error {
248-
for _, pair := range env {
249-
name, val, ok := strings.Cut(pair, "=")
250-
if !ok {
251-
return errors.New("invalid environment variable: missing '='")
252-
}
253-
if name == "" {
254-
return errors.New("invalid environment variable: name cannot be empty")
255-
}
256-
if strings.IndexByte(name, 0) >= 0 {
257-
return fmt.Errorf("invalid environment variable %q: name contains nul byte (\\x00)", name)
258-
}
259-
if strings.IndexByte(val, 0) >= 0 {
260-
return fmt.Errorf("invalid environment variable %q: value contains nul byte (\\x00)", name)
261-
}
262-
if err := os.Setenv(name, val); err != nil {
263-
return err
264-
}
265-
}
266-
return nil
267-
}
268-
269248
// verifyCwd ensures that the current directory is actually inside the mount
270249
// namespace root of the current process.
271250
func verifyCwd() error {
@@ -294,8 +273,8 @@ func verifyCwd() error {
294273

295274
// finalizeNamespace drops the caps, sets the correct user
296275
// and working dir, and closes any leaked file descriptors
297-
// before executing the command inside the namespace
298-
func finalizeNamespace(config *initConfig) error {
276+
// before executing the command inside the namespace.
277+
func finalizeNamespace(config *initConfig, addHome bool) error {
299278
// Ensure that all unwanted fds we may have accidentally
300279
// inherited are marked close-on-exec so they stay out of the
301280
// container
@@ -341,7 +320,7 @@ func finalizeNamespace(config *initConfig) error {
341320
if err := system.SetKeepCaps(); err != nil {
342321
return fmt.Errorf("unable to set keep caps: %w", err)
343322
}
344-
if err := setupUser(config); err != nil {
323+
if err := setupUser(config, addHome); err != nil {
345324
return fmt.Errorf("unable to setup user: %w", err)
346325
}
347326
// Change working directory AFTER the user has been set up, if we haven't done it yet.
@@ -459,8 +438,9 @@ func syncParentSeccomp(pipe *syncSocket, seccompFd int) error {
459438
return readSync(pipe, procSeccompDone)
460439
}
461440

462-
// setupUser changes the groups, gid, and uid for the user inside the container
463-
func setupUser(config *initConfig) error {
441+
// setupUser changes the groups, gid, and uid for the user inside the container,
442+
// and appends user's HOME to config.Env if addHome is true.
443+
func setupUser(config *initConfig, addHome bool) error {
464444
// Set up defaults.
465445
defaultExecUser := user.ExecUser{
466446
Uid: 0,
@@ -541,11 +521,9 @@ func setupUser(config *initConfig) error {
541521
return err
542522
}
543523

544-
// if we didn't get HOME already, set it based on the user's HOME
545-
if envHome := os.Getenv("HOME"); envHome == "" {
546-
if err := os.Setenv("HOME", execUser.Home); err != nil {
547-
return err
548-
}
524+
// If we didn't get HOME already, set it based on the user's HOME.
525+
if addHome {
526+
config.Env = append(config.Env, "HOME="+execUser.Home)
549527
}
550528
return nil
551529
}

0 commit comments

Comments
 (0)