Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c33639ca authored by Joe Hattori's avatar Joe Hattori
Browse files

libmodprobe: Implement ModuleDependencyGraph

This commit introduces ModuleDependencyGraph, which will be used by the
ueventd coldboot. Instead of loading a module altogether with its
dependencies, this graph enables obtaining dependency free modules that
can be loaded right away. This approach will be useful to prevent
concurrently loading the same kernel module.

Test: atest system/core/libmodprobe/module_dependency_graph_test.cpp
Bug: 421025789
Change-Id: I0748434e2f19189e16bbd278ce7e6c4c2662552e
parent da9f36ee
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ cc_library_static {
        "libmodprobe_ext.cpp",
        "module_config.cpp",
        "module_config_ext.cpp",
        "module_dependency_graph.cpp",
        "utils.cpp",
    ],
    shared_libs: [
@@ -42,6 +43,8 @@ cc_test {
        "module_config_test.cpp",
        "module_config.cpp",
        "module_config_ext_test.cpp",
        "module_dependency_graph.cpp",
        "module_dependency_graph_test.cpp",
        "utils.cpp",
    ],
    static_libs: [
+133 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include <modprobe/module_config.h>

#include <memory>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include <vector>

#include <android-base/thread_annotations.h>

namespace android {
namespace modprobe {

// The status of a module in the dependency graph.
// If the module is blocklisted, the status is set to Blocklisted and does not change.
// If not, the status will change from NotRequested, to Pending, to Loaded or LoadFailed.
// LoadFailed can then be updated to
// - Pending when the module is requested to retry.
// - Loaded when a retry has succeeded.
enum class ModuleStatus {
    // The module is not requested to be loaded.
    NotRequested,
    // The module is requested to be loaded but not loaded.
    Pending,
    // The module has been loaded.
    Loaded,
    // The module has failed to load.
    LoadFailed,
    // The module is blocklisted.
    Blocklisted,
};

struct Module {
    std::string name;
    std::string path;

    // We use ModuleSet to describe the directed edge in the dependency graph.  We use WeakModuleSet
    // to describe the reverse directed edge. There is no memory leak here since the graph is a DAG
    // as we CHECK there is no loop.
    using WeakModuleSet = std::set<std::weak_ptr<Module>, std::owner_less<std::weak_ptr<Module>>>;
    using ModuleSet = std::set<std::shared_ptr<Module>>;

    // Set of dependencies of this module. A dependency is unmet if:
    // - it's a hard-dep and state != Loaded.
    // - it's a soft-dep and state != Loaded && state != LoadFailed. Note that failed loads are
    //   treated as "met" since soft-deps are optional.
    ModuleSet unmet_dependencies;
    // Reverse map of `unmet_dependencies`.
    WeakModuleSet rev_unmet_dependencies;
    // "soft dependencies" (softdep) are a way to specify preferred loading order or dependencies
    // that are needed for optimal functionality or to avoid issues.
    // There are two types of soft dependencies: pre-softdep and post-softdep.
    // - pre-softdep: If module A is a pre-softdep of module B, then A should be loaded before B is
    //               loaded. It is simply an implied edge in the dependency graph.
    // - post-softdep: If module A is a post-softdep of module B, then A should be loaded after B is
    //                 loaded. It is not only an edge but also implies that the loading of module A
    //                 should be initiated upon the loading of module B.
    ModuleSet pre_softdeps;
    WeakModuleSet post_softdeps;

    ModuleStatus status{ModuleStatus::NotRequested};

    bool IsReady() const;

    void MarkBlocklisted();
};

/**
 * Manages dependencies between kernel modules to facilitate parallel loading.
 *
 * This class builds a dependency graph based on ModuleConfig. It allows requesting a set of modules
 * to be loaded and then retrieving batches of modules that are ready for loading, respecting their
 * dependencies. Once a module is processed (loaded or failed), it should be marked as such to
 * unblock any modules that depend on it.
 *
 * This class is thread-safe.
 */
class ModuleDependencyGraph {
  public:
    ModuleDependencyGraph(const ModuleConfig& module_config);

    // Request a module to be loaded with its dependencies.
    void AddModule(const std::string& module_name);
    // Marks a module as loaded. This is supposed to be called after a successful init_module(2)
    void MarkModuleLoaded(const std::string& module_path);
    // Marks a module loading as failed. This is supposed to be called after a failed init_module(2)
    void MarkModuleLoadFailed(const std::string& module_path);
    // Returns module paths that are ready to be loaded. Internally, it returns ready_module_paths_
    // and clears it.
    std::unordered_set<std::string> PopReadyModules();

  private:
    const std::string& ResolveAlias(const std::string& module_name);
    std::shared_ptr<Module> GetModule(const std::string& module_name)
            EXCLUSIVE_LOCKS_REQUIRED(graph_lock_);
    void AddModuleLocked(std::shared_ptr<Module>& module) EXCLUSIVE_LOCKS_REQUIRED(graph_lock_);
    void MarkModuleLoadResult(const std::string& module_path, bool failed);

    void AddUnmetDependency(std::shared_ptr<Module> module, std::shared_ptr<Module> dep_module);
    void RemoveUnmetDependency(std::shared_ptr<Module> module, std::shared_ptr<Module> dep_module)
            EXCLUSIVE_LOCKS_REQUIRED(graph_lock_);

    const std::unordered_map<std::string, std::string> path_to_name_;
    const std::vector<std::pair<std::string, std::string>> module_aliases_;

    // A set of modules that are requested to be loaded.
    std::unordered_map<std::string, std::shared_ptr<Module>> modules_ GUARDED_BY(graph_lock_);
    // Modules that are ready to be loaded.
    std::unordered_set<std::string> ready_module_paths_ GUARDED_BY(graph_lock_);

    std::mutex graph_lock_;
};

}  // namespace modprobe
}  // namespace android
+246 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "include/modprobe/module_dependency_graph.h"
#include <modprobe/module_dependency_graph.h>

#include <fnmatch.h>

#include <android-base/file.h>
#include <android-base/logging.h>

namespace android {
namespace modprobe {

bool Module::IsReady() const {
    return unmet_dependencies.empty() && status == ModuleStatus::Pending;
}

void Module::MarkBlocklisted() {
    status = ModuleStatus::Blocklisted;
    for (const std::weak_ptr<Module>& rev_dep_weak : rev_unmet_dependencies) {
        if (std::shared_ptr<Module> rev_dep = rev_dep_weak.lock()) {
            rev_dep->MarkBlocklisted();
        }
    }
}

static std::unordered_map<std::string, std::string> BuildPathToName(
        const std::unordered_map<std::string, std::vector<std::string>>& parsed_deps) {
    std::unordered_map<std::string, std::string> path_to_name;
    for (const auto& [mod, deps] : parsed_deps) {
        CHECK(!deps.empty())
                << "The first element of a dependency list should be the module itself";
        path_to_name.emplace(deps[0], mod);
    }
    return path_to_name;
}

const std::string& ModuleDependencyGraph::ResolveAlias(const std::string& module_name) {
    for (const auto& [alias, aliased_name] : module_aliases_) {
        if (fnmatch(alias.c_str(), module_name.c_str(), 0) == 0) {
            return aliased_name;
        }
    }
    return module_name;
}

std::shared_ptr<Module> ModuleDependencyGraph::GetModule(const std::string& module_name) {
    const std::string& resolved_name = ResolveAlias(module_name);
    if (!modules_.contains(resolved_name)) {
        LOG(ERROR) << "LMP: DependencyGraph: Module " << module_name << " not in .dep file";
        return nullptr;
    }
    return modules_.at(resolved_name);
}

static bool HasLoop(const std::shared_ptr<Module>& mod,
                    std::unordered_set<std::shared_ptr<Module>>& visited,
                    std::unordered_set<std::shared_ptr<Module>>& recursion_stack) {
    visited.insert(mod);
    recursion_stack.insert(mod);

    for (const std::shared_ptr<Module>& dep : mod->unmet_dependencies) {
        if (recursion_stack.count(dep)) {
            return true;
        }
        if (!visited.count(dep)) {
            if (HasLoop(dep, visited, recursion_stack)) {
                return true;
            }
        }
    }

    recursion_stack.erase(mod);
    return false;
}

static bool HasLoop(const std::unordered_map<std::string, std::shared_ptr<Module>>& modules) {
    std::unordered_set<std::shared_ptr<Module>> visited;
    std::unordered_set<std::shared_ptr<Module>> recursion_stack;

    for (const auto& [_, mod] : modules) {
        if (!visited.count(mod)) {
            if (HasLoop(mod, visited, recursion_stack)) {
                return true;
            }
        }
    }

    return false;
}

ModuleDependencyGraph::ModuleDependencyGraph(const ModuleConfig& config)
    : path_to_name_(BuildPathToName(config.module_deps)), module_aliases_(config.module_aliases) {
    for (const auto& [path, name] : path_to_name_) {
        std::shared_ptr<Module> mod = std::make_shared<Module>();
        mod->name = name;
        mod->path = path;
        modules_.emplace(name, mod);
    }

    for (const auto& [_, deps] : config.module_deps) {
        CHECK(!deps.empty())
                << "The first element of a dependency list should be the module itself";
        const std::string& mod_name = path_to_name_.at(deps[0]);
        std::shared_ptr<Module> mod = modules_.at(mod_name);
        for (size_t i = 1; i < deps.size(); i++) {
            if (path_to_name_.contains(deps[i])) {
                const std::string& dep_name = path_to_name_.at(deps[i]);
                std::shared_ptr<Module> dep_module = modules_.at(dep_name);
                AddUnmetDependency(mod, dep_module);
            }
        }
    }

    for (const auto& [mod_name, softdep] : config.module_pre_softdep) {
        if (std::shared_ptr<Module> pre_softdep = GetModule(softdep)) {
            std::shared_ptr<Module> mod = modules_.at(mod_name);
            mod->pre_softdeps.insert(pre_softdep);
            AddUnmetDependency(mod, pre_softdep);
        }
    }

    for (const auto& [mod_name, softdep] : config.module_post_softdep) {
        if (std::shared_ptr<Module> post_softdep = GetModule(softdep)) {
            std::shared_ptr<Module> mod = modules_.at(mod_name);
            mod->post_softdeps.insert(post_softdep);
            AddUnmetDependency(post_softdep, mod);
        }
    }

    for (const std::string& name : config.module_blocklist) {
        if (std::shared_ptr<Module> mod = GetModule(name)) {
            mod->MarkBlocklisted();
        }
    }

    CHECK(!HasLoop(modules_)) << "Dependency graph has a loop";
}

void ModuleDependencyGraph::AddUnmetDependency(std::shared_ptr<Module> mod,
                                               std::shared_ptr<Module> dep_mod) {
    mod->unmet_dependencies.insert(dep_mod);
    dep_mod->rev_unmet_dependencies.insert(mod);
}

void ModuleDependencyGraph::RemoveUnmetDependency(std::shared_ptr<Module> mod,
                                                  std::shared_ptr<Module> dep_mod) {
    mod->unmet_dependencies.erase(dep_mod);
    dep_mod->rev_unmet_dependencies.erase(mod);
    if (mod->IsReady()) {
        ready_module_paths_.insert(mod->path);
    }
}

void ModuleDependencyGraph::AddModule(const std::string& module_name) {
    std::lock_guard<std::mutex> lock(graph_lock_);

    if (std::shared_ptr<Module> mod = GetModule(module_name)) {
        AddModuleLocked(mod);
    }
}

void ModuleDependencyGraph::AddModuleLocked(std::shared_ptr<Module>& mod) {
    for (std::weak_ptr<Module> softdep_weak : mod->post_softdeps) {
        if (std::shared_ptr<Module> softdep = softdep_weak.lock()) {
            AddModuleLocked(softdep);
        }
    }

    switch (mod->status) {
        case ModuleStatus::LoadFailed:
            LOG(VERBOSE) << "LMP: DependencyGraph: Retrying previously failed module " << mod->name;
            FALLTHROUGH_INTENDED;
        case ModuleStatus::NotRequested:
            mod->status = ModuleStatus::Pending;
            if (mod->IsReady()) {
                ready_module_paths_.insert(mod->path);
            }
            for (std::shared_ptr<Module> dep : mod->unmet_dependencies) {
                AddModuleLocked(dep);
            }
            break;
        case ModuleStatus::Blocklisted:
            LOG(VERBOSE) << "LMP: DependencyGraph: Skipping blocklisted module " << mod->name;
            break;
        case ModuleStatus::Loaded:
            LOG(VERBOSE) << "LMP: DependencyGraph: Module " << mod->name << " already loaded";
            break;
        case ModuleStatus::Pending:
            break;
    }
}

void ModuleDependencyGraph::MarkModuleLoaded(const std::string& module_path) {
    MarkModuleLoadResult(module_path, false);
}

void ModuleDependencyGraph::MarkModuleLoadFailed(const std::string& module_path) {
    MarkModuleLoadResult(module_path, true);
}

void ModuleDependencyGraph::MarkModuleLoadResult(const std::string& module_path, bool failed) {
    std::lock_guard<std::mutex> lock(graph_lock_);

    const std::string& mod_name = path_to_name_.at(module_path);
    std::shared_ptr<Module> mod = modules_.at(mod_name);

    CHECK(mod->status == ModuleStatus::Pending || mod->status == ModuleStatus::LoadFailed);

    mod->status = failed ? ModuleStatus::LoadFailed : ModuleStatus::Loaded;

    Module::WeakModuleSet rev_deps = mod->rev_unmet_dependencies;
    for (const std::weak_ptr<Module>& rev_dep_weak : rev_deps) {
        if (std::shared_ptr<Module> rev_dep = rev_dep_weak.lock()) {
            // A hard-dep is satisfied only if the module is successfully loaded, but a soft-dep is
            // satisfied regardless of the loading being successful or failed.
            if (!failed || rev_dep->pre_softdeps.contains(mod)) {
                RemoveUnmetDependency(rev_dep, mod);
            }
        }
    }
}

std::unordered_set<std::string> ModuleDependencyGraph::PopReadyModules() {
    std::lock_guard<std::mutex> lock(graph_lock_);
    std::unordered_set<std::string> ready;
    ready.swap(ready_module_paths_);
    return ready;
}

}  // namespace modprobe
}  // namespace android
+386 −0

File added.

Preview size limit exceeded, changes collapsed.