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