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  }