Skip to content

Plugins

ETenal edited this page Sep 28, 2023 · 20 revisions

ExpBridge provides a comprehensive interface for reproducing upstream kernel bugs. It can easily integrate with existing bug assessment tools, as a form of a standalone plugin of ExpBridge.

In fact, all the current functionalities of ExpBridge (reproducing bugs on downstream, extracting kernel trace, automatically tuning the original PoCs, etc.) were implemented as one or more plugins. Plugins can share data with each other and collaborate with other plugins. ExpBridge also provides rich API for common functionalities, such as launching VM, detecting kernel crashes, etc.

Workflow

A blank plugin the following entries:

  • prepare() is a common entry to prepare a plugin. It has no arguments, users are able to pass customized arguments through ExpBridge configuration file. prepare() function can pass arguments to prepare_on_demand() if necessary.

  • prepare_on_demand() is used to setup variables and conditions. It's also handy when you are testing a plugin by passing the arguments directly to the plugin without a config file.

  • success() can only be called once for each case. It makes the current case move to the succeed category.

  • run() is the main entry of a plugin. It's where you implement the main functionality of your plugin. It returns True to create a success stamp, otherwise, no stamp will be created.

  • generate_report() executes after run() returns True.

  • cleanup() executes after plugin is finished.

from plugins import AnalysisModule

class Template(AnalysisModule):
    NAME = "Template"
    REPORT_START = "======================Template Report======================"
    REPORT_END =   "==================================================================="
    REPORT_NAME = "Report_Template"
    DEPENDENCY_PLUGINS = []

    def __init__(self):
        super().__init__()
        
    def prepare(self):
        plugin = self.cfg.get_plugin(self.NAME)
        if plugin == None:
            self.err_msg("No such plugin {}".format(self.NAME))
        try:
            self.greeting = int(plugin.greeting)
        except AttributeError:
            self.err_msg("Failed to get greeting")
            return False
        return self.prepare_on_demand()
    
    def prepare_on_demand(self):
        self._prepared = True
        return True
    
    def success(self):
        return self._move_to_success

    def run(self):
        """
        do something
        True: plugin return successfully
        False: something goes wrong, stamp will not be created
        """
        self.logger.info("Hello you, {}".format(self.greeting))
        return True
    
    def generate_report(self):
        final_report = "\n".join(self.report)
        self.info_msg(final_report)
        self._write_to(final_report, self.REPORT_NAME)
    
    def _write_to(self, content, name):
        file_path = "{}/{}".format(self.path_case_plugin, name)
        super()._write_to(content, file_path)

    def cleanup(self):
        super().cleanup()

API Reference

Plugin

class AnalysisModule()

  • name

    name is the string of plugin name

  • analyzor

    analyzor is the instance of current plugin

  • init(self, manager)

    init() passes manager to the plugin. The manager contains all necessary info of the current case, including case hash, config, logger, case path and etc. init() is called automatically by the deployer whenever a plugin is loaded.

  • setup(self)

    setup() sets the plugin path and creates the plugin work folder in the case directory. It also prepares the plugin logger.

  • install_analyzor(self, analyzor)

    install_analyzor() installs the next plugin. It's automatically called by the deployer.

  • run(self)

    run() eventually executes the plugin's run() function. Once the plugin has finished running, it dumps the results to the plugin folder, and procceed the cleanup process.

  • prepare(self)

    prepare() checks whether the dependencies has met. If it does, procceed the prepare() of the plugin.

  • generate_report(self)

    generate_report() generates the plugin report in the plugin folder.

  • success(self)

    success() calls the plugin's success().

  • create_stamp(self)

    create_stamp() generates the finish stamp for the plugin.

  • check_stamp(self)

    check_stamp() checks whether the plugin has a finish stamp

  • null_results(self)

    null_results() resets the finish and results of the plugin

  • plugin_finished(self, plugin_name)

    plugin_finished() returns whether a plugin has finished successfully.

  • plugin_capable(self, plugin_name)

    plugin_capable() returns whether a plugin is enabled.

  • cleanup(self)

    pass

  • dump_results(self)

    dump_results() generates a results.json in the plugin folder.

  • build_mainline_kernel(self, commit=None, config=None, image=None, gcc_version=None, kernel=None, patch="", keep_ori_config=False, extra_cmd="", kernel_repo="", branch="")

    build_mainline_kernel() controls the building process of a upstream kernel. commit specifies the git commit of the kernel source. If commit is None, it uses the case kernel commit. config links to a url of a kernel config that is used for compilation. If config is None, it uses the case kernel config. image specfies the image name, e.g., stretch. gcc_version is decided by case report date unless if it's None. kernel can be upstream, net, linux-next and etc. It will be set by the case if it's not specified. patch points to a path of a kernel patch, it will be apply to the source code if it's a valid patch. If keep_ori_config is True, the kernel config will not be changed. extra_cmd will be executed before compiling the kernel. kernel_repo is a url to a Linux kernel repo if it's need to be specified. branch indicates a particular branch of Linux kernel source code.

  • err_msg(self, msg)

    err_msg() logs an error message.

  • info_msg(self, msg)

    info_msg() logs a info message

  • debug_msg(self, msg)

    debug_msg logs a debug message

Reproducer

class Reproducer()

  • ssh_port

    ssh_port specifies the ssh port of the reproducer VM

  • mon_port

    mon_port specifies the qemu monitor port of the reproducer VM

  • gdb_port

    gdb_port specifies the gdb port of the reproducer VM

  • prepare(self)

    prepare() creates an image snapshot of the testing distro.

  • create_snapshot(self, src, img_dir, image_name, target_format="qcow2")

    create_snapshot() creates an image snapshot of the testing distro. src specifies the testing distro image. img_dir is the path of the directory that keep the snapshot image. image_name indicates the snapshot image name. target_format indicates the format of the snapshot image. It needs to be set to raw when creating snapshot of bullseye and android image.

  • save_crash_log(self, log_msg, name)

    save_crash_log() save the content of log_msg to {self.path_case}/crash_log-{name}

  • need_repro(self)

    need_repro() checks whether a case need to reproduce on the testing distro. A case will not reproduce when ExpBridge decides this bug doesn't affect the testing distro or the patch already exist in the testing distro kernel.

  • reproduce(self, func, func_args, work_dir, timeout, vm_tag="reproducer", root=True, attempt=3, **kwargs)

    reproduce() is the main function for bug reproducing. It invokes launch_qemu() to boot up the kernel and wait for the callback function func to execute. func_args pass the arguments to func. work_dir specifies the folder that store the QEMU log and launch script. timeout indicates the maximum time for running the QEMU. vm_tag is used as a identifier for QEMU log. PoC will run as root user if root is True. attempt indicates how many times that ExpBridge tries to reproduce the bug if it fails. **kwargs contains arguments for VM class, they can be specified as well.

  • launch_qemu(self, c_hash=0, log_suffix="", log_name=None, timeout=None, enable_gdb=False, enable_qemu_mon=False, gdb_port=None, mon_port=None, ssh_port=None, **kwargs)

    launch_qemu() returns a VM class. c_hash represents that hash value of the bug, it usually comes with the bug from syzbot. log_suffix append to the log file for better identification. log_name specifies the name of the log file. timeout indicates the maximum time for running the QEMU. enable_gdb controls whether open the gdb port. enable_qemu_mon controls whether open the QEMU monitor port. gdb_port or mon_port will be ignored they are not enabled. ssh_port specifies the ssh port, it will be automatically set an unused port if it's not specified. **kwargs contains arguments for VM class, they can be specified as well.

  • run_qemu(self, qemu, func, *args)

    run_qemu() boots up the QEMU and invoke the callback function func. *args is the arguments of func

VM

class VM()

  • __init__(self, linux :str, kernel :Vendor(), port, image, hash_tag, key, vmlinux=None, tag='', arch='amd64', work_path='/tmp/', mem="4G", cpu="2", gdb_port=-1, mon_port=-1, timeout=None, debug=False, logger=None, log_name='vm.log', log_suffix="", snapshot=True)

    linux is the path to the upstream Linux directory. (Only for upstream) kernel is the Vendor() class initiated from ExpBridge config. port indicates the ssh port. image is the path of the kernel image. hash_tag indicates the case hahs from syzbot. key is the path of the ssh key. vmlinux is the path of vmlinux (Only for upstream). tag is used for better identification. arch indicates the architecture of the kernel. work_path stores logs and launch scripts of QEMU. mem indicates the how many memory is used by the VM. cpu indicates how many cpu cores are used by the VM. gdb_port indicates the gdb port. mon_port indicates the QEMU monitor port. timeout is maximum running time of the QEMU. debug enables the debug mode. logger specifies the logger instance. log_name indicates the name of the log file. log_suffix add additional suffix to the log file name. snapshot indicates whether using snapshot for booting the image.

  • reset(self)

    reset() resets QEMU instance by empty multiple VM() flag, including qemu_ready, kill_qemu, trigger_crash, and etc.

  • run(self, alternative_func=None, alternative_func_output=None, args=())

    run() boots up the kernel in QEMU and prepares monitor, logger, and timer for the VM() instance. alternative_func specifies the callback function that will be called after booting up. alternative_func_output is a Queue that all the output writes to. args contains the arguments of the alternative_func.

  • wait(self)

    wait() function waits until the alternative_func exits. It blocks the process.

  • shutdown(self)

    shutdown runs the command shutdown -h now to shutdown the VM() instance.

  • kill_vm(self)

    kill_vm force the the VM() instance exits by killing all the related processes.

  • upload(self, user, src: list, dst, wait: bool)

    upload relies on scp to tansfer file to QEMU. user indicates the scp user. src is a list of files on the local machine that are ready to upload. dst is the path in the virtual machine. wait determines whether block the process.

  • download(self, user, src: list, dst, wait: bool)

    download uses scp to transfer file from the QEMU to local machine. user indicates the scp user. src is a list of files on the virtual machine that are ready to upload. dst is the path in the local machine. wait determines whether block the process.

  • command(self, cmds, user, wait: bool, timeout=None)

    command uses ssh to execute command in the virtual machine. cmds is the command. user indicates the ssh user. wait determines whether block the process. timeout indicates the timeout for the ssh.

  • is_qemu_ready(self)

    is_qemu_ready() determines the status of the QEMU by checking the ssh connect.