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  }