Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

selinux: package to query SELinux status and verify/restore file contexts #6285

Merged
merged 7 commits into from Dec 13, 2018
43 changes: 43 additions & 0 deletions selinux/export_test.go
@@ -0,0 +1,43 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018 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 selinux

import (
"io/ioutil"

"gopkg.in/check.v1"
)

var (
GetSELinuxMount = getSELinuxMount
)

func MockMountInfo(c *check.C, text string) (where string, restore func()) {
old := procSelfMountInfo
dir := c.MkDir()
f, err := ioutil.TempFile(dir, "mountinfo")
c.Assert(err, check.IsNil)
err = ioutil.WriteFile(f.Name(), []byte(text), 0644)
c.Assert(err, check.IsNil)
procSelfMountInfo = f.Name()
restore = func() {
procSelfMountInfo = old
}
return procSelfMountInfo, restore
}
26 changes: 26 additions & 0 deletions selinux/label.go
@@ -0,0 +1,26 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018 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 selinux

// RestoreMode configures how default path context is restored
type RestoreMode struct {
// Recursive indicates whether the default context shall be restored
// recursively
Recursive bool
}
30 changes: 30 additions & 0 deletions selinux/label_darwin.go
@@ -0,0 +1,30 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need the per-plaform impls because we will use selinux in the cmd/snap code, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes


/*
* Copyright (C) 2018 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 selinux

// VerifyPathContext checks whether a given path is labeled according to its default
// SELinux context
func VerifyPathContext(aPath string) (bool, error) {
return true, nil
}

// RestoreContext restores the default SELinux context of given path
func RestoreContext(aPath string, mode RestoreMode) error {
return nil
}
79 changes: 79 additions & 0 deletions selinux/label_linux.go
@@ -0,0 +1,79 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018 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 selinux

import (
"os"
"os/exec"
"regexp"

"github.com/snapcore/snapd/osutil"
)

var (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: drop the var ( ... ) and use just one-liner var ...

// actual matchpathcon -V output:
// /home/guest/snap has context unconfined_u:object_r:user_home_t:s0, should be unconfined_u:object_r:snappy_home_t:s0
matchIncorrectLabel = regexp.MustCompile("^.* has context .* should be .*\n$")
)

// VerifyPathContext checks whether a given path is labeled according to its default
// SELinux context
func VerifyPathContext(aPath string) (bool, error) {
if _, err := os.Stat(aPath); err != nil {
// path that cannot be accessed cannot be verified
return false, err
}
// matchpathcon -V verifies whether the context of a path matches the
// default
cmd := exec.Command("matchpathcon", "-V", aPath)
cmd.Env = append(os.Environ(), "LC_ALL=C")
out, err := cmd.Output()
if err == nil {
// the path was verified
return true, nil
}
exit, _ := osutil.ExitCode(err)
// exits with 1 when the verification failed or other error occurred,
// when verification failed a message like this will be printed to
// stdout:
// <the-path> has context <some-context>, should be <some-other-context>
// match the message so that we can distinguish a failed verification
// case from other errors
if exit == 1 && matchIncorrectLabel.Match(out) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems we need a test that calls the real tool if available to test this bit, otherwise we are just checking the expected output against again our own expectations, if the tool output changes nothing breaks here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling the actual tool we'd have to have the policy module installed in the host system. The only way to verify this reasonably is a spread test that does the same thing I'm doing locally: mkdir ~/snap/foo && chcon -t user_home_t -R ~/snap && snap run <snap> && ls -RZ ~/snap.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We definitely want some kind of test (here or in the one using this) that tests this bit as directly/with as little layers as possible. Otherwise the other approach would be to write our own tool in C using libselinux, then we control the format completely.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed a spread test to #6265 where this API is actually exercised. That's probably as much as can be done now until #6270 lands.

return false, nil
}
return false, err
}

// RestoreContext restores the default SELinux context of given path
func RestoreContext(aPath string, mode RestoreMode) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we tend to use pointer for option structs so that nil can be passed and mean the default as well but no strong opinion for this particular case

if _, err := os.Stat(aPath); err != nil {
// path that cannot be accessed cannot be restored
return err
}

args := make([]string, 0, 2)
if mode.Recursive {
// -R: recursive
args = append(args, "-R")
}
args = append(args, aPath)

return exec.Command("restorecon", args...).Run()
}
139 changes: 139 additions & 0 deletions selinux/label_linux_test.go
@@ -0,0 +1,139 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018 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 selinux_test

import (
"fmt"
"io/ioutil"
"os/exec"
"path/filepath"

"gopkg.in/check.v1"

"github.com/snapcore/snapd/selinux"
"github.com/snapcore/snapd/testutil"
)

type labelSuite struct {
path string
}

var _ = check.Suite(&labelSuite{})

func (l *labelSuite) SetUpTest(c *check.C) {
l.path = filepath.Join(c.MkDir(), "foo")
ioutil.WriteFile(l.path, []byte("foo"), 0644)
}

func (l *labelSuite) TestVerifyHappyOk(c *check.C) {
cmd := testutil.MockCommand(c, "matchpathcon", "")
defer cmd.Restore()

ok, err := selinux.VerifyPathContext(l.path)
c.Assert(err, check.IsNil)
c.Assert(ok, check.Equals, true)
c.Assert(cmd.Calls(), check.DeepEquals, [][]string{
{"matchpathcon", "-V", l.path},
})
}

func (l *labelSuite) TestVerifyFailGibberish(c *check.C) {
cmd := testutil.MockCommand(c, "matchpathcon", "echo gibberish; exit 1 ")
defer cmd.Restore()

ok, err := selinux.VerifyPathContext(l.path)
c.Assert(err, check.ErrorMatches, "exit status 1")
c.Assert(ok, check.Equals, false)
}

func (l *labelSuite) TestVerifyFailStatus(c *check.C) {
cmd := testutil.MockCommand(c, "matchpathcon", "echo gibberish; exit 5 ")
defer cmd.Restore()

ok, err := selinux.VerifyPathContext(l.path)
c.Assert(err, check.ErrorMatches, "exit status 5")
c.Assert(ok, check.Equals, false)
}

func (l *labelSuite) TestVerifyFailNoPath(c *check.C) {
cmd := testutil.MockCommand(c, "matchpathcon", ``)
defer cmd.Restore()

ok, err := selinux.VerifyPathContext("does-not-exist")
c.Assert(err, check.ErrorMatches, ".* does-not-exist: no such file or directory")
c.Assert(ok, check.Equals, false)
}

func (l *labelSuite) TestVerifyFailNoTool(c *check.C) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will fail on fedora like systems, no? aren't we running unit tests there?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I'll need to skip this iff the tool is already present. AFAIK we run unit tests only Ubuntu and Debian

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if _, err := exec.LookPath("matchpathcon"); err == nil {
c.Skip("matchpathcon found in $PATH")
}
ok, err := selinux.VerifyPathContext(l.path)
c.Assert(err, check.ErrorMatches, `exec: "matchpathcon": executable file not found in \$PATH`)
c.Assert(ok, check.Equals, false)
}

func (l *labelSuite) TestVerifyHappyMismatch(c *check.C) {
cmd := testutil.MockCommand(c, "matchpathcon", fmt.Sprintf(`
echo %s has context unconfined_u:object_r:user_home_t:s0, should be unconfined_u:object_r:snappy_home_t:s0
exit 1`, l.path))
defer cmd.Restore()

ok, err := selinux.VerifyPathContext(l.path)
c.Assert(err, check.IsNil)
c.Assert(ok, check.Equals, false)
}

func (l *labelSuite) TestRestoreHappy(c *check.C) {
cmd := testutil.MockCommand(c, "restorecon", "")
defer cmd.Restore()

err := selinux.RestoreContext(l.path, selinux.RestoreMode{})
c.Assert(err, check.IsNil)
c.Assert(cmd.Calls(), check.DeepEquals, [][]string{
{"restorecon", l.path},
})

cmd.ForgetCalls()

err = selinux.RestoreContext(l.path, selinux.RestoreMode{Recursive: true})
c.Assert(err, check.IsNil)
c.Assert(cmd.Calls(), check.DeepEquals, [][]string{
{"restorecon", "-R", l.path},
})
}

func (l *labelSuite) TestRestoreFailNoTool(c *check.C) {
if _, err := exec.LookPath("matchpathcon"); err == nil {
c.Skip("matchpathcon found in $PATH")
}
err := selinux.RestoreContext(l.path, selinux.RestoreMode{})
c.Assert(err, check.ErrorMatches, `exec: "restorecon": executable file not found in \$PATH`)
}

func (l *labelSuite) TestRestoreFail(c *check.C) {
cmd := testutil.MockCommand(c, "restorecon", "exit 1")
defer cmd.Restore()

err := selinux.RestoreContext(l.path, selinux.RestoreMode{})
c.Assert(err, check.ErrorMatches, "exit status 1")

err = selinux.RestoreContext("does-not-exist", selinux.RestoreMode{})
c.Assert(err, check.ErrorMatches, ".* does-not-exist: no such file or directory")
}
29 changes: 29 additions & 0 deletions selinux/selinux_darwin.go
@@ -0,0 +1,29 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018 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 selinux

// IsEnabled checks whether SELinux is enabled
func IsEnabled() (bool, error) {
return false, nil
}

// IsEnabled checks whether SELinux is in enforcing mode
func IsEnforcing() (bool, error) {
return false, nil
}