From bb503eec83834035281f03c0f00c836023abe167 Mon Sep 17 00:00:00 2001 From: SFG545 Date: Wed, 8 Jul 2026 22:08:15 -0500 Subject: [PATCH] Implement minimal depmod-compatible applet for vx - Added depmod.cpp to scan kernel module ELF files, read .modinfo, and emit metadata files. - Implemented functionality to write binary trie indexes for libkmod/modprobe. - Introduced command-line options for module processing. - Created test script test_depmod.sh to verify help and version commands. --- README.md | 3 +- meson.build | 24 +- meson_options.txt | 1 + src/depmod/depmod.8 | 69 +++ src/depmod/depmod.cpp | 1250 +++++++++++++++++++++++++++++++++++++++++ src/main.c | 6 + tests/test_depmod.sh | 4 + 7 files changed, 1355 insertions(+), 2 deletions(-) create mode 100644 src/depmod/depmod.8 create mode 100644 src/depmod/depmod.cpp create mode 100644 tests/test_depmod.sh diff --git a/README.md b/README.md index 9ebfb33..ecacdab 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ It bundles a growing set of FreeBSD-derived userland tools into a single `vx` bi - `find` - `xargs` - `lspci`, `setpci` +- `depmod` ## Build @@ -47,4 +48,4 @@ or via installed symlinks such as `grep`, `find`, or `patch`. ## Notes - The project force-includes `src/compat-headers/compat.h` to provide Linux compatibility shims for BSD userland code. -- Some behavior is intentionally BSD-flavored, even where GNU tools differ. +- Some behavior is intentionally BSD-flavored, even where GNU tools differ. \ No newline at end of file diff --git a/meson.build b/meson.build index cf41574..51aa4e1 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,8 @@ -project('vx', 'c', +project('vx', 'c', 'cpp', version : '0.2.0', default_options : [ 'c_std=c11', + 'cpp_std=c++17', 'warning_level=2', ], ) @@ -19,6 +20,7 @@ all_utils = [ 'xargs', 'lspci', 'setpci', + 'depmod', ] requested_utils = get_option('utils') @@ -45,6 +47,7 @@ enable_find = enabled_utils.contains('find') enable_xargs = enabled_utils.contains('xargs') enable_lspci = enabled_utils.contains('lspci') enable_setpci = enabled_utils.contains('setpci') +enable_depmod = enabled_utils.contains('depmod') vx_cfg = configuration_data() vx_cfg.set10('VX_ENABLE_PATCH', enable_patch) @@ -59,6 +62,7 @@ vx_cfg.set10('VX_ENABLE_FIND', enable_find) vx_cfg.set10('VX_ENABLE_XARGS', enable_xargs) vx_cfg.set10('VX_ENABLE_LSPCI', enable_lspci) vx_cfg.set10('VX_ENABLE_SETPCI', enable_setpci) +vx_cfg.set10('VX_ENABLE_DEPMOD', enable_depmod) configure_file(output : 'vx-config.h', configuration : vx_cfg) # --- Portability shim for building BSD userland on Linux --- @@ -163,6 +167,10 @@ setpci_src = files( 'src/pci/setpci.c', ) +depmod_src = files( + 'src/depmod/depmod.cpp', +) + pci_inc = include_directories('src/pci') gzip_defines = [ @@ -248,6 +256,11 @@ if enable_setpci vx_sources += setpci_src endif +if enable_depmod + vx_sources += depmod_src + vx_dependencies += [dependency('zlib'), dependency('libzstd')] +endif + vx = executable('vx', vx_sources, include_directories : vx_include_dirs, @@ -295,6 +308,9 @@ endif if enable_setpci install_applets += ['setpci'] endif +if enable_depmod + install_applets += ['depmod'] +endif foreach name : install_applets meson.add_install_script('sh', '-c', @@ -348,6 +364,9 @@ endif if enable_setpci manpages += ['src/pci/setpci.8'] endif +if enable_depmod + manpages += ['src/depmod/depmod.8'] +endif install_man(manpages) @@ -409,6 +428,9 @@ endif if enable_setpci test_targets += ['test_setpci'] endif +if enable_depmod + test_targets += ['test_depmod'] +endif foreach t : test_targets test(t, find_program('tests/' + t + '.sh'), diff --git a/meson_options.txt b/meson_options.txt index 8f1054a..5b05d25 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -15,6 +15,7 @@ option('utils', 'xargs', 'lspci', 'setpci', + 'depmod', ], description : 'Utilities to include in vx (default: all)' ) diff --git a/src/depmod/depmod.8 b/src/depmod/depmod.8 new file mode 100644 index 0000000..3d25a92 --- /dev/null +++ b/src/depmod/depmod.8 @@ -0,0 +1,69 @@ +.Dd July 8, 2026 +.Dt DEPMOD 8 +.Os +.Sh NAME +.Nm depmod +.Nd generate Linux kernel module dependency metadata +.Sh SYNOPSIS +.Nm +.Op Fl aAenvwVh +.Op Fl b Ar basedir +.Op Fl m Ar moduledir +.Op Fl o Ar outdir +.Op Fl C Ar config +.Op Fl E Ar Module.symvers +.Op Fl F Ar System.map +.Op Fl P Ar prefix +.Op Ar version +.Op Ar module ... +.Sh DESCRIPTION +The +.Nm +applet scans Linux kernel modules below +.Pa /lib/modules/ +and writes the usual module metadata files, including +.Pa modules.dep , +.Pa modules.alias , +.Pa modules.symbols , +and the matching kmod binary index files. +.Pp +This implementation is intentionally small and does not use libkmod. It reads +uncompressed modules plus gzip and zstd compressed modules. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl a , Fl -all +Scan all modules. This is the default when no module files are named. +.It Fl A , Fl -quick +Exit without regenerating output when +.Pa modules.dep +is newer than the scanned modules. +.It Fl b Ar basedir , Fl -basedir Ns = Ns Ar basedir +Use +.Ar basedir +as the input root. +.It Fl m Ar moduledir , Fl -moduledir Ns = Ns Ar moduledir +Use a module directory other than +.Pa /lib/modules . +.It Fl o Ar outdir , Fl -outdir Ns = Ns Ar outdir +Use +.Ar outdir +as the output root. +.It Fl n , Fl -show , Fl -dry-run +Print text output to standard output instead of writing files. +.It Fl e , Fl -errsyms +Warn about unresolved module dependencies and symbols. +.It Fl P Ar prefix , Fl -symbol-prefix Ns = Ns Ar prefix +Ignore an architecture symbol prefix while matching symbols. +.It Fl V , Fl -version +Print version information. +.It Fl h , Fl -help +Print usage information. +.El +.Sh COMPATIBILITY +The options +.Fl C , +.Fl E , +and +.Fl F +are accepted for command-line compatibility, but this implementation does not +parse depmod.d configuration, Module.symvers, or System.map content. diff --git a/src/depmod/depmod.cpp b/src/depmod/depmod.cpp new file mode 100644 index 0000000..8db74c0 --- /dev/null +++ b/src/depmod/depmod.cpp @@ -0,0 +1,1250 @@ +// SPDX-License-Identifier: MIT +/* + * Minimal depmod-compatible applet for vx. + * + * This intentionally does not use libkmod. It scans kernel module ELF files, + * reads .modinfo, emits the usual text metadata files, and writes the binary + * trie indexes consumed by libkmod/modprobe. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +constexpr uint32_t INDEX_MAGIC = 0xB007F457u; +constexpr uint32_t INDEX_VERSION = (0x0002u << 16) | 0x0001u; +constexpr uint32_t INDEX_NODE_PREFIX = 0x80000000u; +constexpr uint32_t INDEX_NODE_VALUES = 0x40000000u; +constexpr uint32_t INDEX_NODE_CHILDS = 0x20000000u; +constexpr uint32_t INDEX_NODE_MASK = 0x0fffffffu; + +struct Options { + bool all = false; + bool quick = false; + bool dry_run = false; + bool errsyms = false; + bool verbose = false; + bool warn = false; + std::string basedir = "/"; + std::string moduledir = "/lib/modules"; + std::string outdir; + std::string config; + std::string symvers; + std::string filesyms; + char symbol_prefix = '\0'; + std::string version; + std::vector files; +}; + +struct Module { + std::string path; // absolute input path + std::string relpath; // path relative to / + std::string modname; // normalized module name + std::vector depends_names; + std::vector deps_paths; + std::vector aliases; + std::vector softdeps; + std::vector weakdeps; + std::vector exports; + std::vector undefined; + int order = 0x3fffffff; + size_t scan_index = 0; +}; + +static void warnx(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + fputs("depmod: ", stderr); + vfprintf(stderr, fmt, ap); + fputc('\n', stderr); + va_end(ap); +} + +static bool has_suffix(const std::string &s, const char *suffix) +{ + const size_t n = strlen(suffix); + return s.size() >= n && s.compare(s.size() - n, n, suffix) == 0; +} + +static std::string trim_slashes_right(std::string s) +{ + while (s.size() > 1 && s.back() == '/') + s.pop_back(); + return s.empty() ? std::string("/") : s; +} + +static std::string trim_slashes_left(std::string s) +{ + while (!s.empty() && s.front() == '/') + s.erase(s.begin()); + return s; +} + +static std::string path_join(const std::string &a, const std::string &b) +{ + if (a.empty() || a == "/") + return "/" + trim_slashes_left(b); + if (b.empty()) + return a; + if (a.back() == '/') + return a + trim_slashes_left(b); + return a + "/" + trim_slashes_left(b); +} + +static std::string basename_of(const std::string &path) +{ + size_t slash = path.find_last_of('/'); + if (slash == std::string::npos) + return path; + return path.substr(slash + 1); +} + +static std::string dirname_of(const std::string &path) +{ + size_t slash = path.find_last_of('/'); + if (slash == std::string::npos) + return "."; + if (slash == 0) + return "/"; + return path.substr(0, slash); +} + +static std::string strip_module_suffix(std::string name) +{ + if (has_suffix(name, ".zst")) + name.resize(name.size() - 4); + else if (has_suffix(name, ".gz")) + name.resize(name.size() - 3); + + if (has_suffix(name, ".ko")) + name.resize(name.size() - 3); + return name; +} + +static std::string normalize_modname(std::string name) +{ + name = strip_module_suffix(basename_of(name)); + for (char &c : name) { + if (c == '-') + c = '_'; + } + return name; +} + +static std::string strip_known_symbol_prefix(std::string s, char prefix) +{ + if (!s.empty() && s[0] == '.') + s.erase(s.begin()); + if (prefix != '\0' && !s.empty() && s[0] == prefix) + s.erase(s.begin()); + return s; +} + +static bool mkdir_p(const std::string &path) +{ + if (path.empty()) + return false; + std::string cur; + size_t pos = 0; + if (path[0] == '/') { + cur = "/"; + pos = 1; + } + while (pos <= path.size()) { + size_t slash = path.find('/', pos); + std::string part = path.substr(pos, slash == std::string::npos ? std::string::npos : slash - pos); + if (!part.empty()) { + if (cur.empty() || cur == "/") + cur += part; + else + cur += "/" + part; + if (mkdir(cur.c_str(), 0755) != 0 && errno != EEXIST) + return false; + } + if (slash == std::string::npos) + break; + pos = slash + 1; + } + return true; +} + +static bool read_file_raw(const std::string &path, std::vector &out) +{ + FILE *f = fopen(path.c_str(), "rb"); + if (!f) + return false; + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return false; + } + long len = ftell(f); + if (len < 0) { + fclose(f); + return false; + } + rewind(f); + out.resize(static_cast(len)); + if (!out.empty() && fread(out.data(), 1, out.size(), f) != out.size()) { + fclose(f); + return false; + } + fclose(f); + return true; +} + +static bool decompress_gzip(const std::vector &in, std::vector &out) +{ + z_stream zs{}; + if (inflateInit2(&zs, 16 + MAX_WBITS) != Z_OK) + return false; + + zs.next_in = const_cast(reinterpret_cast(in.data())); + zs.avail_in = static_cast(in.size()); + + std::vector buf(256 * 1024); + int ret = Z_OK; + while (ret == Z_OK) { + zs.next_out = reinterpret_cast(buf.data()); + zs.avail_out = static_cast(buf.size()); + ret = inflate(&zs, Z_NO_FLUSH); + size_t have = buf.size() - zs.avail_out; + out.insert(out.end(), buf.begin(), buf.begin() + have); + } + + inflateEnd(&zs); + return ret == Z_STREAM_END; +} + +static bool decompress_zstd(const std::vector &in, std::vector &out) +{ + unsigned long long frame_size = ZSTD_getFrameContentSize(in.data(), in.size()); + if (frame_size != ZSTD_CONTENTSIZE_ERROR && frame_size != ZSTD_CONTENTSIZE_UNKNOWN) { + out.resize(static_cast(frame_size)); + size_t ret = ZSTD_decompress(out.data(), out.size(), in.data(), in.size()); + if (ZSTD_isError(ret)) + return false; + out.resize(ret); + return true; + } + + ZSTD_DStream *ds = ZSTD_createDStream(); + if (!ds) + return false; + ZSTD_initDStream(ds); + ZSTD_inBuffer input{in.data(), in.size(), 0}; + std::vector buf(ZSTD_DStreamOutSize()); + while (input.pos < input.size) { + ZSTD_outBuffer output{buf.data(), buf.size(), 0}; + size_t ret = ZSTD_decompressStream(ds, &output, &input); + if (ZSTD_isError(ret)) { + ZSTD_freeDStream(ds); + return false; + } + out.insert(out.end(), buf.begin(), buf.begin() + output.pos); + } + ZSTD_freeDStream(ds); + return true; +} + +static bool read_module_image(const std::string &path, std::vector &out) +{ + std::vector raw; + if (!read_file_raw(path, raw)) + return false; + + if (has_suffix(path, ".ko.gz")) + return decompress_gzip(raw, out); + if (has_suffix(path, ".ko.zst")) + return decompress_zstd(raw, out); + + out = std::move(raw); + return true; +} + +template +static T rd_le(const uint8_t *p) +{ + T v = 0; + for (size_t i = 0; i < sizeof(T); i++) + v |= static_cast(p[i]) << (i * 8); + return v; +} + +template +static T rd_be(const uint8_t *p) +{ + T v = 0; + for (size_t i = 0; i < sizeof(T); i++) + v = static_cast((v << 8) | p[i]); + return v; +} + +struct ElfReader { + struct Section { + std::string name; + uint32_t type = 0; + uint64_t offset = 0; + uint64_t size = 0; + uint32_t link = 0; + uint64_t entsize = 0; + }; + + const std::vector &data; + bool le = true; + bool is64 = true; + std::vector
sections; + + explicit ElfReader(const std::vector &d) : data(d) {} + + template + T rd(size_t off) const + { + if (off + sizeof(T) > data.size()) + return 0; + return le ? rd_le(data.data() + off) : rd_be(data.data() + off); + } + + bool parse() + { + if (data.size() < 64 || data[0] != 0x7f || data[1] != 'E' || data[2] != 'L' || data[3] != 'F') + return false; + is64 = data[4] == 2; + if (data[4] != 1 && data[4] != 2) + return false; + if (data[5] == 1) + le = true; + else if (data[5] == 2) + le = false; + else + return false; + + uint64_t shoff = 0; + uint16_t shentsize = 0, shnum = 0, shstrndx = 0; + if (is64) { + shoff = rd(0x28); + shentsize = rd(0x3a); + shnum = rd(0x3c); + shstrndx = rd(0x3e); + } else { + shoff = rd(0x20); + shentsize = rd(0x2e); + shnum = rd(0x30); + shstrndx = rd(0x32); + } + if (shoff == 0 || shentsize == 0 || shnum == 0 || shstrndx >= shnum) + return false; + if (shoff + static_cast(shentsize) * shnum > data.size()) + return false; + + std::vector name_offsets(shnum); + sections.resize(shnum); + for (uint16_t i = 0; i < shnum; i++) { + size_t off = static_cast(shoff + static_cast(i) * shentsize); + Section s; + if (is64) { + name_offsets[i] = rd(off + 0x00); + s.type = rd(off + 0x04); + s.offset = rd(off + 0x18); + s.size = rd(off + 0x20); + s.link = rd(off + 0x28); + s.entsize = rd(off + 0x38); + } else { + name_offsets[i] = rd(off + 0x00); + s.type = rd(off + 0x04); + s.offset = rd(off + 0x10); + s.size = rd(off + 0x14); + s.link = rd(off + 0x18); + s.entsize = rd(off + 0x24); + } + if (s.offset + s.size > data.size()) + return false; + sections[i] = std::move(s); + } + + const Section &shstr = sections[shstrndx]; + for (uint16_t i = 0; i < shnum; i++) { + uint32_t noff = name_offsets[i]; + if (noff >= shstr.size) + continue; + size_t start = static_cast(shstr.offset + noff); + size_t end = start; + while (end < data.size() && data[end] != '\0') + end++; + sections[i].name.assign(reinterpret_cast(data.data() + start), end - start); + } + return true; + } + + const Section *find_section(const char *name) const + { + for (const auto &s : sections) { + if (s.name == name) + return &s; + } + return nullptr; + } + + std::string str_at(const Section &s, uint32_t off) const + { + if (off >= s.size) + return {}; + size_t start = static_cast(s.offset + off); + size_t end = start; + while (end < data.size() && data[end] != '\0') + end++; + return std::string(reinterpret_cast(data.data() + start), end - start); + } + + void read_modinfo(Module &mod) const + { + const Section *mi = find_section(".modinfo"); + if (!mi) + return; + size_t pos = static_cast(mi->offset); + size_t end = static_cast(mi->offset + mi->size); + while (pos < end) { + size_t next = pos; + while (next < end && data[next] != '\0') + next++; + if (next > pos) { + std::string entry(reinterpret_cast(data.data() + pos), next - pos); + size_t eq = entry.find('='); + if (eq != std::string::npos) { + std::string key = entry.substr(0, eq); + std::string val = entry.substr(eq + 1); + if (key == "depends") { + size_t p = 0; + while (p <= val.size()) { + size_t comma = val.find(',', p); + std::string dep = val.substr(p, comma == std::string::npos ? std::string::npos : comma - p); + if (!dep.empty()) + mod.depends_names.push_back(normalize_modname(dep)); + if (comma == std::string::npos) + break; + p = comma + 1; + } + } else if (key == "alias") { + mod.aliases.push_back(val); + } else if (key == "softdep") { + mod.softdeps.push_back(val); + } else if (key == "weakdep") { + mod.weakdeps.push_back(val); + } else if (key == "name" && !val.empty()) { + mod.modname = normalize_modname(val); + } + } + } + pos = next + 1; + } + } + + void read_symbols(Module &mod, char prefix) const + { + constexpr uint32_t SHT_SYMTAB = 2; + constexpr uint16_t SHN_UNDEF = 0; + static const char *export_prefixes[] = { + "__ksymtab_unused_gpl_", + "__ksymtab_gpl_future_", + "__ksymtab_unused_", + "__ksymtab_gpl_", + "__ksymtab_", + "__ksymtab_strings+", + }; + + for (size_t si = 0; si < sections.size(); si++) { + const Section &symsec = sections[si]; + if (symsec.type != SHT_SYMTAB || symsec.entsize == 0 || symsec.link >= sections.size()) + continue; + const Section &strsec = sections[symsec.link]; + size_t count = static_cast(symsec.size / symsec.entsize); + for (size_t i = 0; i < count; i++) { + size_t off = static_cast(symsec.offset + i * symsec.entsize); + uint32_t st_name = 0; + uint8_t st_info = 0; + uint16_t st_shndx = 0; + if (is64) { + st_name = rd(off + 0x00); + st_info = rd(off + 0x04); + st_shndx = rd(off + 0x06); + } else { + st_name = rd(off + 0x00); + st_info = rd(off + 0x0c); + st_shndx = rd(off + 0x0e); + } + std::string name = str_at(strsec, st_name); + if (name.empty()) + continue; + + unsigned bind = st_info >> 4; + if (st_shndx == SHN_UNDEF && (bind == 1 || bind == 2)) { + mod.undefined.push_back(strip_known_symbol_prefix(name, prefix)); + continue; + } + + for (const char *pfx : export_prefixes) { + size_t n = strlen(pfx); + if (name.compare(0, n, pfx) == 0 && name.size() > n) { + mod.exports.push_back(strip_known_symbol_prefix(name.substr(n), prefix)); + break; + } + } + } + } + std::sort(mod.exports.begin(), mod.exports.end()); + mod.exports.erase(std::unique(mod.exports.begin(), mod.exports.end()), mod.exports.end()); + std::sort(mod.undefined.begin(), mod.undefined.end()); + mod.undefined.erase(std::unique(mod.undefined.begin(), mod.undefined.end()), mod.undefined.end()); + } +}; + +static bool parse_module(Module &mod, const Options &opts) +{ + std::vector image; + if (!read_module_image(mod.path, image)) { + warnx("could not read/decompress %s", mod.path.c_str()); + return false; + } + ElfReader elf(image); + if (!elf.parse()) { + warnx("could not parse ELF module %s", mod.path.c_str()); + return false; + } + elf.read_modinfo(mod); + if (mod.modname.empty()) + mod.modname = normalize_modname(mod.relpath); + elf.read_symbols(mod, opts.symbol_prefix); + return true; +} + +static bool is_module_file(const std::string &path) +{ + return has_suffix(path, ".ko") || has_suffix(path, ".ko.gz") || has_suffix(path, ".ko.zst"); +} + +static void scan_modules_recursive(const std::string &root, const std::string &dir, std::vector &out) +{ + DIR *d = opendir(dir.c_str()); + if (!d) + return; + while (dirent *de = readdir(d)) { + if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + continue; + std::string p = path_join(dir, de->d_name); + struct stat st{}; + if (lstat(p.c_str(), &st) != 0) + continue; + if (S_ISDIR(st.st_mode)) { + scan_modules_recursive(root, p, out); + } else if (S_ISREG(st.st_mode) && is_module_file(p)) { + out.push_back(p); + } + } + closedir(d); +} + +static std::string rel_to_root(const std::string &root, const std::string &path) +{ + std::string r = trim_slashes_right(root); + if (path.compare(0, r.size(), r) == 0 && (path.size() == r.size() || path[r.size()] == '/')) { + std::string rel = path.substr(r.size()); + return trim_slashes_left(rel); + } + return path; +} + +static void load_modules_order(const std::string &module_root, std::vector &mods) +{ + std::string order_path = path_join(module_root, "modules.order"); + FILE *f = fopen(order_path.c_str(), "r"); + if (!f) + return; + std::unordered_map order; + char *line = nullptr; + size_t cap = 0; + int idx = 0; + while (getline(&line, &cap, f) >= 0) { + std::string s(line); + while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) + s.pop_back(); + if (s.empty() || s[0] == '#') + continue; + order[s] = idx++; + order[strip_module_suffix(s)] = idx; + } + free(line); + fclose(f); + for (auto &m : mods) { + auto it = order.find(m.relpath); + if (it == order.end()) + it = order.find(strip_module_suffix(m.relpath)); + if (it != order.end()) + m.order = it->second; + } +} + +static bool module_less(const Module &a, const Module &b) +{ + if (a.order != b.order) + return a.order < b.order; + return a.relpath < b.relpath; +} + + +static void resolve_dependencies(std::vector &mods, const Options &opts) +{ + std::unordered_map by_name; + std::unordered_map export_owner; + for (size_t i = 0; i < mods.size(); i++) { + by_name[mods[i].modname] = i; + for (const auto &sym : mods[i].exports) { + if (!sym.empty() && export_owner.find(sym) == export_owner.end()) + export_owner[sym] = i; + } + } + + std::vector> direct(mods.size()); + for (size_t i = 0; i < mods.size(); i++) { + for (const auto &dep : mods[i].depends_names) { + auto it = by_name.find(dep); + if (it != by_name.end() && it->second != i) + direct[i].insert(it->second); + else if (opts.errsyms) + warnx("%s depends on unknown module %s", mods[i].relpath.c_str(), dep.c_str()); + } + for (const auto &sym : mods[i].undefined) { + auto it = export_owner.find(sym); + if (it != export_owner.end() && it->second != i) + direct[i].insert(it->second); + else if (opts.errsyms) + warnx("%s needs unknown symbol %s", mods[i].relpath.c_str(), sym.c_str()); + } + } + + for (size_t i = 0; i < mods.size(); i++) { + std::set seen; + std::vector flat; + std::function dfs = [&](size_t n) { + std::vector children(direct[n].begin(), direct[n].end()); + std::sort(children.begin(), children.end(), [&](size_t a, size_t b) { + return module_less(mods[a], mods[b]); + }); + for (size_t d : children) { + if (d == i || seen.count(d)) + continue; + seen.insert(d); + dfs(d); + flat.push_back(d); + } + }; + dfs(i); + for (size_t d : flat) + mods[i].deps_paths.push_back(mods[d].relpath); + } +} + +static std::string dep_line(const Module &m) +{ + std::string line = m.relpath + ":"; + for (const auto &d : m.deps_paths) + line += " " + d; + return line; +} + +static std::string alias_normalize(const std::string &alias) +{ + // Match kmod's alias index normalization: dashes are stored as + // underscores, except inside bracket ranges such as [0-9]. + std::string out; + out.reserve(alias.size()); + bool in_range = false; + for (char c : alias) { + if (c == '[') + in_range = true; + if (c == ']' && in_range) + in_range = false; + if (c == '-' && !in_range) + out.push_back('_'); + else + out.push_back(c); + } + return out; +} + +class BinaryIndex { + struct Node { + std::map> children; + std::vector> values; + uint32_t size = 0; + uint32_t total = 0; + }; + + Node root_; + + static bool ascii_ok(const std::string &s) + { + for (unsigned char c : s) { + if (c >= 128) + return false; + } + return true; + } + + static uint32_t mask(const Node &n) + { + uint32_t m = 0; + if (!n.children.empty()) + m |= INDEX_NODE_CHILDS; + if (!n.values.empty()) + m |= INDEX_NODE_VALUES; + return m; + } + + static uint32_t calc(Node &n) + { + n.size = 0; + n.total = 0; + if (!n.children.empty()) { + uint8_t first = n.children.begin()->first; + uint8_t last = n.children.rbegin()->first; + n.size += 2; + n.size += (static_cast(last) - first + 1) * sizeof(uint32_t); + for (auto &kv : n.children) + n.total += calc(*kv.second); + } + if (!n.values.empty()) { + n.size += sizeof(uint32_t); + for (const auto &v : n.values) { + n.size += sizeof(uint32_t); + n.size += static_cast(v.second.size() + 1); + } + } + n.total += n.size; + return n.total; + } + + static bool put_u32(FILE *f, uint32_t v) + { + uint32_t be = htonl(v); + return fwrite(&be, sizeof(be), 1, f) == 1; + } + + static bool write_node(FILE *f, const Node &n, uint32_t offset) + { + std::map child_offsets; + if (!n.children.empty()) { + uint32_t child_base = offset + n.size; + uint32_t accum = 0; + for (const auto &kv : n.children) { + const Node &child = *kv.second; + child_offsets[kv.first] = (child_base + accum) | mask(child); + accum += child.total; + } + uint8_t first = n.children.begin()->first; + uint8_t last = n.children.rbegin()->first; + fputc(first, f); + fputc(last, f); + for (uint32_t c = first; c <= last; c++) { + auto it = child_offsets.find(static_cast(c)); + if (!put_u32(f, it == child_offsets.end() ? 0 : it->second)) + return false; + } + } + if (!n.values.empty()) { + if (!put_u32(f, static_cast(n.values.size()))) + return false; + for (const auto &v : n.values) { + if (!put_u32(f, v.first)) + return false; + if (fwrite(v.second.c_str(), 1, v.second.size() + 1, f) != v.second.size() + 1) + return false; + } + } + uint32_t child_offset = offset + n.size; + for (const auto &kv : n.children) { + if (!write_node(f, *kv.second, child_offset)) + return false; + child_offset += kv.second->total; + } + return true; + } + + public: + void insert(const std::string &key, const std::string &value, uint32_t priority) + { + if (!ascii_ok(key) || !ascii_ok(value)) + return; + Node *n = &root_; + for (unsigned char c : key) { + auto &child = n->children[c]; + if (!child) + child = std::make_unique(); + n = child.get(); + } + auto dup = std::find_if(n->values.begin(), n->values.end(), [&](const auto &p) { + return p.second == value; + }); + if (dup == n->values.end()) + n->values.emplace_back(priority, value); + std::sort(n->values.begin(), n->values.end(), [](const auto &a, const auto &b) { + if (a.first != b.first) + return a.first < b.first; + return a.second < b.second; + }); + } + + bool write(FILE *f) + { + calc(root_); + constexpr uint32_t first_off = 3 * sizeof(uint32_t); + return put_u32(f, INDEX_MAGIC) && put_u32(f, INDEX_VERSION) && + put_u32(f, first_off | mask(root_)) && write_node(f, root_, first_off); + } +}; + +static bool write_file(const std::string &path, const std::string &content) +{ + if (!mkdir_p(dirname_of(path))) + return false; + std::string tmp = path + ".tmp"; + FILE *f = fopen(tmp.c_str(), "wb"); + if (!f) + return false; + bool ok = content.empty() || fwrite(content.data(), 1, content.size(), f) == content.size(); + ok = ok && fclose(f) == 0; + if (!ok) { + unlink(tmp.c_str()); + return false; + } + if (rename(tmp.c_str(), path.c_str()) != 0) { + unlink(tmp.c_str()); + return false; + } + return true; +} + +static bool write_index_file(const std::string &path, BinaryIndex &idx) +{ + if (!mkdir_p(dirname_of(path))) + return false; + std::string tmp = path + ".tmp"; + FILE *f = fopen(tmp.c_str(), "wb"); + if (!f) + return false; + bool ok = idx.write(f); + ok = ok && fclose(f) == 0; + if (!ok) { + unlink(tmp.c_str()); + return false; + } + if (rename(tmp.c_str(), path.c_str()) != 0) { + unlink(tmp.c_str()); + return false; + } + return true; +} + +static std::string build_modules_dep(const std::vector &mods) +{ + std::string out; + for (const auto &m : mods) + out += dep_line(m) + "\n"; + return out; +} + +static std::string build_modules_alias(const std::vector &mods) +{ + std::string out = "# Aliases extracted from modules themselves.\n"; + for (const auto &m : mods) { + for (const auto &a : m.aliases) + out += "alias " + a + " " + m.modname + "\n"; + } + return out; +} + +static std::string build_modules_softdep(const std::vector &mods) +{ + std::string out = "# Soft dependencies extracted from modules themselves.\n"; + for (const auto &m : mods) { + for (const auto &s : m.softdeps) + out += "softdep " + m.modname + " " + s + "\n"; + } + return out; +} + +static std::string build_modules_weakdep(const std::vector &mods) +{ + std::string out = "# Weak dependencies extracted from modules themselves.\n"; + for (const auto &m : mods) { + for (const auto &s : m.weakdeps) + out += "weakdep " + m.modname + " " + s + "\n"; + } + return out; +} + +static std::string build_modules_symbols(const std::vector &mods) +{ + std::string out = "# Aliases for symbols, used by symbol_request().\n"; + for (const auto &m : mods) { + for (const auto &s : m.exports) + out += "alias symbol:" + s + " " + m.modname + "\n"; + } + return out; +} + +static std::string build_modules_devname(const std::vector &mods) +{ + std::string out; + bool header = false; + for (const auto &m : mods) { + std::string devname; + char type = 0; + unsigned major = 0, minor = 0; + for (const auto &a : m.aliases) { + if (a.compare(0, 8, "devname:") == 0) + devname = a.substr(8); + unsigned maj = 0, min = 0; + if (sscanf(a.c_str(), "char-major-%u-%u", &maj, &min) == 2) { + type = 'c'; major = maj; minor = min; + } else if (sscanf(a.c_str(), "block-major-%u-%u", &maj, &min) == 2) { + type = 'b'; major = maj; minor = min; + } + } + if (!devname.empty() && type) { + if (!header) { + out += "# Device nodes to trigger on-demand module loading.\n"; + header = true; + } + char buf[256]; + snprintf(buf, sizeof(buf), "%s %s %c%u:%u\n", m.modname.c_str(), devname.c_str(), type, major, minor); + out += buf; + } + } + if (out.empty()) + out = "# Device nodes to trigger on-demand module loading.\n"; + return out; +} + +static void add_builtin_index(const std::string &module_root, BinaryIndex &idx) +{ + std::string p = path_join(module_root, "modules.builtin"); + FILE *f = fopen(p.c_str(), "r"); + if (!f) + return; + char *line = nullptr; + size_t cap = 0; + uint32_t prio = 0; + while (getline(&line, &cap, f) >= 0) { + std::string s(line); + while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) + s.pop_back(); + if (!s.empty()) + idx.insert(normalize_modname(s), "", prio++); + } + free(line); + fclose(f); +} + +static void add_builtin_alias_index(const std::string &module_root, BinaryIndex &idx) +{ + std::string p = path_join(module_root, "modules.builtin.modinfo"); + std::vector data; + if (!read_file_raw(p, data)) + return; + size_t pos = 0; + uint32_t prio = 0; + while (pos < data.size()) { + size_t next = pos; + while (next < data.size() && data[next] != '\0') + next++; + if (next > pos) { + std::string e(reinterpret_cast(data.data() + pos), next - pos); + size_t dot = e.find('.'); + size_t eq = e.find('=', dot == std::string::npos ? 0 : dot + 1); + if (dot != std::string::npos && eq != std::string::npos) { + std::string mod = e.substr(0, dot); + std::string key = e.substr(dot + 1, eq - dot - 1); + std::string val = e.substr(eq + 1); + if (key == "alias") + idx.insert(alias_normalize(val), normalize_modname(mod), prio++); + } + } + pos = next + 1; + } +} + +static bool output_all(const std::string &module_root, const std::string &out_root, + const std::vector &mods, const Options &opts) +{ + std::string dep = build_modules_dep(mods); + std::string alias = build_modules_alias(mods); + std::string softdep = build_modules_softdep(mods); + std::string weakdep = build_modules_weakdep(mods); + std::string symbols = build_modules_symbols(mods); + std::string devname = build_modules_devname(mods); + + if (opts.dry_run) { + fputs(dep.c_str(), stdout); + fputs(alias.c_str(), stdout); + fputs(softdep.c_str(), stdout); + fputs(weakdep.c_str(), stdout); + fputs(symbols.c_str(), stdout); + fputs(devname.c_str(), stdout); + return true; + } + + bool ok = true; + ok = write_file(path_join(out_root, "modules.dep"), dep) && ok; + ok = write_file(path_join(out_root, "modules.alias"), alias) && ok; + ok = write_file(path_join(out_root, "modules.softdep"), softdep) && ok; + ok = write_file(path_join(out_root, "modules.weakdep"), weakdep) && ok; + ok = write_file(path_join(out_root, "modules.symbols"), symbols) && ok; + ok = write_file(path_join(out_root, "modules.devname"), devname) && ok; + + BinaryIndex depidx; + BinaryIndex aliasidx; + BinaryIndex symidx; + BinaryIndex builtinidx; + BinaryIndex builtinaliasidx; + for (uint32_t i = 0; i < mods.size(); i++) { + const Module &m = mods[i]; + depidx.insert(m.modname, dep_line(m), i); + for (const auto &a : m.aliases) + aliasidx.insert(alias_normalize(a), m.modname, i); + for (const auto &s : m.exports) + symidx.insert("symbol:" + s, m.modname, i); + } + add_builtin_index(module_root, builtinidx); + add_builtin_alias_index(module_root, builtinaliasidx); + + ok = write_index_file(path_join(out_root, "modules.dep.bin"), depidx) && ok; + ok = write_index_file(path_join(out_root, "modules.alias.bin"), aliasidx) && ok; + ok = write_index_file(path_join(out_root, "modules.symbols.bin"), symidx) && ok; + ok = write_index_file(path_join(out_root, "modules.builtin.bin"), builtinidx) && ok; + ok = write_index_file(path_join(out_root, "modules.builtin.alias.bin"), builtinaliasidx) && ok; + return ok; +} + +static bool quick_is_current(const std::string &module_root, const std::vector &paths) +{ + struct stat depst{}; + if (stat(path_join(module_root, "modules.dep").c_str(), &depst) != 0) + return false; + for (const auto &p : paths) { + struct stat st{}; + if (stat(p.c_str(), &st) != 0) + return false; + if (st.st_mtime > depst.st_mtime) + return false; + } + return true; +} + +static std::string current_kernel_release() +{ + struct utsname uts{}; + if (uname(&uts) == 0) + return uts.release; + return ""; +} + +static void usage(FILE *out) +{ + fputs("Usage:\n" + "\tdepmod -[aA] [options] [forced_version]\n" + "\tdepmod [options] [forced_version] [modules...]\n" + "\n" + "Options:\n" + "\t-a, --all Probe all modules\n" + "\t-A, --quick Skip if modules.dep is newer than modules\n" + "\t-b, --basedir=DIR Input root path (default: /)\n" + "\t-m, --moduledir=DIR Module directory (default: /lib/modules)\n" + "\t-o, --outdir=DIR Output root path (default: basedir)\n" + "\t-C, --config=PATH Accepted for compatibility\n" + "\t-e, --errsyms Warn about unresolved module symbols\n" + "\t-F, --filesyms=FILE Accepted for compatibility\n" + "\t-E, --symvers=FILE Accepted for compatibility\n" + "\t-n, --show Write text output to stdout only\n" + "\t-P, --symbol-prefix Ignore architecture symbol prefix\n" + "\t-v, --verbose Print scanned modules\n" + "\t-w, --warn Enable duplicate warnings\n" + "\t-V, --version Show version\n" + "\t-h, --help Show this help\n", out); +} + +static bool looks_like_version(const std::string &s) +{ + if (is_module_file(s) || s.find('/') != std::string::npos) + return false; + return true; +} + +static int parse_args(int argc, char **argv, Options &opts) +{ + static const option long_opts[] = { + {"all", no_argument, nullptr, 'a'}, + {"quick", no_argument, nullptr, 'A'}, + {"basedir", required_argument, nullptr, 'b'}, + {"moduledir", required_argument, nullptr, 'm'}, + {"outdir", required_argument, nullptr, 'o'}, + {"config", required_argument, nullptr, 'C'}, + {"symvers", required_argument, nullptr, 'E'}, + {"filesyms", required_argument, nullptr, 'F'}, + {"errsyms", no_argument, nullptr, 'e'}, + {"verbose", no_argument, nullptr, 'v'}, + {"show", no_argument, nullptr, 'n'}, + {"dry-run", no_argument, nullptr, 'n'}, + {"symbol-prefix", required_argument, nullptr, 'P'}, + {"warn", no_argument, nullptr, 'w'}, + {"version", no_argument, nullptr, 'V'}, + {"help", no_argument, nullptr, 'h'}, + {nullptr, 0, nullptr, 0}, + }; + + optind = 1; + int c; + while ((c = getopt_long(argc, argv, "aAb:m:o:C:E:F:evnP:wVh", long_opts, nullptr)) != -1) { + switch (c) { + case 'a': opts.all = true; break; + case 'A': opts.quick = true; break; + case 'b': opts.basedir = optarg; break; + case 'm': opts.moduledir = optarg; break; + case 'o': opts.outdir = optarg; break; + case 'C': opts.config = optarg; break; + case 'E': opts.symvers = optarg; break; + case 'F': opts.filesyms = optarg; break; + case 'e': opts.errsyms = true; break; + case 'v': opts.verbose = true; break; + case 'n': opts.dry_run = true; break; + case 'P': opts.symbol_prefix = optarg && optarg[0] ? optarg[0] : '\0'; break; + case 'w': opts.warn = true; break; + case 'V': puts("vx depmod 0.1"); return 1; + case 'h': usage(stdout); return 1; + default: usage(stderr); return -1; + } + } + + std::vector rest; + for (int i = optind; i < argc; i++) + rest.emplace_back(argv[i]); + + if (!rest.empty() && looks_like_version(rest[0])) { + opts.version = rest[0]; + for (size_t i = 1; i < rest.size(); i++) + opts.files.push_back(rest[i]); + } else { + opts.files = std::move(rest); + } + + if (opts.version.empty()) + opts.version = current_kernel_release(); + if (opts.version.empty()) { + warnx("could not determine kernel release"); + return -1; + } + if (opts.outdir.empty()) + opts.outdir = opts.basedir; + + opts.basedir = trim_slashes_right(opts.basedir); + opts.outdir = trim_slashes_right(opts.outdir); + return 0; +} + +} // namespace + +extern "C" int depmod_main(int argc, char **argv) +{ + Options opts; + int pres = parse_args(argc, argv, opts); + if (pres > 0) + return 0; + if (pres < 0) + return 1; + + std::string module_parent = path_join(opts.basedir, opts.moduledir); + std::string module_root = path_join(module_parent, opts.version); + std::string out_parent = path_join(opts.outdir, opts.moduledir); + std::string out_root = path_join(out_parent, opts.version); + + std::vector paths; + if (!opts.files.empty()) { + for (const auto &f : opts.files) { + if (f.empty()) + continue; + if (f[0] == '/') + paths.push_back(f); + else + paths.push_back(path_join(module_root, f)); + } + } else { + opts.all = true; + scan_modules_recursive(module_root, module_root, paths); + } + + std::sort(paths.begin(), paths.end()); + paths.erase(std::unique(paths.begin(), paths.end()), paths.end()); + + if (opts.quick && quick_is_current(out_root, paths)) + return 0; + + if (paths.empty()) { + struct stat st{}; + if (stat(module_root.c_str(), &st) != 0) { + warnx("could not open module directory %s: %s", module_root.c_str(), strerror(errno)); + return 1; + } + } + + std::vector mods; + mods.reserve(paths.size()); + for (const auto &p : paths) { + Module m; + m.path = p; + m.relpath = rel_to_root(module_root, p); + m.modname = normalize_modname(m.relpath); + m.scan_index = mods.size(); + if (parse_module(m, opts)) { + if (opts.verbose) + fprintf(stdout, "%s\n", m.relpath.c_str()); + mods.push_back(std::move(m)); + } + } + + load_modules_order(module_root, mods); + std::sort(mods.begin(), mods.end(), module_less); + for (size_t i = 0; i < mods.size(); i++) + mods[i].scan_index = i; + resolve_dependencies(mods, opts); + + if (!output_all(module_root, out_root, mods, opts)) { + warnx("failed to write dependency files under %s", out_root.c_str()); + return 1; + } + return 0; +} diff --git a/src/main.c b/src/main.c index d991a9d..6b7e7d6 100644 --- a/src/main.c +++ b/src/main.c @@ -41,6 +41,9 @@ int lspci_main(int argc, char *argv[]); #if VX_ENABLE_SETPCI int setpci_main(int argc, char *argv[]); #endif +#if VX_ENABLE_DEPMOD +int depmod_main(int argc, char *argv[]); +#endif struct applet { const char *name; @@ -91,6 +94,9 @@ static const struct applet applets[] = { #if VX_ENABLE_SETPCI { "setpci", setpci_main }, #endif +#if VX_ENABLE_DEPMOD + { "depmod", depmod_main }, +#endif }; static const size_t n_applets = sizeof(applets) / sizeof(applets[0]); diff --git a/tests/test_depmod.sh b/tests/test_depmod.sh new file mode 100644 index 0000000..d777c4a --- /dev/null +++ b/tests/test_depmod.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu +"$VX" depmod --help >/dev/null +"$VX" depmod --version >/dev/null