cmd/snap: make users Xauthority file available in snap environment #3177

Merged
merged 15 commits into from Apr 25, 2017

Conversation

Projects
None yet
5 participants
Contributor

morphis commented Apr 11, 2017

Not all desktop environments set XAUTHORITY to something which is available
inside the environment we create for snaps. Some place the authority file
inside /tmp which a snap doesn't share with the host system. To workaround
this we copy the file XAUTHORITY points to when the snap is started into a
snap specific location in XDG_RUNTIME_DIR of the current user and allow
access via the x11 interface. This will work across all available desktop
environments.

@mvo5 mvo5 added this to the 2.25 milestone Apr 11, 2017

cmd/snap/cmd_run.go
+
+ // If everything is ok, we can now point the snap to the new
+ // location of the Xauthority file.
+ setEnv("XAUTHORITY", targetPath)
@zyga

zyga Apr 11, 2017

Contributor

This is a nitpick but I'm not a fan of setenv, it's not safe in multi threaded programs and we can avoid it by remembering the new XAUTHORITY value and setting it at exec time.

@morphis

morphis Apr 12, 2017

Contributor

Let me rework that if that is the common sense, @mvo5 ?

interfaces/builtin/x11.go
@@ -33,6 +33,10 @@ const x11ConnectedPlugAppArmor = `
/var/cache/fontconfig/ r,
/var/cache/fontconfig/** mr,
+
+# Allow access to the user specific copy of the xauth file specified
+# in the XAUTHORITY environment variable snap run creates on startup.
@zyga

zyga Apr 11, 2017

Contributor
# Allow access to the user specific copy of the xauth file specified
# in the XAUTHORITY environment variable, that "snap run" creates on startup.
@morphis

morphis Apr 12, 2017

Contributor

Done

x11/xauth.go
+ "os"
+)
+
+type xauth struct {
@zyga

zyga Apr 11, 2017

Contributor

Where is this documented (as a file format) can you please link to such documentation.

@morphis

morphis Apr 12, 2017

Contributor

Done

x11/xauth.go
+}
+
+func readChunk(f *os.File) ([]byte, error) {
+ b := make([]byte, 2)
@zyga

zyga Apr 11, 2017

Contributor

also known as [2]byte

@morphis

morphis Apr 11, 2017

Contributor

A static [2]byte didn't work with the f.Read call below if I remember well. Let me try that again.

@zyga

zyga Apr 11, 2017

Contributor

You need to get the slice for f.Read but the make makes no sense IMO (pun intended)

@morphis

morphis Apr 12, 2017

Contributor

Done

x11/xauth.go
+ return nil
+}
+
+func ValidateXauthority(path string) (int, error) {
@zyga

zyga Apr 11, 2017

Contributor

This seems misnamed, it's not really validating and it returns an int. How about countAuthCookies(path string) (int, error)

@morphis

morphis Apr 11, 2017

Contributor

Why not.

@zyga

zyga Apr 12, 2017

Contributor

See below, I changed my mind later during the review. this should just return error.

@morphis

morphis Apr 12, 2017

Contributor

Done.

x11/xauth.go
+ return cookies, nil
+}
+
+func MockXauthority(cookies int) string {
@zyga

zyga Apr 11, 2017

Contributor

You should put test code for foo.go in foo_test.go. Alternatively if the test needs access to private function you can use the export_test.go trick.

@morphis

morphis Apr 12, 2017

Contributor

Done

x11/xauth.go
+}
+
+func MockXauthority(cookies int) string {
+ f, _ := ioutil.TempFile("", "xauth")
@zyga

zyga Apr 11, 2017

Contributor

Typically you'd pass an instance of c.C in and then c.Assert(err, IsNil).

You should also do defer f.Close() after checking that err is nil.

@morphis

morphis Apr 12, 2017

Contributor

Done

x11/xauth_test.go
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type XauthTestSuite struct {}
@zyga

zyga Apr 11, 2017

Contributor

The suite can be private, e.g. xauthSuite

@morphis

morphis Apr 12, 2017

Contributor

Done

x11/xauth_test.go
+func (s *XauthTestSuite) TestXauthFileNotAvailable(c *C) {
+ n, err := x11.ValidateXauthority("/does/not/exist")
+ c.Assert(n, Equals, 0)
+ c.Assert(err, Not(IsNil))
@zyga

zyga Apr 11, 2017

Contributor

c.Assert(err, NotNil)

@mvo5

mvo5 Apr 12, 2017

Collaborator

(because it gives a nicer error :)

@morphis

morphis Apr 12, 2017

Contributor

Good to know! Done.

x11/xauth_test.go
+ c.Assert(n, Equals, len(data))
+
+ n, err = x11.ValidateXauthority(f.Name())
+ c.Assert(err, DeepEquals, fmt.Errorf("Could not read enough bytes"))
@zyga

zyga Apr 11, 2017

Contributor

c.Assert(err, ErrorMatches, "Could not read enough bytes")

@morphis

morphis Apr 12, 2017

Contributor

Done.

x11/xauth_test.go
+}
+
+func (s *XauthTestSuite) TestXauthFileExistsButHasInvalidContent(c *C) {
+ f, err := ioutil.TempFile("", "xauth")
@zyga

zyga Apr 11, 2017

Contributor

If you structure your APIs so that you can read from io.Reader and then have a wrapper that does the open/defer close/read dance then you can just write all those tests with string readers instead.

x11/xauth_test.go
+
+func (s *XauthTestSuite) TestValidXauthFile(c *C) {
+ path := x11.MockXauthority(1)
+ n, err := x11.ValidateXauthority(path)
@zyga

zyga Apr 11, 2017

Contributor

I changed my mind, this should just return an error and not any numbers.

cmd/snap/cmd_run.go
)
var (
syscallExec = syscall.Exec
userCurrent = user.Current
+ getEnv = os.Getenv
@mvo5

mvo5 Apr 12, 2017

Collaborator

(Super nicktpick) most code uses the convention osGetenv = os.Getenv (same in the line below).

@morphis

morphis Apr 12, 2017

Contributor

Done

This looks very nice, thank you! Don't be discouraged from the amount of comments, its all nitpicks and comments about the conventions.

cmd/snap/cmd_run.go
+ cookies, err := x11.ValidateXauthority(tmpXauthPath)
+ if err != nil {
+ return err
+ } else if cookies == 0 {
@mvo5

mvo5 Apr 12, 2017

Collaborator

(nitpick) I don't think we need the else. If err != nil it already returns.

@morphis

morphis Apr 12, 2017

Contributor

done.

cmd/snap/cmd_run.go
+ }
+
+ if !osutil.FileExists(baseTargetDir) {
+ os.MkdirAll(baseTargetDir, 0700)
@mvo5

mvo5 Apr 12, 2017

Collaborator

We probably want an error check here, something like: if err := os.MkdirAll(...); err != nil {return err}

cmd/snap/cmd_run.go
+ os.MkdirAll(baseTargetDir, 0700)
+ }
+
+ err = osutil.CopyFile(tmpXauthPath, targetPath, flags)
@mvo5

mvo5 Apr 12, 2017

Collaborator

This can be done in a single line: if err := osutil.CopyFile(...); err != nil {.

cmd/snap/cmd_run.go
@@ -230,6 +302,10 @@ func runSnapConfine(info *snap.Info, securityTag, snapApp, command, hook string,
logger.Noticef("WARNING: cannot create user data directory: %s", err)
}
+ if err := migrateXauthority(info); err != nil {
+ logger.Noticef("WARNING: cannot copy user Xauthority file: %s", err)
@mvo5

mvo5 Apr 12, 2017

Collaborator

👍

x11/xauth.go
+ n, err := f.Read(b)
+ if err != nil {
+ return nil, err
+ } else if n != 2 {
@mvo5

mvo5 Apr 12, 2017

Collaborator

The else is not really needed here, is it? If err != nil it returns anyway.

@morphis

morphis Apr 12, 2017

Contributor

If I read https://golang.org/pkg/os/#File.Read correctly then there is the case that you supply an array of a specifc length but don't get all bytes filled back without any error. We need to ensure here that we get 2 bytes. Otherwise the file is invalid.

@mvo5

mvo5 Apr 18, 2017

Collaborator

Sorry, I did not express myself very well. I was thinking of just a tiny style change:

n, err := f.Read(b)
+	if err != nil {
+		return nil, err
+	} 
+       if n != 2 {

(i.e. drop the "else" but keep the check for n!=2)

x11/xauth.go
+ n, err = f.Read(chunk)
+ if err != nil {
+ return nil, err
+ } else if n != size {
@mvo5

mvo5 Apr 12, 2017

Collaborator

Same comment as above about the else

@morphis

morphis Apr 12, 2017

Contributor

See comment above.

x11/xauth.go
+ return chunk, nil
+}
+
+func (xa *xauth) ReadFromFile(f *os.File) error {
@mvo5

mvo5 Apr 12, 2017

Collaborator

It looks like this is used only internally so I think we can make it private.

@morphis

morphis Apr 12, 2017

Contributor

Good catch, will do.

x11/xauth.go
+}
+
+func ValidateXauthority(path string) (int, error) {
+ f, err := os.OpenFile(path, os.O_RDONLY, 0600)
@mvo5

mvo5 Apr 12, 2017

Collaborator

I think you can just use os.Open(path) here, internally it will open the file readonly and its slightly shorter.

x11/xauth_test.go
+func (s *XauthTestSuite) TestXauthFileNotAvailable(c *C) {
+ n, err := x11.ValidateXauthority("/does/not/exist")
+ c.Assert(n, Equals, 0)
+ c.Assert(err, Not(IsNil))
@zyga

zyga Apr 11, 2017

Contributor

c.Assert(err, NotNil)

@mvo5

mvo5 Apr 12, 2017

Collaborator

(because it gives a nicer error :)

@morphis

morphis Apr 12, 2017

Contributor

Good to know! Done.

x11/xauth_test.go
+}
+
+func (s *XauthTestSuite) TestXauthFileExistsButIsEmpty(c *C) {
+ f, err := ioutil.TempFile("", "xauth")
@mvo5

mvo5 Apr 12, 2017

Collaborator

(nitpick^99) - you could use tempdir := c.MkDir(); ioutil.WriteFile(filepath.Join(tempfile, "xauth"), nil, 0600) here which is a whopping one line shorter ;) so really not worth it. Mostly wanted to show c.MkDir() as its nice and the gocheck code will do the cleanup for us of that temp dir (you could create a tempdir in in SetupTest() and use use that for the tempfiles without having to worry about cleanup. But I'm really into nitpick land now :)

@morphis

morphis Apr 12, 2017

Contributor

Done, I am ok with nitpicks :-)

cmd/snap/cmd_run.go
+func migrateXauthority(info *snap.Info) error {
+ u, err := userCurrent()
+ if err != nil {
+ return fmt.Errorf(i18n.G("cannot get the current user: %v"), err)
@zyga

zyga Apr 13, 2017

Contributor

I think we %s errors rather than %v them

Looks much nicer, thank you for iterating on this. I left a few more comments but I think this is getting closer to being merged. I'm still a bit convinced we should avoid setenv but I would like to discuss this with @mvo5. I would like to see some spread tests as well, maybe a simple test where we have a canned correct Xauth file that we see validated and copied to the execution environment (no need to run anything graphical).

cmd/snap/cmd_run.go
+ return fmt.Errorf(i18n.G("cannot get the current user: %v"), err)
+ }
+
+ // If we're running as root then we don't do anything
@zyga

zyga Apr 13, 2017

Contributor

Why is that? Is root restricted somehow?

@morphis

morphis Apr 13, 2017

Contributor

Idea was that it doesn't make sense to allow root using X11 but there could be edge cases where people do this. Regular case will be always that a user != root has an XAUTHORITY set. Will drop this. @jdstrand please correct me if I miss anything.

cmd/snap/cmd_run.go
+
+ // Only validate Xauthority file again when both files don't match
+ // ohterwise we can continue using the existing Xauthority file
+ if osutil.FileExists(targetPath) && osutil.FilesAreEqual(targetPath, tmpXauthPath) {
@zyga

zyga Apr 13, 2017

Contributor

Nice :-)

cmd/snap/cmd_run.go
+ // either the data can't be parsed or there are no cookies in
+ // the file is invalid.
+ if err := x11.ValidateXauthority(tmpXauthPath); err != nil {
+ return nil
@zyga

zyga Apr 13, 2017

Contributor

How about logging the error?

@morphis

morphis Apr 13, 2017

Contributor

Will do.

cmd/snap/cmd_run.go
+
+ // If everything is ok, we can now point the snap to the new
+ // location of the Xauthority file.
+ osSetenv("XAUTHORITY", targetPath)
@zyga

zyga Apr 13, 2017

Contributor

Just re-stating the setenv-is-dangerous comment I made earlier. I'll let @mvo5 comment on this, if we want to re-work it to avoid setenv entirely.

+ "os"
+)
+
+// See https://cgit.freedesktop.org/xorg/lib/libXau/tree/AuRead.c and
@zyga

zyga Apr 13, 2017

Contributor

❤️ Thanks for referencing that!

x11/xauth.go
+
+func (xa *xauth) readFromFile(f *os.File) error {
+ b := [2]byte{}
+ _, err := f.Read(b[:])
@zyga

zyga Apr 13, 2017

Contributor

Don't you have to check the amount of data read here?

@morphis

morphis Apr 13, 2017

Contributor

Missed that one. Thanks!

Contributor

morphis commented Apr 13, 2017

@zyga Adding a spread test for this.

I'm not done with the PR review but have enough for an initial comment.

cmd/snap/cmd_run.go
@@ -216,6 +220,68 @@ func snapRunHook(snapName, snapRevision, hookName string) error {
return runSnapConfine(info, hook.SecurityTag(), snapName, "", hook.Name, nil)
}
+func migrateXauthority(info *snap.Info) error {
+ u, err := userCurrent()
@jdstrand

jdstrand Apr 13, 2017

Contributor

Nit-pick: it might make the code easier to read if this was closer to where it is first used.

cmd/snap/cmd_run.go
+ // Nothing to do for us. Most likely running outside of any
+ // graphical X11 session.
+ return nil
+ }
@jdstrand

jdstrand Apr 13, 2017

Contributor

In thinking about this more, there is an attack I'd like to add a little more protection against. Consider:

  1. sysadmin updates sudoers to allow running 'sudo /snap/bin/foo' by some non-admin user
  2. that user does XAUTHORITY=/etc/shadow sudo /snap/bin/foo
  3. /etc/shadow would be copied to /run/user/0/Xauthority
  4. foo is somehow controllable by the non-admin user (eg, flaw in foo, developer of foo, etc) such that /run/user/0/Xauthority (ie, /etc/shadow) can be shipped off, copied to SNAP_DATA, etc

This sort of attack is precisely why I recommended validating the xauth file (ie, ValidateXauthority, below) but I think a nice hardening/defensive-coding measure would be to check if XAUTHORITY is in /tmp to help guard against if our validation code has a bug. The whole reason we are doing this is because /tmp is in a different mount namespace and some distros put it in there, setting XAUTHORITY. Let's just fix that rather than trying to be overly generic. As such, please, after you've stored off xauthPath (ie, right where this comment is), please add something to the effect of:

	// Abs() also calls Clean()
	// https://golang.org/pkg/path/filepath/#Abs
	xauthPathAbs, err := filepath.Abs(xauthPath)
	if err != nil {
		return nil
	}
	// remove all symlinks from path
	xauthPathCan, err := filepath.EvalSymlinks(xauthPathAbs)
	if err != nil {
		return nil
	}
	// verify XAUTHORITY matches the cleaned path
	if xauthPath != xauthPathCan {
		logger.Noticef("WARNING: %s != %s\n", xauthPath, xauthPathCan)
		return nil
	}
	// Only do the migration from /tmp since /tmp is what is in a different
	// mount namespace. There is a TOCTOU between this and the
	// copy below, but this is just a quick check before validating the file
	if !strings.HasPrefix(xauthPath, "/tmp") {
		logger.Noticef("WARNING: %s is not in /tmp", xauthPath)
		return nil
	}
@jdstrand

jdstrand Apr 13, 2017

Contributor

I don't like the TOCTOU here and will think about improving this bit.

@jdstrand

jdstrand Apr 13, 2017

Contributor

The way to do this is change the algorithm a bit. This also means we don't need a tempfile which is something Gustavo was looking for in the forum

  1. if defined and XAUTHORITY not zero length, open XAUTHORITY to get a file object and return nil if it doesn't exist
  2. perform all the above recommended checks on file.Name() (the name that was used to open the file) and return nil in case of shenanigans or not in /tmp
  3. read the contents from the file object
  4. if the file contents are the same as what is already in XDG_RUNTIME_DIR/.Xauthority, set XAUTHORITY (via snapenv?) and return
  5. otherwise, validate the content of the file object. if invalid, return nil with warning
  6. since valid, securely write it out to XDG_RUNTIME_DIR/.Xauthority, 0600 and owned by userCurrent (let's just set up the permissions securely and not worry about copying the perms). set XAUTHORITY (via snapenv?) and return
@morphis

morphis Apr 18, 2017

Contributor

@jdstrand I've implemented this now. Can you check if it matches now your expectations? The code is still a bit rough and tests are not adjusted but wanted to have these changes up as quick as possible to not loose time on your general feedback. Will work on optimizations and test cases next.

cmd/snap/cmd_run.go
+ }
+
+ // Copy Xauthority file into a temporary place so we can safely
+ // process it further there.
@jdstrand

jdstrand Apr 13, 2017

Contributor

Please add 'To avoid TOCTOU races, ...'

@morphis

morphis Apr 18, 2017

Contributor

Done.

cmd/snap/cmd_run.go
+ }
+
+ baseTargetDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid)
+ targetPath := filepath.Join(baseTargetDir, "Xauthority")
@jdstrand

jdstrand Apr 13, 2017

Contributor

I'm leaning towards this being '.Xauthority' instead. If we are going to pick a name, let's pick one that doesn't clutter up the user's XDG_RUNTIME_DIR.

@morphis

morphis Apr 18, 2017

Contributor

Done.

cmd/snap/cmd_run.go
+ targetPath := filepath.Join(baseTargetDir, "Xauthority")
+
+ // Only validate Xauthority file again when both files don't match
+ // ohterwise we can continue using the existing Xauthority file
@jdstrand

jdstrand Apr 13, 2017

Contributor

Can you append to this comment: "This is ok to do here because we aren't trying to protect against the user changing the Xauthority file in XDG_RUNTIME_DIR outside of snapd."

@morphis

morphis Apr 18, 2017

Contributor

Done.

cmd/snap/cmd_run.go
+ // Only validate Xauthority file again when both files don't match
+ // ohterwise we can continue using the existing Xauthority file
+ if osutil.FileExists(targetPath) && osutil.FilesAreEqual(targetPath, tmpXauthPath) {
+ osSetenv("XAUTHORITY", targetPath)
@jdstrand

jdstrand Apr 13, 2017

Contributor

Wondering if this should be in snap/snapenv/snapenv.go as it handles setting various other environment variables.

@morphis

morphis Apr 18, 2017

Contributor

Let me look at this and see what I can come up with.

cmd/snap/cmd_run.go
+
+ // Ensure that we have a valid Xauthority. It is invalid when
+ // either the data can't be parsed or there are no cookies in
+ // the file is invalid.
@jdstrand

jdstrand Apr 13, 2017

Contributor

Please adjust to be:

// To guard against setting XAUTHORITY to non-xauth files, check
// that we have a valid Xauthority. Specifically, the file must be
// parseable as an Xauthority file and not be empty.
@morphis

morphis Apr 18, 2017

Contributor

Done

cmd/snap/cmd_run.go
+ if err := os.MkdirAll(baseTargetDir, 0700); err != nil {
+ return err
+ }
+ }
@jdstrand

jdstrand Apr 13, 2017

Contributor

This is interesting because it is getting into the bug where snapd is not creating XDG_RUNTIME_DIR for the snap itself. I think we absolutely should set /run/user/<uid>/snap.$SNAP_NAME, but conditionally creating only /run/user/<uid> here is not correct because we are creating it only sometimes when we should be creating it all the time. Also, the permissions should be:

  • /run - 0755
  • /run/user - 0755
  • /run/user/<uid> - 0700
  • /run/user/<uid>/snap.$SNAP_NAME - 0700 (not implemented)

/run will (in practice) always exist, but it's conceivable /run/user may not on core if we ever don't use systemd (looks like src/login/logind-user.c in systemd creates this for us). Since we can assume /run and /run/user are created, I think that this should be changed to os.Mkdir() from os.MkdirAll() and moved somewhere outside of this function and before this function is called (then we can just error out/return nil if /run/user/<uid> doesn't exist). Where you move should also be where we create /run/user/<uid>/snap.$SNAP_NAME.

@morphis

morphis Apr 18, 2017

Contributor

I think the creation of a correct XDG_RUNTIME_DIR and where it is shouldn't be part of this PR and is a thing we need to debate separately. I will change the code here for now to not attempt to create XDG_RUNTIME_DIR. @jdstrand Is that ok for you?

interfaces/builtin/x11.go
+# Allow access to the user specific copy of the xauth file specified
+# in the XAUTHORITY environment variable, that "snap run" creates on
+# startup.
+owner /run/user/[0-9]*/Xauthority r,
@jdstrand

jdstrand Apr 13, 2017

Contributor

If changing to '.Xauthority', change this too.

+
+// ValidateXauthority validates a given Xauthority file. The file is valid
+// if it can be parsed and contains at least one cookie.
+func ValidateXauthority(path string) error {
@jdstrand

jdstrand Apr 13, 2017

Contributor

A cheap check would be to verify it is owned by user.Current.

@morphis

morphis Apr 18, 2017

Contributor

Added in migrateXauthority as this is just about validation of any xauth file regardless who owns it.

x11/xauth.go
+ }
+ // FIXME we can do further validation of the cookies like
+ // checking for valid families etc.
+ cookies += 1
@jdstrand

jdstrand Apr 13, 2017

Contributor

golint says to use cookies++ instead.

@morphis

morphis Apr 18, 2017

Contributor

Done.

I still want to take a closer look at the validate code, but here is the feedback you requested.

cmd/snap/cmd_run.go
+func migrateXauthority(info *snap.Info) error {
+ u, err := userCurrent()
+ if err != nil {
+ return fmt.Errorf(i18n.G("cannot get the current user: %s"), err)
@jdstrand

jdstrand Apr 18, 2017

Contributor

Why are you using i18n.G() here but nowhere else?

cmd/snap/cmd_run.go
+ baseTargetDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid)
+ if !osutil.FileExists(baseTargetDir) {
+ return fmt.Errorf("Target directory %s does not exist", baseTargetDir)
+ }
@jdstrand

jdstrand Apr 18, 2017

Contributor

I like the intent here, but migrateXauthority is currently unconditionally called for every snap-confine invocation (regardless of if it is needed) and while systemd seems to create /run/user/<uid> for user sessions, it does not for root so this will break daemons and invocations using sudo.

cmd/snap/cmd_run.go
+ fin, err := os.Open(xauthPath)
+ if err != nil {
+ return err
+ }
@jdstrand

jdstrand Apr 18, 2017

Contributor

This leaks the file. Can you defer the close?

cmd/snap/cmd_run.go
+
+ // Verify XAUTHORITY matches the cleaned path
+ if fin.Name() != xauthPathCan {
+ logger.Noticef("WARNING: %s != %s\n", xauthPath, xauthPathCan)
@jdstrand

jdstrand Apr 18, 2017

Contributor

Can you use fin.Name() instead of xauthPath in ther warning?

cmd/snap/cmd_run.go
+ // Only do the migration from /tmp since /tmp is what is in a different
+ // mount namespace. There is a TOCTOU between this and the
+ // copy below, but this is just a quick check before validating the file
+ if !strings.HasPrefix(fin.Name(), "/tmp") {
@jdstrand

jdstrand Apr 18, 2017

Contributor

Can you use '/tmp/' here? You can remove the TOCTOU comment since you are working on an open file now.

cmd/snap/cmd_run.go
+ }
+
+ // Ensure that the file is owned by the current user
+ fi, err := fin.Stat()
@jdstrand

jdstrand Apr 18, 2017

Contributor

You are using fin.Stat() here and it is using fin.Name() but note the actual fstat() system call is done here, not at the time of the open so any chown()s done to the file after the open but before this fstat() will apply (ie, if chown to root between the open and the fstat, the fstat will show as root or if chown to non-root between the open and the fstat, the fstat will show non-root). I think the check is fine where it is though since we aren't trying to protect the user from herself. If the user has rights to chown the file, then we'll continue to honor that, but I think there needs to be a comment. Please use:

	// We are performing a Stat() here to make sure that the user can't
	// steal another user's Xauthority file. Note that while Stat() uses
	// fstat() on the file descriptor created during Open(), the file might
	// have changed ownership between the Open() and the Stat(). That's ok
	// because we aren't trying to block access that the user already has:
	// if the user has the privileges to chown another user's Xauthority
	// file, we won't block that since the user can just steal it without
	// having to use snap run. This code is just to ensure that a user who
	// doesn't have those privileges can't steal the file via snap run
	// (also note that the (potentially untrusted) snap isn't running yet).
cmd/snap/cmd_run.go
+ fi, err := fin.Stat()
+ if err != nil {
+ return err
+ } else {
@jdstrand

jdstrand Apr 18, 2017

Contributor

You can drop this else and move everything over since the above error returns.

cmd/snap/cmd_run.go
+ return fmt.Errorf("Can't validate file owner")
+ }
+ // cheap comparison but better to convert uid to a string than
+ // a string into a number.
@jdstrand

jdstrand Apr 18, 2017

Contributor

Can you clarify this comment by saying that u.Uid is a string which is why you are converting the stat to a uid?

@morphis

morphis Apr 19, 2017

Contributor

Done

cmd/snap/cmd_run.go
+ // cheap comparison but better to convert uid to a string than
+ // a string into a number.
+ if fmt.Sprintf("%d", sys.(*syscall.Stat_t).Uid) != u.Uid {
+ return fmt.Errorf("Xauthority file isn't owned by the current user")
@jdstrand

jdstrand Apr 18, 2017

Contributor

Can you show the uid in this error message as well?

@jdstrand

jdstrand Apr 18, 2017

Contributor

Also, while I definitely like this check, I'm not 100% convinced we should error here. Should we warn and return nil (ie, skip migration) instead?

cmd/snap/cmd_run.go
+ if fout, err = os.Open(targetPath); err != nil {
+ return err
+ }
+ if osutil.FileStreamsEqual(fin, fout) {
@jdstrand

jdstrand Apr 18, 2017

Contributor

Nice :)

cmd/snap/cmd_run.go
+ if osutil.FileStreamsEqual(fin, fout) {
+ osSetenv("XAUTHORITY", targetPath)
+ return nil
+ }
@jdstrand

jdstrand Apr 18, 2017

Contributor

fout leaked here. I would say to defer the close, but you reuse fout below by reopening the file. You either need to rework the open logic, use two different vars or explicitly close here.

cmd/snap/cmd_run.go
+ if err := x11.ValidateXauthority(fin.Name()); err != nil {
+ logger.Noticef("WARNING: invalid Xauthority file: %s", err)
+ return nil
+ }
@jdstrand

jdstrand Apr 18, 2017

Contributor

You need to pass fin here, not fin.Name(), to avoid the TOCTOU issues.

cmd/snap/cmd_run.go
+ return nil
+ }
+
+ fout, err = os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE, 0600)
@jdstrand

jdstrand Apr 18, 2017

Contributor

This file needs to be securely created so it really needs os.O_EXCL, however the program logic atm means this file might exist (with different contents from XAUTHORITY). I suggest deleting the file and then using O_EXCL. Otherwise, create a temporary file somewhere else and move it into place (eg, via osutil.AtomicWriteFile).

This looks very good, very careful and thoughtful code! Still some comments, mostly nitpick and questions (sorry for that!), but this super close to be ready.

cmd/snap/cmd_run.go
+ return "", nil
+ }
+
+ // Verify XAUTHORITY matches the cleaned path
@mvo5

mvo5 Apr 21, 2017

Collaborator

I had a bit of trouble understanding what is going on here, i.e. why this is done etc. A small comment would be helpful I think. Maybe something like: Ensure the XAUTHORITY env is not abused by checking that it point to exactly the file we just opened (no symlinks, no funny "../.." etc). Or a better command, maybe @jdstrand has some input.

@morphis

morphis Apr 21, 2017

Contributor

Done.

cmd/snap/cmd_run.go
+ }
+
+ // Only do the migration from /tmp since /tmp is what is in a different
+ // mount namespace.
@mvo5

mvo5 Apr 21, 2017

Collaborator

Maybe: Only do the migration from /tmp since the real /tmp is not visible for snaps ?

@morphis

morphis Apr 21, 2017

Contributor

Done.

cmd/snap/cmd_run.go
+ return "", nil
+ }
+
+ os.Remove(targetPath)
@mvo5

mvo5 Apr 21, 2017

Collaborator

We probably want to check for errors here, even if we just log them.

@morphis

morphis Apr 21, 2017

Contributor

Done.

cmd/snap/cmd_run.go
+ return "", err
+ }
+
+ // Read data from the beginning of the file
@mvo5

mvo5 Apr 21, 2017

Collaborator

(nitpick): I would move this up before the OpenFile() that openfile,copy,close are next to each other.

@morphis

morphis Apr 21, 2017

Contributor

Done.

cmd/snap/cmd_run.go
+ }
+
+ // Read and write validated Xauthority file to its right location
+ buf := make([]byte, 1024)
@mvo5

mvo5 Apr 21, 2017

Collaborator

Could this be written as:

if _, err = io.Copy(fout, fin); err != nil {
		fout.Close()
                // FIXME: err handling
		os.Remove(targetPath)
		return "", fmt.Errorf(i18n.G("Failed to write new Xauthority file at %s"), targetPath)
}

or is there a specific reason for doing it "manually"?

@morphis

morphis Apr 21, 2017

Contributor

Changed to io.Copy. Didn't know about that one :-)

cmd/snap/cmd_run_test.go
+ c.Assert(err, check.IsNil)
+
+ // Ensure XDG_RUNTIME_DIR exists for the user we're testing with
+ os.MkdirAll(filepath.Join(dirs.XdgRuntimeDirBase, u.Uid), 0700)
@mvo5

mvo5 Apr 21, 2017

Collaborator

We probably want err = os.MkdirAll(...); c.Assert(err,IsNil) here. Just for good measure :)

@morphis

morphis Apr 21, 2017

Contributor

Done

cmd/snap/cmd_run_test.go
+ defer restorer()
+
+ // Ensure we have XDG_RUNTIME_DIR for the current user
+ os.MkdirAll(filepath.Join(dirs.XdgRuntimeDirBase, u.Uid), 0700)
@mvo5

mvo5 Apr 21, 2017

Collaborator

Is that the same os.MkdirAll() as a few lines above? Or a different one? If a different one, maybe a small comment whats different about it?

@morphis

morphis Apr 21, 2017

Contributor

Dropped that line.

tests/main/xauth-migration/task.yaml
+ snap install hello-world
+
+ ensure_xauth_path() {
+ export XAUTHORITY=$1
@mvo5

mvo5 Apr 21, 2017

Collaborator

(nitpick, sorry, shell makes my grumpy ;) - generally I think export XAUTHORITY="$1" (i.e. quotes) is better. However given that its a test etc this is nitpick^99 territory.

One tiny reshuffle and a question :)

cmd/snap/cmd_run.go
+ }
+
+ fout, err = os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
+ defer fout.Close()
@mvo5

mvo5 Apr 21, 2017

Collaborator

This needs to be after the "if err != nil"check, if err != nil then *os.File is nil and this would crash.

@morphis

morphis Apr 21, 2017

Contributor

Done.

cmd/snap/cmd_run.go
+
+ // Read and write validated Xauthority file to its right location
+ if _, err = io.Copy(fout, fin); err != nil {
+ err := os.Remove(targetPath)
@mvo5

mvo5 Apr 21, 2017

Collaborator

This could be a single line if err := os.Remove(...); err != nill. Also maybe slightly confusing, if the copy fails and the remove also fails we swallow one of the errors. Maybe logger.Notice about the remove failure? Or going back to your original code and simply ignoring the error. Sorry for complicating things.

@morphis

morphis Apr 21, 2017

Contributor

Done.

mvo5 approved these changes Apr 21, 2017

I've reviewed the parsing algorithm as well as all the requested file operations and the overall approach and implementation look really good. Thanks! From my point of view, just a few small things and testsuite additions.

+ // string than a string into a number.
+ if fmt.Sprintf("%d", sys.(*syscall.Stat_t).Uid) != u.Uid {
+ return "", fmt.Errorf(i18n.G("Xauthority file isn't owned by the current user %s"), u.Uid)
+ }
@jdstrand

jdstrand Apr 21, 2017

Contributor

Didn't get an answer to my previous question, so asking again: "Also, while I definitely like this check, I'm not 100% convinced we should error here. Should we warn and return nil (ie, skip migration) instead?"

@morphis

morphis Apr 24, 2017

Contributor

That is what we do already. This part returns and error which is logged by the caller (see runSnapConfine) as a warning but the app is still being executed. In terms of Xauthority migration this is an error but we leave it up the caller here to decide what should happen when the migration fails.

@jdstrand

jdstrand Apr 24, 2017

Contributor

Oh, duh-- not sure why I added that comment-- maybe I was looking at an out of date tab... Anyway, yes, you addressed it.

@morphis

morphis Apr 24, 2017

Contributor

No problem, we figured it out :-)

cmd/snap/cmd_run.go
+ return "", nil
+ }
+
+ if osutil.FileExists(targetPath) {
@jdstrand

jdstrand Apr 21, 2017

Contributor

A comment here would be nice. Perhaps "Replace the file securely by removing and creating with O_CREATE|O_EXCL"

cmd/snap/cmd_run.go
+ if err != nil {
+ return "", err
+ }
+ defer fout.Close()
@jdstrand

jdstrand Apr 21, 2017

Contributor

It's a bit weird that you os.Open()d above using fout, deferred the Close() then os.OpenFile()d here using fout and defer this Close(). Perhaps with os.Open(), above, just explicitly Close() so there is no confusion?

@morphis

morphis Apr 24, 2017

Contributor

Thanks for the comment. The code was indeed a bit confusing. Reworked it now.

+ # Data
+ echo -n -e \\x00\\x01\\xff >> $1
+ done
+ }
@jdstrand

jdstrand Apr 21, 2017

Contributor

For the spread tests, I suggest using a real Xauthority file generated with xauth otherwise you are mocking data for precisely the way the code is written (mocking in this way in the unit tests is fine imo). Eg:

rm -f "$1"
touch "$1"
chmod 600 "$1"
xauth -f "$1" add localhost:0 . 00112233445566778899aabbccddeeff
xauth -f "$1" list

Using xauth add you don't need a running X server.

@morphis

morphis Apr 24, 2017

Contributor

We can add this as additional test but we don't have xauth available on core type systems. I will add this as additional test case for non core systems.

@jdstrand

jdstrand Apr 24, 2017

Contributor

Sure, that's fine, thanks.

+ # Xauthority should be correctly migrated
+ ensure_xauth_path /tmp/valid-xauthority /run/user/0/.Xauthority
+ test -e /run/user/0/.Xauthority
+
@jdstrand

jdstrand Apr 21, 2017

Contributor

Adding a test for an xauth file that has 2 cookies would be good to, so make sure they all got copied and not just one.

@jdstrand

jdstrand Apr 24, 2017

Contributor

I see that now, thanks.

+// https://cgit.freedesktop.org/xorg/lib/libXau/tree/include/X11/Xauth.h
+// for details about the actual file format.
+type xauth struct {
+ Family uint16
@jdstrand

jdstrand Apr 21, 2017

Contributor

I guess you chose this to make sure the C 'unsigned short' would always fit? I'm not terribly excited about this assumption since I believe some standards state unsigned short is a minimum of 16 bits. However, C99 and C11 both state the size of an unsigned int as precisely 16 bits. Anecdotally, I double checked the sizeof(unsigned short) on s390x, powerpc, ppc64el, i386, amd64, armhf, and arm64 and it as indeed 16 bits. As such, I think this is fine as is.

@morphis

morphis Apr 24, 2017

Contributor

Good, thanks for the comment.

x11/xauth.go
+ return err
+ } else if n != 2 {
+ return fmt.Errorf("Could not read enough bytes")
+ }
@jdstrand

jdstrand Apr 21, 2017

Contributor

These 7 lines of code are duplicated here at the top of readChunk and extremely similar in the next part in readChunk. I wonder if it would make sense to abstract this out into readBytes(n int), then you call readBytes(2) to get the Family, readBytes(2) to get the size then readBytes(size) to get the chunk.

Also, you reference the X sources (which is great!) but it would be good to explicitly comment that Family is two bytes, and len for the strings is two bytes.

@morphis

morphis Apr 24, 2017

Contributor

Added a function readBytes and a comment to explain the lengths used.

x11/xauth_test.go
+ c.Assert(err, IsNil)
+ err = x11.ValidateXauthority(path)
+ c.Assert(err, IsNil)
+}
@jdstrand

jdstrand Apr 21, 2017

Contributor

Adding a unit test with two cookies in the xauth file would be good too.

@morphis

morphis Apr 24, 2017

Contributor

Done.

+
+ # Generate valid Xauthority file which `snap run` will accept
+ mock_xauthority /tmp/valid-xauthority 4
+ chmod 600 /tmp/valid-xauthority
@jdstrand

jdstrand Apr 21, 2017

Contributor

One more thing, doing a sha256sum on the xauth in /tmp against the one in XDG_RUNTIME_DIR would be great here. I anecdotally checked exactly this on s390x (big endien), ppc64el, i386, amd64, armhf, and arm64 (all little endien) to blackbox test it, so it is good, but a spread test would ensure it stays good. :)

@morphis

morphis Apr 25, 2017

Contributor

Done.

Contributor

morphis commented Apr 24, 2017

@jdstrand Thanks for all the comments. Will rework necessary portions today.

I'm going to mark this as 'Approved' since all that is needed are a couple of testsuite additions and removing a bit of redundant code.

cmd/snap/cmd_run.go
+ if err != nil {
+ logger.Noticef("WARNING: failed to remove existing file %s", targetPath)
+ }
+ }
@jdstrand

jdstrand Apr 24, 2017

Contributor

I'm not sure if the os.Remove() at line 327 was added later or I just didn't notice it, but with it and the O_CREAT|O_EXCL, you don't need this stanza (it is unreachable; though you probably want the error checking from here moved up to line 327 since it doesn't have it).

@morphis

morphis Apr 25, 2017

Contributor

Dropped and moved the logger.Noticef call a bit up.

x11/xauth.go
+ if err := readBytes(f, b[:]); err != nil {
+ return err
+ }
+ xa.Family = binary.BigEndian.Uint16(b[:])
@jdstrand

jdstrand Apr 24, 2017

Contributor

A comment here on the family length like you did for readChunk would be nice.

Simon Fels added some commits Apr 3, 2017

cmd/snap: make users Xauthority file available in snap environment
Not all desktop environments set XAUTHORITY to something which is available
inside the environment we create for snaps. Some place the authority file
inside /tmp which a snap doesn't share with the host system. To workaround
this we copy the file XAUTHORITY points to when the snap is started into a
snap specific location in XDG_RUNTIME_DIR of the current user and allow
access via the x11 interface. This will work across all available desktop
environments.

@mvo5 mvo5 merged commit 2f300e5 into snapcore:master Apr 25, 2017

6 checks passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
xenial-amd64 autopkgtest finished (success)
Details
xenial-i386 autopkgtest finished (success)
Details
xenial-ppc64el autopkgtest finished (success)
Details
yakkety-amd64 autopkgtest finished (success)
Details
zesty-amd64 autopkgtest finished (success)
Details

@morphis Thank you very much for implementing this.

A few simple late notes, mostly about error handling but a few about other details. Can we please make sure these are handled soon so the next release takes them in?

Thanks!

+
+ fin, err := os.Open(xauthPath)
+ if err != nil {
+ return "", err
@niemeyer

niemeyer Apr 25, 2017

Contributor

Message needs to point out that this is about the Xauthority file, as the filename might not give a hint. So user ends up with a "cannot open" message without knowing why it's even trying to open.

@morphis

morphis Apr 26, 2017

Contributor

Not really. If you look into runSnapConfine from there is already a logger.Noticef call which puts a message around this error.

+ // it point to exactly the file we just opened (no symlinks,
+ // no funny "../.." etc)
+ if fin.Name() != xauthPathCan {
+ logger.Noticef("WARNING: %s != %s\n", fin.Name(), xauthPathCan)
@niemeyer

niemeyer Apr 25, 2017

Contributor

This is too vague. Something along those lines would be better:

("XAUTHORITY environment value is not a clean path: %q", xuathPathCan)

@morphis

morphis Apr 26, 2017

Contributor

Done

+ }
+ sys := fi.Sys()
+ if sys == nil {
+ return "", fmt.Errorf(i18n.G("Can't validate file owner"))
@niemeyer

niemeyer Apr 25, 2017

Contributor

"Can't" => "cannot", and needs to include more details about which file that is.

@morphis

morphis Apr 26, 2017

Contributor

Done

+
+ fout.Close()
+ if err := os.Remove(targetPath); err != nil {
+ logger.Noticef("WARNING: failed to remove existing file %s", targetPath)
@niemeyer

niemeyer Apr 25, 2017

Contributor

Here we apparently warn and then return the same error, which is then warned again from the call site.

@morphis

morphis Apr 26, 2017

Contributor

Changed.

+ // that we have a valid Xauthority. Specifically, the file must be
+ // parseable as an Xauthority file and not be empty.
+ if err := x11.ValidateXauthorityFromFile(fin); err != nil {
+ logger.Noticef("WARNING: invalid Xauthority file: %s", err)
@niemeyer

niemeyer Apr 25, 2017

Contributor

It's not clear why/when do we sometimes return the error and warn above, and sometimes warn and return nil. It'd be good to make this more consistent. If we're warning from the call site, let's return the error here as well, since this is an error similar to the other cases here, and let the call site warn too.

@morphis

morphis Apr 26, 2017

Contributor

I am return in most cases now an error and only print a warning when we can't return an error. Please check if the implementation now fits what you were thinking about.

+ // Read and write validated Xauthority file to its right location
+ if _, err = io.Copy(fout, fin); err != nil {
+ if err := os.Remove(targetPath); err != nil {
+ logger.Noticef("WARNING: failed to remove file at %s", targetPath)
@niemeyer

niemeyer Apr 25, 2017

Contributor

"failed to" => "cannot", and the original error is useful to include after a colon at the end, so we know why we could not remove it.

@morphis

morphis Apr 26, 2017

Contributor

Done.

+ if err := os.Remove(targetPath); err != nil {
+ logger.Noticef("WARNING: failed to remove file at %s", targetPath)
+ }
+ return "", fmt.Errorf(i18n.G("Failed to write new Xauthority file at %s"), targetPath)
@niemeyer

niemeyer Apr 25, 2017

Contributor

"Failed to" => "cannot", and the original error is useful to include after a colon at the end, so we know why we could not write it.

@morphis

morphis Apr 26, 2017

Contributor

Done

-func streamsEqual(fa, fb io.Reader) bool {
+// FileStreamsEqual compares two file streams and returns true if both
+// have the same content.
+func FileStreamsEqual(fa, fb io.Reader) bool {
@niemeyer

niemeyer Apr 25, 2017

Contributor

Those are really streams rather than files, or Readers if we want to be more precise. Files are *os.File.

This also seems somewhat unrelated to the operating system, but we can easily reorganize later. Let's please just fix the name since we changed it in this PR.

@morphis

morphis Apr 26, 2017

Contributor

Done

@@ -34,14 +34,17 @@ import (
//
// It merges it with the existing os.Environ() and ensures the SNAP_*
// overrides the any pre-existing environment variables.
-func ExecEnv(info *snap.Info) []string {
+func ExecEnv(info *snap.Info, extra map[string]string) []string {
@niemeyer

niemeyer Apr 25, 2017

Contributor

New parameter should be documented.

@morphis

morphis Apr 26, 2017

Contributor

Done

+ Data []byte
+}
+
+func readBytes(f *os.File, data []byte) error {
@niemeyer

niemeyer Apr 25, 2017

Contributor

This looks like a reimplementation of io.ReadFull, except it lacks the handling of retries (EAGAIN, etc).

@morphis

morphis Apr 26, 2017

Contributor

Changed to io.ReadFull

+ }
+
+ if n != len(data) {
+ return fmt.Errorf("Could not read enough bytes")
@niemeyer

niemeyer Apr 25, 2017

Contributor

failed to / could not / can't / unable to => cannot

Our errors are always lowercased as well unless there's a good reason not to be.

@morphis

morphis Apr 26, 2017

Contributor

DOne

+ }
+
+ size := int(binary.BigEndian.Uint16(b[:]))
+ chunk := make([]byte, size)
@niemeyer

niemeyer Apr 25, 2017

Contributor

This should do some trivial validation of that size before attempting to allocate memory after it.

+
+// ValidateXauthority validates a given Xauthority file. The file is valid
+// if it can be parsed and contains at least one cookie.
+func ValidateXauthorityFromFile(f *os.File) error {
@niemeyer

niemeyer Apr 25, 2017

Contributor

Do we need to take a file, or is an io.Reader enough?

If so, I suggest naming this one as ValidateXauthority taking an io.Reader, and the other one as ValidateXauthorityFile taking a path.

@morphis

morphis Apr 26, 2017

Contributor

Done

Contributor

morphis commented Apr 25, 2017

@niemeyer Will do! Thanks for those comments!

morphis pushed a commit to morphis/snapd that referenced this pull request Apr 26, 2017

morphis pushed a commit to morphis/snapd that referenced this pull request Apr 26, 2017

morphis pushed a commit to morphis/snapd that referenced this pull request Apr 27, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment