Files
hardware_samsung-extra_inte…/debug-tools/bootlogger/Logger.cpp
2023-12-06 18:30:28 +09:00

443 lines
13 KiB
C++

/*
* Copyright 2021 Soo Hwan Na "Royna"
*
* 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 <android-base/file.h>
#include <android-base/properties.h>
#include <chrono>
#include <cstdlib>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/sysinfo.h>
#include <unistd.h>
#include <atomic>
#include <cstring>
#include <fstream>
#include <functional>
#include <filesystem>
#include <iostream>
#include <map>
#include <memory>
#include <regex>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#include "LoggerInternal.h"
using android::base::GetProperty;
using android::base::GetBoolProperty;
using android::base::WaitForProperty;
using android::base::WriteStringToFile;
using std::chrono_literals::operator""s; // NOLINT (misc-unused-using-decls)
namespace fs = std::filesystem;
// TODO is there anything better than global variable?
static fs::path kLogDir;
// Base context for outputs with file
struct OutputContext {
// File path (absolute) of this context.
// Note that .txt suffix is auto appended in constructor.
std::string kFilePath;
// Just the filename only
std::string kFileName;
// Takes one argument 'filename' without file extension
OutputContext(const std::string &filename) : kFileName(filename) {
auto localLogDir = kLogDir; // Copy-construct
kFilePath = localLogDir.append(kFileName + ".txt").string();
}
// Takes two arguments 'filename' and is_filter
OutputContext(const std::string &filename, const bool isFilter)
: OutputContext(filename) {
is_filter = isFilter;
}
// No default constructor
OutputContext() = delete;
/**
* Open outfilestream.
*/
bool openOutput(void) {
const char *kFilePathStr = kFilePath.c_str();
ALOGI("%s: Opening '%s'%s", __func__, kFilePathStr, is_filter ? " (filter)" : "");
fd = open(kFilePathStr, O_RDWR | O_CREAT, 0644);
if (fd < 0)
PLOGE("Failed to open '%s'", kFilePathStr);
return fd >= 0;
}
/**
* Writes the string to this context's file
*
* @param string data
*/
void writeToOutput(const std::string &data) {
len += write(fd, data.c_str(), data.size());
len += write(fd, "\n", 1);
if (len > BUF_SIZE) {
fsync(fd);
len = 0;
}
}
operator bool() const { return fd >= 0; }
/**
* To be called on ~OutputContext Sub-classes
*/
void cleanupOutputCtx() {
struct stat buf {};
int rc = fstat(fd, &buf);
if (rc == 0 && buf.st_size == 0) {
ALOGD("Deleting '%s' because it is empty", kFilePath.c_str());
std::remove(kFilePath.c_str());
}
close(fd);
fd = -1;
}
virtual ~OutputContext() {}
private:
int fd = -1;
int len = 0;
bool is_filter = false;
};
/**
* Filter support to LoggerContext's stream and outputting to a file.
*/
struct LogFilterContext {
// Function to be invoked to filter
// Note: The line passed to this function is modifiable.
virtual bool filter(const std::string &line) const = 0;
// Filter name, must be a vaild file name itself.
std::string kFilterName;
// Provide a single constant for regEX usage
const std::regex_constants::match_flag_type kRegexMatchflags =
std::regex_constants::format_sed;
// Constructor accepting filtername
LogFilterContext(const std::string &name) : kFilterName(name) {}
// No default one
LogFilterContext() = delete;
// Virtual dtor
virtual ~LogFilterContext() {}
};
struct LoggerContext : OutputContext {
/**
* Opens the log file stream handle
*
* @return FILE* handle
*/
virtual FILE *openSource(void) = 0;
/**
* Closes log file stream handle and does cleanup
*
* @param fp The file stream to close and cleanup. NonNull.
*/
virtual void closeSource(FILE *fp) = 0;
/**
* Register a LogFilterContext to this stream.
*
* @param ctx The context to register
*/
void registerLogFilter(std::shared_ptr<LogFilterContext> ctx) {
if (ctx) {
ALOGD("%s: registered filter '%s' to '%s' logger", __func__,
ctx->kFilterName.c_str(), name.c_str());
filters.emplace(
ctx, std::make_unique<OutputContext>(ctx->kFilterName + '.' + name,
/*isFilter*/ true));
}
}
/**
* Start the associated logger
*
* @param run Pointer to run/stop control variable
*/
void startLogger(std::atomic_bool *run) {
char buf[BUF_SIZE];
auto fp = openSource();
if (fp) {
memset(buf, 0, BUF_SIZE);
if (openOutput()) {
for (auto &f : filters) {
f.second->openOutput();
}
// Erase failed-to-open contexts
for (auto it = filters.begin(), last = filters.end(); it != last;) {
if (!it->second.get())
it = filters.erase(it);
else
++it;
}
while (*run) {
auto ret = fgets(buf, sizeof(buf), fp);
std::istringstream ss(buf);
std::string line;
if (ret) {
while (std::getline(ss, line)) {
for (auto &f : filters) {
std::string fline = line;
if (f.first->filter(fline))
f.second->writeToOutput(fline);
}
writeToOutput(line);
}
}
}
for (auto &f : filters) {
f.second->cleanupOutputCtx();
}
// ofstream will auto close
} else {
PLOGE("[Context %s] Opening output '%s'", name.c_str(),
kFilePath.c_str());
}
closeSource(fp);
} else {
PLOGE("[Context %s] Opening source", name.c_str());
}
}
LoggerContext(const std::string &fname) : OutputContext(fname), name(fname) {
ALOGD("%s: Logger context '%s' created", __func__, fname.c_str());
}
virtual ~LoggerContext(){};
private:
std::string name;
std::unordered_map<std::shared_ptr<LogFilterContext>,
std::unique_ptr<OutputContext>>
filters;
};
// DMESG
struct DmesgContext : LoggerContext {
FILE *openSource(void) override { return fopen("/proc/kmsg", "r"); }
void closeSource(FILE *fp) override { fclose(fp); }
DmesgContext() : LoggerContext("kmsg") {}
~DmesgContext() override { cleanupOutputCtx(); }
};
// Logcat
struct LogcatContext : LoggerContext {
FILE *openSource(void) override { return popen("/system/bin/logcat", "r"); }
void closeSource(FILE *fp) override { pclose(fp); }
LogcatContext() : LoggerContext("logcat") {}
~LogcatContext() override { cleanupOutputCtx(); }
};
// Filters - AVC
struct AvcFilterContext : LogFilterContext {
bool filter(const std::string &line) const override {
// Matches "avc: denied { ioctl } for comm=..." for example
const static auto kAvcMessageRegEX =
std::regex(R"(avc:\s+denied\s+\{\s\w+\s\}\sfor\s)");
bool match = std::regex_search(line, kAvcMessageRegEX, kRegexMatchflags);
if (match && _ctx) {
const std::lock_guard<std::mutex> _(_lock);
parseOneAvcContext(line, *_ctx);
}
return match;
}
std::shared_ptr<AvcContexts> _ctx;
std::mutex& _lock;
AvcFilterContext(std::shared_ptr<AvcContexts> ctx, std::mutex& lock) :
LogFilterContext("avc"), _ctx(ctx), _lock(lock) {}
AvcFilterContext() = delete;
~AvcFilterContext() override = default;
};
// Filters - libc property
struct libcPropFilterContext : LogFilterContext {
bool filter(const std::string &line) const override {
// libc : Access denied finding property "
const static auto kPropertyAccessRegEX =
std::regex(R"(libc\s+:\s+\w+\s\w+\s\w+\s\w+\s\")");
static std::vector<std::string> propsDenied;
std::smatch kPropMatch;
// Matches "libc : Access denied finding property ..."
if (std::regex_search(line, kPropMatch, kPropertyAccessRegEX,
kRegexMatchflags)) {
// Trim property name from "property: \"ro.a.b\""
// line: property "{prop name}"
std::string prop = kPropMatch.suffix();
// line: {prop name}"
prop = prop.substr(0, prop.find_first_of('"'));
// Starts with ctl. ?
if (prop.find("ctl.") == 0)
return true;
// Cache the properties
if (std::find(propsDenied.begin(), propsDenied.end(), prop) == propsDenied.end()) {
propsDenied.emplace_back(prop);
return true;
}
}
return false;
}
libcPropFilterContext() : LogFilterContext("libc_props") {}
~libcPropFilterContext() override = default;
};
using std::chrono::duration_cast;
static void recordBootTime() {
struct sysinfo x;
std::chrono::seconds uptime;
std::string logbuf;
if ((sysinfo(&x) == 0)) {
uptime = std::chrono::seconds(x.uptime);
logbuf = LOG_TAG ": Boot completed in ";
auto mins = duration_cast<std::chrono::minutes>(uptime);
if (mins.count() > 0)
logbuf += std::to_string(mins.count()) + 'm' + ' ';
logbuf += std::to_string((uptime - duration_cast<std::chrono::seconds>(mins)).count()) + 's';
WriteStringToFile(logbuf, "/dev/kmsg");
}
}
int main(int argc, const char** argv) {
std::vector<std::thread> threads;
std::atomic_bool run;
std::error_code ec;
std::string kLogRoot;
KernelConfig_t kConfig;
bool system_log = false;
int rc;
std::mutex lock;
if (argc != 2) {
fprintf(stderr, "Usage: %s [log directory]\n", argv[0]);
return EXIT_FAILURE;
}
kLogRoot = argv[1];
if (kLogRoot.empty()) {
fprintf(stderr, "%s: Invaild empty string for log directory\n", argv[0]);
return EXIT_FAILURE;
}
kLogDir = fs::path(kLogRoot);
if (GetProperty("sys.boot_completed", "") == "1") {
ALOGI("Running in system log mode");
system_log = true;
}
if (system_log)
kLogDir.append("system");
else
kLogDir.append("boot");
DmesgContext kDmesgCtx;
LogcatContext kLogcatCtx;
auto kAvcCtx = std::make_shared<std::vector<AvcContext>>();
auto kAvcFilter = std::make_shared<AvcFilterContext>(kAvcCtx, lock);
auto kLibcPropsFilter = std::make_shared<libcPropFilterContext>();
ALOGI("Logger starting with logdir '%s' ...", kLogDir.c_str());
for (auto const& ent : fs::directory_iterator(kLogRoot, ec)) {
if (fs::is_directory(ent, ec))
fs::remove_all(ent, ec);
else
fs::remove(ent, ec);
if (ec) {
ALOGW("Cannot remove '%s': %s", ent.path().string().c_str(), ec.message().c_str());
ec.clear();
}
}
// If error_code is set here, it means from the directory_iterator,
// as error code is always cleared if failure inside the loop.
if (ec) {
ALOGE("Failed to remove files in log directory: %s", ec.message().c_str());
ec.clear();
} else {
ALOGI("Cleared log directory files");
}
// Determine audit support
rc = ReadKernelConfig(kConfig);
if (rc == 0) {
if (kConfig["CONFIG_AUDIT"] == ConfigValue::BUILT_IN) {
ALOGD("Detected CONFIG_AUDIT=y in kernel configuration");
} else {
ALOGI("Kernel configuration does not have CONFIG_AUDIT=y, disabling avc filters.");
kAvcFilter.reset();
kAvcCtx.reset();
}
}
// Create log dir again
fs::create_directory(kLogDir, ec);
if (ec) {
ALOGE("Failed to create directory '%s': %s", kLogDir.c_str(), ec.message().c_str());
return EXIT_FAILURE;
}
run = true;
// If this prop is true, logd logs kernel message to logcat
// Don't make duplicate (Also it will race against kernel logs)
if (!GetBoolProperty("ro.logd.kernel", false)) {
kDmesgCtx.registerLogFilter(kAvcFilter);
threads.emplace_back(std::thread([&] { kDmesgCtx.startLogger(&run); }));
}
kLogcatCtx.registerLogFilter(kAvcFilter);
kLogcatCtx.registerLogFilter(kLibcPropsFilter);
threads.emplace_back(std::thread([&] { kLogcatCtx.startLogger(&run); }));
if (system_log) {
WaitForProperty("persist.ext.logdump.enabled", "false");
} else {
WaitForProperty("sys.boot_completed", "1");
recordBootTime();
// Delay a bit to finish
std::this_thread::sleep_for(3s);
}
run = false;
for (auto &i : threads)
i.join();
if (kAvcCtx) {
std::vector<std::string> allowrules;
std::stringstream iss;
for (const auto& e : *kAvcCtx) {
std::string line;
writeAllowRules(e, line);
allowrules.emplace_back(line);
}
std::sort(allowrules.begin(), allowrules.end());
allowrules.erase(std::unique(allowrules.begin(), allowrules.end()), allowrules.end());
for (const auto& l : allowrules)
iss << l;
auto rules = kLogDir;
rules.append("rules.autogen.te");
WriteStringToFile(iss.str(), rules);
}
return 0;
}