| // Copyright 2021 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Unit tests for crash reporting functionality. |
| |
| #include "secanomalyd/reporter.h" |
| |
| #include <fcntl.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <tuple> |
| #include <vector> |
| |
| #include <base/files/scoped_file.h> |
| #include <base/posix/eintr_wrapper.h> |
| #include <base/strings/string_split.h> |
| #include <brillo/process/process_mock.h> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "secanomalyd/daemon.h" |
| #include "secanomalyd/mounts.h" |
| #include "secanomalyd/processes.h" |
| |
| using ::testing::AnyOf; |
| using ::testing::Eq; |
| using ::testing::MatchesRegex; |
| using ::testing::Return; |
| |
| namespace secanomalyd { |
| |
| namespace { |
| |
| constexpr int kWeight = 100; |
| |
| constexpr char kUsrLocal[] = "/usr/local"; |
| |
| constexpr char kWxMountUsrLocal[] = |
| "/dev/sda1 /usr/local ext4 " |
| "rw,seclabel,nodev,noatime,resgid=20119,commit=600,data=ordered 0 0"; |
| |
| constexpr char kWxMountUsrLocal_FullDescription[] = |
| "/dev/sda1 /usr/local ext4 " |
| "rw,seclabel,nodev,noatime,resgid=20119,commit=600,data=ordered"; |
| |
| constexpr char kMounts[] = |
| "/dev/root / ext2 rw,seclabel,relatime 0 0\n" |
| // |
| "proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0\n" |
| // |
| "tmpfs /run/namespaces tmpfs " |
| "rw,seclabel,nosuid,nodev,noexec,relatime,mode=755 0 0\n" |
| // |
| "/dev/sdb1 /media/removable/USB\040Drive ext2 " |
| "rw,dirsync,nosuid,nodev,noexec,seclabel,relatime,nosymfollow\n" |
| // |
| "fuse:/home/chronos/u-f0df208cd7759644d43f8d7c4c5900e4a4875275/MyFiles/" |
| "Downloads/sample.rar /media/archive/sample.rar fuse.rarfs " |
| "ro,dirsync,nosuid,nodev,noexec,relatime,nosymfollow," |
| "user_id=1000,group_id=1001,default_permissions,allow_other 0 0\n" |
| // |
| "/dev/sda1 /usr/local ext4 " |
| "rw,seclabel,nodev,noatime,resgid=20119,commit=600,data=ordered 0 0"; |
| |
| struct Process { |
| pid_t pid; |
| pid_t ppid; |
| ino_t pidns; |
| ino_t mntns; |
| ino_t userns; |
| std::string comm; |
| std::string args; |
| ProcEntry::SandboxStatus sandbox; |
| }; |
| |
| std::map<std::string, Process> kProcesses = { |
| {"init", |
| { |
| .pid = 1, |
| .ppid = 0, |
| .pidns = 4026531841, |
| .mntns = 4026531836, |
| .userns = 4026531837, |
| .comm = "init", |
| .args = "/sbin/init", |
| .sandbox = 0b000000, |
| }}, |
| {"agetty", |
| { |
| .pid = 471, |
| .ppid = 1, |
| .pidns = 4026531841, |
| .mntns = 4026531836, |
| .userns = 4026531837, |
| .comm = "agetty", |
| .args = "agetty 115200 ttyS0 linux", |
| .sandbox = 0b000000, |
| }}, |
| {"chrome", |
| { |
| .pid = 1111, |
| .ppid = 1033, |
| .pidns = 4026531841, |
| .mntns = 4026531836, |
| .userns = 4026531848, |
| .comm = "chrome", |
| .args = "/opt/google/chrome/chrome --type=renderer", |
| .sandbox = 0b111010, |
| }}}; |
| |
| constexpr char kInit_FullDescription[] = |
| "init " |
| "/sbin/init"; |
| constexpr char kAgetty_FullDescription[] = |
| "agetty " |
| "agetty 115200 ttyS0 linux"; |
| |
| base::ScopedFD GetDevNullFd() { |
| return base::ScopedFD(HANDLE_EINTR(open("/dev/null", O_WRONLY))); |
| } |
| |
| base::ScopedFD GetDevZeroFd() { |
| return base::ScopedFD(HANDLE_EINTR(open("/dev/zero", O_RDONLY))); |
| } |
| |
| } // namespace |
| |
| TEST(SignatureTest, SignatureForOneMount) { |
| MountEntryMap wx_mounts; |
| wx_mounts.emplace(kUsrLocal, kWxMountUsrLocal); |
| |
| std::string signature = GenerateMountSignature(wx_mounts); |
| EXPECT_THAT(signature, MatchesRegex("-usr-local-[0-9A-F]{10}")); |
| } |
| |
| TEST(SignatureTest, SignatureForRootMount) { |
| MountEntryMap wx_mounts; |
| wx_mounts.emplace("/", "/dev/root / ext2 rw,seclabel,relatime 0 0"); |
| |
| std::string signature = GenerateMountSignature(wx_mounts); |
| EXPECT_THAT(signature, MatchesRegex("slashroot-[0-9A-F]{10}")); |
| } |
| |
| TEST(SignatureTest, SignatureForMultipleMounts) { |
| MountEntryMap wx_mounts; |
| wx_mounts.emplace(kUsrLocal, kWxMountUsrLocal); |
| |
| MountEntry wx_root( |
| "/dev/sda1 /mnt/stateful_partition ext4 " |
| "rw,seclabel,nosuid,nodev,noatime," |
| "resgid=20119,commit=600,data=ordered 0 0"); |
| MountEntry wx_usb( |
| "/dev/sdb1 /media/removable/USB_Drive ext2 " |
| "rw,dirsync,nosuid,nodev,seclabel,relatime,nosymfollow"); |
| |
| wx_mounts.emplace(wx_root.dest(), wx_root); |
| wx_mounts.emplace(wx_usb.dest(), wx_usb); |
| |
| std::string signature = GenerateMountSignature(wx_mounts); |
| EXPECT_THAT(signature, |
| MatchesRegex("-media-removable-USB_Drive-[0-9A-F]{10}")); |
| |
| // Make sure the signature doesn't change when insertion order changes. |
| wx_mounts.clear(); |
| wx_mounts.emplace(wx_usb.dest(), wx_usb); |
| wx_mounts.emplace(wx_root.dest(), wx_root); |
| wx_mounts.emplace(kUsrLocal, kWxMountUsrLocal); |
| |
| std::string new_signature = GenerateMountSignature(wx_mounts); |
| ASSERT_THAT(new_signature, |
| MatchesRegex("-media-removable-USB_Drive-[0-9A-F]{10}")); |
| EXPECT_EQ(signature, new_signature); |
| } |
| |
| TEST(SignatureTest, SignatureForPathWithHash) { |
| MountEntryMap wx_mounts; |
| |
| MountEntry wx_shadow_root_1( |
| "/dev/sda1 /home/root/deadbeef1234567890badbeef1234567890deadb ext4 " |
| "rw,nosuid,nodev,noatime,nosymfollow"); |
| MountEntry wx_shadow_root_2( |
| "/dev/sda1 /home/root/1234567890badbeefdeadbeef1234567890badbe ext4 " |
| "rw,nosuid,nodev,noatime,nosymfollow"); |
| |
| wx_mounts.emplace(wx_shadow_root_1.dest(), wx_shadow_root_1); |
| std::string signature1 = GenerateMountSignature(wx_mounts); |
| |
| wx_mounts.clear(); |
| wx_mounts.emplace(wx_shadow_root_2.dest(), wx_shadow_root_2); |
| std::string signature2 = GenerateMountSignature(wx_mounts); |
| |
| ASSERT_EQ(signature1, signature2); |
| } |
| |
| TEST(SignatureTest, SignatureEmptyProcs) { |
| ProcEntries procs; |
| std::optional<std::string> signature = GenerateProcSignature(procs); |
| EXPECT_EQ(signature, std::nullopt); |
| } |
| |
| TEST(SignatureTest, SignatureForOneProc) { |
| ProcEntries procs; |
| Process proc = kProcesses["init"]; |
| procs.emplace_back(ProcEntry(proc.pid, proc.ppid, proc.pidns, proc.mntns, |
| proc.userns, proc.comm, proc.args, |
| proc.sandbox)); |
| std::optional<std::string> signature = GenerateProcSignature(procs); |
| ASSERT_TRUE(signature.has_value()); |
| EXPECT_EQ(signature.value(), proc.comm); |
| } |
| |
| TEST(SignatureTest, SignatureForMultipleProcs) { |
| ProcEntries procs; |
| Process init_proc = kProcesses["init"]; |
| Process agetty_proc = kProcesses["agetty"]; |
| procs.emplace_back(ProcEntry( |
| init_proc.pid, init_proc.ppid, init_proc.pidns, init_proc.mntns, |
| init_proc.userns, init_proc.comm, init_proc.args, init_proc.sandbox)); |
| procs.emplace_back(ProcEntry(agetty_proc.pid, agetty_proc.ppid, |
| agetty_proc.pidns, agetty_proc.mntns, |
| agetty_proc.userns, agetty_proc.comm, |
| agetty_proc.args, agetty_proc.sandbox)); |
| std::optional<std::string> signature = GenerateProcSignature(procs); |
| |
| ASSERT_TRUE(signature.has_value()); |
| // The signature is the comm value of a randomly chosen processes. |
| EXPECT_THAT(signature.value(), |
| AnyOf(Eq(init_proc.comm), Eq(agetty_proc.comm))); |
| } |
| |
| TEST(SignatureTest, SignatureForEmptyPaths) { |
| FilePaths paths = {}; |
| std::optional<std::string> signature = GeneratePathSignature(paths); |
| ASSERT_EQ(signature, std::nullopt); |
| } |
| |
| TEST(SignatureTest, SignatureForOnePath) { |
| FilePaths paths = {base::FilePath("/usr/sbin/bad_exe")}; |
| std::optional<std::string> signature = GeneratePathSignature(paths); |
| ASSERT_TRUE(signature.has_value()); |
| EXPECT_EQ(signature.value(), "/usr/sbin/bad_exe"); |
| } |
| |
| TEST(SignatureTest, SignatureForMultiplePaths) { |
| FilePaths paths = {base::FilePath("/usr/sbin/bad_exe_1"), |
| base::FilePath("/usr/sbin/bad_exe_2")}; |
| std::optional<std::string> signature = GeneratePathSignature(paths); |
| ASSERT_TRUE(signature.has_value()); |
| EXPECT_EQ(signature.value(), paths.begin()->value()); |
| } |
| |
| // A simple W+X mount report will only contain one W+X mount and empty |
| // accompanying sections. |
| TEST(ReporterTest, SimpleWxMountReport) { |
| MountEntryMap wx_mounts; |
| wx_mounts.emplace(kUsrLocal, kWxMountUsrLocal); |
| ProcEntries proc_entries; |
| FilePaths memfd_execs; |
| |
| MaybeReport report = GenerateAnomalousSystemReport( |
| wx_mounts, proc_entries, memfd_execs, std::nullopt, std::nullopt); |
| |
| ASSERT_TRUE(report.has_value()); |
| |
| std::vector<std::string_view> lines = base::SplitStringPiece( |
| report.value(), "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| // One signature line, one metadata line, four headers, one anomalous mount |
| // and two "empty section" messages. |
| ASSERT_EQ(lines.size(), 9u); |
| |
| // Signature. |
| EXPECT_THAT(std::string(lines[0]), MatchesRegex("-usr-local-[0-9A-F]{10}")); |
| |
| // Metadata. |
| base::StringPairs kvpairs; |
| ASSERT_TRUE( |
| base::SplitStringIntoKeyValuePairs(lines[1], '\x01', '\x02', &kvpairs)); |
| for (const auto& kv : kvpairs) { |
| if (kv.first == "signal") { |
| // The anomaly was a writable+executable mount so the signal is |
| // "wx-mount". |
| EXPECT_EQ(kv.second, "wx-mount"); |
| } else if (kv.first == "dest") { |
| // Metadata 'dest' key matches signature. |
| EXPECT_EQ(kv.second, "/usr/local"); |
| } |
| } |
| |
| // Headers. |
| EXPECT_EQ(std::string(lines[2]), "=== Anomalous conditions ==="); |
| EXPECT_EQ(std::string(lines[3]), "=== W+X mounts ==="); |
| EXPECT_EQ(std::string(lines[5]), "=== All mounts ==="); |
| EXPECT_EQ(std::string(lines[7]), "=== All processes ==="); |
| |
| // Anomalous mount. |
| EXPECT_EQ(std::string(lines[4]), kWxMountUsrLocal_FullDescription); |
| |
| // Empty sections. |
| EXPECT_EQ(std::string(lines[6]), "Could not obtain mounts"); |
| EXPECT_EQ(std::string(lines[8]), "Could not obtain processes"); |
| } |
| |
| // A simple forbidden intersection report will only contain one violating |
| // process and empty accompanying sections. |
| TEST(ReporterTest, SimpleForbiddenIntersectionReport) { |
| MountEntryMap wx_mounts; |
| ProcEntries anomalous_procs; |
| Process init_proc = kProcesses["init"]; |
| anomalous_procs.emplace_back(ProcEntry( |
| init_proc.pid, init_proc.ppid, init_proc.pidns, init_proc.mntns, |
| init_proc.userns, init_proc.comm, init_proc.args, init_proc.sandbox)); |
| FilePaths memfd_execs; |
| |
| MaybeReport report = GenerateAnomalousSystemReport( |
| wx_mounts, anomalous_procs, memfd_execs, std::nullopt, std::nullopt); |
| |
| ASSERT_TRUE(report.has_value()); |
| |
| std::vector<std::string_view> lines = base::SplitStringPiece( |
| report.value(), "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| // One signature line, one metadata line, four headers, one anomalous |
| // process and two "empty section" messages. |
| ASSERT_EQ(lines.size(), 9u); |
| |
| // Signature. |
| EXPECT_EQ(std::string(lines[0]), init_proc.comm); |
| |
| // Metadata. |
| base::StringPairs kvpairs; |
| ASSERT_TRUE( |
| base::SplitStringIntoKeyValuePairs(lines[1], '\x01', '\x02', &kvpairs)); |
| for (const auto& kv : kvpairs) { |
| if (kv.first == "signal") { |
| // The anomaly was a forbidden intersection process so the signal is |
| // "forbidden-intersection-process". |
| EXPECT_EQ(kv.second, "forbidden-intersection-process"); |
| } else if (kv.first == "comm") { |
| // Metadata 'dest' key matches signature. |
| EXPECT_EQ(kv.second, init_proc.comm); |
| } |
| } |
| |
| // Headers. |
| EXPECT_EQ(std::string(lines[2]), "=== Anomalous conditions ==="); |
| EXPECT_EQ(std::string(lines[3]), "=== Forbidden intersection processes ==="); |
| EXPECT_EQ(std::string(lines[5]), "=== All mounts ==="); |
| EXPECT_EQ(std::string(lines[7]), "=== All processes ==="); |
| |
| // Anomalous mount. |
| EXPECT_EQ(std::string(lines[4]), kInit_FullDescription); |
| |
| // Empty sections. |
| EXPECT_EQ(std::string(lines[6]), "Could not obtain mounts"); |
| EXPECT_EQ(std::string(lines[8]), "Could not obtain processes"); |
| } |
| |
| // A simple memfd exec report will only contain one memfd execution attempt and |
| // empty accompanying sections. |
| TEST(ReporterTest, SimpleMemfdExecReport) { |
| MountEntryMap wx_mounts; |
| ProcEntries proc_entries; |
| FilePaths memfd_execs = {base::FilePath("/usr/sbin/bad_exe")}; |
| |
| MaybeReport report = GenerateAnomalousSystemReport( |
| wx_mounts, proc_entries, memfd_execs, std::nullopt, std::nullopt); |
| |
| ASSERT_TRUE(report.has_value()); |
| |
| std::vector<std::string_view> lines = base::SplitStringPiece( |
| report.value(), "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| // One signature line, one metadata line, four headers, one memfd exec attempt |
| // and two "empty section" messages. |
| ASSERT_EQ(lines.size(), 9u); |
| |
| // Signature. |
| EXPECT_THAT(std::string(lines[0]), "/usr/sbin/bad_exe"); |
| |
| // Metadata. |
| base::StringPairs kvpairs; |
| ASSERT_TRUE( |
| base::SplitStringIntoKeyValuePairs(lines[1], '\x01', '\x02', &kvpairs)); |
| for (const auto& kv : kvpairs) { |
| if (kv.first == "signal") { |
| // The anomaly was a memfd execution attempt so the signal is |
| // "memfd-exec-attempt". |
| EXPECT_EQ(kv.second, "memfd-exec-attempt"); |
| } else if (kv.first == "executable") { |
| // Metadata 'executable' key matches signature. |
| EXPECT_EQ(kv.second, "/usr/sbin/bad_exe"); |
| } |
| } |
| |
| // Headers. |
| EXPECT_EQ(std::string(lines[2]), "=== Anomalous conditions ==="); |
| EXPECT_EQ(std::string(lines[3]), "=== Executables attempting memfd exec ==="); |
| EXPECT_EQ(std::string(lines[5]), "=== All mounts ==="); |
| EXPECT_EQ(std::string(lines[7]), "=== All processes ==="); |
| |
| // Anomalous mount. |
| EXPECT_EQ(std::string(lines[4]), "/usr/sbin/bad_exe"); |
| |
| // Empty sections. |
| EXPECT_EQ(std::string(lines[6]), "Could not obtain mounts"); |
| EXPECT_EQ(std::string(lines[8]), "Could not obtain processes"); |
| } |
| |
| // Test reports with W+X mount and forbidden intersection process anomalies, |
| // complete with the accompanying sections for all mounts and all processes. |
| TEST(ReporterTest, MountAndProcessAnomalyReport) { |
| MountEntryMap wx_mounts; |
| wx_mounts.emplace(kUsrLocal, kWxMountUsrLocal); |
| |
| MaybeMountEntries all_mounts = ReadMountsFromString(kMounts); |
| MaybeMountEntries uploadable_mounts = FilterPrivateMounts(all_mounts); |
| |
| ProcEntries anomalous_procs; |
| Process init_proc = kProcesses["init"]; |
| Process agetty_proc = kProcesses["agetty"]; |
| anomalous_procs.emplace_back(ProcEntry( |
| init_proc.pid, init_proc.ppid, init_proc.pidns, init_proc.mntns, |
| init_proc.userns, init_proc.comm, init_proc.args, init_proc.sandbox)); |
| anomalous_procs.emplace_back( |
| ProcEntry(agetty_proc.pid, agetty_proc.ppid, agetty_proc.pidns, |
| agetty_proc.mntns, agetty_proc.userns, agetty_proc.comm, |
| agetty_proc.args, agetty_proc.sandbox)); |
| |
| ProcEntries all_procs; |
| Process chrome_proc = kProcesses["chrome"]; |
| all_procs.emplace_back(ProcEntry( |
| init_proc.pid, init_proc.ppid, init_proc.pidns, init_proc.mntns, |
| init_proc.userns, init_proc.comm, init_proc.args, init_proc.sandbox)); |
| all_procs.emplace_back(ProcEntry(agetty_proc.pid, agetty_proc.ppid, |
| agetty_proc.pidns, agetty_proc.mntns, |
| agetty_proc.userns, agetty_proc.comm, |
| agetty_proc.args, agetty_proc.sandbox)); |
| all_procs.emplace_back(ProcEntry(chrome_proc.pid, chrome_proc.ppid, |
| chrome_proc.pidns, chrome_proc.mntns, |
| chrome_proc.userns, chrome_proc.comm, |
| chrome_proc.args, chrome_proc.sandbox)); |
| MaybeProcEntries maybe_procs = MaybeProcEntries(all_procs); |
| |
| FilePaths memfd_execs; |
| |
| MaybeReport report = GenerateAnomalousSystemReport( |
| wx_mounts, anomalous_procs, memfd_execs, uploadable_mounts, maybe_procs); |
| |
| ASSERT_TRUE(report.has_value()); |
| |
| std::vector<std::string_view> lines = base::SplitStringPiece( |
| report.value(), "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| // One signature line, one metadata line, four headers, one W+X mount |
| // anomaly, four mounts, two process anomalies and three processes. |
| ASSERT_EQ(lines.size(), 17u); |
| |
| // Signature. |
| EXPECT_THAT(std::string(lines[0]), MatchesRegex("-usr-local-[0-9A-F]{10}")); |
| |
| // Metadata. |
| base::StringPairs kvpairs; |
| ASSERT_TRUE( |
| base::SplitStringIntoKeyValuePairs(lines[1], '\x01', '\x02', &kvpairs)); |
| for (const auto& kv : kvpairs) { |
| if (kv.first == "signal") { |
| // Both anomaly types are present so the signal is "multiple-anomalies". |
| EXPECT_EQ(kv.second, "multiple-anomalies"); |
| } else if (kv.first == "dest") { |
| EXPECT_EQ(kv.second, "/usr/local"); |
| } |
| } |
| |
| // Headers. |
| EXPECT_EQ(std::string(lines[2]), "=== Anomalous conditions ==="); |
| EXPECT_EQ(std::string(lines[3]), "=== W+X mounts ==="); |
| EXPECT_EQ(std::string(lines[5]), "=== Forbidden intersection processes ==="); |
| EXPECT_EQ(std::string(lines[8]), "=== All mounts ==="); |
| EXPECT_EQ(std::string(lines[13]), "=== All processes ==="); |
| |
| // Anomalous mount. |
| EXPECT_EQ(std::string(lines[4]), kWxMountUsrLocal_FullDescription); |
| |
| // Anomalous processes. |
| EXPECT_EQ(std::string(lines[6]), kInit_FullDescription); |
| EXPECT_EQ(std::string(lines[7]), kAgetty_FullDescription); |
| |
| // Actual mounts. |
| EXPECT_EQ(std::string(lines[9]), "/dev/root / ext2 rw,seclabel,relatime"); |
| EXPECT_EQ(std::string(lines[10]), |
| "proc /proc proc rw,nosuid,nodev,noexec,relatime"); |
| EXPECT_EQ(std::string(lines[11]), |
| "tmpfs /run/namespaces tmpfs " |
| "rw,seclabel,nosuid,nodev,noexec,relatime,mode=755"); |
| EXPECT_EQ(std::string(lines[12]), kWxMountUsrLocal_FullDescription); |
| |
| // Actual processes. |
| EXPECT_EQ(std::string(lines[14]), init_proc.args); |
| EXPECT_EQ(std::string(lines[15]), agetty_proc.args); |
| EXPECT_EQ(std::string(lines[16]), chrome_proc.args); |
| } |
| |
| // A full report will contain at least one instance of each anomaly type, |
| // complete with the accompanying sections for all mounts and all processes. |
| TEST(ReporterTest, FullReport) { |
| MountEntryMap wx_mounts; |
| wx_mounts.emplace(kUsrLocal, kWxMountUsrLocal); |
| |
| MaybeMountEntries all_mounts = ReadMountsFromString(kMounts); |
| MaybeMountEntries uploadable_mounts = FilterPrivateMounts(all_mounts); |
| |
| ProcEntries anomalous_procs; |
| Process init_proc = kProcesses["init"]; |
| Process agetty_proc = kProcesses["agetty"]; |
| anomalous_procs.emplace_back(ProcEntry( |
| init_proc.pid, init_proc.ppid, init_proc.pidns, init_proc.mntns, |
| init_proc.userns, init_proc.comm, init_proc.args, init_proc.sandbox)); |
| anomalous_procs.emplace_back( |
| ProcEntry(agetty_proc.pid, agetty_proc.ppid, agetty_proc.pidns, |
| agetty_proc.mntns, agetty_proc.userns, agetty_proc.comm, |
| agetty_proc.args, agetty_proc.sandbox)); |
| |
| ProcEntries all_procs; |
| Process chrome_proc = kProcesses["chrome"]; |
| all_procs.emplace_back(ProcEntry( |
| init_proc.pid, init_proc.ppid, init_proc.pidns, init_proc.mntns, |
| init_proc.userns, init_proc.comm, init_proc.args, init_proc.sandbox)); |
| all_procs.emplace_back(ProcEntry(agetty_proc.pid, agetty_proc.ppid, |
| agetty_proc.pidns, agetty_proc.mntns, |
| agetty_proc.userns, agetty_proc.comm, |
| agetty_proc.args, agetty_proc.sandbox)); |
| all_procs.emplace_back(ProcEntry(chrome_proc.pid, chrome_proc.ppid, |
| chrome_proc.pidns, chrome_proc.mntns, |
| chrome_proc.userns, chrome_proc.comm, |
| chrome_proc.args, chrome_proc.sandbox)); |
| MaybeProcEntries maybe_procs = MaybeProcEntries(all_procs); |
| |
| FilePaths memfd_execs = {base::FilePath("/usr/sbin/bad_exe")}; |
| |
| MaybeReport report = GenerateAnomalousSystemReport( |
| wx_mounts, anomalous_procs, memfd_execs, uploadable_mounts, maybe_procs); |
| |
| ASSERT_TRUE(report.has_value()); |
| |
| std::vector<std::string_view> lines = base::SplitStringPiece( |
| report.value(), "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| // One signature line, one metadata line, four headers, one W+X mount, two |
| // process anomalies, one memfd exec attempt, four regular mounts and three |
| // regular processes. |
| ASSERT_EQ(lines.size(), 19u); |
| |
| // Signature. |
| EXPECT_THAT(std::string(lines[0]), "/usr/sbin/bad_exe"); |
| |
| // Metadata. |
| base::StringPairs kvpairs; |
| ASSERT_TRUE( |
| base::SplitStringIntoKeyValuePairs(lines[1], '\x01', '\x02', &kvpairs)); |
| for (const auto& kv : kvpairs) { |
| if (kv.first == "signal") { |
| // All anomaly types are present so the signal is "multiple-anomalies" and |
| // the memfd exec attempt is used for the signature. |
| EXPECT_EQ(kv.second, "multiple-anomalies"); |
| } else if (kv.first == "executable") { |
| EXPECT_EQ(kv.second, "/usr/sbin/bad_exe"); |
| } |
| } |
| |
| // Headers. |
| EXPECT_EQ(std::string(lines[2]), "=== Anomalous conditions ==="); |
| EXPECT_EQ(std::string(lines[3]), "=== W+X mounts ==="); |
| EXPECT_EQ(std::string(lines[5]), "=== Forbidden intersection processes ==="); |
| EXPECT_EQ(std::string(lines[8]), "=== Executables attempting memfd exec ==="); |
| EXPECT_EQ(std::string(lines[10]), "=== All mounts ==="); |
| EXPECT_EQ(std::string(lines[15]), "=== All processes ==="); |
| |
| // Anomalous mount. |
| EXPECT_EQ(std::string(lines[4]), kWxMountUsrLocal_FullDescription); |
| |
| // Anomalous processes. |
| EXPECT_EQ(std::string(lines[6]), kInit_FullDescription); |
| EXPECT_EQ(std::string(lines[7]), kAgetty_FullDescription); |
| |
| // Memfd execution attempts. |
| EXPECT_EQ(std::string(lines[9]), "/usr/sbin/bad_exe"); |
| |
| // Actual mounts. |
| EXPECT_EQ(std::string(lines[11]), "/dev/root / ext2 rw,seclabel,relatime"); |
| EXPECT_EQ(std::string(lines[12]), |
| "proc /proc proc rw,nosuid,nodev,noexec,relatime"); |
| EXPECT_EQ(std::string(lines[13]), |
| "tmpfs /run/namespaces tmpfs " |
| "rw,seclabel,nosuid,nodev,noexec,relatime,mode=755"); |
| EXPECT_EQ(std::string(lines[14]), kWxMountUsrLocal_FullDescription); |
| |
| // Actual processes. |
| EXPECT_EQ(std::string(lines[16]), init_proc.args); |
| EXPECT_EQ(std::string(lines[17]), agetty_proc.args); |
| EXPECT_EQ(std::string(lines[18]), chrome_proc.args); |
| } |
| |
| TEST(ReporterTest, CrashReporterSuceeds) { |
| MountEntryMap wx_mounts; |
| wx_mounts.emplace(kUsrLocal, kWxMountUsrLocal); |
| |
| std::unique_ptr<brillo::ProcessMock> crash_reporter( |
| new brillo::ProcessMock()); |
| |
| base::ScopedFD dev_null = GetDevNullFd(); |
| |
| EXPECT_CALL(*crash_reporter, Start()).WillOnce(Return(true)); |
| EXPECT_CALL(*crash_reporter, |
| RedirectUsingPipe(STDIN_FILENO, true /*is_input*/)); |
| EXPECT_CALL(*crash_reporter, GetPipe(STDIN_FILENO)) |
| .WillOnce(Return(dev_null.get())); |
| EXPECT_CALL(*crash_reporter, Wait()).WillOnce(Return(0)); |
| |
| EXPECT_TRUE(SendReport("This is a report", crash_reporter.get(), kWeight, |
| true /*report_in_dev_mode*/)); |
| |
| // SendReport() puts the subprocess' stdin fd into a scoper class, so it's |
| // been closed by the time SendReport() returns. |
| std::ignore = dev_null.release(); |
| } |
| |
| TEST(ReporterTest, StartFails) { |
| MountEntryMap wx_mounts; |
| wx_mounts.emplace(kUsrLocal, kWxMountUsrLocal); |
| |
| std::unique_ptr<brillo::ProcessMock> crash_reporter( |
| new brillo::ProcessMock()); |
| |
| base::ScopedFD dev_null = GetDevNullFd(); |
| |
| EXPECT_CALL(*crash_reporter, Start()).WillOnce(Return(false)); |
| EXPECT_CALL(*crash_reporter, |
| RedirectUsingPipe(STDIN_FILENO, true /*is_input*/)); |
| |
| EXPECT_FALSE(SendReport("This is a report", crash_reporter.get(), kWeight, |
| true /*report_in_dev_mode*/)); |
| } |
| |
| TEST(ReporterTest, GetPipeFails) { |
| MountEntryMap wx_mounts; |
| wx_mounts.emplace(kUsrLocal, kWxMountUsrLocal); |
| |
| std::unique_ptr<brillo::ProcessMock> crash_reporter( |
| new brillo::ProcessMock()); |
| |
| EXPECT_CALL(*crash_reporter, Start()).WillOnce(Return(true)); |
| EXPECT_CALL(*crash_reporter, |
| RedirectUsingPipe(STDIN_FILENO, true /*is_input*/)); |
| // Return -1 which is the error value for GetPipe(). |
| EXPECT_CALL(*crash_reporter, GetPipe(STDIN_FILENO)).WillOnce(Return(-1)); |
| |
| EXPECT_FALSE(SendReport("This is a report", crash_reporter.get(), kWeight, |
| true /*report_in_dev_mode*/)); |
| } |
| |
| TEST(ReporterTest, WriteFileDescriptorFails) { |
| MountEntryMap wx_mounts; |
| wx_mounts.emplace(kUsrLocal, kWxMountUsrLocal); |
| |
| std::unique_ptr<brillo::ProcessMock> crash_reporter( |
| new brillo::ProcessMock()); |
| |
| base::ScopedFD dev_zero = GetDevZeroFd(); |
| |
| EXPECT_CALL(*crash_reporter, Start()).WillOnce(Return(true)); |
| EXPECT_CALL(*crash_reporter, |
| RedirectUsingPipe(STDIN_FILENO, true /*is_input*/)); |
| // /dev/zero cannot be written to, so attempting to write the report will |
| // fail. |
| EXPECT_CALL(*crash_reporter, GetPipe(STDIN_FILENO)) |
| .WillOnce(Return(dev_zero.get())); |
| |
| EXPECT_FALSE(SendReport("This is a report", crash_reporter.get(), kWeight, |
| true /*report_in_dev_mode*/)); |
| |
| // SendReport() puts the subprocess' stdin fd into a scoper class, so it's |
| // been closed by the time SendReport() returns. |
| std::ignore = dev_zero.release(); |
| } |
| |
| TEST(ReporterTest, WaitFails) { |
| MountEntryMap wx_mounts; |
| wx_mounts.emplace(kUsrLocal, kWxMountUsrLocal); |
| |
| std::unique_ptr<brillo::ProcessMock> crash_reporter( |
| new brillo::ProcessMock()); |
| |
| base::ScopedFD dev_null = GetDevNullFd(); |
| |
| EXPECT_CALL(*crash_reporter, Start()).WillOnce(Return(true)); |
| EXPECT_CALL(*crash_reporter, |
| RedirectUsingPipe(STDIN_FILENO, true /*is_input*/)); |
| EXPECT_CALL(*crash_reporter, GetPipe(STDIN_FILENO)) |
| .WillOnce(Return(dev_null.get())); |
| EXPECT_CALL(*crash_reporter, Wait()).WillOnce(Return(1)); |
| |
| EXPECT_FALSE(SendReport("This is a report", crash_reporter.get(), kWeight, |
| true /*report_in_dev_mode*/)); |
| |
| // SendReport() puts the subprocess' stdin fd into a scoper class, so it's |
| // been closed by the time SendReport() returns. |
| std::ignore = dev_null.release(); |
| } |
| |
| } // namespace secanomalyd |