github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/errtracker/errtracker.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017 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 errtracker 21 22 import ( 23 "bytes" 24 "crypto/md5" 25 "crypto/sha512" 26 "fmt" 27 "io" 28 "io/ioutil" 29 "net/http" 30 "os" 31 "os/exec" 32 "path/filepath" 33 "strings" 34 "time" 35 36 "github.com/snapcore/bolt" 37 "gopkg.in/mgo.v2/bson" 38 39 "github.com/snapcore/snapd/arch" 40 "github.com/snapcore/snapd/dirs" 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 "github.com/snapcore/snapd/snapdtool" 46 ) 47 48 var ( 49 CrashDbURLBase string 50 SnapdVersion string 51 52 // The machine-id file is at different locations depending on how the system 53 // is setup. On Fedora for example /var/lib/dbus/machine-id doesn't exist 54 // but we have /etc/machine-id. See 55 // https://www.freedesktop.org/software/systemd/man/machine-id.html for a 56 // few more details. 57 machineIDs = []string{"/etc/machine-id", "/var/lib/dbus/machine-id"} 58 59 mockedHostSnapd = "" 60 mockedCoreSnapd = "" 61 62 snapConfineProfile = "/etc/apparmor.d/usr.lib.snapd.snap-confine" 63 64 procCpuinfo = "/proc/cpuinfo" 65 procSelfExe = "/proc/self/exe" 66 procSelfCwd = "/proc/self/cwd" 67 procSelfCmdline = "/proc/self/cmdline" 68 69 osGetenv = os.Getenv 70 timeNow = time.Now 71 ) 72 73 type reportsDB struct { 74 db *bolt.DB 75 76 // map of hash(dupsig) -> time-of-report 77 reportedBucket *bolt.Bucket 78 79 // time until an error report is cleaned from the database, 80 // usually 7 days 81 cleanupTime time.Duration 82 } 83 84 func hashString(s string) string { 85 h := sha512.New() 86 io.WriteString(h, s) 87 return fmt.Sprintf("%x", h.Sum(nil)) 88 } 89 90 func newReportsDB(fname string) (*reportsDB, error) { 91 if err := os.MkdirAll(filepath.Dir(fname), 0755); err != nil { 92 return nil, err 93 } 94 bdb, err := bolt.Open(fname, 0600, &bolt.Options{ 95 Timeout: 10 * time.Second, 96 }) 97 if err != nil { 98 return nil, err 99 } 100 bdb.Update(func(tx *bolt.Tx) error { 101 _, err := tx.CreateBucketIfNotExists([]byte("reported")) 102 if err != nil { 103 return fmt.Errorf("create bucket: %s", err) 104 } 105 return nil 106 }) 107 108 db := &reportsDB{ 109 db: bdb, 110 cleanupTime: time.Duration(7 * 24 * time.Hour), 111 } 112 113 return db, nil 114 } 115 116 func (db *reportsDB) Close() error { 117 return db.db.Close() 118 } 119 120 // AlreadyReported returns true if an identical report has been sent recently 121 func (db *reportsDB) AlreadyReported(dupSig string) bool { 122 // robustness 123 if db == nil { 124 return false 125 } 126 var reported []byte 127 db.db.View(func(tx *bolt.Tx) error { 128 b := tx.Bucket([]byte("reported")) 129 reported = b.Get([]byte(hashString(dupSig))) 130 return nil 131 }) 132 return len(reported) > 0 133 } 134 135 func (db *reportsDB) cleanupOldRecords() { 136 db.db.Update(func(tx *bolt.Tx) error { 137 now := time.Now() 138 139 b := tx.Bucket([]byte("reported")) 140 b.ForEach(func(dupSigHash, reportTime []byte) error { 141 var t time.Time 142 t.UnmarshalBinary(reportTime) 143 144 if now.After(t.Add(db.cleanupTime)) { 145 if err := b.Delete(dupSigHash); err != nil { 146 return err 147 } 148 } 149 return nil 150 }) 151 return nil 152 }) 153 } 154 155 // MarkReported marks an error report as reported to the error tracker 156 func (db *reportsDB) MarkReported(dupSig string) error { 157 // robustness 158 if db == nil { 159 return fmt.Errorf("cannot mark error report as reported with an uninitialized reports database") 160 } 161 db.cleanupOldRecords() 162 163 return db.db.Update(func(tx *bolt.Tx) error { 164 b := tx.Bucket([]byte("reported")) 165 tb, err := time.Now().MarshalBinary() 166 if err != nil { 167 return err 168 } 169 return b.Put([]byte(hashString(dupSig)), tb) 170 }) 171 } 172 173 func whoopsieEnabled() bool { 174 cmd := exec.Command("systemctl", "is-enabled", "whoopsie.service") 175 output, _ := cmd.CombinedOutput() 176 switch string(output) { 177 case "enabled\n": 178 return true 179 case "disabled\n": 180 return false 181 default: 182 logger.Debugf("unexpected output when checking for whoopsie.service (not installed?): %s", output) 183 return true 184 } 185 } 186 187 // distroRelease returns a distro release as it is expected by daisy.ubuntu.com 188 func distroRelease() string { 189 ID := release.ReleaseInfo.ID 190 if ID == "ubuntu" { 191 ID = "Ubuntu" 192 } 193 194 return fmt.Sprintf("%s %s", ID, release.ReleaseInfo.VersionID) 195 } 196 197 func readMachineID() ([]byte, error) { 198 for _, id := range machineIDs { 199 machineID, err := ioutil.ReadFile(id) 200 if err == nil { 201 return bytes.TrimSpace(machineID), nil 202 } else if !os.IsNotExist(err) { 203 logger.Noticef("cannot read %s: %s", id, err) 204 } 205 } 206 207 return nil, fmt.Errorf("cannot report: no suitable machine id file found") 208 } 209 210 func snapConfineProfileDigest(suffix string) string { 211 profileText, err := ioutil.ReadFile(filepath.Join(dirs.GlobalRootDir, snapConfineProfile+suffix)) 212 if err != nil { 213 return "" 214 } 215 // NOTE: uses md5sum for easier comparison against dpkg meta-data 216 return fmt.Sprintf("%x", md5.Sum(profileText)) 217 } 218 219 var didSnapdReExec = func() string { 220 didReexec, err := snapdtool.IsReexecd() 221 if err != nil { 222 return "unknown" 223 } 224 if didReexec { 225 return "yes" 226 } 227 return "no" 228 } 229 230 // Report reports an error with the given snap to the error tracker 231 func Report(snap, errMsg, dupSig string, extra map[string]string) (string, error) { 232 if extra == nil { 233 extra = make(map[string]string) 234 } 235 extra["ProblemType"] = "Snap" 236 extra["Snap"] = snap 237 238 // check if we haven't already reported this error 239 db, err := newReportsDB(dirs.ErrtrackerDbDir) 240 if err != nil { 241 return "", fmt.Errorf("cannot open error reports database: %v", err) 242 } 243 defer db.Close() 244 245 if db.AlreadyReported(dupSig) { 246 return "already-reported", nil 247 } 248 249 // do the actual report 250 oopsID, err := report(errMsg, dupSig, extra) 251 if err != nil { 252 return "", err 253 } 254 if err := db.MarkReported(dupSig); err != nil { 255 logger.Noticef("cannot mark %s as reported: %s", oopsID, err) 256 } 257 258 return oopsID, nil 259 } 260 261 // ReportRepair reports an error with the given repair assertion script 262 // to the error tracker 263 func ReportRepair(repair, errMsg, dupSig string, extra map[string]string) (string, error) { 264 if extra == nil { 265 extra = make(map[string]string) 266 } 267 extra["ProblemType"] = "Repair" 268 extra["Repair"] = repair 269 270 return report(errMsg, dupSig, extra) 271 } 272 273 func detectVirt() string { 274 cmd := exec.Command("systemd-detect-virt") 275 output, err := cmd.CombinedOutput() 276 if err != nil { 277 return "" 278 } 279 return strings.TrimSpace(string(output)) 280 } 281 282 func journalError() string { 283 // TODO: look into using systemd package (needs refactor) 284 285 // Before changing this line to be more consistent or nicer or anything 286 // else, remember it needs to run a lot of different systemd's: today, 287 // anything from 238 (on arch) to 204 (on ubuntu 14.04); this is why 288 // doing the refactor to the systemd package to only worry about this in 289 // there might be worth it. 290 output, err := exec.Command("journalctl", "-b", "--priority=warning..err", "--lines=1000").CombinedOutput() 291 if err != nil { 292 if len(output) == 0 { 293 return fmt.Sprintf("error: %v", err) 294 } 295 output = append(output, fmt.Sprintf("\nerror: %v", err)...) 296 } 297 return string(output) 298 } 299 300 func procCpuinfoMinimal() string { 301 buf, err := ioutil.ReadFile(procCpuinfo) 302 if err != nil { 303 // if we can't read cpuinfo, we want to know _why_ 304 return fmt.Sprintf("error: %v", err) 305 } 306 idx := bytes.LastIndex(buf, []byte("\nprocessor\t:")) 307 308 // if not found (which will happen on non-x86 architectures, which is ok 309 // because they'd typically not have the same info over and over again), 310 // return whole buffer; otherwise, return from just after the \n 311 return string(buf[idx+1:]) 312 } 313 314 func procExe() string { 315 out, err := os.Readlink(procSelfExe) 316 if err != nil { 317 return fmt.Sprintf("error: %v", err) 318 } 319 return out 320 } 321 322 func procCwd() string { 323 out, err := os.Readlink(procSelfCwd) 324 if err != nil { 325 return fmt.Sprintf("error: %v", err) 326 } 327 return out 328 } 329 330 func procCmdline() string { 331 out, err := ioutil.ReadFile(procSelfCmdline) 332 if err != nil { 333 return fmt.Sprintf("error: %v", err) 334 } 335 return string(out) 336 } 337 338 func environ() string { 339 safeVars := []string{ 340 "SHELL", "TERM", "LANGUAGE", "LANG", "LC_CTYPE", 341 "LC_COLLATE", "LC_TIME", "LC_NUMERIC", 342 "LC_MONETARY", "LC_MESSAGES", "LC_PAPER", 343 "LC_NAME", "LC_ADDRESS", "LC_TELEPHONE", 344 "LC_MEASUREMENT", "LC_IDENTIFICATION", "LOCPATH", 345 } 346 unsafeVars := []string{"XDG_RUNTIME_DIR", "LD_PRELOAD", "LD_LIBRARY_PATH"} 347 knownPaths := map[string]bool{ 348 "/snap/bin": true, 349 "/var/lib/snapd/snap/bin": true, 350 "/sbin": true, 351 "/bin": true, 352 "/usr/sbin": true, 353 "/usr/bin": true, 354 "/usr/local/sbin": true, 355 "/usr/local/bin": true, 356 "/usr/local/games": true, 357 "/usr/games": true, 358 } 359 360 // + 1 for PATH 361 out := make([]string, 0, len(safeVars)+len(unsafeVars)+1) 362 363 for _, k := range safeVars { 364 if v := osGetenv(k); v != "" { 365 out = append(out, fmt.Sprintf("%s=%s", k, v)) 366 } 367 } 368 369 for _, k := range unsafeVars { 370 if v := osGetenv(k); v != "" { 371 out = append(out, k+"=<set>") 372 } 373 } 374 375 if paths := filepath.SplitList(osGetenv("PATH")); len(paths) > 0 { 376 for i, p := range paths { 377 p = filepath.Clean(p) 378 if !knownPaths[p] { 379 if strings.Contains(p, "/home") || strings.Contains(p, "/tmp") { 380 p = "(user)" 381 } else { 382 p = "(custom)" 383 } 384 } 385 paths[i] = p 386 } 387 out = append(out, fmt.Sprintf("PATH=%s", strings.Join(paths, string(filepath.ListSeparator)))) 388 } 389 390 return strings.Join(out, "\n") 391 } 392 393 func report(errMsg, dupSig string, extra map[string]string) (string, error) { 394 if CrashDbURLBase == "" { 395 return "", nil 396 } 397 if extra == nil || extra["ProblemType"] == "" { 398 return "", fmt.Errorf(`key "ProblemType" not set in %v`, extra) 399 } 400 401 if !whoopsieEnabled() { 402 return "", nil 403 } 404 405 machineID, err := readMachineID() 406 if err != nil { 407 return "", err 408 } 409 410 identifier := fmt.Sprintf("%x", sha512.Sum512(machineID)) 411 412 crashDbUrl := fmt.Sprintf("%s/%s", CrashDbURLBase, identifier) 413 414 hostSnapdPath := filepath.Join(dirs.DistroLibExecDir, "snapd") 415 coreSnapdPath := filepath.Join(dirs.SnapMountDir, "core/current/usr/lib/snapd/snapd") 416 if mockedHostSnapd != "" { 417 hostSnapdPath = mockedHostSnapd 418 } 419 if mockedCoreSnapd != "" { 420 coreSnapdPath = mockedCoreSnapd 421 } 422 hostBuildID, _ := osutil.ReadBuildID(hostSnapdPath) 423 coreBuildID, _ := osutil.ReadBuildID(coreSnapdPath) 424 if hostBuildID == "" { 425 hostBuildID = "unknown" 426 } 427 if coreBuildID == "" { 428 coreBuildID = "unknown" 429 } 430 431 report := map[string]string{ 432 "Architecture": arch.DpkgArchitecture(), 433 "SnapdVersion": SnapdVersion, 434 "DistroRelease": distroRelease(), 435 "HostSnapdBuildID": hostBuildID, 436 "CoreSnapdBuildID": coreBuildID, 437 "Date": timeNow().Format(time.ANSIC), 438 "KernelVersion": osutil.KernelVersion(), 439 "ErrorMessage": errMsg, 440 "DuplicateSignature": dupSig, 441 442 "JournalError": journalError(), 443 "ExecutablePath": procExe(), 444 "ProcCmdline": procCmdline(), 445 "ProcCpuinfoMinimal": procCpuinfoMinimal(), 446 "ProcCwd": procCwd(), 447 "ProcEnviron": environ(), 448 "DetectedVirt": detectVirt(), 449 "SourcePackage": "snapd", 450 451 "DidSnapdReExec": didSnapdReExec(), 452 } 453 454 if desktop := osGetenv("XDG_CURRENT_DESKTOP"); desktop != "" { 455 report["CurrentDesktop"] = desktop 456 } 457 458 for k, v := range extra { 459 // only set if empty 460 if _, ok := report[k]; !ok { 461 report[k] = v 462 } 463 } 464 465 // include md5 hashes of the apparmor conffile for easier debbuging 466 // of not-updated snap-confine apparmor profiles 467 for _, sp := range []struct { 468 suffix string 469 key string 470 }{ 471 {"", "MD5SumSnapConfineAppArmorProfile"}, 472 {".dpkg-new", "MD5SumSnapConfineAppArmorProfileDpkgNew"}, 473 {".real", "MD5SumSnapConfineAppArmorProfileReal"}, 474 {".real.dpkg-new", "MD5SumSnapConfineAppArmorProfileRealDpkgNew"}, 475 } { 476 digest := snapConfineProfileDigest(sp.suffix) 477 if digest != "" { 478 report[sp.key] = digest 479 } 480 481 } 482 483 // see if we run in testing mode 484 if snapdenv.Testing() { 485 logger.Noticef("errtracker.Report is *not* sent because SNAPPY_TESTING is set") 486 logger.Noticef("report: %v", report) 487 return "oops-not-sent", nil 488 } 489 490 // send it for real 491 reportBson, err := bson.Marshal(report) 492 if err != nil { 493 return "", err 494 } 495 client := &http.Client{} 496 req, err := http.NewRequest("POST", crashDbUrl, bytes.NewBuffer(reportBson)) 497 if err != nil { 498 return "", err 499 } 500 req.Header.Add("Content-Type", "application/octet-stream") 501 req.Header.Add("X-Whoopsie-Version", snapdenv.UserAgent()) 502 resp, err := client.Do(req) 503 if err != nil { 504 return "", err 505 } 506 defer resp.Body.Close() 507 if resp.StatusCode != 200 { 508 return "", fmt.Errorf("cannot upload error report, return code: %d", resp.StatusCode) 509 } 510 oopsID, err := ioutil.ReadAll(resp.Body) 511 if err != nil { 512 return "", err 513 } 514 515 return string(oopsID), nil 516 }