mirror of
https://github.com/opencontainers/runc.git
synced 2025-09-27 03:46:19 +08:00

Since opencontainers/cgroups v0.0.2 (commitb206a015
), all stuct Resources fields are annotated with "omitempty" attribute. As a result, the loaded configuration may have Resources == nil. It is totally OK (rootless containers may have no resources configured) except since commit6c5441e5
, cgroup v1 fs manager requires Resources to be set in the call to NewManager (this is a cgroup v1 deficiency, or maybe our implementation deficiency, or both). To work around this, let's add code to ensure Resources is never nil after loading from state.json. Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
220 lines
6.0 KiB
Go
220 lines
6.0 KiB
Go
package libcontainer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
securejoin "github.com/cyphar/filepath-securejoin"
|
|
"golang.org/x/sys/unix"
|
|
|
|
"github.com/opencontainers/cgroups"
|
|
"github.com/opencontainers/cgroups/manager"
|
|
"github.com/opencontainers/runc/libcontainer/configs"
|
|
"github.com/opencontainers/runc/libcontainer/configs/validate"
|
|
"github.com/opencontainers/runc/libcontainer/intelrdt"
|
|
"github.com/opencontainers/runc/libcontainer/utils"
|
|
)
|
|
|
|
const (
|
|
stateFilename = "state.json"
|
|
execFifoFilename = "exec.fifo"
|
|
)
|
|
|
|
// Create creates a new container with the given id inside a given state
|
|
// directory (root), and returns a Container object.
|
|
//
|
|
// The root is a state directory which many containers can share. It can be
|
|
// used later to get the list of containers, or to get information about a
|
|
// particular container (see Load).
|
|
//
|
|
// The id must not be empty and consist of only the following characters:
|
|
// ASCII letters, digits, underscore, plus, minus, period. The id must be
|
|
// unique and non-existent for the given root path.
|
|
func Create(root, id string, config *configs.Config) (*Container, error) {
|
|
if root == "" {
|
|
return nil, errors.New("root not set")
|
|
}
|
|
if err := validateID(id); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validate.Validate(config); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.MkdirAll(root, 0o700); err != nil {
|
|
return nil, err
|
|
}
|
|
stateDir, err := securejoin.SecureJoin(root, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := os.Stat(stateDir); err == nil {
|
|
return nil, ErrExist
|
|
} else if !os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
|
|
cm, err := manager.New(config.Cgroups)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check that cgroup does not exist or empty (no processes).
|
|
// Note for cgroup v1 this check is not thorough, as there are multiple
|
|
// separate hierarchies, while both Exists() and GetAllPids() only use
|
|
// one for "devices" controller (assuming others are the same, which is
|
|
// probably true in almost all scenarios). Checking all the hierarchies
|
|
// would be too expensive.
|
|
if cm.Exists() {
|
|
pids, err := cm.GetAllPids()
|
|
// Reading PIDs can race with cgroups removal, so ignore ENOENT and ENODEV.
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, unix.ENODEV) {
|
|
return nil, fmt.Errorf("unable to get cgroup PIDs: %w", err)
|
|
}
|
|
if len(pids) != 0 {
|
|
return nil, fmt.Errorf("container's cgroup is not empty: %d process(es) found", len(pids))
|
|
}
|
|
}
|
|
|
|
// Check that cgroup is not frozen. Do not use Exists() here
|
|
// since in cgroup v1 it only checks "devices" controller.
|
|
st, err := cm.GetFreezerState()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to get cgroup freezer state: %w", err)
|
|
}
|
|
if st == cgroups.Frozen {
|
|
return nil, errors.New("container's cgroup unexpectedly frozen")
|
|
}
|
|
|
|
// Parent directory is already created above, so Mkdir is enough.
|
|
if err := os.Mkdir(stateDir, 0o711); err != nil {
|
|
return nil, err
|
|
}
|
|
c := &Container{
|
|
id: id,
|
|
stateDir: stateDir,
|
|
config: config,
|
|
cgroupManager: cm,
|
|
intelRdtManager: intelrdt.NewManager(config, id, ""),
|
|
}
|
|
c.state = &stoppedState{c: c}
|
|
return c, nil
|
|
}
|
|
|
|
// Load takes a path to the state directory (root) and an id of an existing
|
|
// container, and returns a Container object reconstructed from the saved
|
|
// state. This presents a read only view of the container.
|
|
func Load(root, id string) (*Container, error) {
|
|
if root == "" {
|
|
return nil, errors.New("root not set")
|
|
}
|
|
// when load, we need to check id is valid or not.
|
|
if err := validateID(id); err != nil {
|
|
return nil, err
|
|
}
|
|
stateDir, err := securejoin.SecureJoin(root, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
state, err := loadState(stateDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r := &nonChildProcess{
|
|
processPid: state.InitProcessPid,
|
|
processStartTime: state.InitProcessStartTime,
|
|
fds: state.ExternalDescriptors,
|
|
}
|
|
cm, err := manager.NewWithPaths(state.Config.Cgroups, state.CgroupPaths)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c := &Container{
|
|
initProcess: r,
|
|
initProcessStartTime: state.InitProcessStartTime,
|
|
id: id,
|
|
config: &state.Config,
|
|
cgroupManager: cm,
|
|
intelRdtManager: intelrdt.NewManager(&state.Config, id, state.IntelRdtPath),
|
|
stateDir: stateDir,
|
|
created: state.Created,
|
|
}
|
|
c.state = &loadedState{c: c}
|
|
if err := c.refreshState(); err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func loadState(root string) (*State, error) {
|
|
stateFilePath, err := securejoin.SecureJoin(root, stateFilename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f, err := os.Open(stateFilePath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, ErrNotExist
|
|
}
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
var state *State
|
|
if err := json.NewDecoder(f).Decode(&state); err != nil {
|
|
return nil, err
|
|
}
|
|
// Cgroup v1 fs manager expect Resources to never be nil.
|
|
if state.Config.Cgroups.Resources == nil {
|
|
state.Config.Cgroups.Resources = &cgroups.Resources{}
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
// validateID checks if the supplied container ID is valid, returning
|
|
// the ErrInvalidID in case it is not.
|
|
//
|
|
// The format of valid ID was never formally defined, instead the code
|
|
// was modified to allow or disallow specific characters.
|
|
//
|
|
// Currently, a valid ID is a non-empty string consisting only of
|
|
// the following characters:
|
|
// - uppercase (A-Z) and lowercase (a-z) Latin letters;
|
|
// - digits (0-9);
|
|
// - underscore (_);
|
|
// - plus sign (+);
|
|
// - minus sign (-);
|
|
// - period (.).
|
|
//
|
|
// In addition, IDs that can't be used to represent a file name
|
|
// (such as . or ..) are rejected.
|
|
|
|
func validateID(id string) error {
|
|
if len(id) < 1 {
|
|
return ErrInvalidID
|
|
}
|
|
|
|
// Allowed characters: 0-9 A-Z a-z _ + - .
|
|
for i := range len(id) {
|
|
c := id[i]
|
|
switch {
|
|
case c >= 'a' && c <= 'z':
|
|
case c >= 'A' && c <= 'Z':
|
|
case c >= '0' && c <= '9':
|
|
case c == '_':
|
|
case c == '+':
|
|
case c == '-':
|
|
case c == '.':
|
|
default:
|
|
return ErrInvalidID
|
|
}
|
|
|
|
}
|
|
|
|
if string(os.PathSeparator)+id != utils.CleanPath(string(os.PathSeparator)+id) {
|
|
return ErrInvalidID
|
|
}
|
|
|
|
return nil
|
|
}
|