github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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 "bytes" 24 "errors" 25 "fmt" 26 "os" 27 "path/filepath" 28 "strings" 29 30 "github.com/snapcore/snapd/osutil" 31 "github.com/snapcore/snapd/snap/snapdir" 32 "github.com/snapcore/snapd/snap/squashfs" 33 ) 34 35 // Container is the interface to interact with the low-level snap files. 36 type Container interface { 37 // Size returns the size of the snap in bytes. 38 Size() (int64, error) 39 40 // ReadFile returns the content of a single file from the snap. 41 ReadFile(relative string) ([]byte, error) 42 43 // Walk is like filepath.Walk, without the ordering guarantee. 44 Walk(relative string, walkFn filepath.WalkFunc) error 45 46 // ListDir returns the content of a single directory inside the snap. 47 ListDir(path string) ([]string, error) 48 49 // Install copies the snap file to targetPath (and possibly unpacks it to mountDir) 50 Install(targetPath, mountDir string) error 51 52 // Unpack unpacks the src parts to the dst directory 53 Unpack(src, dst string) error 54 } 55 56 // backend implements a specific snap format 57 type snapFormat struct { 58 magic []byte 59 open func(fn string) (Container, error) 60 } 61 62 // formatHandlers is the registry of known formats, squashfs is the only one atm. 63 var formatHandlers = []snapFormat{ 64 {squashfs.Magic, func(p string) (Container, error) { 65 return squashfs.New(p), nil 66 }}, 67 } 68 69 // Open opens a given snap file with the right backend. 70 func Open(path string) (Container, error) { 71 72 if osutil.IsDirectory(path) { 73 if osutil.FileExists(filepath.Join(path, "meta", "snap.yaml")) { 74 return snapdir.New(path), nil 75 } 76 77 return nil, NotSnapError{Path: path} 78 } 79 80 // open the file and check magic 81 f, err := os.Open(path) 82 if err != nil { 83 return nil, fmt.Errorf("cannot open snap: %v", err) 84 } 85 defer f.Close() 86 87 header := make([]byte, 20) 88 if _, err := f.ReadAt(header, 0); err != nil { 89 return nil, fmt.Errorf("cannot read snap: %v", err) 90 } 91 92 for _, h := range formatHandlers { 93 if bytes.HasPrefix(header, h.magic) { 94 return h.open(path) 95 } 96 } 97 98 return nil, fmt.Errorf("cannot open snap: unknown header: %q", header) 99 } 100 101 var ( 102 // ErrBadModes is returned by ValidateContainer when the container has files with the wrong file modes for their role 103 ErrBadModes = errors.New("snap is unusable due to bad permissions") 104 // ErrMissingPaths is returned by ValidateContainer when the container is missing required files or directories 105 ErrMissingPaths = errors.New("snap is unusable due to missing files") 106 ) 107 108 // ValidateContainer does a minimal sanity check on the container. 109 func ValidateContainer(c Container, s *Info, logf func(format string, v ...interface{})) error { 110 // needsrx keeps track of things that need to have at least 0555 perms 111 needsrx := map[string]bool{ 112 ".": true, 113 "meta": true, 114 } 115 // needsx keeps track of things that need to have at least 0111 perms 116 needsx := map[string]bool{} 117 // needsr keeps track of things that need to have at least 0444 perms 118 needsr := map[string]bool{ 119 "meta/snap.yaml": true, 120 } 121 // needsf keeps track of things that need to be regular files (or symlinks to regular files) 122 needsf := map[string]bool{} 123 // noskipd tracks directories we want to descend into despite not being in needs* 124 noskipd := map[string]bool{} 125 126 for _, app := range s.Apps { 127 // for non-services, paths go into the needsrx bag because users 128 // need rx perms to execute it 129 bag := needsrx 130 paths := []string{app.Command} 131 if app.IsService() { 132 // services' paths just need to not be skipped by the validator 133 bag = noskipd 134 // additional paths to check for services: 135 // XXX maybe have a method on app to keep this in sync 136 paths = append(paths, app.StopCommand, app.ReloadCommand, app.PostStopCommand) 137 } 138 139 for _, path := range paths { 140 path = normPath(path) 141 if path == "" { 142 continue 143 } 144 145 needsf[path] = true 146 if app.IsService() { 147 needsx[path] = true 148 } 149 for ; path != "."; path = filepath.Dir(path) { 150 bag[path] = true 151 } 152 } 153 154 // completer is special :-/ 155 if path := normPath(app.Completer); path != "" { 156 needsr[path] = true 157 for path = filepath.Dir(path); path != "."; path = filepath.Dir(path) { 158 needsrx[path] = true 159 } 160 } 161 } 162 // note all needsr so far need to be regular files (or symlinks) 163 for k := range needsr { 164 needsf[k] = true 165 } 166 // thing can get jumbled up 167 for path := range needsrx { 168 delete(needsx, path) 169 delete(needsr, path) 170 } 171 for path := range needsx { 172 if needsr[path] { 173 delete(needsx, path) 174 delete(needsr, path) 175 needsrx[path] = true 176 } 177 } 178 seen := make(map[string]bool, len(needsx)+len(needsrx)+len(needsr)) 179 180 // bad modes are logged instead of being returned because the end user 181 // can do nothing with the info (and the developer can read the logs) 182 hasBadModes := false 183 err := c.Walk(".", func(path string, info os.FileInfo, err error) error { 184 if err != nil { 185 return err 186 } 187 188 mode := info.Mode() 189 if needsrx[path] || needsx[path] || needsr[path] { 190 seen[path] = true 191 } 192 if !needsrx[path] && !needsx[path] && !needsr[path] && !strings.HasPrefix(path, "meta/") { 193 if mode.IsDir() { 194 if noskipd[path] { 195 return nil 196 } 197 return filepath.SkipDir 198 } 199 return nil 200 } 201 202 if needsrx[path] || mode.IsDir() { 203 if mode.Perm()&0555 != 0555 { 204 logf("in snap %q: %q should be world-readable and executable, and isn't: %s", s.InstanceName(), path, mode) 205 hasBadModes = true 206 } 207 } else { 208 if needsf[path] { 209 // this assumes that if it's a symlink it's OK. Arguably we 210 // should instead follow the symlink. We'd have to expose 211 // Lstat(), and guard against loops, and ... huge can of 212 // worms, and as this validator is meant as a developer aid 213 // more than anything else, not worth it IMHO (as I can't 214 // imagine this happening by accident). 215 if mode&(os.ModeDir|os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 { 216 logf("in snap %q: %q should be a regular file (or a symlink) and isn't", s.InstanceName(), path) 217 hasBadModes = true 218 } 219 } 220 if needsx[path] || strings.HasPrefix(path, "meta/hooks/") { 221 if mode.Perm()&0111 == 0 { 222 logf("in snap %q: %q should be executable, and isn't: %s", s.InstanceName(), path, mode) 223 hasBadModes = true 224 } 225 } else { 226 // in needsr, or under meta but not a hook 227 if mode.Perm()&0444 != 0444 { 228 logf("in snap %q: %q should be world-readable, and isn't: %s", s.InstanceName(), path, mode) 229 hasBadModes = true 230 } 231 } 232 } 233 return nil 234 }) 235 if err != nil { 236 return err 237 } 238 if len(seen) != len(needsx)+len(needsrx)+len(needsr) { 239 for _, needs := range []map[string]bool{needsx, needsrx, needsr} { 240 for path := range needs { 241 if !seen[path] { 242 logf("in snap %q: path %q does not exist", s.InstanceName(), path) 243 } 244 } 245 } 246 return ErrMissingPaths 247 } 248 249 if hasBadModes { 250 return ErrBadModes 251 } 252 return nil 253 } 254 255 // normPath is a helper for validateContainer. It takes a relative path (e.g. an 256 // app's RestartCommand, which might be empty to mean there is no such thing), 257 // and cleans it. 258 // 259 // * empty paths are returned as is 260 // * if the path is not relative, it's initial / is dropped 261 // * if the path goes "outside" (ie starts with ../), the empty string is 262 // returned (i.e. "ignore") 263 // * if there's a space in the command, ignore the rest of the string 264 // (see also cmd/snap-exec/main.go's comment about strings.Split) 265 func normPath(path string) string { 266 if path == "" { 267 return "" 268 } 269 270 path = strings.TrimPrefix(filepath.Clean(path), "/") 271 if strings.HasPrefix(path, "../") { 272 // not something inside the snap 273 return "" 274 } 275 if idx := strings.IndexByte(path, ' '); idx > -1 { 276 return path[:idx] 277 } 278 279 return path 280 }