Commit a9d7662f authored by Jan Koester's avatar Jan Koester
Browse files

driver load

parent 5cd2dfc4
Loading
Loading
Loading
Loading
+1 −0
Changes for CMakeLists.txt: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ add_executable(syslite
    src/tty.cpp
    src/network.cpp
    src/wireguard.cpp
    src/modules.cpp
    src/busserver.cpp
    src/syslite.cpp
)

src/modules.cpp

0 → 100644
+451 −0
Changes for src/modules.cpp: 451 added lines, 0 removed lines.
Original line number Diff line number Diff line
#include "modules.h"

#include <algorithm>
#include <cerrno>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>

#include <elf.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/utsname.h>
#include <unistd.h>
#include <linux/netlink.h>

#ifndef MODULE_INIT_COMPRESSED_FILE
#define MODULE_INIT_COMPRESSED_FILE 4
#endif

namespace syslite {

namespace {

std::string trim(const std::string &s) {
    size_t a = s.find_first_not_of(" \t\r\n");
    if (a == std::string::npos) return "";
    size_t b = s.find_last_not_of(" \t\r\n");
    return s.substr(a, b - a + 1);
}

// module names: '-' and '_' are the same
std::string normalize(std::string name) {
    std::replace(name.begin(), name.end(), '-', '_');
    return name;
}

// ".../e1000e.ko[.xz]" -> "e1000e"
std::string nameFromPath(const std::string &path) {
    std::string f = std::filesystem::path(path).filename().string();
    return normalize(f.substr(0, f.find(".ko")));
}

bool isModuleFile(const std::string &f) {
    for (const char *ext : {".ko", ".ko.xz", ".ko.gz", ".ko.zst"}) {
        size_t n = std::strlen(ext);
        if (f.size() > n && f.compare(f.size() - n, n, ext) == 0) return true;
    }
    return false;
}

bool compressed(const std::string &path) {
    size_t n = path.size();
    return !(n > 3 && path.compare(n - 3, 3, ".ko") == 0);
}

// "pci:v0000..." -> "pci"; patterns starting with a wildcard go to "*"
std::string aliasBucket(const std::string &s) {
    std::string b = s.substr(0, s.find(':'));
    return b.find_first_of("*?[") == std::string::npos ? b : "*";
}

// the lines of a configuration file, without comments and empty lines
std::vector<std::string> configLines(const std::filesystem::path &path) {
    std::vector<std::string> out;
    std::ifstream f(path);
    std::string line;
    while (std::getline(f, line)) {
        line = trim(line);
        if (!line.empty() && line[0] != '#' && line[0] != ';') out.push_back(line);
    }
    return out;
}

// the *.conf files of dirs, by file name; an earlier dir hides the same
// name in a later one (systemd/kmod semantics)
std::vector<std::filesystem::path> confFiles(std::initializer_list<const char *> dirs) {
    std::map<std::string, std::filesystem::path> files;
    for (const char *d : dirs) {
        std::error_code ec;
        for (auto &e : std::filesystem::directory_iterator(d, ec))
            if (e.path().extension() == ".conf") files.emplace(e.path().filename().string(), e.path());
    }
    std::vector<std::filesystem::path> out;
    for (auto &f : files) out.push_back(f.second);
    return out;
}

// The strings of an ELF file's .modinfo section ("alias=...", "depends=...")
template <class Ehdr, class Shdr>
std::vector<std::string> modinfoOf(int fd) {
    std::vector<std::string> out;
    Ehdr eh;
    if (pread(fd, &eh, sizeof(eh), 0) != static_cast<ssize_t>(sizeof(eh))) return out;
    if (eh.e_shentsize != sizeof(Shdr) || eh.e_shnum == 0 || eh.e_shnum > 4096 || eh.e_shstrndx >= eh.e_shnum)
        return out;
    std::vector<Shdr> sh(eh.e_shnum);
    const size_t shBytes = sizeof(Shdr) * eh.e_shnum;
    if (pread(fd, sh.data(), shBytes, static_cast<off_t>(eh.e_shoff)) != static_cast<ssize_t>(shBytes)) return out;
    auto read = [&](const Shdr &s, std::string &buf) {
        if (s.sh_size > (64u << 20)) return false;
        buf.resize(s.sh_size);
        return pread(fd, &buf[0], buf.size(), static_cast<off_t>(s.sh_offset)) == static_cast<ssize_t>(buf.size());
    };
    std::string names;
    if (!read(sh[eh.e_shstrndx], names)) return out;
    for (auto &s : sh) {
        if (s.sh_name >= names.size() || std::strcmp(names.c_str() + s.sh_name, ".modinfo") != 0) continue;
        std::string info;
        if (!read(s, info)) return out;
        std::istringstream iss(info);
        std::string entry;
        while (std::getline(iss, entry, '\0'))
            if (!entry.empty()) out.push_back(entry);
        break;
    }
    return out;
}

std::vector<std::string> readModinfo(const std::string &path) {
    int fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC);
    if (fd < 0) return {};
    unsigned char ident[EI_NIDENT] = {};
    std::vector<std::string> out;
    if (pread(fd, ident, sizeof(ident), 0) == EI_NIDENT && std::memcmp(ident, ELFMAG, SELFMAG) == 0) {
        if (ident[EI_CLASS] == ELFCLASS64) out = modinfoOf<Elf64_Ehdr, Elf64_Shdr>(fd);
        else if (ident[EI_CLASS] == ELFCLASS32) out = modinfoOf<Elf32_Ehdr, Elf32_Shdr>(fd);
    }
    ::close(fd);
    return out;
}

} // namespace

ModuleLoader::~ModuleLoader() { stopMonitor(); }

bool ModuleLoader::init() {
    utsname u{};
    if (uname(&u) != 0) return false;
    _dir = std::string("/lib/modules/") + u.release;
    std::error_code ec;
    if (!std::filesystem::is_directory(_dir, ec)) {
        std::cout << "[syslite/mod] No modules for kernel " << u.release << " (" << _dir << ")" << std::endl;
        return false;
    }
    readDepmodIndex();
    if (_modules.empty() || _aliases.empty()) scanModuleFiles();
    readModprobeConfig();
    size_t aliases = 0;
    for (auto &b : _aliases) aliases += b.second.size();
    std::cout << "[syslite/mod] " << _modules.size() << " modules, " << aliases << " aliases (" << _dir << ")"
              << std::endl;
    return !_modules.empty();
}

// depmod's modules.dep ("path: dep-path ..."), modules.alias ("alias
// pattern module") and modules.builtin (paths)
void ModuleLoader::readDepmodIndex() {
    std::ifstream dep(_dir + "/modules.dep");
    std::string line;
    while (std::getline(dep, line)) {
        auto colon = line.find(':');
        if (colon == std::string::npos) continue;
        std::string path = trim(line.substr(0, colon));
        Module m;
        m.path = path[0] == '/' ? path : _dir + "/" + path;
        std::istringstream deps(line.substr(colon + 1));
        std::string d;
        while (deps >> d) m.deps.push_back(nameFromPath(d));
        _modules[nameFromPath(path)] = std::move(m);
    }
    std::ifstream alias(_dir + "/modules.alias");
    while (std::getline(alias, line)) {
        std::istringstream iss(line);
        std::string kw, pattern, module;
        if (!(iss >> kw >> pattern >> module) || kw != "alias") continue;
        _aliases[aliasBucket(pattern)].push_back({pattern, normalize(module)});
    }
    std::ifstream builtin(_dir + "/modules.builtin");
    while (std::getline(builtin, line))
        if (!trim(line).empty()) _builtin.insert(nameFromPath(trim(line)));
}

// Without depmod's index: the .ko files' own .modinfo (alias=, depends=).
// Compressed modules can be loaded, but their aliases aren't known.
void ModuleLoader::scanModuleFiles() {
    _modules.clear();
    _aliases.clear();
    size_t unreadable = 0;
    std::error_code ec;
    for (auto it = std::filesystem::recursive_directory_iterator(
             _dir, std::filesystem::directory_options::skip_permission_denied, ec);
         it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) {
        if (ec) break;
        const std::string path = it->path().string();
        if (!it->is_regular_file(ec) || !isModuleFile(path)) continue;
        Module m;
        m.path = path;
        std::string name = nameFromPath(path);
        std::vector<std::string> aliases;  // name= may come after them
        if (compressed(path)) {
            ++unreadable;
        } else {
            for (auto &entry : readModinfo(path)) {
                auto eq = entry.find('=');
                if (eq == std::string::npos) continue;
                std::string key = entry.substr(0, eq), val = entry.substr(eq + 1);
                if (key == "name") name = normalize(val);
                else if (key == "depends") {
                    std::istringstream deps(val);
                    std::string d;
                    while (std::getline(deps, d, ','))
                        if (!d.empty()) m.deps.push_back(normalize(d));
                } else if (key == "alias") {
                    aliases.push_back(val);
                }
            }
        }
        for (auto &a : aliases) _aliases[aliasBucket(a)].push_back({a, name});
        _modules[name] = std::move(m);
    }
    if (unreadable)
        std::cerr << "[syslite/mod] no modules.alias and " << unreadable
                  << " compressed modules: their hardware isn't recognized (run depmod)" << std::endl;
}

// modprobe.d: "blacklist <module>", "options <module> <parameters>";
// kernel command line: modprobe.blacklist=a,b and <module>.<param>=<value>
void ModuleLoader::readModprobeConfig() {
    for (auto &file : confFiles({"/etc/modprobe.d", "/run/modprobe.d", "/usr/lib/modprobe.d", "/lib/modprobe.d"}))
        for (auto &line : configLines(file)) {
            std::istringstream iss(line);
            std::string kw, module;
            iss >> kw >> module;
            if (module.empty()) continue;
            if (kw == "blacklist") {
                _blacklist.insert(normalize(module));
            } else if (kw == "options") {
                std::string rest;
                std::getline(iss, rest);
                std::string &opts = _options[normalize(module)];
                opts += (opts.empty() ? "" : " ") + trim(rest);
            }
        }
    std::ifstream cmdline("/proc/cmdline");
    std::string arg;
    while (cmdline >> arg) {
        auto eq = arg.find('=');
        if (eq == std::string::npos) continue;
        std::string key = arg.substr(0, eq), val = arg.substr(eq + 1);
        if (key == "modprobe.blacklist") {
            std::istringstream list(val);
            std::string m;
            while (std::getline(list, m, ','))
                if (!m.empty()) _blacklist.insert(normalize(m));
            continue;
        }
        auto dot = key.find('.');
        if (dot == std::string::npos) continue;
        std::string module = normalize(key.substr(0, dot));
        if (!_modules.count(module)) continue;  // built-in ones: the kernel's business
        std::string &opts = _options[module];
        opts += (opts.empty() ? "" : " ") + key.substr(dot + 1) + "=" + val;
    }
}

std::vector<std::string> ModuleLoader::modulesFor(const std::string &modalias) const {
    std::vector<std::string> out;
    for (const std::string &bucket : {aliasBucket(modalias), std::string("*")}) {
        auto it = _aliases.find(bucket);
        if (it == _aliases.end()) continue;
        for (auto &a : it->second)
            if (fnmatch(a.pattern.c_str(), modalias.c_str(), 0) == 0 && !_blacklist.count(a.module) &&
                std::find(out.begin(), out.end(), a.module) == out.end())
                out.push_back(a.module);
        if (bucket == "*") break;
    }
    return out;
}

bool ModuleLoader::isLoaded(const std::string &name) {
    std::error_code ec;
    return std::filesystem::exists("/sys/module/" + name + "/initstate", ec);
}

bool ModuleLoader::load(const std::string &nameOrAlias, std::string *err) {
    const std::string name = normalize(nameOrAlias);
    std::set<std::string> visiting;
    if (_modules.count(name) || _builtin.count(name)) return loadModule(name, visiting, err);
    bool any = false;
    for (auto &m : modulesFor(nameOrAlias)) {
        visiting.clear();
        any = loadModule(m, visiting, err) || any;
    }
    if (!any && err && err->empty()) *err = "module " + nameOrAlias + " not found";
    return any;
}

bool ModuleLoader::loadModule(const std::string &name, std::set<std::string> &visiting, std::string *err) {
    if (_builtin.count(name) || isLoaded(name)) return true;
    auto it = _modules.find(name);
    if (it == _modules.end()) {
        if (err) *err = "module " + name + " not found";
        return false;
    }
    if (!visiting.insert(name).second) return true;  // a dependency cycle: the kernel decides
    // modules.dep lists all dependencies, the ones loaded first last
    const Module &m = it->second;
    for (auto d = m.deps.rbegin(); d != m.deps.rend(); ++d)
        if (!loadModule(*d, visiting, err)) {
            if (err) *err = name + " needs " + *d + ": " + *err;
            return false;
        }

    int fd = ::open(m.path.c_str(), O_RDONLY | O_CLOEXEC);
    if (fd < 0) {
        if (err) *err = m.path + ": " + std::strerror(errno);
        return false;
    }
    auto opt = _options.find(name);
    const std::string params = opt == _options.end() ? "" : opt->second;
    int flags = compressed(m.path) ? MODULE_INIT_COMPRESSED_FILE : 0;
    long rc = syscall(SYS_finit_module, fd, params.c_str(), flags);
    int e = errno;
    ::close(fd);
    if (rc != 0 && e != EEXIST) {
        if (err) *err = name + ": " + std::strerror(e);
        return false;
    }
    if (rc == 0)
        std::cout << "[syslite/mod] Loaded " << name << (params.empty() ? "" : " (" + params + ")") << std::endl;
    return true;
}

void ModuleLoader::loadForAlias(const std::string &modalias, bool once) {
    if (!_seenAliases.insert(modalias).second && once) return;
    for (auto &m : modulesFor(modalias)) {
        if (_failed.count(m)) continue;
        std::set<std::string> visiting;
        std::string why;
        if (!loadModule(m, visiting, &why)) {
            _failed.insert(m);
            std::cerr << "[syslite/mod] Cannot load " << m << " for " << modalias << ": " << why << std::endl;
        }
    }
}

// ─── coldplug ───────────────────────────────────────────────────────────────

void ModuleLoader::coldplug() {
    // named modules: modules-load.d (systemd) and /etc/modules (Debian)
    std::vector<std::string> named;
    for (auto &file : confFiles({"/etc/modules-load.d", "/run/modules-load.d", "/usr/lib/modules-load.d",
                                 "/lib/modules-load.d"}))
        for (auto &line : configLines(file)) named.push_back(line);
    for (auto &line : configLines("/etc/modules")) {
        std::istringstream iss(line);
        std::string m;
        if (iss >> m) named.push_back(m);
    }
    for (auto &m : named) {
        std::string why;
        if (!load(m, &why)) std::cerr << "[syslite/mod] Cannot load " << m << ": " << why << std::endl;
    }
    scanDevices();
}

// Every device's modalias in /sys/devices. Collected first: loading
// changes the tree being walked (the devices those modules bring along
// come as uevents).
void ModuleLoader::scanDevices() {
    std::set<std::string> found;
    std::error_code ec;
    for (auto it = std::filesystem::recursive_directory_iterator(
             "/sys/devices", std::filesystem::directory_options::skip_permission_denied, ec);
         it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) {
        if (ec) {
            ec.clear();
            continue;
        }
        if (it->path().filename() != "modalias") continue;
        std::ifstream f(it->path());
        std::string alias;
        if (std::getline(f, alias) && !trim(alias).empty()) found.insert(trim(alias));
    }
    for (auto &a : found) loadForAlias(a, true);
}

// ─── hotplug (kernel uevents) ───────────────────────────────────────────────

void ModuleLoader::startMonitor() {
    int fd = ::socket(AF_NETLINK, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT);
    if (fd < 0) return;
    // room for the burst of events coldplug causes
    int size = 8 << 20;
    if (setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, &size, sizeof(size)) != 0)
        setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
    sockaddr_nl sa{};
    sa.nl_family = AF_NETLINK;
    sa.nl_groups = 1;  // the kernel's events
    if (::bind(fd, reinterpret_cast<sockaddr *>(&sa), sizeof(sa)) < 0) {
        ::close(fd);
        return;
    }
    _ueventFd = fd;
}

void ModuleLoader::stopMonitor() {
    if (_ueventFd >= 0) ::close(_ueventFd);
    _ueventFd = -1;
}

void ModuleLoader::checkEvents() {
    if (_ueventFd < 0) return;
    std::vector<std::string> aliases;
    bool lost = false;
    char buf[16384];
    for (;;) {
        sockaddr_nl from{};
        socklen_t fromLen = sizeof(from);
        ssize_t n = ::recvfrom(_ueventFd, buf, sizeof(buf) - 1, MSG_DONTWAIT, reinterpret_cast<sockaddr *>(&from),
                               &fromLen);
        if (n < 0) {
            if (errno == ENOBUFS) { lost = true; continue; }  // overflowed: rescan below
            if (errno == EINTR) continue;
            break;  // EAGAIN: all read
        }
        if (from.nl_pid != 0) continue;  // only the kernel's
        buf[n] = '\0';
        // "add@/devices/...\0ACTION=add\0...\0MODALIAS=...\0"
        std::string action, modalias;
        for (ssize_t off = 0; off < n;) {
            const char *s = buf + off;
            size_t len = std::strlen(s);
            if (std::strncmp(s, "ACTION=", 7) == 0) action = s + 7;
            else if (std::strncmp(s, "MODALIAS=", 9) == 0) modalias = s + 9;
            off += static_cast<ssize_t>(len) + 1;
        }
        if (action == "add" && !modalias.empty()) aliases.push_back(modalias);
    }
    for (auto &a : aliases) loadForAlias(a, false);
    if (lost) {
        std::cerr << "[syslite/mod] uevents lost, rescanning the devices" << std::endl;
        scanDevices();
    }
}

} // namespace syslite

src/modules.h

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

// Kernel modules for the hardware that is there (what udev's coldplug and
// kmod's modprobe do, in syslite): at boot every device's modalias in /sys
// is matched against the modules' aliases and the modules found are loaded
// (dependencies first); then devices that appear later (uevents) get theirs.
//
// The index comes from depmod's files in /lib/modules/<release>
// (modules.alias, modules.dep, modules.builtin) or, without them, from the
// .modinfo sections of the .ko files themselves. modprobe.d's "blacklist"
// (also modprobe.blacklist= on the kernel command line) and "options" are
// honoured; modules-load.d and /etc/modules name modules loaded at boot.

#include <map>
#include <set>
#include <string>
#include <vector>

namespace syslite {

    class ModuleLoader {
    public:
        ~ModuleLoader();

        // Reads the index of /lib/modules/<running kernel>; false if there
        // are no modules (a kernel with everything built in)
        bool init();

        // Listens for devices appearing (NETLINK_KOBJECT_UEVENT); before
        // coldplug(), so devices its modules bring along aren't missed
        void startMonitor();
        void stopMonitor();

        // modules-load.d, /etc/modules, then the modules of every device in /sys
        void coldplug();

        // Loads the modules of devices that appeared since (non-blocking)
        void checkEvents();

        // Loads a module by name or alias, its dependencies first (like
        // modprobe); true if it is there now (also: built in, loaded before)
        bool load(const std::string &nameOrAlias, std::string *err = nullptr);

    private:
        struct Module {
            std::string path;                  // the .ko file
            std::vector<std::string> deps;     // module names
        };
        struct Alias {
            std::string pattern;               // fnmatch(3) pattern
            std::string module;
        };

        void readDepmodIndex();
        void scanModuleFiles();
        void readModprobeConfig();
        void scanDevices();
        // the modules matching a device's modalias
        std::vector<std::string> modulesFor(const std::string &modalias) const;
        // a device's modalias: load what matches (once: skip a modalias
        // handled before -- coldplug; a device plugged in again: not once)
        void loadForAlias(const std::string &modalias, bool once);
        bool loadModule(const std::string &name, std::set<std::string> &visiting, std::string *err);
        static bool isLoaded(const std::string &name);

        std::string _dir;                                   // /lib/modules/<release>
        std::map<std::string, Module> _modules;             // by name ('-' as '_')
        std::map<std::string, std::vector<Alias>> _aliases; // by the bus ("pci", "usb", ...)
        std::set<std::string> _builtin;
        std::set<std::string> _blacklist;
        std::map<std::string, std::string> _options;        // module -> parameters
        std::set<std::string> _seenAliases;                 // modaliases coldplug handled
        std::set<std::string> _failed;                      // don't retry (and log) these
        int _ueventFd = -1;
    };

} // namespace syslite
+10 −0
Changes for src/syslite.cpp: 10 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -655,6 +655,12 @@ namespace syslite {

            mountVirtualFilesystems();
            Journal::instance().reopen();  // / is writable now
            // the drivers of the hardware that is there, before the network
            // is configured (NICs); later devices come as uevents
            if (_modules.init()) {
                _modules.startMonitor();
                _modules.coldplug();
            }
            setHostname();
            runSysctl();
            configureNetwork();
@@ -714,6 +720,9 @@ namespace syslite {
                _network.reload();
            }

            // Load the modules of devices that appeared (kernel uevents)
            _modules.checkEvents();

            // Configure network devices that appeared (netlink RTM_NEWLINK);
            // the ones already set up are left alone
            if (_network.checkDeviceEvents())
@@ -739,6 +748,7 @@ namespace syslite {
        std::cout << "[syslite] Shutting down..." << std::endl;

        _network.stopDeviceMonitor();
        _modules.stopMonitor();
        _ttys.stopAll();
        _services.stopAll();

+2 −0
Changes for src/syslite.h: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@
#include "tty.h"
#include "network.h"
#include "busserver.h"
#include "modules.h"

#include <string>
#include <atomic>
@@ -56,6 +57,7 @@ namespace syslite {
        ServiceManager _services;
        TtyManager     _ttys;
        NetworkManager _network;
        ModuleLoader   _modules;
        BusServer      _bus;
        std::map<std::string, std::pair<ServiceState, pid_t>> _lastStates;