github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/cmd/snap/cmd_auto_import.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2020 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 main 21 22 import ( 23 "bufio" 24 "crypto" 25 "encoding/base64" 26 "fmt" 27 "io/ioutil" 28 "os" 29 "os/exec" 30 "path/filepath" 31 "sort" 32 "strings" 33 "syscall" 34 35 "github.com/jessevdk/go-flags" 36 37 "github.com/snapcore/snapd/boot" 38 "github.com/snapcore/snapd/client" 39 "github.com/snapcore/snapd/dirs" 40 "github.com/snapcore/snapd/i18n" 41 "github.com/snapcore/snapd/logger" 42 "github.com/snapcore/snapd/osutil" 43 "github.com/snapcore/snapd/release" 44 "github.com/snapcore/snapd/snapdenv" 45 ) 46 47 const autoImportsName = "auto-import.assert" 48 49 var mountInfoPath = "/proc/self/mountinfo" 50 51 func autoImportCandidates() ([]string, error) { 52 var cands []string 53 54 // see https://www.kernel.org/doc/Documentation/filesystems/proc.txt, 55 // sec. 3.5 56 f, err := os.Open(mountInfoPath) 57 if err != nil { 58 return nil, err 59 } 60 defer f.Close() 61 62 isTesting := snapdenv.Testing() 63 64 // TODO: re-write this to use osutil.LoadMountInfo instead of doing the 65 // parsing ourselves 66 67 scanner := bufio.NewScanner(f) 68 for scanner.Scan() { 69 l := strings.Fields(scanner.Text()) 70 71 // Per proc.txt:3.5, /proc/<pid>/mountinfo looks like 72 // 73 // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue 74 // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) 75 // 76 // and (7) has zero or more elements, find the "-" separator. 77 i := 6 78 for i < len(l) && l[i] != "-" { 79 i++ 80 } 81 if i+2 >= len(l) { 82 continue 83 } 84 85 mountSrc := l[i+2] 86 87 // skip everything that is not a device (cgroups, debugfs etc) 88 if !strings.HasPrefix(mountSrc, "/dev/") { 89 continue 90 } 91 // skip all loop devices (snaps) 92 if strings.HasPrefix(mountSrc, "/dev/loop") { 93 continue 94 } 95 // skip all ram disks (unless in tests) 96 if !isTesting && strings.HasPrefix(mountSrc, "/dev/ram") { 97 continue 98 } 99 100 // TODO: should the following 2 checks try to be more smart like 101 // `snap-bootstrap initramfs-mounts` and try to find the boot disk 102 // and determine what partitions to skip using the disks package? 103 104 // skip all initramfs mounted disks on uc20 105 mountPoint := l[4] 106 if strings.HasPrefix(mountPoint, boot.InitramfsRunMntDir) { 107 continue 108 } 109 110 // skip all seed dir mount points too, as these are bind mounts to the 111 // initramfs dirs on uc20, this can show up as 112 // /writable/system-data/var/lib/snapd/seed as well as 113 // /var/lib/snapd/seed 114 if strings.HasSuffix(mountPoint, dirs.SnapSeedDir) { 115 continue 116 } 117 118 cand := filepath.Join(mountPoint, autoImportsName) 119 if osutil.FileExists(cand) { 120 cands = append(cands, cand) 121 } 122 } 123 124 return cands, scanner.Err() 125 } 126 127 func queueFile(src string) error { 128 // refuse huge files, this is for assertions 129 fi, err := os.Stat(src) 130 if err != nil { 131 return err 132 } 133 // 640kb ought be to enough for anyone 134 if fi.Size() > 640*1024 { 135 msg := fmt.Errorf("cannot queue %s, file size too big: %v", src, fi.Size()) 136 logger.Noticef("error: %v", msg) 137 return msg 138 } 139 140 // ensure name is predictable, weak hash is ok 141 hash, _, err := osutil.FileDigest(src, crypto.SHA3_384) 142 if err != nil { 143 return err 144 } 145 146 dst := filepath.Join(dirs.SnapAssertsSpoolDir, fmt.Sprintf("%s.assert", base64.URLEncoding.EncodeToString(hash))) 147 if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { 148 return err 149 } 150 151 return osutil.CopyFile(src, dst, osutil.CopyFlagOverwrite) 152 } 153 154 func autoImportFromSpool(cli *client.Client) (added int, err error) { 155 files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir) 156 if os.IsNotExist(err) { 157 return 0, nil 158 } 159 if err != nil { 160 return 0, err 161 } 162 163 for _, fi := range files { 164 cand := filepath.Join(dirs.SnapAssertsSpoolDir, fi.Name()) 165 if err := ackFile(cli, cand); err != nil { 166 logger.Noticef("error: cannot import %s: %s", cand, err) 167 continue 168 } else { 169 logger.Noticef("imported %s", cand) 170 added++ 171 } 172 // FIXME: only remove stuff older than N days? 173 if err := os.Remove(cand); err != nil { 174 return 0, err 175 } 176 } 177 178 return added, nil 179 } 180 181 func autoImportFromAllMounts(cli *client.Client) (int, error) { 182 cands, err := autoImportCandidates() 183 if err != nil { 184 return 0, err 185 } 186 187 added := 0 188 for _, cand := range cands { 189 err := ackFile(cli, cand) 190 // the server is not ready yet 191 if _, ok := err.(client.ConnectionError); ok { 192 logger.Noticef("queuing for later %s", cand) 193 if err := queueFile(cand); err != nil { 194 return 0, err 195 } 196 continue 197 } 198 if err != nil { 199 logger.Noticef("error: cannot import %s: %s", cand, err) 200 continue 201 } else { 202 logger.Noticef("imported %s", cand) 203 } 204 added++ 205 } 206 207 return added, nil 208 } 209 210 var ioutilTempDir = ioutil.TempDir 211 212 func tryMount(deviceName string) (string, error) { 213 tmpMountTarget, err := ioutilTempDir("", "snapd-auto-import-mount-") 214 if err != nil { 215 err = fmt.Errorf("cannot create temporary mount point: %v", err) 216 logger.Noticef("error: %v", err) 217 return "", err 218 } 219 // udev does not provide much environment ;) 220 if os.Getenv("PATH") == "" { 221 os.Setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin") 222 } 223 // not using syscall.Mount() because we don't know the fs type in advance 224 cmd := exec.Command("mount", "-t", "ext4,vfat", "-o", "ro", "--make-private", deviceName, tmpMountTarget) 225 if output, err := cmd.CombinedOutput(); err != nil { 226 os.Remove(tmpMountTarget) 227 err = fmt.Errorf("cannot mount %s: %s", deviceName, osutil.OutputErr(output, err)) 228 logger.Noticef("error: %v", err) 229 return "", err 230 } 231 232 return tmpMountTarget, nil 233 } 234 235 var syscallUnmount = syscall.Unmount 236 237 func doUmount(mp string) error { 238 if err := syscallUnmount(mp, 0); err != nil { 239 return err 240 } 241 return os.Remove(mp) 242 } 243 244 type cmdAutoImport struct { 245 clientMixin 246 Mount []string `long:"mount" arg-name:"<device path>"` 247 248 ForceClassic bool `long:"force-classic"` 249 } 250 251 var shortAutoImportHelp = i18n.G("Inspect devices for actionable information") 252 253 var longAutoImportHelp = i18n.G(` 254 The auto-import command searches available mounted devices looking for 255 assertions that are signed by trusted authorities, and potentially 256 performs system changes based on them. 257 258 If one or more device paths are provided via --mount, these are temporarily 259 mounted to be inspected as well. Even in that case the command will still 260 consider all available mounted devices for inspection. 261 262 Assertions to be imported must be made available in the auto-import.assert file 263 in the root of the filesystem. 264 `) 265 266 func init() { 267 cmd := addCommand("auto-import", 268 shortAutoImportHelp, 269 longAutoImportHelp, 270 func() flags.Commander { 271 return &cmdAutoImport{} 272 }, map[string]string{ 273 // TRANSLATORS: This should not start with a lowercase letter. 274 "mount": i18n.G("Temporarily mount device before inspecting"), 275 // TRANSLATORS: This should not start with a lowercase letter. 276 "force-classic": i18n.G("Force import on classic systems"), 277 }, nil) 278 cmd.hidden = true 279 } 280 281 func (x *cmdAutoImport) autoAddUsers() error { 282 cmd := cmdCreateUser{ 283 clientMixin: x.clientMixin, 284 Known: true, 285 Sudoer: true, 286 } 287 return cmd.Execute(nil) 288 } 289 290 func removableBlockDevices() (removableDevices []string) { 291 // eg. /sys/block/sda/removable 292 removable, err := filepath.Glob(filepath.Join(dirs.GlobalRootDir, "/sys/block/*/removable")) 293 if err != nil { 294 return nil 295 } 296 for _, removableAttr := range removable { 297 val, err := ioutil.ReadFile(removableAttr) 298 if err != nil || string(val) != "1\n" { 299 // non removable 300 continue 301 } 302 // let's see if it has partitions 303 dev := filepath.Base(filepath.Dir(removableAttr)) 304 305 pattern := fmt.Sprintf(filepath.Join(dirs.GlobalRootDir, "/sys/block/%s/%s*/partition"), dev, dev) 306 // eg. /sys/block/sda/sda1/partition 307 partitionAttrs, _ := filepath.Glob(pattern) 308 309 if len(partitionAttrs) == 0 { 310 // not partitioned? try to use the main device 311 removableDevices = append(removableDevices, fmt.Sprintf("/dev/%s", dev)) 312 continue 313 } 314 315 for _, partAttr := range partitionAttrs { 316 val, err := ioutil.ReadFile(partAttr) 317 if err != nil || string(val) != "1\n" { 318 // non partition? 319 continue 320 } 321 pdev := filepath.Base(filepath.Dir(partAttr)) 322 removableDevices = append(removableDevices, fmt.Sprintf("/dev/%s", pdev)) 323 // hasPartitions = true 324 } 325 } 326 sort.Strings(removableDevices) 327 return removableDevices 328 } 329 330 // inInstallmode returns true if it's UC20 system in install mode 331 func inInstallMode() bool { 332 mode, _, err := boot.ModeAndRecoverySystemFromKernelCommandLine() 333 if err != nil { 334 return false 335 } 336 return mode == "install" 337 } 338 339 func (x *cmdAutoImport) Execute(args []string) error { 340 if len(args) > 0 { 341 return ErrExtraArgs 342 } 343 344 if release.OnClassic && !x.ForceClassic { 345 fmt.Fprintf(Stderr, "auto-import is disabled on classic\n") 346 return nil 347 } 348 // TODO:UC20: workaround for LP: #1860231 349 if inInstallMode() { 350 fmt.Fprintf(Stderr, "auto-import is disabled in install-mode\n") 351 return nil 352 } 353 354 devices := x.Mount 355 if len(devices) == 0 { 356 // coldplug scenario, try all removable devices 357 devices = removableBlockDevices() 358 } 359 360 for _, path := range devices { 361 // udev adds new /dev/loopX devices on the fly when a 362 // loop mount happens and there is no loop device left. 363 // 364 // We need to ignore these events because otherwise both 365 // our mount and the "mount -o loop" fight over the same 366 // device and we get nasty errors 367 if strings.HasPrefix(path, "/dev/loop") { 368 continue 369 } 370 371 mp, err := tryMount(path) 372 if err != nil { 373 continue // Error was reported. Continue looking. 374 } 375 defer doUmount(mp) 376 } 377 378 added1, err := autoImportFromSpool(x.client) 379 if err != nil { 380 return err 381 } 382 383 added2, err := autoImportFromAllMounts(x.client) 384 if err != nil { 385 return err 386 } 387 388 if added1+added2 > 0 { 389 return x.autoAddUsers() 390 } 391 392 return nil 393 }