Merge d67207f93536ef76ab0d79464f921a2bbe92f22a on remote branch

Change-Id: Ic03002cf67cb174c963557a16f73bca0fdebd0c6
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..ce3da9a
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,59 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_defaults {
+    name: "dmesgd_defaults",
+    cflags: [
+        "-Wall",
+        "-Wextra",
+        "-Werror",
+        "-Wno-unused-argument",
+        "-Wno-unused-function",
+        "-Wno-nullability-completeness",
+        "-Os",
+    ],
+}
+
+cc_binary {
+    name: "dmesgd",
+    srcs: [
+        "dmesgd.cpp",
+        "dmesg_parser.cpp",
+    ],
+    defaults: ["dmesgd_defaults"],
+    shared_libs: [
+        "libbase",
+        "libevent",
+        "liblog",
+        "libservices",
+        "libutils",
+    ],
+    init_rc: ["dmesgd.rc"],
+}
+
+cc_test {
+    name: "dmesg_parser_test",
+    defaults: ["dmesgd_defaults"],
+    require_root: false,
+    srcs: [
+        "dmesg_parser.cpp",
+        "dmesg_parser_test.cpp",
+    ],
+}
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..3e4631b
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,2 @@
+enh@google.com
+glider@google.com
diff --git a/dmesg_parser.cpp b/dmesg_parser.cpp
new file mode 100644
index 0000000..6034965
--- /dev/null
+++ b/dmesg_parser.cpp
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <regex>
+#include <string>
+
+#include "dmesg_parser.h"
+
+namespace dmesg_parser {
+
+const std::string kTimestampRe = "^\\[[^\\]]+\\]\\s";
+
+DmesgParser::DmesgParser() : report_ready_(false) {
+    std::string bug_types;
+    for (auto t : {"KFENCE", "KASAN"}) {
+        if (bug_types.empty()) {
+            bug_types = t;
+        } else {
+            bug_types.append("|");
+            bug_types.append(t);
+        }
+    }
+    std::string bug_re = kTimestampRe + "\\[([0-9T\\s]+)\\]\\s(BUG: (" + bug_types + "):.*)";
+    this->bug_pattern_ = std::regex(bug_re);
+    this->ignore_pattern_ = std::regex("([ _][Rx]..|raw): [0-9a-f]{16}|"
+                                       "Hardware name:|Comm:");
+    this->addr64_pattern_ = std::regex("\\b(?:0x)?[0-9a-f]{16}\\b");
+}
+
+/*
+ * Read a single line terminated by a newline, and process it as follows:
+ * 1. If we haven't seen a bug header, skip the current line unless it contains
+ *    "BUG:".
+ *    If it does, parse the line to extract the task ID (T1234), tool name
+ *    (KASAN or KFENCE) and the whole report title (needed for report
+ *    deduplication).
+ * 2. If the current line does not contain the known task ID, skip it.
+ * 3. If the current line contains a delimiter ("====="), stop accepting new
+ *    lines.
+ * 4. Otherwise strip potential sensitive data from the current line and append
+ *    it to the report.
+ */
+void DmesgParser::ProcessLine(const std::string& line) {
+    if (report_ready_) return;
+
+    // We haven't encountered a BUG: line yet.
+    if (current_report_.empty()) {
+        std::smatch m;
+        if (std::regex_search(line, m, bug_pattern_)) {
+            std::string task_re = kTimestampRe + "\\[" + std::string(m[1]) + "\\]\\s";
+            task_line_pattern_ = std::regex(task_re);
+            task_delimiter_pattern_ = std::regex(task_re + "={10,}");
+            current_title_ = m[2];
+            current_tool_ = m[3];
+            current_report_ = this->StripSensitiveData(line);
+        }
+        return;
+    }
+
+    // If there is a delimiter, mark the current report as ready.
+    if (std::regex_search(line, task_delimiter_pattern_)) {
+        report_ready_ = true;
+        return;
+    }
+
+    if (std::regex_search(line, task_line_pattern_)) current_report_ += StripSensitiveData(line);
+}
+
+/*
+ * Return true iff the current report is ready (it was terminated by the "====="
+ * delimiter.
+ */
+bool DmesgParser::ReportReady() const {
+    return report_ready_;
+}
+
+/*
+ * Return the tool that generated the currently collected report.
+ */
+std::string DmesgParser::ReportType() const {
+    return current_tool_;
+}
+
+/*
+ * Return the title of the currently collected report.
+ */
+std::string DmesgParser::ReportTitle() const {
+    return current_title_;
+}
+
+/*
+ * Return the report collected so far and reset the parser.
+ */
+std::string DmesgParser::FlushReport() {
+    report_ready_ = false;
+    return std::move(current_report_);
+}
+
+/*
+ * Strip potentially sensitive data from the reports by performing the
+ * following actions:
+ *  1. Drop the entire line, if it contains a process name:
+ *       [   69.547684] [ T6006]c7   6006  CPU: 7 PID: 6006 Comm: sh Tainted:
+ *
+ *     or hardware name:
+ *       [   69.558923] [ T6006]c7   6006  Hardware name: Phone1
+ *
+ *     or a memory dump, e.g.:
+ *
+ *        ... raw: 4000000000010200 0000000000000000 0000000000000000
+ *
+ *      or register dump:
+ *
+ *        ... RIP: 0033:0x7f96443109da
+ *        ... RSP: 002b:00007ffcf0b51b08 EFLAGS: 00000202 ORIG_RAX: 00000000000000af
+ *        ... RAX: ffffffffffffffda RBX: 000055dc3ee521a0 RCX: 00007f96443109da
+ *
+ *      (on x86_64)
+ *
+ *        ... pc : lpm_cpuidle_enter+0x258/0x384
+ *        ... lr : lpm_cpuidle_enter+0x1d4/0x384
+ *        ... sp : ffffff800820bea0
+ *        ... x29: ffffff800820bea0 x28: ffffffc2305f3ce0
+ *        ... ...
+ *        ... x9 : 0000000000000001 x8 : 0000000000000000
+ *
+ *      (on ARM64)
+ *
+ *  2. For substrings that are known to be followed by sensitive information,
+ *     cut the line after those substrings and append "DELETED\n",
+ *     e.g. " by task ":
+ *        ... Read at addr f0ffff87c23fdf7f by task sh/9971
+ *     and "Corrupted memory at":
+ *        ... Corrupted memory at 0xf0ffff87c23fdf00 [ ! . . . . . . . . . . . . . . . ]
+ *
+ *  3. Replace all strings that look like 64-bit hexadecimal values, with
+ *     XXXXXXXXXXXXXXXX.
+ */
+std::string DmesgParser::StripSensitiveData(const std::string& line) const {
+    if (std::regex_search(line, ignore_pattern_)) return "";
+
+    std::string ret = line;
+    for (std::string infix : {"Corrupted memory at ", " by task "}) {
+        auto pos = ret.find(infix);
+        if (pos != std::string::npos) {
+            ret = ret.substr(0, pos + infix.size()) + "DELETED\n";
+        }
+    }
+    ret = std::regex_replace(ret, addr64_pattern_, "XXXXXXXXXXXXXXXX");
+    return ret;
+}
+
+}  // namespace dmesg_parser
diff --git a/dmesg_parser.h b/dmesg_parser.h
new file mode 100644
index 0000000..7f6b6a9
--- /dev/null
+++ b/dmesg_parser.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <regex>
+#include <string>
+
+namespace dmesg_parser {
+
+class DmesgParser {
+  public:
+    DmesgParser();
+    void ProcessLine(const std::string& line);
+    bool ReportReady() const;
+    std::string ReportType() const;
+    std::string ReportTitle() const;
+    std::string FlushReport();
+
+  private:
+    std::string StripSensitiveData(const std::string& line) const;
+
+    bool report_ready_;
+    std::string last_report_;
+    std::regex bug_pattern_, ignore_pattern_, addr64_pattern_;
+    std::regex task_line_pattern_, task_delimiter_pattern_;
+    std::string current_report_;
+    std::string current_task_, current_tool_, current_title_;
+};
+
+}  // namespace dmesg_parser
diff --git a/dmesg_parser_test.cpp b/dmesg_parser_test.cpp
new file mode 100644
index 0000000..0d288b6
--- /dev/null
+++ b/dmesg_parser_test.cpp
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+#include "dmesg_parser.h"
+
+class DmesgParserTest : public ::testing::Test {
+  public:
+    void ReadLines(const std::vector<std::string>& lines);
+    bool CheckReport(const std::vector<std::string>& lines);
+
+    dmesg_parser::DmesgParser parser;
+    std::string parsed_report;
+};
+
+void DmesgParserTest::ReadLines(const std::vector<std::string>& lines) {
+    for (auto line : lines) parser.ProcessLine(line);
+}
+
+bool DmesgParserTest::CheckReport(const std::vector<std::string>& lines) {
+    if (!parser.ReportReady()) return false;
+    parsed_report = parser.FlushReport();
+
+    std::string report;
+    for (auto line : lines) {
+        report += line;
+    }
+    EXPECT_EQ(report, parsed_report);
+    return report == parsed_report;
+}
+
+TEST_F(DmesgParserTest, SimpleKasanReport) {
+    std::vector<std::string> in = {
+            "[  495.412333] [    T1] init: this line will be dropped\n",
+            "[  495.412345] [ T9971] "
+            "==================================================================\n",
+            "[  495.496694] [ T9971] BUG: KASAN: invalid-access in crash_write+0x134/0x140\n",
+            "[  495.712345] [ T9971] "
+            "==================================================================\n",
+            "[  495.767899] [ T9971] logs after the separator do not belong to report\n",
+    };
+
+    std::vector<std::string> report = {
+            "[  495.496694] [ T9971] BUG: KASAN: invalid-access in crash_write+0x134/0x140\n",
+    };
+
+    ReadLines(in);
+    ASSERT_TRUE(parser.ReportReady());
+    ASSERT_EQ("KASAN", parser.ReportType());
+    ASSERT_EQ("BUG: KASAN: invalid-access in crash_write+0x134/0x140", parser.ReportTitle());
+    ASSERT_TRUE(CheckReport(report));
+}
+
+TEST_F(DmesgParserTest, StrippedKasanReport) {
+    /*
+     * From the following report, only the lines from T9971 between the "======="
+     * delimiters will be preserved, and only those that do not contain raw
+     * memory.
+     * Task name is also stripped off, because it may contain sensitive data.
+     */
+    std::vector<std::string> in = {
+            "[  495.412333] [    T1] init: this line will be dropped\n",
+            "[  495.412345] [ T9971] "
+            "==================================================================\n",
+            "[  495.496694] [ T9971] BUG: KASAN: invalid-access in crash_write+0x134/0x140\n",
+            "[  495.501234] [  T333] random_process: interleaving output with our error report\n",
+            "[  495.503671] [ T9971] Read at addr f0ffff87c23fdf7f by task sh/9971\n",
+            "[  495.510025] [ T9971] Pointer tag: [f0], memory tag: [fe]\n",
+            "[  495.515400] [ T9971] \n",
+            "[  495.667603] [ T9971] raw: 4000000000010200 0000000000000000 0000000000000000 "
+            "0000000100200020\n",
+            "[  495.667634] [ T9971] raw: dead000000000100 dead000000000200 ffffffc14900fc00 "
+            "0000000000000000\n",
+            "[  495.712345] [ T9971] "
+            "==================================================================\n",
+            "[  495.767899] [ T9971] logs after the separator do not belong to report\n",
+    };
+
+    std::vector<std::string> report = {
+            "[  495.496694] [ T9971] BUG: KASAN: invalid-access in crash_write+0x134/0x140\n",
+            "[  495.503671] [ T9971] Read at addr XXXXXXXXXXXXXXXX by task DELETED\n",
+            "[  495.510025] [ T9971] Pointer tag: [f0], memory tag: [fe]\n",
+            "[  495.515400] [ T9971] \n",
+    };
+
+    ReadLines(in);
+    ASSERT_TRUE(parser.ReportReady());
+    ASSERT_EQ("KASAN", parser.ReportType());
+    ASSERT_EQ("BUG: KASAN: invalid-access in crash_write+0x134/0x140", parser.ReportTitle());
+    ASSERT_TRUE(CheckReport(report));
+}
+
+TEST_F(DmesgParserTest, SimpleKfenceReport) {
+    std::vector<std::string> in = {
+            "[  495.412333] [    T1] init: this line will be dropped\n",
+            "[  495.412345] [ T9971] "
+            "==================================================================\n",
+            "[  495.496694] [ T9971] BUG: KFENCE: memory corruption in "
+            "test_corruption+0x98/0x19c\n",
+            "[  495.712345] [ T9971] "
+            "==================================================================\n",
+            "[  495.767899] [ T9971] logs after the separator do not belong to report\n",
+    };
+
+    std::vector<std::string> report = {
+            "[  495.496694] [ T9971] BUG: KFENCE: memory corruption in "
+            "test_corruption+0x98/0x19c\n",
+    };
+
+    ReadLines(in);
+    ASSERT_TRUE(parser.ReportReady());
+    ASSERT_EQ("KFENCE", parser.ReportType());
+    ASSERT_EQ("BUG: KFENCE: memory corruption in test_corruption+0x98/0x19c", parser.ReportTitle());
+    ASSERT_TRUE(CheckReport(report));
+}
+
+TEST_F(DmesgParserTest, StrippedKfenceReport) {
+    std::vector<std::string> in = {
+            "[  200.412333] [    T1] init: this line will be dropped\n",
+            "[  213.648234] [ T8752] "
+            "==================================================================\n",
+            "[  213.648253] [ T8752] BUG: KFENCE: out-of-bounds write in crash_write+0x14c/0x174\n",
+            "[  213.648262] [ T8752] Out-of-bounds write at 0xffffff8938a05000 (4096B left of "
+            "kfence-#2):\n",
+            "[  213.648270] [ T8752]  crash_write+0x14c/0x174\n",
+            "[  213.648367] [ T8752] kfence-#2 [0xffffff8938a06000-0xffffff8938a0603f, size=64, "
+            "cache=kmalloc-128] allocated by task 1:\n",
+            "[  213.648471] [ T8752] CPU: 1 PID: 8752 Comm: sh Tainted: G         C O\n",
+            "[  213.648478] [ T8752] Hardware name: Phone 1\n",
+            "[  213.648498] [ T8752] "
+            "==================================================================\n",
+            "[  495.767899] [ T8752] logs after the separator do not belong to report\n",
+    };
+
+    std::vector<std::string> report = {
+            "[  213.648253] [ T8752] BUG: KFENCE: out-of-bounds write in crash_write+0x14c/0x174\n",
+            "[  213.648262] [ T8752] Out-of-bounds write at XXXXXXXXXXXXXXXX (4096B left of "
+            "kfence-#2):\n",
+            "[  213.648270] [ T8752]  crash_write+0x14c/0x174\n",
+            "[  213.648367] [ T8752] kfence-#2 [XXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXX, size=64, "
+            "cache=kmalloc-128] allocated by task DELETED\n",
+    };
+
+    ReadLines(in);
+    ASSERT_TRUE(parser.ReportReady());
+    ASSERT_EQ("KFENCE", parser.ReportType());
+    ASSERT_EQ("BUG: KFENCE: out-of-bounds write in crash_write+0x14c/0x174", parser.ReportTitle());
+    ASSERT_TRUE(CheckReport(report));
+}
+
+TEST_F(DmesgParserTest, PartialReport) {
+    std::vector<std::string> in = {
+            "[  213.648234] [ T8752] "
+            "==================================================================\n",
+            "[  213.648253] [ T8752] BUG: KFENCE: out-of-bounds write in crash_write+0x14c/0x174\n",
+            "[  213.648262] [ T8752] Out-of-bounds write at 0xffffff8938a05000 (4096B left of "
+            "kfence-#2):\n",
+            "[  213.648270] [ T8752]  crash_write+0x14c/0x174\n",
+    };
+
+    ReadLines(in);
+    ASSERT_FALSE(parser.ReportReady());
+}
+
+TEST_F(DmesgParserTest, TwoReports) {
+    std::vector<std::string> in = {
+            "[  200.412333] [    T1] init: this line will be dropped\n",
+            "[  213.648234] [ T8752] "
+            "==================================================================\n",
+            "[  213.648253] [ T8752] BUG: KFENCE: out-of-bounds write in crash_write+0x14c/0x174\n",
+            "[  213.648262] [ T8752] Out-of-bounds write at 0xffffff8938a05000 (4096B left of "
+            "kfence-#2):\n",
+            "[  214.648234] [ T9971] "
+            "==================================================================\n",
+            "[  215.496694] [ T9971] BUG: KFENCE: memory corruption in "
+            "test_corruption+0x98/0x19c\n",
+            "[  216.648270] [ T8752]  crash_write+0x14c/0x174\n",
+            "[  217.648234] [ T8752] "
+            "==================================================================\n",
+    };
+
+    std::vector<std::string> report = {
+            "[  213.648253] [ T8752] BUG: KFENCE: out-of-bounds write in crash_write+0x14c/0x174\n",
+            "[  213.648262] [ T8752] Out-of-bounds write at XXXXXXXXXXXXXXXX (4096B left of "
+            "kfence-#2):\n",
+            "[  216.648270] [ T8752]  crash_write+0x14c/0x174\n",
+    };
+
+    ReadLines(in);
+    ASSERT_TRUE(parser.ReportReady());
+    ASSERT_EQ("KFENCE", parser.ReportType());
+    ASSERT_EQ("BUG: KFENCE: out-of-bounds write in crash_write+0x14c/0x174", parser.ReportTitle());
+    ASSERT_TRUE(CheckReport(report));
+}
diff --git a/dmesgd.cpp b/dmesgd.cpp
new file mode 100644
index 0000000..d9f673f
--- /dev/null
+++ b/dmesgd.cpp
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <sys/epoll.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <array>
+#include <set>
+#include <string>
+
+#include <android/os/DropBoxManager.h>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
+
+#include "dmesg_parser.h"
+
+// If there are too many reports, the device is horribly broken.
+const unsigned int kMaxReports = 10;
+
+const char kSentPath[] = "/data/misc/dmesgd/sent_reports.txt";
+
+static std::set<std::string> ReadSentReports() {
+    std::set<std::string> ret;
+    std::string content;
+    if (!android::base::ReadFileToString(kSentPath, &content)) {
+        PLOG(ERROR) << kSentPath << " is empty";
+        return ret;
+    }
+    auto lines = android::base::Split(content, "\n");
+    for (auto line : lines) {
+        ret.insert(line);
+    }
+    LOG(ERROR) << "Read " << ret.size() << " records from " << kSentPath;
+    return ret;
+}
+
+static void WriteSentReports(std::set<std::string> reports) {
+    if (!android::base::WriteStringToFile(android::base::Join(reports, ""), kSentPath)) {
+        PLOG(ERROR) << "Failed to write to " << kSentPath;
+    }
+}
+
+const char kUnknown[] = "UNKNOWN";
+
+static std::string GetOneBootHeader(const std::string& pretty, const std::string& pname) {
+    return pretty + ": " + android::base::GetProperty(pname, kUnknown) + "\n";
+};
+
+static std::string GetBootHeaders() {
+    std::string ret = GetOneBootHeader("Build", "ro.build.fingerprint");
+    ret += GetOneBootHeader("Hardware", "ro.product.board");
+    ret += GetOneBootHeader("Revision", "ro.revision");
+    ret += GetOneBootHeader("Bootloader", "ro.bootloader");
+    ret += GetOneBootHeader("Radio", "gsm.version.baseband");
+
+    std::string version;
+    if (!android::base::ReadFileToString("/proc/version", &version)) version = kUnknown;
+    ret += "Kernel: " + version + "\n\n";
+    return ret;
+}
+
+static bool StoreReport(const std::string& tag, const std::string& report) {
+    std::string boot_headers = GetBootHeaders();
+    android::sp<android::os::DropBoxManager> dropbox(new android::os::DropBoxManager());
+    auto status = dropbox->addText(android::String16(tag.c_str()), boot_headers + report);
+    if (!status.isOk()) {
+        LOG(ERROR) << "Dropbox failed";
+        return false;
+    }
+    return true;
+}
+
+static int ProcessDmesg(std::set<std::string> &sent_reports) {
+    std::unique_ptr<FILE, decltype(&pclose)> pipe(popen("dmesg", "r"), pclose);
+    if (!pipe) {
+        PLOG(ERROR) << "popen() failed!";
+        return 1;
+    }
+    dmesg_parser::DmesgParser dmesg_parser;
+
+    char* buffer = NULL;
+    size_t buffer_size = 0;
+    while (getline(&buffer, &buffer_size, pipe.get()) != -1) {
+        std::string line(buffer);
+        if (line.back() != '\n') line += "\n";
+        dmesg_parser.ProcessLine(line);
+        if (dmesg_parser.ReportReady()) {
+            std::string tag = "SYSTEM_" + dmesg_parser.ReportType() + "_ERROR_REPORT";
+            std::string title = dmesg_parser.ReportTitle();
+            if ((sent_reports.find(title) == sent_reports.end()) &&
+                (sent_reports.size() < kMaxReports)) {
+                if (StoreReport(tag, dmesg_parser.FlushReport())) sent_reports.insert(title);
+            }
+        }
+    }
+    free(buffer);
+    return 0;
+}
+
+int main(int, char*[]) {
+    auto sent_reports = ReadSentReports();
+    int result = ProcessDmesg(sent_reports);
+    WriteSentReports(sent_reports);
+    return result;
+}
diff --git a/dmesgd.rc b/dmesgd.rc
new file mode 100644
index 0000000..e5ffdd4
--- /dev/null
+++ b/dmesgd.rc
@@ -0,0 +1,16 @@
+# Copyright (C) 2022 The Android Open Source Project
+
+on property:ro.product.cpu.abilist64=* && property:bootreceiver.enable=1
+    mkdir /data/misc/dmesgd 0700 dmesgd system
+    rm /data/misc/dmesgd/sent_reports.txt
+
+on property:ro.product.cpu.abilist64=* && property:bootreceiver.enable=1 && property:dmesgd.start=1
+    start dmesgd
+    setprop dmesgd.start 0
+
+service dmesgd /system/bin/dmesgd
+    user dmesgd
+    group system
+    class main
+    disabled
+    oneshot