github.com/ubuntu-core/snappy@v0.0.0-20210827154228-9e584df982bb/snap/container.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2015 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 snap 21 22 import ( 23 "errors" 24 "io" 25 "os" 26 "path/filepath" 27 "strings" 28 ) 29 30 // Container is the interface to interact with the low-level snap files. 31 type Container interface { 32 // Size returns the size of the snap in bytes. 33 Size() (int64, error) 34 35 // RandomAccessFile returns an implementation to read at any 36 // given location for a single file inside the snap plus 37 // information about the file size. 38 RandomAccessFile(relative string) (interface { 39 io.ReaderAt 40 io.Closer 41 Size() int64 42 }, error) 43 44 // ReadFile returns the content of a single file from the snap. 45 ReadFile(relative string) ([]byte, error) 46 47 // Walk is like filepath.Walk, without the ordering guarantee. 48 Walk(relative string, walkFn filepath.WalkFunc) error 49 50 // ListDir returns the content of a single directory inside the snap. 51 ListDir(path string) ([]string, error) 52 53 // Install copies the snap file to targetPath (and possibly unpacks it to mountDir). 54 // The bool return value indicates if the backend had nothing to do on install. 55 Install(targetPath, mountDir string, opts *InstallOptions) (bool, error) 56 57 // Unpack unpacks the src parts to the dst directory 58 Unpack(src, dst string) error 59 } 60 61 // InstallOptions is for customizing the behavior of Install() from a higher 62 // level function, i.e. from overlord customizing how a snap file is installed 63 // on a system with tmpfs mounted as writable or with full disk encryption and 64 // graded secured on UC20. 65 type InstallOptions struct { 66 // MustNotCrossDevices indicates that the snap file when installed to the 67 // target must not cross devices. For example, installing a snap file from 68 // the ubuntu-seed partition onto the ubuntu-data partition must result in 69 // an installation on ubuntu-data that does not depend or reference 70 // ubuntu-seed at all. 71 MustNotCrossDevices bool 72 } 73 74 var ( 75 // ErrBadModes is returned by ValidateContainer when the container has files with the wrong file modes for their role 76 ErrBadModes = errors.New("snap is unusable due to bad permissions") 77 // ErrMissingPaths is returned by ValidateContainer when the container is missing required files or directories 78 ErrMissingPaths = errors.New("snap is unusable due to missing files") 79 ) 80 81 // ValidateContainer does a minimal sanity check on the container. 82 func ValidateContainer(c Container, s *Info, logf func(format string, v ...interface{})) error { 83 // needsrx keeps track of things that need to have at least 0555 perms 84 needsrx := map[string]bool{ 85 ".": true, 86 "meta": true, 87 } 88 // needsx keeps track of things that need to have at least 0111 perms 89 needsx := map[string]bool{} 90 // needsr keeps track of things that need to have at least 0444 perms 91 needsr := map[string]bool{ 92 "meta/snap.yaml": true, 93 } 94 // needsf keeps track of things that need to be regular files (or symlinks to regular files) 95 needsf := map[string]bool{} 96 // noskipd tracks directories we want to descend into despite not being in needs* 97 noskipd := map[string]bool{} 98 99 for _, app := range s.Apps { 100 // for non-services, paths go into the needsrx bag because users 101 // need rx perms to execute it 102 bag := needsrx 103 paths := []string{app.Command} 104 if app.IsService() { 105 // services' paths just need to not be skipped by the validator 106 bag = noskipd 107 // additional paths to check for services: 108 // XXX maybe have a method on app to keep this in sync 109 paths = append(paths, app.StopCommand, app.ReloadCommand, app.PostStopCommand) 110 } 111 112 for _, path := range paths { 113 path = normPath(path) 114 if path == "" { 115 continue 116 } 117 118 needsf[path] = true 119 if app.IsService() { 120 needsx[path] = true 121 } 122 for ; path != "."; path = filepath.Dir(path) { 123 bag[path] = true 124 } 125 } 126 127 // completer is special :-/ 128 if path := normPath(app.Completer); path != "" { 129 needsr[path] = true 130 for path = filepath.Dir(path); path != "."; path = filepath.Dir(path) { 131 needsrx[path] = true 132 } 133 } 134 } 135 // note all needsr so far need to be regular files (or symlinks) 136 for k := range needsr { 137 needsf[k] = true 138 } 139 // thing can get jumbled up 140 for path := range needsrx { 141 delete(needsx, path) 142 delete(needsr, path) 143 } 144 for path := range needsx { 145 if needsr[path] { 146 delete(needsx, path) 147 delete(needsr, path) 148 needsrx[path] = true 149 } 150 } 151 seen := make(map[string]bool, len(needsx)+len(needsrx)+len(needsr)) 152 153 // bad modes are logged instead of being returned because the end user 154 // can do nothing with the info (and the developer can read the logs) 155 hasBadModes := false 156 err := c.Walk(".", func(path string, info os.FileInfo, err error) error { 157 if err != nil { 158 return err 159 } 160 161 mode := info.Mode() 162 if needsrx[path] || needsx[path] || needsr[path] { 163 seen[path] = true 164 } 165 if !needsrx[path] && !needsx[path] && !needsr[path] && !strings.HasPrefix(path, "meta/") { 166 if mode.IsDir() { 167 if noskipd[path] { 168 return nil 169 } 170 return filepath.SkipDir 171 } 172 return nil 173 } 174 175 if needsrx[path] || mode.IsDir() { 176 if mode.Perm()&0555 != 0555 { 177 logf("in snap %q: %q should be world-readable and executable, and isn't: %s", s.InstanceName(), path, mode) 178 hasBadModes = true 179 } 180 } else { 181 if needsf[path] { 182 // this assumes that if it's a symlink it's OK. Arguably we 183 // should instead follow the symlink. We'd have to expose 184 // Lstat(), and guard against loops, and ... huge can of 185 // worms, and as this validator is meant as a developer aid 186 // more than anything else, not worth it IMHO (as I can't 187 // imagine this happening by accident). 188 if mode&(os.ModeDir|os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 { 189 logf("in snap %q: %q should be a regular file (or a symlink) and isn't", s.InstanceName(), path) 190 hasBadModes = true 191 } 192 } 193 if needsx[path] || strings.HasPrefix(path, "meta/hooks/") { 194 if mode.Perm()&0111 == 0 { 195 logf("in snap %q: %q should be executable, and isn't: %s", s.InstanceName(), path, mode) 196 hasBadModes = true 197 } 198 } else { 199 // in needsr, or under meta but not a hook 200 if mode.Perm()&0444 != 0444 { 201 logf("in snap %q: %q should be world-readable, and isn't: %s", s.InstanceName(), path, mode) 202 hasBadModes = true 203 } 204 } 205 } 206 return nil 207 }) 208 if err != nil { 209 return err 210 } 211 if len(seen) != len(needsx)+len(needsrx)+len(needsr) { 212 for _, needs := range []map[string]bool{needsx, needsrx, needsr} { 213 for path := range needs { 214 if !seen[path] { 215 logf("in snap %q: path %q does not exist", s.InstanceName(), path) 216 } 217 } 218 } 219 return ErrMissingPaths 220 } 221 222 if hasBadModes { 223 return ErrBadModes 224 } 225 return nil 226 } 227 228 // normPath is a helper for validateContainer. It takes a relative path (e.g. an 229 // app's RestartCommand, which might be empty to mean there is no such thing), 230 // and cleans it. 231 // 232 // * empty paths are returned as is 233 // * if the path is not relative, it's initial / is dropped 234 // * if the path goes "outside" (ie starts with ../), the empty string is 235 // returned (i.e. "ignore") 236 // * if there's a space in the command, ignore the rest of the string 237 // (see also cmd/snap-exec/main.go's comment about strings.Split) 238 func normPath(path string) string { 239 if path == "" { 240 return "" 241 } 242 243 path = strings.TrimPrefix(filepath.Clean(path), "/") 244 if strings.HasPrefix(path, "../") { 245 // not something inside the snap 246 return "" 247 } 248 if idx := strings.IndexByte(path, ' '); idx > -1 { 249 return path[:idx] 250 } 251 252 return path 253 }