forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
664 lines
15 KiB
664 lines
15 KiB
// SPDX-License-Identifier: GPL-2.0 |
|
/* |
|
* Intel Management Engine Interface (Intel MEI) Linux driver |
|
* Copyright (c) 2015, Intel Corporation. |
|
*/ |
|
|
|
#include <linux/module.h> |
|
#include <linux/slab.h> |
|
#include <linux/interrupt.h> |
|
#include <linux/debugfs.h> |
|
#include <linux/completion.h> |
|
#include <linux/watchdog.h> |
|
|
|
#include <linux/uuid.h> |
|
#include <linux/mei_cl_bus.h> |
|
|
|
/* |
|
* iAMT Watchdog Device |
|
*/ |
|
#define INTEL_AMT_WATCHDOG_ID "iamt_wdt" |
|
|
|
#define MEI_WDT_DEFAULT_TIMEOUT 120 /* seconds */ |
|
#define MEI_WDT_MIN_TIMEOUT 120 /* seconds */ |
|
#define MEI_WDT_MAX_TIMEOUT 65535 /* seconds */ |
|
|
|
/* Commands */ |
|
#define MEI_MANAGEMENT_CONTROL 0x02 |
|
|
|
/* MEI Management Control version number */ |
|
#define MEI_MC_VERSION_NUMBER 0x10 |
|
|
|
/* Sub Commands */ |
|
#define MEI_MC_START_WD_TIMER_REQ 0x13 |
|
#define MEI_MC_START_WD_TIMER_RES 0x83 |
|
#define MEI_WDT_STATUS_SUCCESS 0 |
|
#define MEI_WDT_WDSTATE_NOT_REQUIRED 0x1 |
|
#define MEI_MC_STOP_WD_TIMER_REQ 0x14 |
|
|
|
/** |
|
* enum mei_wdt_state - internal watchdog state |
|
* |
|
* @MEI_WDT_PROBE: wd in probing stage |
|
* @MEI_WDT_IDLE: wd is idle and not opened |
|
* @MEI_WDT_START: wd was opened, start was called |
|
* @MEI_WDT_RUNNING: wd is expecting keep alive pings |
|
* @MEI_WDT_STOPPING: wd is stopping and will move to IDLE |
|
* @MEI_WDT_NOT_REQUIRED: wd device is not required |
|
*/ |
|
enum mei_wdt_state { |
|
MEI_WDT_PROBE, |
|
MEI_WDT_IDLE, |
|
MEI_WDT_START, |
|
MEI_WDT_RUNNING, |
|
MEI_WDT_STOPPING, |
|
MEI_WDT_NOT_REQUIRED, |
|
}; |
|
|
|
static const char *mei_wdt_state_str(enum mei_wdt_state state) |
|
{ |
|
switch (state) { |
|
case MEI_WDT_PROBE: |
|
return "PROBE"; |
|
case MEI_WDT_IDLE: |
|
return "IDLE"; |
|
case MEI_WDT_START: |
|
return "START"; |
|
case MEI_WDT_RUNNING: |
|
return "RUNNING"; |
|
case MEI_WDT_STOPPING: |
|
return "STOPPING"; |
|
case MEI_WDT_NOT_REQUIRED: |
|
return "NOT_REQUIRED"; |
|
default: |
|
return "unknown"; |
|
} |
|
} |
|
|
|
/** |
|
* struct mei_wdt - mei watchdog driver |
|
* @wdd: watchdog device |
|
* |
|
* @cldev: mei watchdog client device |
|
* @state: watchdog internal state |
|
* @resp_required: ping required response |
|
* @response: ping response completion |
|
* @unregister: unregister worker |
|
* @reg_lock: watchdog device registration lock |
|
* @timeout: watchdog current timeout |
|
* |
|
* @dbgfs_dir: debugfs dir entry |
|
*/ |
|
struct mei_wdt { |
|
struct watchdog_device wdd; |
|
|
|
struct mei_cl_device *cldev; |
|
enum mei_wdt_state state; |
|
bool resp_required; |
|
struct completion response; |
|
struct work_struct unregister; |
|
struct mutex reg_lock; |
|
u16 timeout; |
|
|
|
#if IS_ENABLED(CONFIG_DEBUG_FS) |
|
struct dentry *dbgfs_dir; |
|
#endif /* CONFIG_DEBUG_FS */ |
|
}; |
|
|
|
/** |
|
* struct mei_mc_hdr - Management Control Command Header |
|
* |
|
* @command: Management Control (0x2) |
|
* @bytecount: Number of bytes in the message beyond this byte |
|
* @subcommand: Management Control Subcommand |
|
* @versionnumber: Management Control Version (0x10) |
|
*/ |
|
struct mei_mc_hdr { |
|
u8 command; |
|
u8 bytecount; |
|
u8 subcommand; |
|
u8 versionnumber; |
|
}; |
|
|
|
/** |
|
* struct mei_wdt_start_request - watchdog start/ping |
|
* |
|
* @hdr: Management Control Command Header |
|
* @timeout: timeout value |
|
* @reserved: reserved (legacy) |
|
*/ |
|
struct mei_wdt_start_request { |
|
struct mei_mc_hdr hdr; |
|
u16 timeout; |
|
u8 reserved[17]; |
|
} __packed; |
|
|
|
/** |
|
* struct mei_wdt_start_response - watchdog start/ping response |
|
* |
|
* @hdr: Management Control Command Header |
|
* @status: operation status |
|
* @wdstate: watchdog status bit mask |
|
*/ |
|
struct mei_wdt_start_response { |
|
struct mei_mc_hdr hdr; |
|
u8 status; |
|
u8 wdstate; |
|
} __packed; |
|
|
|
/** |
|
* struct mei_wdt_stop_request - watchdog stop |
|
* |
|
* @hdr: Management Control Command Header |
|
*/ |
|
struct mei_wdt_stop_request { |
|
struct mei_mc_hdr hdr; |
|
} __packed; |
|
|
|
/** |
|
* mei_wdt_ping - send wd start/ping command |
|
* |
|
* @wdt: mei watchdog device |
|
* |
|
* Return: 0 on success, |
|
* negative errno code on failure |
|
*/ |
|
static int mei_wdt_ping(struct mei_wdt *wdt) |
|
{ |
|
struct mei_wdt_start_request req; |
|
const size_t req_len = sizeof(req); |
|
int ret; |
|
|
|
memset(&req, 0, req_len); |
|
req.hdr.command = MEI_MANAGEMENT_CONTROL; |
|
req.hdr.bytecount = req_len - offsetof(struct mei_mc_hdr, subcommand); |
|
req.hdr.subcommand = MEI_MC_START_WD_TIMER_REQ; |
|
req.hdr.versionnumber = MEI_MC_VERSION_NUMBER; |
|
req.timeout = wdt->timeout; |
|
|
|
ret = mei_cldev_send(wdt->cldev, (u8 *)&req, req_len); |
|
if (ret < 0) |
|
return ret; |
|
|
|
return 0; |
|
} |
|
|
|
/** |
|
* mei_wdt_stop - send wd stop command |
|
* |
|
* @wdt: mei watchdog device |
|
* |
|
* Return: 0 on success, |
|
* negative errno code on failure |
|
*/ |
|
static int mei_wdt_stop(struct mei_wdt *wdt) |
|
{ |
|
struct mei_wdt_stop_request req; |
|
const size_t req_len = sizeof(req); |
|
int ret; |
|
|
|
memset(&req, 0, req_len); |
|
req.hdr.command = MEI_MANAGEMENT_CONTROL; |
|
req.hdr.bytecount = req_len - offsetof(struct mei_mc_hdr, subcommand); |
|
req.hdr.subcommand = MEI_MC_STOP_WD_TIMER_REQ; |
|
req.hdr.versionnumber = MEI_MC_VERSION_NUMBER; |
|
|
|
ret = mei_cldev_send(wdt->cldev, (u8 *)&req, req_len); |
|
if (ret < 0) |
|
return ret; |
|
|
|
return 0; |
|
} |
|
|
|
/** |
|
* mei_wdt_ops_start - wd start command from the watchdog core. |
|
* |
|
* @wdd: watchdog device |
|
* |
|
* Return: 0 on success or -ENODEV; |
|
*/ |
|
static int mei_wdt_ops_start(struct watchdog_device *wdd) |
|
{ |
|
struct mei_wdt *wdt = watchdog_get_drvdata(wdd); |
|
|
|
wdt->state = MEI_WDT_START; |
|
wdd->timeout = wdt->timeout; |
|
return 0; |
|
} |
|
|
|
/** |
|
* mei_wdt_ops_stop - wd stop command from the watchdog core. |
|
* |
|
* @wdd: watchdog device |
|
* |
|
* Return: 0 if success, negative errno code for failure |
|
*/ |
|
static int mei_wdt_ops_stop(struct watchdog_device *wdd) |
|
{ |
|
struct mei_wdt *wdt = watchdog_get_drvdata(wdd); |
|
int ret; |
|
|
|
if (wdt->state != MEI_WDT_RUNNING) |
|
return 0; |
|
|
|
wdt->state = MEI_WDT_STOPPING; |
|
|
|
ret = mei_wdt_stop(wdt); |
|
if (ret) |
|
return ret; |
|
|
|
wdt->state = MEI_WDT_IDLE; |
|
|
|
return 0; |
|
} |
|
|
|
/** |
|
* mei_wdt_ops_ping - wd ping command from the watchdog core. |
|
* |
|
* @wdd: watchdog device |
|
* |
|
* Return: 0 if success, negative errno code on failure |
|
*/ |
|
static int mei_wdt_ops_ping(struct watchdog_device *wdd) |
|
{ |
|
struct mei_wdt *wdt = watchdog_get_drvdata(wdd); |
|
int ret; |
|
|
|
if (wdt->state != MEI_WDT_START && wdt->state != MEI_WDT_RUNNING) |
|
return 0; |
|
|
|
if (wdt->resp_required) |
|
init_completion(&wdt->response); |
|
|
|
wdt->state = MEI_WDT_RUNNING; |
|
ret = mei_wdt_ping(wdt); |
|
if (ret) |
|
return ret; |
|
|
|
if (wdt->resp_required) |
|
ret = wait_for_completion_killable(&wdt->response); |
|
|
|
return ret; |
|
} |
|
|
|
/** |
|
* mei_wdt_ops_set_timeout - wd set timeout command from the watchdog core. |
|
* |
|
* @wdd: watchdog device |
|
* @timeout: timeout value to set |
|
* |
|
* Return: 0 if success, negative errno code for failure |
|
*/ |
|
static int mei_wdt_ops_set_timeout(struct watchdog_device *wdd, |
|
unsigned int timeout) |
|
{ |
|
|
|
struct mei_wdt *wdt = watchdog_get_drvdata(wdd); |
|
|
|
/* valid value is already checked by the caller */ |
|
wdt->timeout = timeout; |
|
wdd->timeout = timeout; |
|
|
|
return 0; |
|
} |
|
|
|
static const struct watchdog_ops wd_ops = { |
|
.owner = THIS_MODULE, |
|
.start = mei_wdt_ops_start, |
|
.stop = mei_wdt_ops_stop, |
|
.ping = mei_wdt_ops_ping, |
|
.set_timeout = mei_wdt_ops_set_timeout, |
|
}; |
|
|
|
/* not const as the firmware_version field need to be retrieved */ |
|
static struct watchdog_info wd_info = { |
|
.identity = INTEL_AMT_WATCHDOG_ID, |
|
.options = WDIOF_KEEPALIVEPING | |
|
WDIOF_SETTIMEOUT | |
|
WDIOF_ALARMONLY, |
|
}; |
|
|
|
/** |
|
* __mei_wdt_is_registered - check if wdt is registered |
|
* |
|
* @wdt: mei watchdog device |
|
* |
|
* Return: true if the wdt is registered with the watchdog subsystem |
|
* Locking: should be called under wdt->reg_lock |
|
*/ |
|
static inline bool __mei_wdt_is_registered(struct mei_wdt *wdt) |
|
{ |
|
return !!watchdog_get_drvdata(&wdt->wdd); |
|
} |
|
|
|
/** |
|
* mei_wdt_unregister - unregister from the watchdog subsystem |
|
* |
|
* @wdt: mei watchdog device |
|
*/ |
|
static void mei_wdt_unregister(struct mei_wdt *wdt) |
|
{ |
|
mutex_lock(&wdt->reg_lock); |
|
|
|
if (__mei_wdt_is_registered(wdt)) { |
|
watchdog_unregister_device(&wdt->wdd); |
|
watchdog_set_drvdata(&wdt->wdd, NULL); |
|
memset(&wdt->wdd, 0, sizeof(wdt->wdd)); |
|
} |
|
|
|
mutex_unlock(&wdt->reg_lock); |
|
} |
|
|
|
/** |
|
* mei_wdt_register - register with the watchdog subsystem |
|
* |
|
* @wdt: mei watchdog device |
|
* |
|
* Return: 0 if success, negative errno code for failure |
|
*/ |
|
static int mei_wdt_register(struct mei_wdt *wdt) |
|
{ |
|
struct device *dev; |
|
int ret; |
|
|
|
if (!wdt || !wdt->cldev) |
|
return -EINVAL; |
|
|
|
dev = &wdt->cldev->dev; |
|
|
|
mutex_lock(&wdt->reg_lock); |
|
|
|
if (__mei_wdt_is_registered(wdt)) { |
|
ret = 0; |
|
goto out; |
|
} |
|
|
|
wdt->wdd.info = &wd_info; |
|
wdt->wdd.ops = &wd_ops; |
|
wdt->wdd.parent = dev; |
|
wdt->wdd.timeout = MEI_WDT_DEFAULT_TIMEOUT; |
|
wdt->wdd.min_timeout = MEI_WDT_MIN_TIMEOUT; |
|
wdt->wdd.max_timeout = MEI_WDT_MAX_TIMEOUT; |
|
|
|
watchdog_set_drvdata(&wdt->wdd, wdt); |
|
watchdog_stop_on_reboot(&wdt->wdd); |
|
watchdog_stop_on_unregister(&wdt->wdd); |
|
|
|
ret = watchdog_register_device(&wdt->wdd); |
|
if (ret) |
|
watchdog_set_drvdata(&wdt->wdd, NULL); |
|
|
|
wdt->state = MEI_WDT_IDLE; |
|
|
|
out: |
|
mutex_unlock(&wdt->reg_lock); |
|
return ret; |
|
} |
|
|
|
static void mei_wdt_unregister_work(struct work_struct *work) |
|
{ |
|
struct mei_wdt *wdt = container_of(work, struct mei_wdt, unregister); |
|
|
|
mei_wdt_unregister(wdt); |
|
} |
|
|
|
/** |
|
* mei_wdt_rx - callback for data receive |
|
* |
|
* @cldev: bus device |
|
*/ |
|
static void mei_wdt_rx(struct mei_cl_device *cldev) |
|
{ |
|
struct mei_wdt *wdt = mei_cldev_get_drvdata(cldev); |
|
struct mei_wdt_start_response res; |
|
const size_t res_len = sizeof(res); |
|
int ret; |
|
|
|
ret = mei_cldev_recv(wdt->cldev, (u8 *)&res, res_len); |
|
if (ret < 0) { |
|
dev_err(&cldev->dev, "failure in recv %d\n", ret); |
|
return; |
|
} |
|
|
|
/* Empty response can be sent on stop */ |
|
if (ret == 0) |
|
return; |
|
|
|
if (ret < sizeof(struct mei_mc_hdr)) { |
|
dev_err(&cldev->dev, "recv small data %d\n", ret); |
|
return; |
|
} |
|
|
|
if (res.hdr.command != MEI_MANAGEMENT_CONTROL || |
|
res.hdr.versionnumber != MEI_MC_VERSION_NUMBER) { |
|
dev_err(&cldev->dev, "wrong command received\n"); |
|
return; |
|
} |
|
|
|
if (res.hdr.subcommand != MEI_MC_START_WD_TIMER_RES) { |
|
dev_warn(&cldev->dev, "unsupported command %d :%s[%d]\n", |
|
res.hdr.subcommand, |
|
mei_wdt_state_str(wdt->state), |
|
wdt->state); |
|
return; |
|
} |
|
|
|
/* Run the unregistration in a worker as this can be |
|
* run only after ping completion, otherwise the flow will |
|
* deadlock on watchdog core mutex. |
|
*/ |
|
if (wdt->state == MEI_WDT_RUNNING) { |
|
if (res.wdstate & MEI_WDT_WDSTATE_NOT_REQUIRED) { |
|
wdt->state = MEI_WDT_NOT_REQUIRED; |
|
schedule_work(&wdt->unregister); |
|
} |
|
goto out; |
|
} |
|
|
|
if (wdt->state == MEI_WDT_PROBE) { |
|
if (res.wdstate & MEI_WDT_WDSTATE_NOT_REQUIRED) { |
|
wdt->state = MEI_WDT_NOT_REQUIRED; |
|
} else { |
|
/* stop the watchdog and register watchdog device */ |
|
mei_wdt_stop(wdt); |
|
mei_wdt_register(wdt); |
|
} |
|
return; |
|
} |
|
|
|
dev_warn(&cldev->dev, "not in correct state %s[%d]\n", |
|
mei_wdt_state_str(wdt->state), wdt->state); |
|
|
|
out: |
|
if (!completion_done(&wdt->response)) |
|
complete(&wdt->response); |
|
} |
|
|
|
/** |
|
* mei_wdt_notif - callback for event notification |
|
* |
|
* @cldev: bus device |
|
*/ |
|
static void mei_wdt_notif(struct mei_cl_device *cldev) |
|
{ |
|
struct mei_wdt *wdt = mei_cldev_get_drvdata(cldev); |
|
|
|
if (wdt->state != MEI_WDT_NOT_REQUIRED) |
|
return; |
|
|
|
mei_wdt_register(wdt); |
|
} |
|
|
|
#if IS_ENABLED(CONFIG_DEBUG_FS) |
|
|
|
static ssize_t mei_dbgfs_read_activation(struct file *file, char __user *ubuf, |
|
size_t cnt, loff_t *ppos) |
|
{ |
|
struct mei_wdt *wdt = file->private_data; |
|
const size_t bufsz = 32; |
|
char buf[32]; |
|
ssize_t pos; |
|
|
|
mutex_lock(&wdt->reg_lock); |
|
pos = scnprintf(buf, bufsz, "%s\n", |
|
__mei_wdt_is_registered(wdt) ? "activated" : "deactivated"); |
|
mutex_unlock(&wdt->reg_lock); |
|
|
|
return simple_read_from_buffer(ubuf, cnt, ppos, buf, pos); |
|
} |
|
|
|
static const struct file_operations dbgfs_fops_activation = { |
|
.open = simple_open, |
|
.read = mei_dbgfs_read_activation, |
|
.llseek = generic_file_llseek, |
|
}; |
|
|
|
static ssize_t mei_dbgfs_read_state(struct file *file, char __user *ubuf, |
|
size_t cnt, loff_t *ppos) |
|
{ |
|
struct mei_wdt *wdt = file->private_data; |
|
char buf[32]; |
|
ssize_t pos; |
|
|
|
pos = scnprintf(buf, sizeof(buf), "state: %s\n", |
|
mei_wdt_state_str(wdt->state)); |
|
|
|
return simple_read_from_buffer(ubuf, cnt, ppos, buf, pos); |
|
} |
|
|
|
static const struct file_operations dbgfs_fops_state = { |
|
.open = simple_open, |
|
.read = mei_dbgfs_read_state, |
|
.llseek = generic_file_llseek, |
|
}; |
|
|
|
static void dbgfs_unregister(struct mei_wdt *wdt) |
|
{ |
|
debugfs_remove_recursive(wdt->dbgfs_dir); |
|
wdt->dbgfs_dir = NULL; |
|
} |
|
|
|
static void dbgfs_register(struct mei_wdt *wdt) |
|
{ |
|
struct dentry *dir; |
|
|
|
dir = debugfs_create_dir(KBUILD_MODNAME, NULL); |
|
wdt->dbgfs_dir = dir; |
|
|
|
debugfs_create_file("state", S_IRUSR, dir, wdt, &dbgfs_fops_state); |
|
|
|
debugfs_create_file("activation", S_IRUSR, dir, wdt, |
|
&dbgfs_fops_activation); |
|
} |
|
|
|
#else |
|
|
|
static inline void dbgfs_unregister(struct mei_wdt *wdt) {} |
|
static inline void dbgfs_register(struct mei_wdt *wdt) {} |
|
#endif /* CONFIG_DEBUG_FS */ |
|
|
|
static int mei_wdt_probe(struct mei_cl_device *cldev, |
|
const struct mei_cl_device_id *id) |
|
{ |
|
struct mei_wdt *wdt; |
|
int ret; |
|
|
|
wdt = kzalloc(sizeof(struct mei_wdt), GFP_KERNEL); |
|
if (!wdt) |
|
return -ENOMEM; |
|
|
|
wdt->timeout = MEI_WDT_DEFAULT_TIMEOUT; |
|
wdt->state = MEI_WDT_PROBE; |
|
wdt->cldev = cldev; |
|
wdt->resp_required = mei_cldev_ver(cldev) > 0x1; |
|
mutex_init(&wdt->reg_lock); |
|
init_completion(&wdt->response); |
|
INIT_WORK(&wdt->unregister, mei_wdt_unregister_work); |
|
|
|
mei_cldev_set_drvdata(cldev, wdt); |
|
|
|
ret = mei_cldev_enable(cldev); |
|
if (ret < 0) { |
|
dev_err(&cldev->dev, "Could not enable cl device\n"); |
|
goto err_out; |
|
} |
|
|
|
ret = mei_cldev_register_rx_cb(wdt->cldev, mei_wdt_rx); |
|
if (ret) { |
|
dev_err(&cldev->dev, "Could not reg rx event ret=%d\n", ret); |
|
goto err_disable; |
|
} |
|
|
|
ret = mei_cldev_register_notif_cb(wdt->cldev, mei_wdt_notif); |
|
/* on legacy devices notification is not supported |
|
*/ |
|
if (ret && ret != -EOPNOTSUPP) { |
|
dev_err(&cldev->dev, "Could not reg notif event ret=%d\n", ret); |
|
goto err_disable; |
|
} |
|
|
|
wd_info.firmware_version = mei_cldev_ver(cldev); |
|
|
|
if (wdt->resp_required) |
|
ret = mei_wdt_ping(wdt); |
|
else |
|
ret = mei_wdt_register(wdt); |
|
|
|
if (ret) |
|
goto err_disable; |
|
|
|
dbgfs_register(wdt); |
|
|
|
return 0; |
|
|
|
err_disable: |
|
mei_cldev_disable(cldev); |
|
|
|
err_out: |
|
kfree(wdt); |
|
|
|
return ret; |
|
} |
|
|
|
static void mei_wdt_remove(struct mei_cl_device *cldev) |
|
{ |
|
struct mei_wdt *wdt = mei_cldev_get_drvdata(cldev); |
|
|
|
/* Free the caller in case of fw initiated or unexpected reset */ |
|
if (!completion_done(&wdt->response)) |
|
complete(&wdt->response); |
|
|
|
cancel_work_sync(&wdt->unregister); |
|
|
|
mei_wdt_unregister(wdt); |
|
|
|
mei_cldev_disable(cldev); |
|
|
|
dbgfs_unregister(wdt); |
|
|
|
kfree(wdt); |
|
} |
|
|
|
#define MEI_UUID_WD UUID_LE(0x05B79A6F, 0x4628, 0x4D7F, \ |
|
0x89, 0x9D, 0xA9, 0x15, 0x14, 0xCB, 0x32, 0xAB) |
|
|
|
static const struct mei_cl_device_id mei_wdt_tbl[] = { |
|
{ .uuid = MEI_UUID_WD, .version = MEI_CL_VERSION_ANY }, |
|
/* required last entry */ |
|
{ } |
|
}; |
|
MODULE_DEVICE_TABLE(mei, mei_wdt_tbl); |
|
|
|
static struct mei_cl_driver mei_wdt_driver = { |
|
.id_table = mei_wdt_tbl, |
|
.name = KBUILD_MODNAME, |
|
|
|
.probe = mei_wdt_probe, |
|
.remove = mei_wdt_remove, |
|
}; |
|
|
|
module_mei_cl_driver(mei_wdt_driver); |
|
|
|
MODULE_AUTHOR("Intel Corporation"); |
|
MODULE_LICENSE("GPL v2"); |
|
MODULE_DESCRIPTION("Device driver for Intel MEI iAMT watchdog");
|
|
|