Commit 4bca12d0 authored by jan.koester's avatar jan.koester
Browse files

coredump

parent a9727ee2
Loading
Loading
Loading
Loading
+8 −0
Changes for CMakeLists.txt: 8 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -10,6 +10,11 @@ set(SYSLITE_UNIT_DIR "/etc/syslite/services" CACHE STRING "Service unit file dir
set(SYSLITE_DEFAULT_TARGET "multi-user.target" CACHE STRING "Default boot target")
set(SYSLITE_TTY_COUNT 3 CACHE STRING "Number of TTYs to spawn")
set(SYSLITE_JOURNAL "/var/log/syslite/journal.log" CACHE STRING "Journal: syslite's and the services' output")
set(SYSLITE_COREDUMP_DIR "/var/lib/syslite/coredump" CACHE STRING "Core dumps (see src/coredump.h)")
set(SYSLITE_COREDUMP_MAX_SIZE 2147483648 CACHE STRING "Core dumps: biggest core kept, bytes")
set(SYSLITE_COREDUMP_KEEP 20 CACHE STRING "Core dumps: how many are kept")
set(SYSLITE_COREDUMP_KEEP_FREE 15 CACHE STRING "Core dumps: percent of the file system left free")
set(SYSLITE_COREDUMP_MAX_USE 10 CACHE STRING "Core dumps: percent of the file system all may use")

configure_file(config.h.in ${CMAKE_BINARY_DIR}/config.h)

@@ -30,6 +35,7 @@ add_executable(syslite
    src/unitfile.cpp
    src/mount.cpp
    src/journal.cpp
    src/coredump.cpp
    src/service.cpp
    src/tty.cpp
    src/network.cpp
@@ -66,6 +72,8 @@ install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink syslite
    \$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/sbin/systemctl)")
install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink syslitectl
    \$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/sbin/journalctl)")
install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink syslitectl
    \$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/sbin/coredumpctl)")

install(TARGETS sysbus EXPORT sysbusTargets ARCHIVE DESTINATION lib)
install(DIRECTORY include/sysbus DESTINATION include)
+5 −0
Changes for config.h.in: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -8,3 +8,8 @@
#define SYSLITE_HOSTNAME_LEGACY "/etc/hostname"
#define SYSLITE_JOURNAL         "@SYSLITE_JOURNAL@"
#define SYSLITE_VERSION         "@PROJECT_VERSION@"
#define SYSLITE_COREDUMP_DIR       "@SYSLITE_COREDUMP_DIR@"
#define SYSLITE_COREDUMP_MAX_SIZE  @SYSLITE_COREDUMP_MAX_SIZE@ULL
#define SYSLITE_COREDUMP_KEEP      @SYSLITE_COREDUMP_KEEP@
#define SYSLITE_COREDUMP_KEEP_FREE @SYSLITE_COREDUMP_KEEP_FREE@
#define SYSLITE_COREDUMP_MAX_USE   @SYSLITE_COREDUMP_MAX_USE@

src/coredump.cpp

0 → 100644
+314 −0
Changes for src/coredump.cpp: 314 added lines, 0 removed lines.
Original line number Diff line number Diff line
#include "coredump.h"
#include "journal.h"
#include "config.h"
#include "sysbus/client.h"

#include <algorithm>
#include <cerrno>
#include <climits>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <vector>

#include <fcntl.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <unistd.h>

namespace fs = std::filesystem;

namespace syslite {

    // ---- boot ----

    void setupCoredumps() {
        // the kernel wants an absolute path; this binary, wherever it is
        char buf[PATH_MAX];
        ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
        std::string self = n > 0 ? std::string(buf, n) : "/sbin/syslite";
        if (self.find(' ') != std::string::npos) self = "/sbin/syslite";  // " (deleted)"
        std::string pattern = "|" + self + " --coredump %P %u %g %s %t %c %e";
        if (pattern.size() >= 128) {  // CORENAME_MAX_SIZE
            std::cerr << "[syslite] Core dumps: " << self << " is too long a path for core_pattern" << std::endl;
            return;
        }
        std::ofstream cp("/proc/sys/kernel/core_pattern");
        if (!(cp << pattern << "\n" << std::flush)) {
            std::cerr << "[syslite] Core dumps: cannot set core_pattern" << std::endl;
            return;
        }
        // the crashed process stays in /proc while the handler runs
        // (exe, cmdline, its unit), for up to 16 crashes at once
        std::ofstream("/proc/sys/kernel/core_pipe_limit") << "16\n";
        // for pipes the kernel ignores RLIMIT_CORE but hands it over (%c);
        // the handler honors it, so everyone gets unlimited -- ulimit -c 0
        // still turns cores off for a process
        struct rlimit rl{RLIM_INFINITY, RLIM_INFINITY};
        setrlimit(RLIMIT_CORE, &rl);
        std::cout << "[syslite] Core dumps go to " << SYSLITE_COREDUMP_DIR << std::endl;
    }

    // ---- the handler ----

    namespace {

    const char *signalName(int sig) {
        switch (sig) {
        case 3: return "SIGQUIT";
        case 4: return "SIGILL";
        case 5: return "SIGTRAP";
        case 6: return "SIGABRT";
        case 7: return "SIGBUS";
        case 8: return "SIGFPE";
        case 11: return "SIGSEGV";
        case 24: return "SIGXCPU";
        case 25: return "SIGXFSZ";
        case 31: return "SIGSYS";
        default: return "";
        }
    }

    // one line: control characters as '?'
    std::string oneLine(const std::string &s) {
        std::string out;
        for (unsigned char c : s) out += (c < 0x20 && c != '\t') || c == 0x7f ? '?' : static_cast<char>(c);
        return out;
    }

    std::string readFile(const std::string &path) {
        std::ifstream in(path, std::ios::binary);
        std::ostringstream ss;
        ss << in.rdbuf();
        return ss.str();
    }

    // "412 (sshd) S 1 412 412 ..." -> ppid, session
    bool parentAndSession(pid_t pid, pid_t &ppid, pid_t &sid) {
        std::string stat = readFile("/proc/" + std::to_string(pid) + "/stat");
        size_t close = stat.rfind(')');
        if (close == std::string::npos) return false;
        std::istringstream in(stat.substr(close + 1));
        std::string state;
        pid_t pgrp;
        return static_cast<bool>(in >> state >> ppid >> pgrp >> sid);
    }

    // the unit the process belongs to: a unit's main pid is the process,
    // one of its ancestors, or its session (services run setsid())
    std::string unitOf(pid_t pid) {
        std::vector<pid_t> related{pid};
        pid_t p = pid, ppid, sid;
        for (int i = 0; i < 64 && parentAndSession(p, ppid, sid); ++i) {
            if (i == 0 && sid > 1) related.push_back(sid);
            if (ppid <= 1) break;
            related.push_back(ppid);
            p = ppid;
        }
        sysbus::Client bus;
        if (!bus.connect()) return "";
        sysbus::Message r = bus.call(sysbus::method::ListUnits, {}, 3000);
        if (r.type == sysbus::MessageType::Error) return "";
        std::map<pid_t, std::string> units;
        std::string name;
        for (auto &f : r.fields) {
            if (f.first == "name") name = f.second;
            else if (f.first == "pid" && std::atol(f.second.c_str()) > 1) units[std::atol(f.second.c_str())] = name;
        }
        for (pid_t q : related) {
            auto it = units.find(q);
            if (it != units.end()) return it->second;
        }
        return "";
    }

    std::string sizeText(uint64_t n) {
        const char *unit[] = {"B", "KiB", "MiB", "GiB", "TiB"};
        double v = static_cast<double>(n);
        int u = 0;
        while (v >= 1024 && u < 4) { v /= 1024; ++u; }
        char out[32];
        snprintf(out, sizeof(out), u == 0 ? "%.0f %s" : "%.1f %s", v, unit[u]);
        return out;
    }

    struct Stored {
        fs::path info;
        fs::path core;  // empty: none
        uint64_t size = 0;
        fs::file_time_type mtime;
    };

    // the kept dumps, oldest first
    std::vector<Stored> storedDumps(const fs::path &dir) {
        std::vector<Stored> out;
        std::error_code ec;
        for (auto &e : fs::directory_iterator(dir, ec)) {
            std::string name = e.path().filename().string();
            if (name.rfind("core.", 0) != 0 || e.path().extension() != ".info") continue;
            Stored s;
            s.info = e.path();
            s.mtime = fs::last_write_time(e.path(), ec);
            fs::path core = e.path();
            core.replace_extension();
            if (fs::exists(core, ec)) {
                s.core = core;
                s.size = fs::file_size(core, ec);
            }
            out.push_back(s);
        }
        std::sort(out.begin(), out.end(), [](const Stored &a, const Stored &b) { return a.mtime < b.mtime; });
        return out;
    }

    void removeDump(const Stored &s) {
        std::error_code ec;
        fs::remove(s.info, ec);
        if (!s.core.empty()) fs::remove(s.core, ec);
    }

    }  // namespace

    int coredumpMain(int argc, char **argv) {
        // argv: syslite --coredump PID UID GID SIGNAL TIME LIMIT COMM...
        if (argc < 9) {
            std::cerr << "usage: syslite --coredump PID UID GID SIGNAL TIME LIMIT COMM\n"
                      << "(run by the kernel, see /proc/sys/kernel/core_pattern)\n";
            return 1;
        }
        umask(077);  // cores hold the process' memory: passwords, keys
        pid_t pid = std::atol(argv[2]);
        long uid = std::atol(argv[3]), gid = std::atol(argv[4]);
        int sig = std::atoi(argv[5]);
        time_t when = std::atoll(argv[6]);
        uint64_t limit = std::strtoull(argv[7], nullptr, 10);  // RLIM_INFINITY: 2^64-1
        std::string comm = argv[8];
        for (int i = 9; i < argc; ++i) comm += std::string(" ") + argv[i];  // older kernels split %e

        std::string proc = "/proc/" + std::to_string(pid);
        char buf[PATH_MAX];
        ssize_t n = readlink((proc + "/exe").c_str(), buf, sizeof(buf) - 1);
        std::string exe = n > 0 ? std::string(buf, n) : "";
        std::string cmdline = readFile(proc + "/cmdline");
        while (!cmdline.empty() && cmdline.back() == '\0') cmdline.pop_back();
        std::replace(cmdline.begin(), cmdline.end(), '\0', ' ');
        std::string unit = unitOf(pid);

        std::string safeComm;
        for (unsigned char c : comm) safeComm += isalnum(c) || c == '-' || c == '_' || c == '.' ? static_cast<char>(c) : '_';
        std::string base = "core." + safeComm + "." + std::to_string(uid) + "." + std::to_string(pid) + "." +
                           std::to_string(static_cast<long long>(when));

        fs::path dir = SYSLITE_COREDUMP_DIR;
        std::error_code ec;
        fs::create_directories(dir, ec);
        fs::path corePath = dir / base, infoPath = dir / (base + ".info");

        // room: the size limit, the process' RLIMIT_CORE, the free space
        // to keep, the share of the file system all dumps may use
        std::vector<Stored> old = storedDumps(dir);
        while (!old.empty() && old.size() >= static_cast<size_t>(SYSLITE_COREDUMP_KEEP)) {
            removeDump(old.front());
            old.erase(old.begin());
        }
        uint64_t room = std::min<uint64_t>(limit, SYSLITE_COREDUMP_MAX_SIZE);
        std::string why;
        if (limit == 0) why = "RLIMIT_CORE is 0";
        struct statvfs sv;
        uint64_t maxUse = UINT64_MAX;
        if (statvfs(dir.c_str(), &sv) == 0) {
            uint64_t total = static_cast<uint64_t>(sv.f_blocks) * sv.f_frsize;
            uint64_t avail = static_cast<uint64_t>(sv.f_bavail) * sv.f_frsize;
            uint64_t keepFree = total / 100 * SYSLITE_COREDUMP_KEEP_FREE;
            maxUse = total / 100 * SYSLITE_COREDUMP_MAX_USE;
            room = std::min(room, avail > keepFree ? avail - keepFree : 0);
            room = std::min(room, maxUse);
        }

        uint64_t size = 0;
        bool stored = false;
        if (why.empty()) {
            fs::path tmp = dir / (".#" + base);
            int fd = ::open(tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600);
            if (fd < 0) why = std::string("cannot write ") + tmp.string() + ": " + std::strerror(errno);
            static char chunk[1 << 20];
            while (fd >= 0) {
                ssize_t r = ::read(STDIN_FILENO, chunk, sizeof(chunk));
                if (r < 0 && errno == EINTR) continue;
                if (r <= 0) break;
                if (size + r > room) {
                    why = "bigger than the " + sizeText(room) + " it may take";
                    break;
                }
                // the core is mostly zeros: holes instead
                bool zeros = std::all_of(chunk, chunk + r, [](char c) { return c == 0; });
                bool ok = zeros ? lseek(fd, r, SEEK_CUR) >= 0 : ::write(fd, chunk, r) == r;
                if (!ok) {
                    why = std::string("write failed: ") + std::strerror(errno);
                    break;
                }
                size += r;
            }
            if (fd >= 0) {
                if (why.empty() && (ftruncate(fd, size) != 0 || fsync(fd) != 0))
                    why = std::string("write failed: ") + std::strerror(errno);
                ::close(fd);
                if (why.empty() && ::rename(tmp.c_str(), corePath.c_str()) == 0) stored = true;
                else fs::remove(tmp, ec);
            }
        }

        // where the time is the crash's, in the journal's format
        char date[32], zone[8];
        struct tm tm{};
        localtime_r(&when, &tm);
        strftime(date, sizeof(date), "%Y-%m-%dT%H:%M:%S", &tm);
        if (strftime(zone, sizeof(zone), "%z", &tm) == 0) strcpy(zone, "+0000");

        std::ofstream info(infoPath);
        info << "pid: " << pid << "\n"
             << "uid: " << uid << "\n"
             << "gid: " << gid << "\n"
             << "signal: " << sig << " " << signalName(sig) << "\n"
             << "time: " << date << zone << "\n"
             << "comm: " << oneLine(comm) << "\n"
             << "exe: " << oneLine(exe) << "\n"
             << "cmdline: " << oneLine(cmdline) << "\n"
             << "unit: " << unit << "\n"
             << "size: " << (stored ? size : 0) << "\n"
             << "core: " << (stored ? corePath.string() : "") << "\n";
        if (!stored) info << "missing: " << oneLine(why) << "\n";
        info.close();

        // the whole directory within its share: the oldest go
        if (stored) {
            std::vector<Stored> all = storedDumps(dir);
            uint64_t use = 0;
            for (auto &s : all) use += s.size;
            for (auto &s : all) {
                if (use <= maxUse || s.info == infoPath) break;
                use -= s.size;
                removeDump(s);
            }
        }

        std::string sigText = std::to_string(sig);
        if (*signalName(sig)) sigText += " (" + std::string(signalName(sig)) + ")";
        std::string line = "Process " + std::to_string(pid) + " (" + comm + ") of user " + std::to_string(uid) +
                           " dumped core: signal " + sigText;
        if (!unit.empty()) line += ", unit " + unit;
        line += stored ? "; core in " + corePath.string() + " (" + sizeText(size) + ")"
                       : "; core not kept: " + why;
        Journal::instance().write("coredump", pid, line);
        return 0;
    }

}

src/coredump.h

0 → 100644
+32 −0
Changes for src/coredump.h: 32 added lines, 0 removed lines.
Original line number Diff line number Diff line
#pragma once

// Core dumps, like systemd-coredump: syslite (PID 1) points the kernel's
// core_pattern at itself,
//
//   |/sbin/syslite --coredump %P %u %g %s %t %c %e
//
// and the kernel runs that for every crash, the core on stdin. It is kept
// in SYSLITE_COREDUMP_DIR as
//
//   core.<comm>.<uid>.<pid>.<time>        the core (0600)
//   core.<comm>.<uid>.<pid>.<time>.info   "key: value" lines: pid, uid,
//                                         gid, signal, time, comm, exe,
//                                         cmdline, unit, size, core
//
// and the journal gets a line from "coredump". Not kept (only the .info and
// the journal line): a process with RLIMIT_CORE 0 (ulimit -c 0), a core
// bigger than SYSLITE_COREDUMP_MAX_SIZE, or one that would leave less than
// SYSLITE_COREDUMP_KEEP_FREE percent of the file system free. Only the
// newest SYSLITE_COREDUMP_KEEP dumps stay. coredumpctl (syslitectl
// coredump) lists and shows them.

namespace syslite {

    // boot (PID 1): core_pattern, core_pipe_limit, RLIMIT_CORE unlimited
    // for all processes (so cores are kept unless one says ulimit -c 0)
    void setupCoredumps();

    // syslite --coredump PID UID GID SIGNAL TIME LIMIT COMM...
    int coredumpMain(int argc, char **argv);

}
+5 −0
Changes for src/main.cpp: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
#include "syslite.h"
#include "journal.h"
#include "coredump.h"

#include <iostream>
#include <cstring>
@@ -26,10 +27,14 @@ static void sigHupHandler(int) {
}

int main(int argc, char **argv) {
    // run by the kernel for a crash (core_pattern, see coredump.h)
    if (argc > 1 && std::strcmp(argv[1], "--coredump") == 0) return syslite::coredumpMain(argc, argv);

    for (int i = 1; i < argc; ++i) {
        if (std::strcmp(argv[i], "-h") == 0 || std::strcmp(argv[i], "--help") == 0) {
            std::cout << "syslite — lightweight Linux init system\n"
                      << "  -h, --help     Show help\n"
                      << "  --coredump ... Store a core dump (run by the kernel, see core_pattern)\n"
                      << "  Config is compile-time (no runtime config files needed)\n";
            return 0;
        }
Loading