Skip to content

Commit

Permalink
cmd: add detection for containers and pid1
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
flotter committed Jun 26, 2023
1 parent 8902cbe commit 35aaad6
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 12 deletions.
50 changes: 50 additions & 0 deletions cmd/export_test.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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
}
64 changes: 58 additions & 6 deletions cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,63 @@ package cmd

//go:generate ./mkversion.sh

// Version will be overwritten at build-time via mkversion.sh
var Version = "unknown"
import (
"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 := os.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
}
115 changes: 115 additions & 0 deletions cmd/version_test.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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/<pid>/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/<pid>/status format is:
// <key>:\t<value>
// 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/<pid>/status format is:
// <key>:\t<value>
// 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)
}
14 changes: 8 additions & 6 deletions internals/overlord/overlord.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 35aaad6

Please sign in to comment.