github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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 if !isUnknownUser(err) { 115 return nil, err 116 } 117 u, e := userLookupId(username) 118 if e != nil { 119 // return first error, as it's usually clearer 120 return nil, err 121 } 122 usr = u 123 } 124 users = append(users, usr) 125 126 } 127 return users, nil 128 } 129 130 func allUsers() ([]*user.User, error) { 131 ds, err := filepath.Glob(dirs.SnapDataHomeGlob) 132 if err != nil { 133 // can't happen? 134 return nil, err 135 } 136 137 users := make([]*user.User, 1, len(ds)+1) 138 root, err := user.LookupId("0") 139 if err != nil { 140 return nil, err 141 } 142 users[0] = root 143 seen := make(map[uint32]bool, len(ds)+1) 144 seen[0] = true 145 var st syscall.Stat_t 146 for _, d := range ds { 147 err := syscall.Stat(d, &st) 148 if err != nil { 149 continue 150 } 151 if seen[st.Uid] { 152 continue 153 } 154 seen[st.Uid] = true 155 usr, err := userLookupId(strconv.FormatUint(uint64(st.Uid), 10)) 156 if err != nil { 157 if !isUnknownUser(err) { 158 return nil, err 159 } 160 } else { 161 users = append(users, usr) 162 } 163 } 164 165 return users, nil 166 } 167 168 var ( 169 sysGeteuid = sys.Geteuid 170 execLookPath = exec.LookPath 171 ) 172 173 func pickUserWrapper() string { 174 // runuser and sudo happen to work the same way in this case. The main 175 // reason to prefer runuser over sudo is that runuser is part of 176 // util-linux, which is considered essential, whereas sudo is an addon 177 // which could be removed. However util-linux < 2.23 does not have 178 // runuser, and we support some distros that ship things older than that 179 // (e.g. Ubuntu 14.04) 180 for _, cmd := range []string{"runuser", "sudo"} { 181 if lp, err := execLookPath(cmd); err == nil { 182 return lp 183 } 184 } 185 return "" 186 } 187 188 var userWrapper = pickUserWrapper() 189 190 // tarAsUser returns an exec.Cmd that will, if the current effective user id is 191 // 0 and username is not "root", and if either runuser(1) or sudo(8) are found 192 // on the PATH, run tar as the given user. 193 // 194 // If the effective user id is not 0, or username is "root", exec.Command is 195 // used directly; changing the user id would fail (in the first case) or be a 196 // no-op (in the second). 197 // 198 // If neither runuser nor sudo are found on the path, exec.Command is also used 199 // directly. This will result in tar running as root in this situation (so it 200 // will fail if on NFS; I don't think there's an attack vector though). 201 func tarAsUser(username string, args ...string) *exec.Cmd { 202 if sysGeteuid() == 0 && username != "root" { 203 if userWrapper != "" { 204 uwArgs := make([]string, len(args)+5) 205 uwArgs[0] = userWrapper 206 uwArgs[1] = "-u" 207 uwArgs[2] = username 208 uwArgs[3] = "--" 209 uwArgs[4] = "tar" 210 copy(uwArgs[5:], args) 211 return &exec.Cmd{ 212 Path: userWrapper, 213 Args: uwArgs, 214 } 215 } 216 // TODO: use warnings instead 217 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.") 218 } 219 220 return exec.Command("tar", args...) 221 } 222 223 func MockUserLookup(newLookup func(string) (*user.User, error)) func() { 224 oldLookup := userLookup 225 userLookup = newLookup 226 return func() { 227 userLookup = oldLookup 228 } 229 }