gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/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 usersForUsernamesImpl(usernames []string) ([]*user.User, error) { 98 if len(usernames) == 0 { 99 return allUsers() 100 } 101 users := make([]*user.User, 0, len(usernames)) 102 for _, username := range usernames { 103 usr, err := userLookup(username) 104 if err != nil { 105 // Treat all non-nil errors as user.Unknown{User,Group}Error's, as 106 // currently Go's handling of returned errno from get{pw,gr}nam_r 107 // in the cgo implementation of user.Lookup is lacking, and thus 108 // user.Unknown{User,Group}Error is returned only when errno is 0 109 // and the list of users/groups is empty, but as per the man page 110 // for get{pw,gr}nam_r, there are many other errno's that typical 111 // systems could return to indicate that the user/group wasn't 112 // found, however unfortunately the POSIX standard does not actually 113 // dictate what errno should be used to indicate "user/group not 114 // found", and so even if Go is more robust, it may not ever be 115 // fully robust. See from the man page: 116 // 117 // > It [POSIX.1-2001] does not call "not found" an error, hence 118 // > does not specify what value errno might have in this situation. 119 // > But that makes it impossible to recognize errors. 120 // 121 // See upstream Go issue: https://github.com/golang/go/issues/40334 122 u, e := userLookupId(username) 123 if e != nil { 124 // return first error, as it's usually clearer 125 return nil, err 126 } 127 usr = u 128 } 129 users = append(users, usr) 130 131 } 132 return users, nil 133 } 134 135 func allUsers() ([]*user.User, error) { 136 ds, err := filepath.Glob(dirs.SnapDataHomeGlob) 137 if err != nil { 138 // can't happen? 139 return nil, err 140 } 141 142 users := make([]*user.User, 1, len(ds)+1) 143 root, err := user.LookupId("0") 144 if err != nil { 145 return nil, err 146 } 147 users[0] = root 148 seen := make(map[uint32]bool, len(ds)+1) 149 seen[0] = true 150 var st syscall.Stat_t 151 for _, d := range ds { 152 err := syscall.Stat(d, &st) 153 if err != nil { 154 continue 155 } 156 if seen[st.Uid] { 157 continue 158 } 159 seen[st.Uid] = true 160 usr, err := userLookupId(strconv.FormatUint(uint64(st.Uid), 10)) 161 if err != nil { 162 // Treat all non-nil errors as user.Unknown{User,Group}Error's, as 163 // currently Go's handling of returned errno from get{pw,gr}nam_r 164 // in the cgo implementation of user.Lookup is lacking, and thus 165 // user.Unknown{User,Group}Error is returned only when errno is 0 166 // and the list of users/groups is empty, but as per the man page 167 // for get{pw,gr}nam_r, there are many other errno's that typical 168 // systems could return to indicate that the user/group wasn't 169 // found, however unfortunately the POSIX standard does not actually 170 // dictate what errno should be used to indicate "user/group not 171 // found", and so even if Go is more robust, it may not ever be 172 // fully robust. See from the man page: 173 // 174 // > It [POSIX.1-2001] does not call "not found" an error, hence 175 // > does not specify what value errno might have in this situation. 176 // > But that makes it impossible to recognize errors. 177 // 178 // See upstream Go issue: https://github.com/golang/go/issues/40334 179 continue 180 } else { 181 users = append(users, usr) 182 } 183 } 184 185 return users, nil 186 } 187 188 var ( 189 sysGeteuid = sys.Geteuid 190 execLookPath = exec.LookPath 191 ) 192 193 func pickUserWrapper() string { 194 // runuser and sudo happen to work the same way in this case. The main 195 // reason to prefer runuser over sudo is that runuser is part of 196 // util-linux, which is considered essential, whereas sudo is an addon 197 // which could be removed. However util-linux < 2.23 does not have 198 // runuser, and we support some distros that ship things older than that 199 // (e.g. Ubuntu 14.04) 200 for _, cmd := range []string{"runuser", "sudo"} { 201 if lp, err := execLookPath(cmd); err == nil { 202 return lp 203 } 204 } 205 return "" 206 } 207 208 var userWrapper = pickUserWrapper() 209 210 // tarAsUser returns an exec.Cmd that will, if the current effective user id is 211 // 0 and username is not "root", and if either runuser(1) or sudo(8) are found 212 // on the PATH, run tar as the given user. 213 // 214 // If the effective user id is not 0, or username is "root", exec.Command is 215 // used directly; changing the user id would fail (in the first case) or be a 216 // no-op (in the second). 217 // 218 // If neither runuser nor sudo are found on the path, exec.Command is also used 219 // directly. This will result in tar running as root in this situation (so it 220 // will fail if on NFS; I don't think there's an attack vector though). 221 func tarAsUser(username string, args ...string) *exec.Cmd { 222 if sysGeteuid() == 0 && username != "root" { 223 if userWrapper != "" { 224 uwArgs := make([]string, len(args)+5) 225 uwArgs[0] = userWrapper 226 uwArgs[1] = "-u" 227 uwArgs[2] = username 228 uwArgs[3] = "--" 229 uwArgs[4] = "tar" 230 copy(uwArgs[5:], args) 231 return &exec.Cmd{ 232 Path: userWrapper, 233 Args: uwArgs, 234 } 235 } 236 // TODO: use warnings instead 237 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.") 238 } 239 240 return exec.Command("tar", args...) 241 } 242 243 func MockUserLookup(newLookup func(string) (*user.User, error)) func() { 244 oldLookup := userLookup 245 userLookup = newLookup 246 return func() { 247 userLookup = oldLookup 248 } 249 }