diff --git a/ee/allowedcmd/cmd_linux.go b/ee/allowedcmd/cmd_linux.go index ed511e812..064d9da20 100644 --- a/ee/allowedcmd/cmd_linux.go +++ b/ee/allowedcmd/cmd_linux.go @@ -24,6 +24,10 @@ func Brew(ctx context.Context, arg ...string) (*exec.Cmd, error) { return validatedCmd, nil } +func Coredumpctl(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/coredumpctl", arg...) +} + func Cryptsetup(ctx context.Context, arg ...string) (*exec.Cmd, error) { for _, p := range []string{"/usr/sbin/cryptsetup", "/sbin/cryptsetup"} { validatedCmd, err := validatedCommand(ctx, p, arg...) diff --git a/ee/debug/checkups/checkups.go b/ee/debug/checkups/checkups.go index 49c7e076b..199ecee87 100644 --- a/ee/debug/checkups/checkups.go +++ b/ee/debug/checkups/checkups.go @@ -124,6 +124,7 @@ func checkupsFor(k types.Knapsack, target targetBits) []checkupInt { {&osqRestartCheckup{k: k}, doctorSupported | flareSupported}, {&uninstallHistoryCheckup{k: k}, flareSupported}, {&desktopMenu{k: k}, flareSupported}, + {&coredumpCheckup{}, doctorSupported | flareSupported}, } checkupsToRun := make([]checkupInt, 0) diff --git a/ee/debug/checkups/coredump_linux.go b/ee/debug/checkups/coredump_linux.go new file mode 100644 index 000000000..35ae08f67 --- /dev/null +++ b/ee/debug/checkups/coredump_linux.go @@ -0,0 +1,143 @@ +//go:build linux +// +build linux + +package checkups + +import ( + "archive/zip" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/kolide/launcher/ee/agent" + "github.com/kolide/launcher/ee/allowedcmd" +) + +type coredumpCheckup struct { + status Status + summary string + data map[string]any +} + +func (c *coredumpCheckup) Name() string { + return "Coredump Report" +} + +func (c *coredumpCheckup) Run(ctx context.Context, extraWriter io.Writer) error { + c.data = make(map[string]any) + + c.status = Passing + for _, binaryName := range []string{"launcher", "osqueryd"} { + coredumpListRaw, err := c.coredumpList(ctx, binaryName) + if err != nil { + c.summary += fmt.Sprintf("could not get coredump data for %s; ", binaryName) + c.data[binaryName] = fmt.Sprintf("listing coredumps: %v", err) + continue + } + + if coredumpListRaw == nil { + c.summary += fmt.Sprintf("%s does not have any coredumps; ", binaryName) + c.data[binaryName] = "N/A" + continue + } + + // At least one coredump exists for at least one binary + c.status = Failing + c.summary += fmt.Sprintf("%s has at least one coredump; ", binaryName) + c.data[binaryName] = string(coredumpListRaw) + } + c.summary = strings.TrimSuffix(strings.TrimSpace(c.summary), ";") + + if extraWriter == io.Discard || c.status == Passing { + // Either not a flare, or we don't have any coredumps to grab info about + return nil + } + + // Gather extra information about the coredumps + extraZip := zip.NewWriter(extraWriter) + defer extraZip.Close() + for _, binaryName := range []string{"launcher", "osqueryd"} { + if err := c.writeCoredumpInfo(ctx, binaryName, extraZip); err != nil { + fmt.Fprintf(extraWriter, "Writing coredump info for %s: %v", binaryName, err) + } + } + + return nil +} + +func (c *coredumpCheckup) coredumpList(ctx context.Context, binaryName string) ([]byte, error) { + coredumpctlListCmd, err := allowedcmd.Coredumpctl(ctx, "--no-pager", "--no-legend", "--json=short", "list", binaryName) + if err != nil { + return nil, fmt.Errorf("could not create coredumpctl command: %w", err) + } + + out, err := coredumpctlListCmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("runing coredumpctl list %s: %w", binaryName, err) + } + + // No coredumps! + if strings.Contains(string(out), "No coredumps found") { + return nil, nil + } + + return out, nil +} + +func (c *coredumpCheckup) writeCoredumpInfo(ctx context.Context, binaryName string, z *zip.Writer) error { + // Print info about all matching coredumps + coredumpctlInfoCmd, err := allowedcmd.Coredumpctl(ctx, "--no-pager", "info", binaryName) + if err != nil { + return fmt.Errorf("could not create coredumpctl info command: %w", err) + } + infoOut, err := coredumpctlInfoCmd.CombinedOutput() + if err != nil { + return fmt.Errorf("runing coredumpctl info %s: %w", binaryName, err) + } + coredumpctlInfoFile, err := z.Create(filepath.Join(".", fmt.Sprintf("coredumpctl-info-%s.txt", binaryName))) + if err != nil { + return fmt.Errorf("creating coredumpctl-info.txt for %s in zip: %w", binaryName, err) + } + if _, err := coredumpctlInfoFile.Write(infoOut); err != nil { + return fmt.Errorf("writing coredumpctl-info.txt in %s zip: %w", binaryName, err) + } + + // Now, try to get a coredump -- this will grab the most recent one + tempDir, err := agent.MkdirTemp("coredump-flare") + if err != nil { + return fmt.Errorf("making temporary directory for coredump from %s: %w", binaryName, err) + } + defer os.RemoveAll(tempDir) + tempDumpFile := filepath.Join(tempDir, fmt.Sprintf("coredump-%s.dump", binaryName)) + coredumpctlDumpCmd, err := allowedcmd.Coredumpctl(ctx, "--no-pager", fmt.Sprintf("--output=%s", tempDumpFile), "dump", binaryName) + if err != nil { + return fmt.Errorf("could not create coredumpctl dump command: %w", err) + } + if err := coredumpctlDumpCmd.Run(); err != nil { + return fmt.Errorf("runing coredumpctl dump %s: %w", binaryName, err) + } + if err := addFileToZip(z, tempDumpFile); err != nil { + return fmt.Errorf("adding coredumpctl dump %s output file to zip: %w", binaryName, err) + } + + return nil +} + +func (c *coredumpCheckup) ExtraFileName() string { + return "coredumps.zip" +} + +func (c *coredumpCheckup) Status() Status { + return c.status +} + +func (c *coredumpCheckup) Summary() string { + return c.summary +} + +func (c *coredumpCheckup) Data() any { + return c.data +} diff --git a/ee/debug/checkups/coredump_other.go b/ee/debug/checkups/coredump_other.go new file mode 100644 index 000000000..93f8da19f --- /dev/null +++ b/ee/debug/checkups/coredump_other.go @@ -0,0 +1,36 @@ +//go:build !linux +// +build !linux + +package checkups + +import ( + "context" + "io" +) + +type coredumpCheckup struct { +} + +func (c *coredumpCheckup) Name() string { + return "" +} + +func (c *coredumpCheckup) ExtraFileName() string { + return "" +} + +func (c *coredumpCheckup) Run(_ context.Context, _ io.Writer) error { + return nil +} + +func (c *coredumpCheckup) Status() Status { + return Informational +} + +func (c *coredumpCheckup) Summary() string { + return "" +} + +func (c *coredumpCheckup) Data() any { + return nil +}