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)