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 }