github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/overlord/snapshotstate/backend/helpers.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package backend
    21  
    22  import (
    23  	"archive/zip"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"os/exec"
    28  	"os/user"
    29  	"path/filepath"
    30  	"strconv"
    31  	"strings"
    32  	"syscall"
    33  
    34  	"github.com/snapcore/snapd/client"
    35  	"github.com/snapcore/snapd/dirs"
    36  	"github.com/snapcore/snapd/logger"
    37  	"github.com/snapcore/snapd/osutil/sys"
    38  )
    39  
    40  func zipMember(f *os.File, member string) (r io.ReadCloser, sz int64, err error) {
    41  	// rewind the file
    42  	// (shouldn't be needed, but doesn't hurt too much)
    43  	if _, err := f.Seek(0, 0); err != nil {
    44  		return nil, -1, err
    45  	}
    46  
    47  	fi, err := f.Stat()
    48  	if err != nil {
    49  		return nil, -1, err
    50  	}
    51  
    52  	arch, err := zip.NewReader(f, fi.Size())
    53  	if err != nil {
    54  		return nil, -1, err
    55  	}
    56  
    57  	for _, fh := range arch.File {
    58  		if fh.Name == member {
    59  			r, err = fh.Open()
    60  			return r, int64(fh.UncompressedSize64), err
    61  		}
    62  	}
    63  
    64  	return nil, -1, fmt.Errorf("missing archive member %q", member)
    65  }
    66  
    67  func userArchiveName(usr *user.User) string {
    68  	return filepath.Join(userArchivePrefix, usr.Username+userArchiveSuffix)
    69  }
    70  
    71  func isUserArchive(entry string) bool {
    72  	return strings.HasPrefix(entry, userArchivePrefix) && strings.HasSuffix(entry, userArchiveSuffix)
    73  }
    74  
    75  func entryUsername(entry string) string {
    76  	// this _will_ panic if !isUserArchive(entry)
    77  	return entry[len(userArchivePrefix) : len(entry)-len(userArchiveSuffix)]
    78  }
    79  
    80  type bySnap []*client.Snapshot
    81  
    82  func (a bySnap) Len() int           { return len(a) }
    83  func (a bySnap) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
    84  func (a bySnap) Less(i, j int) bool { return a[i].Snap < a[j].Snap }
    85  
    86  type byID []client.SnapshotSet
    87  
    88  func (a byID) Len() int           { return len(a) }
    89  func (a byID) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
    90  func (a byID) Less(i, j int) bool { return a[i].ID < a[j].ID }
    91  
    92  var (
    93  	userLookup   = user.Lookup
    94  	userLookupId = user.LookupId
    95  )
    96  
    97  func isUnknownUser(err error) bool {
    98  	switch err.(type) {
    99  	case user.UnknownUserError, user.UnknownUserIdError:
   100  		return true
   101  	default:
   102  		return false
   103  	}
   104  }
   105  
   106  func usersForUsernamesImpl(usernames []string) ([]*user.User, error) {
   107  	if len(usernames) == 0 {
   108  		return allUsers()
   109  	}
   110  	users := make([]*user.User, 0, len(usernames))
   111  	for _, username := range usernames {
   112  		usr, err := userLookup(username)
   113  		if err != nil {
   114  			// Treat all non-nil errors as user.Unknown{User,Group}Error's, as
   115  			// currently Go's handling of returned errno from get{pw,gr}nam_r
   116  			// in the cgo implementation of user.Lookup is lacking, and thus
   117  			// user.Unknown{User,Group}Error is returned only when errno is 0
   118  			// and the list of users/groups is empty, but as per the man page
   119  			// for get{pw,gr}nam_r, there are many other errno's that typical
   120  			// systems could return to indicate that the user/group wasn't
   121  			// found, however unfortunately the POSIX standard does not actually
   122  			// dictate what errno should be used to indicate "user/group not
   123  			// found", and so even if Go is more robust, it may not ever be
   124  			// fully robust. See from the man page:
   125  			//
   126  			// > It [POSIX.1-2001] does not call "not found" an error, hence
   127  			// > does not specify what value errno might have in this situation.
   128  			// > But that makes it impossible to recognize errors.
   129  			//
   130  			// See upstream Go issue: https://github.com/golang/go/issues/40334
   131  			u, e := userLookupId(username)
   132  			if e != nil {
   133  				// return first error, as it's usually clearer
   134  				return nil, err
   135  			}
   136  			usr = u
   137  		}
   138  		users = append(users, usr)
   139  
   140  	}
   141  	return users, nil
   142  }
   143  
   144  func allUsers() ([]*user.User, error) {
   145  	ds, err := filepath.Glob(dirs.SnapDataHomeGlob)
   146  	if err != nil {
   147  		// can't happen?
   148  		return nil, err
   149  	}
   150  
   151  	users := make([]*user.User, 1, len(ds)+1)
   152  	root, err := user.LookupId("0")
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	users[0] = root
   157  	seen := make(map[uint32]bool, len(ds)+1)
   158  	seen[0] = true
   159  	var st syscall.Stat_t
   160  	for _, d := range ds {
   161  		err := syscall.Stat(d, &st)
   162  		if err != nil {
   163  			continue
   164  		}
   165  		if seen[st.Uid] {
   166  			continue
   167  		}
   168  		seen[st.Uid] = true
   169  		usr, err := userLookupId(strconv.FormatUint(uint64(st.Uid), 10))
   170  		if err != nil {
   171  			// Treat all non-nil errors as user.Unknown{User,Group}Error's, as
   172  			// currently Go's handling of returned errno from get{pw,gr}nam_r
   173  			// in the cgo implementation of user.Lookup is lacking, and thus
   174  			// user.Unknown{User,Group}Error is returned only when errno is 0
   175  			// and the list of users/groups is empty, but as per the man page
   176  			// for get{pw,gr}nam_r, there are many other errno's that typical
   177  			// systems could return to indicate that the user/group wasn't
   178  			// found, however unfortunately the POSIX standard does not actually
   179  			// dictate what errno should be used to indicate "user/group not
   180  			// found", and so even if Go is more robust, it may not ever be
   181  			// fully robust. See from the man page:
   182  			//
   183  			// > It [POSIX.1-2001] does not call "not found" an error, hence
   184  			// > does not specify what value errno might have in this situation.
   185  			// > But that makes it impossible to recognize errors.
   186  			//
   187  			// See upstream Go issue: https://github.com/golang/go/issues/40334
   188  			continue
   189  		} else {
   190  			users = append(users, usr)
   191  		}
   192  	}
   193  
   194  	return users, nil
   195  }
   196  
   197  var (
   198  	sysGeteuid   = sys.Geteuid
   199  	execLookPath = exec.LookPath
   200  )
   201  
   202  func pickUserWrapper() string {
   203  	// runuser and sudo happen to work the same way in this case.  The main
   204  	// reason to prefer runuser over sudo is that runuser is part of
   205  	// util-linux, which is considered essential, whereas sudo is an addon
   206  	// which could be removed.  However util-linux < 2.23 does not have
   207  	// runuser, and we support some distros that ship things older than that
   208  	// (e.g. Ubuntu 14.04)
   209  	for _, cmd := range []string{"runuser", "sudo"} {
   210  		if lp, err := execLookPath(cmd); err == nil {
   211  			return lp
   212  		}
   213  	}
   214  	return ""
   215  }
   216  
   217  var userWrapper = pickUserWrapper()
   218  
   219  // tarAsUser returns an exec.Cmd that will, if the current effective user id is
   220  // 0 and username is not "root", and if either runuser(1) or sudo(8) are found
   221  // on the PATH, run tar as the given user.
   222  //
   223  // If the effective user id is not 0, or username is "root", exec.Command is
   224  // used directly; changing the user id would fail (in the first case) or be a
   225  // no-op (in the second).
   226  //
   227  // If neither runuser nor sudo are found on the path, exec.Command is also used
   228  // directly. This will result in tar running as root in this situation (so it
   229  // will fail if on NFS; I don't think there's an attack vector though).
   230  func tarAsUser(username string, args ...string) *exec.Cmd {
   231  	if sysGeteuid() == 0 && username != "root" {
   232  		if userWrapper != "" {
   233  			uwArgs := make([]string, len(args)+5)
   234  			uwArgs[0] = userWrapper
   235  			uwArgs[1] = "-u"
   236  			uwArgs[2] = username
   237  			uwArgs[3] = "--"
   238  			uwArgs[4] = "tar"
   239  			copy(uwArgs[5:], args)
   240  			return &exec.Cmd{
   241  				Path: userWrapper,
   242  				Args: uwArgs,
   243  			}
   244  		}
   245  		// TODO: use warnings instead
   246  		logger.Noticef("No user wrapper found; running tar for user data as root. Please make sure 'sudo' or 'runuser' (from util-linux) is on $PATH to avoid this.")
   247  	}
   248  
   249  	return exec.Command("tar", args...)
   250  }
   251  
   252  func MockUserLookup(newLookup func(string) (*user.User, error)) func() {
   253  	oldLookup := userLookup
   254  	userLookup = newLookup
   255  	return func() {
   256  		userLookup = oldLookup
   257  	}
   258  }