Loading CMakeLists.txt +1 −0 Changes for CMakeLists.txt: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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 ) Loading 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 src/syslite.cpp +10 −0 Changes for src/syslite.cpp: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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(); Loading Loading @@ -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()) Loading @@ -739,6 +748,7 @@ namespace syslite { std::cout << "[syslite] Shutting down..." << std::endl; _network.stopDeviceMonitor(); _modules.stopMonitor(); _ttys.stopAll(); _services.stopAll(); Loading src/syslite.h +2 −0 Changes for src/syslite.h: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ #include "tty.h" #include "network.h" #include "busserver.h" #include "modules.h" #include <string> #include <atomic> Loading Loading @@ -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; Loading Loading
CMakeLists.txt +1 −0 Changes for CMakeLists.txt: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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 ) Loading
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
src/syslite.cpp +10 −0 Changes for src/syslite.cpp: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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(); Loading Loading @@ -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()) Loading @@ -739,6 +748,7 @@ namespace syslite { std::cout << "[syslite] Shutting down..." << std::endl; _network.stopDeviceMonitor(); _modules.stopMonitor(); _ttys.stopAll(); _services.stopAll(); Loading
src/syslite.h +2 −0 Changes for src/syslite.h: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ #include "tty.h" #include "network.h" #include "busserver.h" #include "modules.h" #include <string> #include <atomic> Loading Loading @@ -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; Loading