From 949485528ea7374f8d68cc2a3a4d588cf8f425c5 Mon Sep 17 00:00:00 2001 From: Fred Lotter Date: Mon, 26 Jun 2023 10:26:23 +0200 Subject: [PATCH] cmd: add detection for containers and pid1 The system manager currently makes some assumptions about the environment it is running in. For example, it assumes that a shutdown program is available in userspace and accessible with PATH configured appropriately. internals/daemon/daemon.go: : cmd := exec.Command("shutdown", "-r", ... : This patch adds two detection mechanisms that will allow code to make environment specific decisions in the future (not part of this patch): - cmd.Containerised() returns true if running inside a container runtime - cmd.InitProcess() returns true if the system manager was started as PID 1 In addition, the overlord code currently disables reboot failure detection if the system manager is running as PID 1. However, this change is only required for container runtimes, and not generically. - Update the boot id workaround code to only apply for container runtimes. --- cmd/export_test.go | 50 ++++++++++++++ cmd/version.go | 65 +++++++++++++++++-- cmd/version_test.go | 115 +++++++++++++++++++++++++++++++++ internals/overlord/overlord.go | 14 ++-- 4 files changed, 232 insertions(+), 12 deletions(-) create mode 100644 cmd/export_test.go create mode 100644 cmd/version_test.go diff --git a/cmd/export_test.go b/cmd/export_test.go new file mode 100644 index 000000000..eba7b9561 --- /dev/null +++ b/cmd/export_test.go @@ -0,0 +1,50 @@ +// Copyright (c) 2014-2023 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "sync" +) + +// MockPid2ProcPath assigns a temporary path to where the PID2 +// status can be found. +func MockPid2ProcPath(path string) (restore func()) { + orig := pid2ProcPath + pid2ProcPath = path + return func() { pid2ProcPath = orig } +} + +// MockPid allows faking the pid of this process +func MockPid(pid int) (restore func()) { + orig := selfPid + selfPid = pid + return func() { selfPid = orig } +} + +// MockVersion allows mocking the version which would +// otherwise only be real once the generator script +// has run. +func MockVersion(version string) (restore func()) { + old := Version + Version = version + return func() { Version = old } +} + +// ResetContainerInit forces the container runtime check +// to retry with globals reset +func ResetContainerInit() { + containerOnce = sync.Once{} + containerRuntime = true +} diff --git a/cmd/version.go b/cmd/version.go index 95d43d4fe..f3f59c38e 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -16,11 +16,64 @@ package cmd //go:generate ./mkversion.sh -// Version will be overwritten at build-time via mkversion.sh -var Version = "unknown" +import ( + "io/ioutil" + "os" + "strconv" + "strings" + "sync" +) -func MockVersion(version string) (restore func()) { - old := Version - Version = version - return func() { Version = old } +var ( + // Version will be overwritten at build-time via mkversion.sh + Version = "unknown" + + pid2ProcPath = "/proc/2/status" + selfPid = os.Getpid() + + containerOnce sync.Once + containerRuntime bool = true +) + +// Containerised returns true if we are running inside a container runtime +// such as lxd or Docker. The detection is only intended for Linux host +// environments as it looks for the Linux kernel kthreadd process, started +// by the Linux kernel after init. Kernel initialised processes do not have +// a parent as its not a child of init (PID1) and as a result has its ppid +// set to zero. We use this property to detect the absence of a container +// runtime. If /proc is not mounted, the detection will assume its a +// container, so make sure early mounts are completed before running this +// check on an unconfined host. +func Containerised() bool { + containerOnce.Do(func() { + if s, err := ioutil.ReadFile(pid2ProcPath); err == nil { + lines := strings.Split(string(s), "\n") + for _, l := range lines { + kv := strings.Split(l, "\t") + if zeroPPid(kv) { + containerRuntime = false + break + } + } + } + }) + + return containerRuntime +} + +// zeroPPid returns true if a PPid key with a zero value was found, +// otherwise false. +func zeroPPid(kv []string) (found bool) { + if kv[0] == "PPid:" { + if ppid, err := strconv.Atoi(kv[1]); err == nil && ppid == 0 { + found = true + } + } + return found +} + +// InitProcess returns true if the system manager is the first process +// started by the Linux kernel. +func InitProcess() bool { + return selfPid == 1 } diff --git a/cmd/version_test.go b/cmd/version_test.go new file mode 100644 index 000000000..19a223851 --- /dev/null +++ b/cmd/version_test.go @@ -0,0 +1,115 @@ +// Copyright (c) 2014-2023 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd_test + +import ( + "io/ioutil" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/canonical/pebble/cmd" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type cmdTestSuite struct{} + +var _ = Suite(&cmdTestSuite{}) + +// createProcPid2Status creates a /proc//status file. +func createProcPid2Status(c *C, data string) string { + path := filepath.Join(c.MkDir(), "status") + err := ioutil.WriteFile(path, []byte(data), 0o644) + c.Assert(err, IsNil) + return path +} + +func (s *cmdTestSuite) SetUpTest(c *C) { + // Allow each test to trigger a check + cmd.ResetContainerInit() +} + +func (s *cmdTestSuite) TestContainerisedInvalidPath(c *C) { + // This path is not valid so the test must therefore + // assume the PID2 process does not exist, and therefore + // we are inside a container. This may trigger a false + // positive if /proc is not mounted before this is called. + defer cmd.MockPid2ProcPath("/1/2/3/4/5")() + c.Assert(cmd.Containerised(), Equals, true) +} + +// TestContainerisedValidPath runs individual tests in the loop +// resetting before each test to prevent the sync.Once from +// loading a previously cached value. +func (s *cmdTestSuite) TestContainerisedValidPath(c *C) { + + for _, d := range []struct { + status string + container bool + }{ + // Note the /proc//status format is: + // :\t + // The delimiter is a tab, not spaces. + {` +Pid: 2 +PPid: 0 +Something: 32`, false}, + {` +Pid: 2 +PPid: 1 +Something: 32`, true}, + {` +something +1 2 3 4`, true}, + } { + cmd.ResetContainerInit() + path := createProcPid2Status(c, d.status) + defer cmd.MockPid2ProcPath(path)() + c.Assert(cmd.Containerised(), Equals, d.container) + } +} + +// TestContainerisedCaching ensures we do not redo detection as +// the container state could be used more than once in the codebase. +func (s *cmdTestSuite) TestContainerisedCaching(c *C) { + // Note the /proc//status format is: + // :\t + // The delimiter is a tab, not spaces. + path := createProcPid2Status(c, ` +Pid: 2 +PPid: 0 +Something: 32`) + defer cmd.MockPid2ProcPath(path)() + c.Assert(cmd.Containerised(), Equals, false) + + path = createProcPid2Status(c, ` +Pid: 2 +PPid: 1 +Something: 32`) + defer cmd.MockPid2ProcPath(path)() + // This occurrence should not read the file, and return the cached value + c.Assert(cmd.Containerised(), Equals, false) +} + +// TestInitProcess checks if the init detection is plumbed in correctly. +func (s *cmdTestSuite) TestInitProcess(c *C) { + defer cmd.MockPid(1234)() + c.Assert(cmd.InitProcess(), Equals, false) + defer cmd.MockPid(1)() + c.Assert(cmd.InitProcess(), Equals, true) +} diff --git a/internals/overlord/overlord.go b/internals/overlord/overlord.go index 771a670dd..5f17fc56a 100644 --- a/internals/overlord/overlord.go +++ b/internals/overlord/overlord.go @@ -27,6 +27,7 @@ import ( "github.com/canonical/x-go/randutil" "gopkg.in/tomb.v2" + "github.com/canonical/pebble/cmd" "github.com/canonical/pebble/internals/osutil" "github.com/canonical/pebble/internals/overlord/checkstate" "github.com/canonical/pebble/internals/overlord/cmdstate" @@ -147,12 +148,13 @@ func loadState(statePath string, restartHandler restart.Handler, backend state.B if err != nil { return nil, fmt.Errorf("fatal: cannot find current boot ID: %v", err) } - // If pebble is PID 1 we don't care about /proc/sys/kernel/random/boot_id - // as we are most likely running in a container. LXD mounts it's own boot_id - // to correctly emulate the boot_id behaviour of non-containerized systems. - // Within containerd/docker, boot_id is consistent with the host, which provides - // us no context of restarts, so instead fallback to /proc/sys/kernel/random/uuid. - if os.Getpid() == 1 { + + if cmd.Containerised() { + // We need a unique boot id to support failed reboot detection logic in the + // overlord. This is not guaranteed for a container runtime because not + // all implementations (e.g. Docker) updates the boot id on restart of the + // container. In this case we always return a different id on request, + // which will disable reboot failure detection for container runtimes. curBootID, err = randutil.RandomKernelUUID() if err != nil { return nil, fmt.Errorf("fatal: cannot generate psuedo boot-id: %v", err)