Skip to content

Commit

Permalink
Add SCSI notifications subsystem
Browse files Browse the repository at this point in the history
Linux kernel contains a lot of standardized pub/sub
notification chains. SCSI layer, as a very old one,
lacks the support for any form of notifications. We
need the information about devices coming and going
so we added the SCSI notification layer ourselves,
plugging into the standard Linux notifications
library.
  • Loading branch information
ttg-public committed Jan 1, 1970
1 parent d032ac4 commit 098bae2
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 0 deletions.
13 changes: 13 additions & 0 deletions internal/notifier_base.h
@@ -0,0 +1,13 @@
#ifndef REDPILL_NOTIFIER_BASE_H
#define REDPILL_NOTIFIER_BASE_H

#define notifier_reg_in() pr_loc_dbg("Registering %s notifier", NOTIFIER_NAME);
#define notifier_reg_ok() pr_loc_inf("Successfully registered %s notifier", NOTIFIER_NAME);
#define notifier_ureg_in() pr_loc_dbg("Unregistering %s notifier", NOTIFIER_NAME);
#define notifier_ureg_ok() pr_loc_inf("Successfully unregistered %s notifier", NOTIFIER_NAME);
#define notifier_sub(nb) \
pr_loc_dbg("%pF (priority=%d) subscribed to %s events", (nb)->notifier_call, (nb)->priority, NOTIFIER_NAME);
#define notifier_unsub(nb) \
pr_loc_dbg("%pF (priority=%d) unsubscribed from %s events", (nb)->notifier_call, (nb)->priority, NOTIFIER_NAME);

#endif //REDPILL_NOTIFIER_BASE_H
239 changes: 239 additions & 0 deletions internal/scsi/scsi_notifier.c
@@ -0,0 +1,239 @@
/**
* Notification chain implementation for SCSI devices
*
* Linux kernel contains a subsystem responsible for delivering notifications about asynchronous events. It implements a
* pub/sub model. As many subsystems predate existence of the so-called Notification Chains these subsystems usually
* lack any pub/sub functionality. SCSI is no exception. SCSI layer/driver is ancient and huge. It does not have any way
* of delivering events to other parts of the system. This submodule retrofits notification chains to the SCSI layer to
* notify about new devices being added to the system. It can be easily extended to notify about removed devices as
* well.
*
* Before using this submodule you should read the notice below + the gitbooks article if you have never worked with
* Linux notification chains.
*
* !! READ ME - THIS IS IMPORTANT !!
* The core notifier.h contains the following return constants:
* - NOTIFY_DONE: "don't care about that event really"
* - NOTIFY_OK: "good, processed" (really the same as DONE; semantic is defined by a particular publisher[!])
* - NOTIFY_BAD: "stop the notification chain! I veto that action!"
* - NOTIFY_STOP: "stop the notification chain. It's all good."
* This SCSI notifier defines them as such:
* - NOTIFY_DONE, NOTIFY_OK: processed, continue calling other
* - NOTIFY_BAD:
* scsi_event=SCSI_EVT_DEV_PROBING: stop sd_probe() with EBUSY error; subscribers with lower priority will not exec
* scsi_event=SCSI_EVT_DEV_PROBED_OK: subscribers with lower priority will not exec
* scsi_event=SCSI_EVT_DEV_PROBED_ERR: subscribers with lower priority will not exec
* - NOTIFY_STOP:
* scsi_event=SCSI_EVT_DEV_PROBING: stop sd_probe() with 0 err-code; subscribers with lower priority will not exec
* scsi_event=SCSI_EVT_DEV_PROBED_OK: subscribers with lower priority will not exec
* scsi_event=SCSI_EVT_DEV_PROBED_ERR: subscribers with lower priority will not exec
*
* SUPPORTED DEVICES
* Currently only SCSI disks are supported. This isn't a technical limitation but rather a practical one - we don't want
* to trigger notifications for all-all SCSI devices (which include hosts, buses, etc). If needed a new set of functions
* subscribe_.../ubsubscribe_... can easily be added which don't filter by type.
*
* TODO
* This notifier does not support notifying about disconnection of the device. It should as we need to know if device
* disappeared (e.g. while processing shimming of boot devices).
*
* ADDITIONAL TOOLS
* It is highly recommended to use scsi_toolbox when subscribing to notifications from the SCSI subsystem.
*
* References:
* - https://0xax.gitbooks.io/linux-insides/content/Concepts/linux-cpu-4.html (about notification chains subsystem)
*/
#include "scsi_notifier.h"
#include "../../common.h"
#include "../notifier_base.h" //notifier_*()
#include "scsi_notifier_list.h"
#include "scsi_toolbox.h"
#include "../intercept_driver_register.h" //watching for sd driver loading
#include <scsi/scsi_device.h> //to_scsi_device()

#define NOTIFIER_NAME "SCSI device"

/*********************************** Interacting with an active/loaded SCSI driver ************************************/
static driver_watcher_instance *driver_watcher = NULL;
static int (*org_sd_probe) (struct device *dev) = NULL; //set during register

/**
* Main notification routine hooking sd_probe()
*/
static int sd_probe_shim(struct device *dev)
{
pr_loc_dbg("Probing SCSI device using %s", __FUNCTION__);
if (!is_scsi_leaf(dev)) {
pr_loc_dbg("%s: new SCSI device connected - not a leaf, ignoring", __FUNCTION__);
return org_sd_probe(dev);
}

struct scsi_device *sdp = to_scsi_device(dev);
if (!is_scsi_disk(sdp)) {
pr_loc_dbg("%s: new SCSI device connected - not a disk, ignoring", __FUNCTION__);
return org_sd_probe(dev);
}

pr_loc_dbg("Triggering SCSI_EVT_DEV_PROBING notifications");
int out = notifier_to_errno(blocking_notifier_call_chain(&rp_scsi_notify_list, SCSI_EVT_DEV_PROBING, sdp));
if (unlikely(out == NOTIFY_STOP)) {
pr_loc_dbg("After SCSI_EVT_DEV_PROBING a callee stopped chain with non-error condition. Faking probe-ok.");
return 0;
} else if (unlikely(out == NOTIFY_BAD)) {
pr_loc_dbg("After SCSI_EVT_DEV_PROBING a callee stopped chain with non-error condition. Faking probe-ok.");
return -EIO; //some generic error
}

pr_loc_dbg("Calling original sd_probe()");
out = org_sd_probe(dev);
scsi_event evt = (out == 0) ? SCSI_EVT_DEV_PROBED_OK : SCSI_EVT_DEV_PROBED_ERR;

pr_loc_dbg("Triggering SCSI_EVT_DEV_PROBED notifications - sd_probe() exit=%d", out);
blocking_notifier_call_chain(&rp_scsi_notify_list, evt, sdp);

return out;
}

/**
* Overrides sd_probe() to provide notifications via sd_probe_shim()
*
* @param drv "sd" driver instance
*/
static inline void install_sd_probe_shim(struct device_driver *drv)
{
pr_loc_dbg("Overriding %pf()<%p> with %pf()<%p>", drv->probe, drv->probe, sd_probe_shim, sd_probe_shim);
org_sd_probe = drv->probe;
drv->probe = sd_probe_shim;
}

/**
* Removes override of sd_probe(), installed by install_sd_probe_shim()
*
* @param drv "sd" driver instance
*/
static inline void uninstall_sd_probe_shim(struct device_driver *drv)
{
if (unlikely(!org_sd_probe)) {
pr_loc_wrn(
"Cannot %s - original drv->probe is not saved. It was either never installed or it's a bug. "
"The current drv->probe is %pf()<%p>",
__FUNCTION__, drv->probe, drv->probe);
return;
}

pr_loc_dbg("Restoring %pf()<%p> to %pf()<%p>", drv->probe, drv->probe, org_sd_probe, org_sd_probe);
drv->probe = org_sd_probe;
org_sd_probe = NULL;
}

/**
* Watches for the sd driver to load in order to shim it. The driver registration is modified before the driver loads.
*/
static driver_watch_notify_result sd_load_watcher(struct device_driver *drv, driver_watch_notify_state event)
{
if (unlikely(event != DWATCH_STATE_COMING))
return DWATCH_NOTIFY_CONTINUE;

pr_loc_dbg("%s driver loaded - triggering sd_probe shim installation", SCSI_DRV_NAME);
install_sd_probe_shim(drv);

driver_watcher = NULL; //returning DWATCH_NOTIFY_DONE causes automatic unwatching
return DWATCH_NOTIFY_DONE;
}

/******************************************** Public API of the notifier **********************************************/
extern struct bus_type scsi_bus_type;

int subscribe_scsi_disk_events(struct notifier_block *nb)
{
notifier_sub(nb);
return blocking_notifier_chain_register(&rp_scsi_notify_list, nb);
}

int unsubscribe_scsi_disk_events(struct notifier_block *nb)
{
notifier_unsub(nb);
return blocking_notifier_chain_unregister(&rp_scsi_notify_list, nb);
}

// We need an additional flag as depending on which method of sd_probe override (watcher vs. existing driver find &
// switch)
static bool notifier_registered = false;
int register_scsi_notifier(void)
{
notifier_reg_in();

if (unlikely(notifier_registered)) {
pr_loc_bug("%s notifier is already registered", NOTIFIER_NAME);
return -EEXIST;
}

struct device_driver *drv = find_scsi_driver();

if(unlikely(drv < 0)) { //some error occurred while looking for the driver
return PTR_ERR(drv); //find_scsi_driver() should already log what went wrong
} else if(drv) { //the driver is already loaded - driver watcher cannot help us
pr_loc_wrn(
"The %s driver was already loaded when %s notifier registered - some devices may already be registered",
SCSI_DRV_NAME, NOTIFIER_NAME);
install_sd_probe_shim(drv);
} else { //driver not yet loaded - driver watcher will trigger sd_probe_shim installation when driver loads
pr_loc_dbg("The %s driver is not ready to dispatch %s notifier events - awaiting driver", SCSI_DRV_NAME,
NOTIFIER_NAME);
driver_watcher = watch_scsi_driver_register(sd_load_watcher, DWATCH_STATE_COMING);
if (unlikely(IS_ERR(driver_watcher))) {
pr_loc_err("Failed to register driver watcher for driver %s", SCSI_DRV_NAME);
return PTR_ERR(driver_watcher);
}
}

notifier_registered = true;

notifier_reg_ok();
return 0;
}

int unregister_scsi_notifier(void)
{
notifier_ureg_in();

if (unlikely(!notifier_registered)) {
pr_loc_bug("%s notifier is not registered", NOTIFIER_NAME);
return -ENOENT;
}

bool is_error = false;
int out = -EINVAL;

//Check if we're watching sd driver (i.e. SCSI notifier was registered and is now being unregistered before the
// driver had a chance to load)
if (unlikely(driver_watcher)) {
pr_loc_dbg("%s notifier is still observing %s driver - stopping observer", NOTIFIER_NAME, SCSI_DRV_NAME);
out = unwatch_driver_register(driver_watcher);
if (unlikely(out != 0)) {
pr_loc_err("Failed to unregister driver watcher - error=%d", out);
is_error = true;
}
}

//sd_probe() was replaced either after watching for the driver or on-the-spot after the driver was already loaded
if (likely(org_sd_probe)) {
struct device_driver *drv = find_scsi_driver();
if (unlikely(IS_ERR(drv))) {
return PTR_ERR(drv); //find_scsi_driver() should already log what went wrong
} else if(likely(drv)) {
uninstall_sd_probe_shim(drv);
} else { //that is almost impossible as sd is built-in, but we if it happens there's nothing to recover
pr_loc_wrn("%s driver went away (?!)", SCSI_DRV_NAME);
is_error = true;
}
}

notifier_registered = false;
if (unlikely(is_error)) {
return out;
} else {
notifier_ureg_ok();
return 0;
}
}
29 changes: 29 additions & 0 deletions internal/scsi/scsi_notifier.h
@@ -0,0 +1,29 @@
#ifndef REDPILL_SCSI_NOTIFIER_H
#define REDPILL_SCSI_NOTIFIER_H

#include <linux/notifier.h> //All other parts including scsi_notifier.h cannot really not use linux/notifier.h

typedef enum {
SCSI_EVT_DEV_PROBING, //device is being probed; it can be modified or outright ignored
SCSI_EVT_DEV_PROBED_OK, //device is probed and ready
SCSI_EVT_DEV_PROBED_ERR, //device was probed but it failed
} scsi_event;

/**
* Callback signature: void (*f)(struct notifier_block *self, unsigned long state, void *data), where:
* unsigned long state => scsi_event event
* void *data => struct scsi_device *sdp
*
* Currently these methods are DELIBERATELY limited to SCSI TYPE_DISK scope. If you need other SCSI devices watching
* add another set of methods (subscribe scsi_device_events() and such, do NOT extend the scope of these methods as
* other parts of the code rely on pre-filtered events as in most cases listening for ALL devices is a lot of noise).
*
* @return
*/
int subscribe_scsi_disk_events(struct notifier_block *nb);
int unsubscribe_scsi_disk_events(struct notifier_block *nb);

int register_scsi_notifier(void);
int unregister_scsi_notifier(void);

#endif //REDPILL_SCSI_NOTIFIER_H
4 changes: 4 additions & 0 deletions internal/scsi/scsi_notifier_list.c
@@ -0,0 +1,4 @@
#include "scsi_notifier_list.h"
#include <linux/notifier.h>

BLOCKING_NOTIFIER_HEAD(rp_scsi_notify_list);
27 changes: 27 additions & 0 deletions internal/scsi/scsi_notifier_list.h
@@ -0,0 +1,27 @@
/**
* This file exists solely as a workaround for GCC bug #275674 - static structures are misdirected as dynamic
*
* Linux contains many clever idioms. One of them is a complex initialization of heads for notifier chains
* (include/linux/notifier.h). They do contain an embedded cast to a struct. GCC <5 detects that as a dynamic allocation
* and refuses to initialize it statically. This breaks all the macros for notifier (e.g. BLOCKING_NOTIFIER_INIT). Old
* kernels (i.e. <3.18) cannot be compiled with GCC >4.9 so... we cannot use a newer GCC but we cannot use older due to
* a bug. One of the solutions would be to convert the whole code of this module to GNU89 but this is painful to use.
*
* Such structures are working in GNU89 mode as well as when defined as a heap variable in a function. However, GCC is
* smart enough to release the memory from within a function (so we cannot just wrap it in a function and return a ptr).
* Due to the complex nature of the struct we didn't want to hardcode it here as they change between kernel version.
* As a workaround we created a separate compilation unit containing just the struct and compile it in GNU89 mode, while
* rest of the project stays at GNU99.
*
* Resources
* - https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63567 (bug report)
* - https://unix.stackexchange.com/a/275674 (kernel v3.18 restriction)
* - https://stackoverflow.com/a/49119902 (linking files compiled with different language standard in GCC)
* - https://www.kernel.org/doc/Documentation/kbuild/makefiles.txt (compilation option per file in Kbuild; sect. 3.7)
*/
#ifndef REDPILL_SCSI_NOTIFIER_LIST_H
#define REDPILL_SCSI_NOTIFIER_LIST_H

extern struct blocking_notifier_head rp_scsi_notify_list;

#endif //REDPILL_SCSI_NOTIFIER_LIST_H
3 changes: 3 additions & 0 deletions redpill_main.c
Expand Up @@ -3,6 +3,7 @@
#include "config/runtime_config.h"
#include "common.h" //commonly used headers in this module
#include "internal/intercept_execve.h" //Handling of execve() replacement
#include "internal/scsi/scsi_notifier.h" //the missing pub/sub handler for SCSI driver
#include "config/cmdline_delegate.h" //Parsing of kernel cmdline
#include "shim/boot_device_shim.h" //Registering & deciding between boot device shims
#include "shim/bios_shim.h" //Shimming various mfgBIOS functions to make them happy
Expand Down Expand Up @@ -46,6 +47,7 @@ static int __init init_(void)
(out = extract_config_from_cmdline(&current_config)) != 0 //This MUST be the first entry
|| (out = populate_runtime_config(&current_config)) != 0 //This MUST be second
|| (out = register_uart_fixer(current_config.hw_config)) != 0 //Fix consoles ASAP
|| (out = register_scsi_notifier()) != 0 //Load SCSI notifier so that boot shim (& others) can use it
|| (out = register_boot_shim(&current_config.boot_media)) //Make sure we're quick with this one
|| (out = register_execve_interceptor()) != 0 //Register this reasonably high as other modules can use it blindly
|| (out = register_bios_shim(current_config.hw_config)) != 0
Expand Down Expand Up @@ -90,6 +92,7 @@ static void __exit cleanup_(void)
unregister_bios_shim,
unregister_execve_interceptor,
unregister_boot_shim,
unregister_scsi_notifier,
unregister_uart_fixer
};

Expand Down

0 comments on commit 098bae2

Please sign in to comment.