debugd: log_tool: support file globs

We have a bunch of commands that use shell+cat just to expand
file globs.  Add support for that ourselves directly.

BUG=chromium:1115805
TEST=feedback log matches files still

Change-Id: I0be26eba8ebf3b95f10241e2a321e7770c89bba2
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2593894
Tested-by: Mike Frysinger <vapier@chromium.org>
Commit-Queue: Mike Frysinger <vapier@chromium.org>
Reviewed-by: Miriam Zimmerman <mutexlox@chromium.org>
diff --git a/debugd/src/log_tool.cc b/debugd/src/log_tool.cc
index 02583ff..c69bd8a 100644
--- a/debugd/src/log_tool.cc
+++ b/debugd/src/log_tool.cc
@@ -4,6 +4,7 @@
 
 #include "debugd/src/log_tool.h"
 
+#include <glob.h>
 #include <grp.h>
 #include <inttypes.h>
 #include <lzma.h>
@@ -76,6 +77,7 @@
 using Log = LogTool::Log;
 constexpr Log::LogType kCommand = Log::kCommand;
 constexpr Log::LogType kFile = Log::kFile;
+constexpr Log::LogType kGlob = Log::kGlob;
 
 class ArcBugReportLog : public LogTool::Log {
  public:
@@ -148,8 +150,7 @@
   // There might be more than one record, so grab them all.
   // Plus, for <linux-3.19, it's named "console-ramoops", but for newer
   // versions, it's named "console-ramoops-#".
-  {kCommand, "console-ramoops",
-    "cat /sys/fs/pstore/console-ramoops* 2>/dev/null"},
+  {kGlob, "console-ramoops", "/sys/fs/pstore/console-ramoops*"},
   {kFile, "cpuinfo", "/proc/cpuinfo"},
   {kFile, "cr50_version", "/var/cache/cr50-version"},
   {kFile, "cros_ec.log", "/var/log/cros_ec.log",
@@ -190,9 +191,9 @@
     " /run/daemon-store/crosvm/*/log/*.log'", kRoot, kRoot},
   // 'dmesg' needs CAP_SYSLOG.
   {kCommand, "dmesg", "/bin/dmesg", kRoot, kRoot},
-  {kCommand, "drm_gem_objects", "cat /sys/kernel/debug/dri/?/gem",
+  {kGlob, "drm_gem_objects", "/sys/kernel/debug/dri/?/gem",
     SandboxedProcess::kDefaultUser, kDebugfsGroup},
-  {kCommand, "drm_state", "cat /sys/kernel/debug/dri/?/state",
+  {kGlob, "drm_state", "/sys/kernel/debug/dri/?/state",
     SandboxedProcess::kDefaultUser, kDebugfsGroup},
   {kFile, "ec_info", "/var/log/ec_info.txt"},
   {kCommand, "edid-decode",
@@ -203,7 +204,7 @@
     "done"},
   {kFile, "eventlog", "/var/log/eventlog.txt"},
   {kCommand, "font_info", "/usr/share/userfeedback/scripts/font_info"},
-  {kCommand, "framebuffer", "cat /sys/kernel/debug/dri/?/framebuffer",
+  {kGlob, "framebuffer", "/sys/kernel/debug/dri/?/framebuffer",
     SandboxedProcess::kDefaultUser, kDebugfsGroup},
   {kCommand, "fwupd_state", "/sbin/initctl emit fwupdtool-getdevices;"
     "cat /var/lib/fwupd/state.json", kRoot, kRoot},
@@ -230,8 +231,7 @@
   {kCommand, "iwlmvm_module_params", CMD_KERNEL_MODULE_PARAMS(iwlmvm)},
   {kCommand, "iwlwifi_module_params", CMD_KERNEL_MODULE_PARAMS(iwlwifi)},
 #endif  // USE_IWLWIFI_DUMP
-  {kCommand, "kernel-crashes",
-    "cat /var/spool/crash/kernel.*.kcrash 2>/dev/null",
+  {kGlob, "kernel-crashes", "/var/spool/crash/kernel.*.kcrash",
     SandboxedProcess::kDefaultUser, "crash-access"},
   {kCommand, "lsblk", "timeout -s KILL 5s lsblk -a", kRoot, kRoot,
     Log::kDefaultMaxBytes, LogTool::Encoding::kAutodetect, true},
@@ -241,7 +241,7 @@
   {kFile, "mali_memory", "/sys/kernel/debug/mali0/gpu_memory",
     SandboxedProcess::kDefaultUser, kDebugfsGroup},
   {kFile, "memd.parameters", "/var/log/memd/memd.parameters"},
-  {kCommand, "memd clips", "cat /var/log/memd/memd.clip* 2>/dev/null"},
+  {kGlob, "memd clips", "/var/log/memd/memd.clip*"},
   {kFile, "meminfo", "/proc/meminfo"},
   {kCommand, "memory_spd_info",
     // mosys may use 'i2c-dev', which may not be loaded yet.
@@ -292,7 +292,7 @@
   {kFile, "powerd.out", "/var/log/powerd.out"},
   {kFile, "powerwash_count", "/var/log/powerwash_count"},
   {kCommand, "ps", "/bin/ps auxZ"},
-  {kCommand, "qcom_fw_info", "grep ^ /sys/kernel/debug/qcom_socinfo/*/*",
+  {kGlob, "qcom_fw_info", "/sys/kernel/debug/qcom_socinfo/*/*",
     SandboxedProcess::kDefaultUser, kDebugfsGroup},
   // /proc/slabinfo is owned by root and has 0400 permission.
   {kFile, "slabinfo", "/proc/slabinfo", kRoot, kRoot},
@@ -570,6 +570,9 @@
     case kFile:
       output = GetFileLogData();
       break;
+    case kGlob:
+      output = GetGlobLogData();
+      break;
     default:
       DCHECK(false) << "unknown log type";
       return "<unknown log type>";
@@ -606,15 +609,15 @@
   return output;
 }
 
-std::string Log::GetFileLogData() const {
-  DCHECK_EQ(type_, kFile);
-  if (type_ != kFile)
-    return "<log type mismatch>";
-
+// static
+std::string Log::GetFileData(const base::FilePath& path,
+                             int64_t max_bytes,
+                             const std::string& user,
+                             const std::string& group) {
   uid_t old_euid = geteuid();
-  uid_t new_euid = UidForUser(user_);
+  uid_t new_euid = UidForUser(user);
   gid_t old_egid = getegid();
-  gid_t new_egid = GidForGroup(group_);
+  gid_t new_egid = GidForGroup(group);
 
   if (new_euid == -1 || new_egid == -1) {
     return "<not available>";
@@ -634,24 +637,23 @@
   }
 
   std::string contents;
-  const base::FilePath path(data_);
   // Handle special files that don't properly report length/allow lseek.
   if (base::FilePath("/dev").IsParent(path) ||
       base::FilePath("/proc").IsParent(path) ||
       base::FilePath("/sys").IsParent(path)) {
     if (!base::ReadFileToString(path, &contents))
       contents = "<not available>";
-    if (contents.size() > max_bytes_)
-      contents.erase(0, contents.size() - max_bytes_);
+    if (contents.size() > max_bytes)
+      contents.erase(0, contents.size() - max_bytes);
   } else {
     base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
     if (!file.IsValid()) {
       contents = "<not available>";
     } else {
       int64_t length = file.GetLength();
-      if (length > max_bytes_) {
-        file.Seek(base::File::FROM_END, -max_bytes_);
-        length = max_bytes_;
+      if (length > max_bytes) {
+        file.Seek(base::File::FROM_END, -max_bytes);
+        length = max_bytes;
       }
       std::vector<char> buf(length);
       int read = file.ReadAtCurrentPos(buf.data(), buf.size());
@@ -673,6 +675,62 @@
   return contents;
 }
 
+std::string Log::GetFileLogData() const {
+  DCHECK_EQ(type_, kFile);
+  if (type_ != kFile)
+    return "<log type mismatch>";
+
+  return GetFileData(base::FilePath(data_), max_bytes_, user_, group_);
+}
+
+std::string Log::GetGlobLogData() const {
+  DCHECK_EQ(type_, kGlob);
+  if (type_ != kGlob)
+    return "<log type mismatch>";
+
+  // NB: base::FileEnumerator requires a directory to walk, and a pattern to
+  // match against each result.  Here we accept full paths with globs in them.
+  glob_t g;
+  // NB: Feel free to add GLOB_BRACE if a user comes up.
+  int gret = glob(data_.c_str(), 0, nullptr, &g);
+  if (gret == GLOB_NOMATCH) {
+    globfree(&g);
+    return "<no matches>";
+  } else if (gret) {
+    globfree(&g);
+    PLOG(ERROR) << "glob " << data_ << " failed";
+    return "<not available>";
+  }
+
+  // The results array will hold 2 entries per file: the filename, and the
+  // results of reading that file.
+  size_t output_size = 0;
+  std::vector<std::string> results;
+  results.reserve(g.gl_pathc * 2);
+
+  for (size_t pathc = 0; pathc < g.gl_pathc; ++pathc) {
+    const base::FilePath path(g.gl_pathv[pathc]);
+    std::string contents = GetFileData(path, max_bytes_, user_, group_);
+    // NB: The 3 represents the bytes we add in the output string below.
+    output_size += path.value().size() + contents.size() + 3;
+    results.push_back(path.value());
+    results.push_back(contents);
+  }
+  globfree(&g);
+
+  // Combine the results into a single string.  We have a header with the
+  // filename followed by that file's contents.  Very basic format.
+  std::string output;
+  output.reserve(output_size);
+  for (auto iter = results.begin(); iter != results.end(); ++iter) {
+    output += *iter + ":\n";
+    ++iter;
+    output += *iter + "\n";
+  }
+
+  return output;
+}
+
 void Log::DisableMinijailForTest() {
   minijail_disabled_for_test_ = true;
 }
diff --git a/debugd/src/log_tool.h b/debugd/src/log_tool.h
index ecd1171..7e8e2bc 100644
--- a/debugd/src/log_tool.h
+++ b/debugd/src/log_tool.h
@@ -43,7 +43,7 @@
 
   class Log {
    public:
-    enum LogType { kCommand, kFile };
+    enum LogType { kCommand, kFile, kGlob };
 
     static constexpr int64_t kDefaultMaxBytes = 512 * 1024;
 
@@ -63,6 +63,7 @@
 
     std::string GetCommandLogData() const;
     std::string GetFileLogData() const;
+    std::string GetGlobLogData() const;
 
     void DisableMinijailForTest();
 
@@ -72,6 +73,10 @@
    private:
     static uid_t UidForUser(const std::string& name);
     static gid_t GidForGroup(const std::string& group);
+    static std::string GetFileData(const base::FilePath& path,
+                                   int64_t max_bytes,
+                                   const std::string& user,
+                                   const std::string& group);
 
     LogType type_;
     std::string name_;
diff --git a/debugd/src/log_tool_test.cc b/debugd/src/log_tool_test.cc
index 3824c49..e27780b 100644
--- a/debugd/src/log_tool_test.cc
+++ b/debugd/src/log_tool_test.cc
@@ -224,11 +224,79 @@
                              file_two.value(), user_name_, group_name_);
   EXPECT_EQ(log_two.GetLogData(), "<empty>");
 
+  // Test truncation.
   base::FilePath file_three = temp.GetPath().Append("test/file_three");
   ASSERT_TRUE(CreateDirectoryAndWriteFile(file_three, "long input value"));
   const LogTool::Log log_three(LogTool::Log::kFile, "test_log_three",
                                file_three.value(), user_name_, group_name_, 5);
   EXPECT_EQ(log_three.GetLogData(), "value");
+
+  // /proc pseudo file.
+  const LogTool::Log log_proc(LogTool::Log::kFile, "asdf", "/proc/cpuinfo",
+                              user_name_, group_name_);
+  // Should be something large.
+  EXPECT_GE(log_proc.GetLogData().size(), 100);
+
+  // Unknown user.
+  const LogTool::Log log_bad_user(LogTool::Log::kFile, "asdf",
+                                  file_three.value(), "!!@@##", group_name_);
+  EXPECT_EQ(log_bad_user.GetLogData(), "<not available>");
+
+  // Unknown group.
+  const LogTool::Log log_bad_group(LogTool::Log::kFile, "asdf",
+                                   file_three.value(), user_name_, "!!@@##");
+  EXPECT_EQ(log_bad_group.GetLogData(), "<not available>");
+
+  // Missing files.
+  const LogTool::Log log_missing(LogTool::Log::kFile, "asdf", "asdf",
+                                 user_name_, group_name_);
+  EXPECT_EQ(log_missing.GetLogData(), "<not available>");
+}
+
+TEST_F(LogTest, GetGlobLogData) {
+  base::ScopedTempDir temp;
+  ASSERT_TRUE(temp.CreateUniqueTempDir());
+
+  // No matches.
+  base::FilePath file_missing = temp.GetPath().Append("*");
+  const LogTool::Log log_missing(LogTool::Log::kGlob, "missing",
+                                 file_missing.value(), user_name_, group_name_);
+  EXPECT_EQ(log_missing.GetLogData(), "<no matches>");
+
+  // Glob a dir.
+  // NB: We write the files in one order, but globbing should sort the results.
+  base::FilePath test_dir = temp.GetPath().Append("test");
+  base::FilePath file_one = test_dir.Append("file_one");
+  ASSERT_TRUE(CreateDirectoryAndWriteFile(file_one, "test_one_contents"));
+  base::FilePath file_two = test_dir.Append("file_two");
+  ASSERT_TRUE(CreateDirectoryAndWriteFile(file_two, ""));
+  base::FilePath file_three = test_dir.Append("file_three");
+  ASSERT_TRUE(CreateDirectoryAndWriteFile(file_three, "long input value"));
+
+  const LogTool::Log log_dir(LogTool::Log::kGlob, "test_log_dir",
+                             test_dir.Append("*").value(), user_name_,
+                             group_name_);
+  EXPECT_EQ(log_dir.GetLogData(),
+            file_one.value() + ":\ntest_one_contents\n" + file_three.value() +
+                ":\nlong input value\n" + file_two.value() + ":\n\n");
+
+  // /proc pseudo file.
+  const LogTool::Log log_proc(LogTool::Log::kGlob, "asdf", "/proc/cpuinf?",
+                              user_name_, group_name_);
+  // Should be something large.
+  EXPECT_GE(log_proc.GetLogData().size(), 100);
+
+  // Unknown user.
+  const LogTool::Log log_bad_user(LogTool::Log::kGlob, "asdf",
+                                  file_three.value(), "!!@@##", group_name_);
+  EXPECT_EQ(log_bad_user.GetLogData(),
+            file_three.value() + ":\n<not available>\n");
+
+  // Unknown group.
+  const LogTool::Log log_bad_group(LogTool::Log::kGlob, "asdf",
+                                   file_three.value(), user_name_, "!!@@##");
+  EXPECT_EQ(log_bad_group.GetLogData(),
+            file_three.value() + ":\n<not available>\n");
 }
 
 TEST_F(LogTest, GetCommandLogData) {