diff --git a/libcontainer/container.go b/libcontainer/container.go index c4aa99ecf5f..2280e07c1b1 100644 --- a/libcontainer/container.go +++ b/libcontainer/container.go @@ -46,7 +46,8 @@ type BaseState struct { ID string `json:"id"` // InitProcessPid is the init process id in the parent namespace. - InitProcessPid int `json:"init_process_pid"` + // Omitted when the init process PID is unknown. + InitProcessPid int `json:"init_process_pid,omitempty"` // InitProcessStartTime is the init process start time in clock cycles since boot time. InitProcessStartTime uint64 `json:"init_process_start"` diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go index 6938ea1be88..66b6377999c 100644 --- a/libcontainer/container_linux.go +++ b/libcontainer/container_linux.go @@ -938,7 +938,7 @@ func (c *Container) currentState() *State { var ( startTime uint64 externalDescriptors []string - pid = -1 + pid int ) if c.initProcess != nil { pid = c.initProcess.pid() diff --git a/libcontainer/container_linux_test.go b/libcontainer/container_linux_test.go index 0d0dc4451d3..33d5253a380 100644 --- a/libcontainer/container_linux_test.go +++ b/libcontainer/container_linux_test.go @@ -1,8 +1,10 @@ package libcontainer import ( + "encoding/json" "fmt" "os" + "path/filepath" "testing" "github.com/opencontainers/cgroups" @@ -290,3 +292,35 @@ func TestGetContainerStateAfterUpdate(t *testing.T) { t.Fatalf("expected Memory to be 2048 but received %q", state.Config.Cgroups.Memory) } } + +func TestSaveStateOmitsInitProcessPidWithoutInit(t *testing.T) { + container := &Container{ + stateDir: t.TempDir(), + id: "myid", + config: &configs.Config{ + Cgroups: &cgroups.Cgroup{ + Resources: &cgroups.Resources{}, + }, + }, + cgroupManager: &mockCgroupManager{}, + } + container.state = &stoppedState{c: container} + + if err := container.saveState(container.currentState()); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(filepath.Join(container.stateDir, stateFilename)) + if err != nil { + t.Fatal(err) + } + + var state map[string]any + if err := json.Unmarshal(data, &state); err != nil { + t.Fatal(err) + } + + if _, ok := state["init_process_pid"]; ok { + t.Fatalf("did not expect init_process_pid in persisted state: %s", data) + } +} diff --git a/libcontainer/process_linux.go b/libcontainer/process_linux.go index f7722535af2..50dd8cbdb3f 100644 --- a/libcontainer/process_linux.go +++ b/libcontainer/process_linux.go @@ -819,6 +819,24 @@ func (p *initProcess) start() (retErr error) { } }() + // Write a provisional state.json before cgroup Apply. This ensures that + // if the runc-create process is killed (e.g., SIGKILL from a higher-level + // runtime due to timeout), runc-delete can still find and clean up the + // container's cgroup and state directory. + // + // We temporarily nil out c.initProcess so that init_process_pid is omitted + // from the saved state (the default when initProcess is nil). This prevents external + // tools from seeing the container as "created" before the init process + // is fully set up, avoiding a race where "runc start" could be called + // with a stale STAGE_PARENT PID that will be reaped during creation. + savedInit := p.container.initProcess + p.container.initProcess = nil + _, uerr := p.container.updateState(nil) + p.container.initProcess = savedInit + if uerr != nil { + return fmt.Errorf("unable to store init state: %w", uerr) + } + // Do this before syncing with child so that no children can escape the // cgroup. We don't need to worry about not doing this and not being root // because we'd be using the rootless cgroup manager in that case.