mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
06f1e07655
The current implementation sets all the environment variables passed in
Process.Env in the current process, one by one, then uses os.Environ to
read those back.
As pointed out in [1], this is slow, as runc calls os.Setenv for every
variable, and there may be a few thousands of those. Looking into how
os.Setenv is implemented, it is indeed slow, especially when cgo is
enabled.
Looking into why it was implemented the way it is, I found commit
9744d72c and traced it to [2], which discusses the actual reasons.
It boils down to these two:
- HOME is not passed into container as it is set in setupUser by
os.Setenv and has no effect on config.Env;
- there is a need to deduplicate the environment variables.
Yet it was decided in [2] to not go ahead with this patch, but
later [3] was opened with the carry of this patch, and merged.
Now, from what I see:
1. Passing environment to exec is way faster than using os.Setenv and
os.Environ (tests show ~20x speed improvement in a simple Go test,
and ~3x improvement in real-world test, see below).
2. Setting environment variables in the runc context may result is some
ugly side effects (think GODEBUG, LD_PRELOAD, or _LIBCONTAINER_*).
3. Nothing in runtime spec says that the environment needs to be
deduplicated, or the order of preference (whether the first or the
last value of a variable with the same name is to be used). We should
stick to what we have in order to maintain backward compatibility.
So, this patch:
- switches to passing env directly to exec;
- adds deduplication mechanism to retain backward compatibility;
- takes care to set PATH from process.Env in the current process
(so that supplied PATH is used to find the binary to execute),
also to retain backward compatibility;
- adds HOME to process.Env if not set;
- ensures any StartContainer CommandHook entries with no environment
set explicitly are run with the same environment as before. Thanks
to @lifubang who noticed that peculiarity.
The benchmark added by the previous commit shows ~3x improvement:
│ before │ after │
│ sec/op │ sec/op vs base │
ExecInBigEnv-20 61.53m ± 1% 21.87m ± 16% -64.46% (p=0.000 n=10)
[1]: https://github.com/opencontainers/runc/pull/1983
[2]: https://github.com/docker-archive/libcontainer/pull/418
[3]: https://github.com/docker-archive/libcontainer/pull/432
Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
60 lines
1.6 KiB
Go
60 lines
1.6 KiB
Go
package libcontainer
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// prepareEnv processes a list of environment variables, preparing it
|
|
// for direct consumption by unix.Exec. In particular, it:
|
|
// - validates each variable is in the NAME=VALUE format and
|
|
// contains no \0 (nil) bytes;
|
|
// - removes any duplicates (keeping only the last value for each key)
|
|
// - sets PATH for the current process, if found in the list.
|
|
//
|
|
// It returns the deduplicated environment, a flag telling whether HOME
|
|
// is present in the input, and an error.
|
|
func prepareEnv(env []string) ([]string, bool, error) {
|
|
if env == nil {
|
|
return nil, false, nil
|
|
}
|
|
// Deduplication code based on dedupEnv from Go 1.22 os/exec.
|
|
|
|
// Construct the output in reverse order, to preserve the
|
|
// last occurrence of each key.
|
|
out := make([]string, 0, len(env))
|
|
saw := make(map[string]bool, len(env))
|
|
for n := len(env); n > 0; n-- {
|
|
kv := env[n-1]
|
|
i := strings.IndexByte(kv, '=')
|
|
if i == -1 {
|
|
return nil, false, errors.New("invalid environment variable: missing '='")
|
|
}
|
|
if i == 0 {
|
|
return nil, false, errors.New("invalid environment variable: name cannot be empty")
|
|
}
|
|
key := kv[:i]
|
|
if saw[key] { // Duplicate.
|
|
continue
|
|
}
|
|
saw[key] = true
|
|
if strings.IndexByte(kv, 0) >= 0 {
|
|
return nil, false, fmt.Errorf("invalid environment variable %q: contains nul byte (\\x00)", key)
|
|
}
|
|
if key == "PATH" {
|
|
// Needs to be set as it is used for binary lookup.
|
|
if err := os.Setenv("PATH", kv[i+1:]); err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
out = append(out, kv)
|
|
}
|
|
// Restore the original order.
|
|
slices.Reverse(out)
|
|
|
|
return out, saw["HOME"], nil
|
|
}
|