diff options
Diffstat (limited to 'net/bluetooth/coredump.c')
| -rw-r--r-- | net/bluetooth/coredump.c | 536 | 
1 files changed, 536 insertions, 0 deletions
diff --git a/net/bluetooth/coredump.c b/net/bluetooth/coredump.c new file mode 100644 index 000000000000..d2d2624ec708 --- /dev/null +++ b/net/bluetooth/coredump.c @@ -0,0 +1,536 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2023 Google Corporation + */ + +#include <linux/devcoredump.h> + +#include <asm/unaligned.h> +#include <net/bluetooth/bluetooth.h> +#include <net/bluetooth/hci_core.h> + +enum hci_devcoredump_pkt_type { +	HCI_DEVCOREDUMP_PKT_INIT, +	HCI_DEVCOREDUMP_PKT_SKB, +	HCI_DEVCOREDUMP_PKT_PATTERN, +	HCI_DEVCOREDUMP_PKT_COMPLETE, +	HCI_DEVCOREDUMP_PKT_ABORT, +}; + +struct hci_devcoredump_skb_cb { +	u16 pkt_type; +}; + +struct hci_devcoredump_skb_pattern { +	u8 pattern; +	u32 len; +} __packed; + +#define hci_dmp_cb(skb)	((struct hci_devcoredump_skb_cb *)((skb)->cb)) + +#define DBG_UNEXPECTED_STATE() \ +	bt_dev_dbg(hdev, \ +		   "Unexpected packet (%d) for state (%d). ", \ +		   hci_dmp_cb(skb)->pkt_type, hdev->dump.state) + +#define MAX_DEVCOREDUMP_HDR_SIZE	512	/* bytes */ + +static int hci_devcd_update_hdr_state(char *buf, size_t size, int state) +{ +	int len = 0; + +	if (!buf) +		return 0; + +	len = scnprintf(buf, size, "Bluetooth devcoredump\nState: %d\n", state); + +	return len + 1; /* scnprintf adds \0 at the end upon state rewrite */ +} + +/* Call with hci_dev_lock only. */ +static int hci_devcd_update_state(struct hci_dev *hdev, int state) +{ +	bt_dev_dbg(hdev, "Updating devcoredump state from %d to %d.", +		   hdev->dump.state, state); + +	hdev->dump.state = state; + +	return hci_devcd_update_hdr_state(hdev->dump.head, +					  hdev->dump.alloc_size, state); +} + +static int hci_devcd_mkheader(struct hci_dev *hdev, struct sk_buff *skb) +{ +	char dump_start[] = "--- Start dump ---\n"; +	char hdr[80]; +	int hdr_len; + +	hdr_len = hci_devcd_update_hdr_state(hdr, sizeof(hdr), +					     HCI_DEVCOREDUMP_IDLE); +	skb_put_data(skb, hdr, hdr_len); + +	if (hdev->dump.dmp_hdr) +		hdev->dump.dmp_hdr(hdev, skb); + +	skb_put_data(skb, dump_start, strlen(dump_start)); + +	return skb->len; +} + +/* Do not call with hci_dev_lock since this calls driver code. */ +static void hci_devcd_notify(struct hci_dev *hdev, int state) +{ +	if (hdev->dump.notify_change) +		hdev->dump.notify_change(hdev, state); +} + +/* Call with hci_dev_lock only. */ +void hci_devcd_reset(struct hci_dev *hdev) +{ +	hdev->dump.head = NULL; +	hdev->dump.tail = NULL; +	hdev->dump.alloc_size = 0; + +	hci_devcd_update_state(hdev, HCI_DEVCOREDUMP_IDLE); + +	cancel_delayed_work(&hdev->dump.dump_timeout); +	skb_queue_purge(&hdev->dump.dump_q); +} + +/* Call with hci_dev_lock only. */ +static void hci_devcd_free(struct hci_dev *hdev) +{ +	if (hdev->dump.head) +		vfree(hdev->dump.head); + +	hci_devcd_reset(hdev); +} + +/* Call with hci_dev_lock only. */ +static int hci_devcd_alloc(struct hci_dev *hdev, u32 size) +{ +	hdev->dump.head = vmalloc(size); +	if (!hdev->dump.head) +		return -ENOMEM; + +	hdev->dump.alloc_size = size; +	hdev->dump.tail = hdev->dump.head; +	hdev->dump.end = hdev->dump.head + size; + +	hci_devcd_update_state(hdev, HCI_DEVCOREDUMP_IDLE); + +	return 0; +} + +/* Call with hci_dev_lock only. */ +static bool hci_devcd_copy(struct hci_dev *hdev, char *buf, u32 size) +{ +	if (hdev->dump.tail + size > hdev->dump.end) +		return false; + +	memcpy(hdev->dump.tail, buf, size); +	hdev->dump.tail += size; + +	return true; +} + +/* Call with hci_dev_lock only. */ +static bool hci_devcd_memset(struct hci_dev *hdev, u8 pattern, u32 len) +{ +	if (hdev->dump.tail + len > hdev->dump.end) +		return false; + +	memset(hdev->dump.tail, pattern, len); +	hdev->dump.tail += len; + +	return true; +} + +/* Call with hci_dev_lock only. */ +static int hci_devcd_prepare(struct hci_dev *hdev, u32 dump_size) +{ +	struct sk_buff *skb; +	int dump_hdr_size; +	int err = 0; + +	skb = alloc_skb(MAX_DEVCOREDUMP_HDR_SIZE, GFP_ATOMIC); +	if (!skb) +		return -ENOMEM; + +	dump_hdr_size = hci_devcd_mkheader(hdev, skb); + +	if (hci_devcd_alloc(hdev, dump_hdr_size + dump_size)) { +		err = -ENOMEM; +		goto hdr_free; +	} + +	/* Insert the device header */ +	if (!hci_devcd_copy(hdev, skb->data, skb->len)) { +		bt_dev_err(hdev, "Failed to insert header"); +		hci_devcd_free(hdev); + +		err = -ENOMEM; +		goto hdr_free; +	} + +hdr_free: +	kfree_skb(skb); + +	return err; +} + +static void hci_devcd_handle_pkt_init(struct hci_dev *hdev, struct sk_buff *skb) +{ +	u32 dump_size; + +	if (hdev->dump.state != HCI_DEVCOREDUMP_IDLE) { +		DBG_UNEXPECTED_STATE(); +		return; +	} + +	if (skb->len != sizeof(dump_size)) { +		bt_dev_dbg(hdev, "Invalid dump init pkt"); +		return; +	} + +	dump_size = get_unaligned_le32(skb_pull_data(skb, 4)); +	if (!dump_size) { +		bt_dev_err(hdev, "Zero size dump init pkt"); +		return; +	} + +	if (hci_devcd_prepare(hdev, dump_size)) { +		bt_dev_err(hdev, "Failed to prepare for dump"); +		return; +	} + +	hci_devcd_update_state(hdev, HCI_DEVCOREDUMP_ACTIVE); +	queue_delayed_work(hdev->workqueue, &hdev->dump.dump_timeout, +			   hdev->dump.timeout); +} + +static void hci_devcd_handle_pkt_skb(struct hci_dev *hdev, struct sk_buff *skb) +{ +	if (hdev->dump.state != HCI_DEVCOREDUMP_ACTIVE) { +		DBG_UNEXPECTED_STATE(); +		return; +	} + +	if (!hci_devcd_copy(hdev, skb->data, skb->len)) +		bt_dev_dbg(hdev, "Failed to insert skb"); +} + +static void hci_devcd_handle_pkt_pattern(struct hci_dev *hdev, +					 struct sk_buff *skb) +{ +	struct hci_devcoredump_skb_pattern *pattern; + +	if (hdev->dump.state != HCI_DEVCOREDUMP_ACTIVE) { +		DBG_UNEXPECTED_STATE(); +		return; +	} + +	if (skb->len != sizeof(*pattern)) { +		bt_dev_dbg(hdev, "Invalid pattern skb"); +		return; +	} + +	pattern = skb_pull_data(skb, sizeof(*pattern)); + +	if (!hci_devcd_memset(hdev, pattern->pattern, pattern->len)) +		bt_dev_dbg(hdev, "Failed to set pattern"); +} + +static void hci_devcd_handle_pkt_complete(struct hci_dev *hdev, +					  struct sk_buff *skb) +{ +	u32 dump_size; + +	if (hdev->dump.state != HCI_DEVCOREDUMP_ACTIVE) { +		DBG_UNEXPECTED_STATE(); +		return; +	} + +	hci_devcd_update_state(hdev, HCI_DEVCOREDUMP_DONE); +	dump_size = hdev->dump.tail - hdev->dump.head; + +	bt_dev_dbg(hdev, "complete with size %u (expect %zu)", dump_size, +		   hdev->dump.alloc_size); + +	dev_coredumpv(&hdev->dev, hdev->dump.head, dump_size, GFP_KERNEL); +} + +static void hci_devcd_handle_pkt_abort(struct hci_dev *hdev, +				       struct sk_buff *skb) +{ +	u32 dump_size; + +	if (hdev->dump.state != HCI_DEVCOREDUMP_ACTIVE) { +		DBG_UNEXPECTED_STATE(); +		return; +	} + +	hci_devcd_update_state(hdev, HCI_DEVCOREDUMP_ABORT); +	dump_size = hdev->dump.tail - hdev->dump.head; + +	bt_dev_dbg(hdev, "aborted with size %u (expect %zu)", dump_size, +		   hdev->dump.alloc_size); + +	/* Emit a devcoredump with the available data */ +	dev_coredumpv(&hdev->dev, hdev->dump.head, dump_size, GFP_KERNEL); +} + +/* Bluetooth devcoredump state machine. + * + * Devcoredump states: + * + *      HCI_DEVCOREDUMP_IDLE: The default state. + * + *      HCI_DEVCOREDUMP_ACTIVE: A devcoredump will be in this state once it has + *              been initialized using hci_devcd_init(). Once active, the driver + *              can append data using hci_devcd_append() or insert a pattern + *              using hci_devcd_append_pattern(). + * + *      HCI_DEVCOREDUMP_DONE: Once the dump collection is complete, the drive + *              can signal the completion using hci_devcd_complete(). A + *              devcoredump is generated indicating the completion event and + *              then the state machine is reset to the default state. + * + *      HCI_DEVCOREDUMP_ABORT: The driver can cancel ongoing dump collection in + *              case of any error using hci_devcd_abort(). A devcoredump is + *              still generated with the available data indicating the abort + *              event and then the state machine is reset to the default state. + * + *      HCI_DEVCOREDUMP_TIMEOUT: A timeout timer for HCI_DEVCOREDUMP_TIMEOUT sec + *              is started during devcoredump initialization. Once the timeout + *              occurs, the driver is notified, a devcoredump is generated with + *              the available data indicating the timeout event and then the + *              state machine is reset to the default state. + * + * The driver must register using hci_devcd_register() before using the hci + * devcoredump APIs. + */ +void hci_devcd_rx(struct work_struct *work) +{ +	struct hci_dev *hdev = container_of(work, struct hci_dev, dump.dump_rx); +	struct sk_buff *skb; +	int start_state; + +	while ((skb = skb_dequeue(&hdev->dump.dump_q))) { +		/* Return if timeout occurs. The timeout handler function +		 * hci_devcd_timeout() will report the available dump data. +		 */ +		if (hdev->dump.state == HCI_DEVCOREDUMP_TIMEOUT) { +			kfree_skb(skb); +			return; +		} + +		hci_dev_lock(hdev); +		start_state = hdev->dump.state; + +		switch (hci_dmp_cb(skb)->pkt_type) { +		case HCI_DEVCOREDUMP_PKT_INIT: +			hci_devcd_handle_pkt_init(hdev, skb); +			break; + +		case HCI_DEVCOREDUMP_PKT_SKB: +			hci_devcd_handle_pkt_skb(hdev, skb); +			break; + +		case HCI_DEVCOREDUMP_PKT_PATTERN: +			hci_devcd_handle_pkt_pattern(hdev, skb); +			break; + +		case HCI_DEVCOREDUMP_PKT_COMPLETE: +			hci_devcd_handle_pkt_complete(hdev, skb); +			break; + +		case HCI_DEVCOREDUMP_PKT_ABORT: +			hci_devcd_handle_pkt_abort(hdev, skb); +			break; + +		default: +			bt_dev_dbg(hdev, "Unknown packet (%d) for state (%d). ", +				   hci_dmp_cb(skb)->pkt_type, hdev->dump.state); +			break; +		} + +		hci_dev_unlock(hdev); +		kfree_skb(skb); + +		/* Notify the driver about any state changes before resetting +		 * the state machine +		 */ +		if (start_state != hdev->dump.state) +			hci_devcd_notify(hdev, hdev->dump.state); + +		/* Reset the state machine if the devcoredump is complete */ +		hci_dev_lock(hdev); +		if (hdev->dump.state == HCI_DEVCOREDUMP_DONE || +		    hdev->dump.state == HCI_DEVCOREDUMP_ABORT) +			hci_devcd_reset(hdev); +		hci_dev_unlock(hdev); +	} +} +EXPORT_SYMBOL(hci_devcd_rx); + +void hci_devcd_timeout(struct work_struct *work) +{ +	struct hci_dev *hdev = container_of(work, struct hci_dev, +					    dump.dump_timeout.work); +	u32 dump_size; + +	hci_devcd_notify(hdev, HCI_DEVCOREDUMP_TIMEOUT); + +	hci_dev_lock(hdev); + +	cancel_work(&hdev->dump.dump_rx); + +	hci_devcd_update_state(hdev, HCI_DEVCOREDUMP_TIMEOUT); + +	dump_size = hdev->dump.tail - hdev->dump.head; +	bt_dev_dbg(hdev, "timeout with size %u (expect %zu)", dump_size, +		   hdev->dump.alloc_size); + +	/* Emit a devcoredump with the available data */ +	dev_coredumpv(&hdev->dev, hdev->dump.head, dump_size, GFP_KERNEL); + +	hci_devcd_reset(hdev); + +	hci_dev_unlock(hdev); +} +EXPORT_SYMBOL(hci_devcd_timeout); + +int hci_devcd_register(struct hci_dev *hdev, coredump_t coredump, +		       dmp_hdr_t dmp_hdr, notify_change_t notify_change) +{ +	/* Driver must implement coredump() and dmp_hdr() functions for +	 * bluetooth devcoredump. The coredump() should trigger a coredump +	 * event on the controller when the device's coredump sysfs entry is +	 * written to. The dmp_hdr() should create a dump header to identify +	 * the controller/fw/driver info. +	 */ +	if (!coredump || !dmp_hdr) +		return -EINVAL; + +	hci_dev_lock(hdev); +	hdev->dump.coredump = coredump; +	hdev->dump.dmp_hdr = dmp_hdr; +	hdev->dump.notify_change = notify_change; +	hdev->dump.supported = true; +	hdev->dump.timeout = DEVCOREDUMP_TIMEOUT; +	hci_dev_unlock(hdev); + +	return 0; +} +EXPORT_SYMBOL(hci_devcd_register); + +static inline bool hci_devcd_enabled(struct hci_dev *hdev) +{ +	return hdev->dump.supported; +} + +int hci_devcd_init(struct hci_dev *hdev, u32 dump_size) +{ +	struct sk_buff *skb; + +	if (!hci_devcd_enabled(hdev)) +		return -EOPNOTSUPP; + +	skb = alloc_skb(sizeof(dump_size), GFP_ATOMIC); +	if (!skb) +		return -ENOMEM; + +	hci_dmp_cb(skb)->pkt_type = HCI_DEVCOREDUMP_PKT_INIT; +	put_unaligned_le32(dump_size, skb_put(skb, 4)); + +	skb_queue_tail(&hdev->dump.dump_q, skb); +	queue_work(hdev->workqueue, &hdev->dump.dump_rx); + +	return 0; +} +EXPORT_SYMBOL(hci_devcd_init); + +int hci_devcd_append(struct hci_dev *hdev, struct sk_buff *skb) +{ +	if (!skb) +		return -ENOMEM; + +	if (!hci_devcd_enabled(hdev)) { +		kfree_skb(skb); +		return -EOPNOTSUPP; +	} + +	hci_dmp_cb(skb)->pkt_type = HCI_DEVCOREDUMP_PKT_SKB; + +	skb_queue_tail(&hdev->dump.dump_q, skb); +	queue_work(hdev->workqueue, &hdev->dump.dump_rx); + +	return 0; +} +EXPORT_SYMBOL(hci_devcd_append); + +int hci_devcd_append_pattern(struct hci_dev *hdev, u8 pattern, u32 len) +{ +	struct hci_devcoredump_skb_pattern p; +	struct sk_buff *skb; + +	if (!hci_devcd_enabled(hdev)) +		return -EOPNOTSUPP; + +	skb = alloc_skb(sizeof(p), GFP_ATOMIC); +	if (!skb) +		return -ENOMEM; + +	p.pattern = pattern; +	p.len = len; + +	hci_dmp_cb(skb)->pkt_type = HCI_DEVCOREDUMP_PKT_PATTERN; +	skb_put_data(skb, &p, sizeof(p)); + +	skb_queue_tail(&hdev->dump.dump_q, skb); +	queue_work(hdev->workqueue, &hdev->dump.dump_rx); + +	return 0; +} +EXPORT_SYMBOL(hci_devcd_append_pattern); + +int hci_devcd_complete(struct hci_dev *hdev) +{ +	struct sk_buff *skb; + +	if (!hci_devcd_enabled(hdev)) +		return -EOPNOTSUPP; + +	skb = alloc_skb(0, GFP_ATOMIC); +	if (!skb) +		return -ENOMEM; + +	hci_dmp_cb(skb)->pkt_type = HCI_DEVCOREDUMP_PKT_COMPLETE; + +	skb_queue_tail(&hdev->dump.dump_q, skb); +	queue_work(hdev->workqueue, &hdev->dump.dump_rx); + +	return 0; +} +EXPORT_SYMBOL(hci_devcd_complete); + +int hci_devcd_abort(struct hci_dev *hdev) +{ +	struct sk_buff *skb; + +	if (!hci_devcd_enabled(hdev)) +		return -EOPNOTSUPP; + +	skb = alloc_skb(0, GFP_ATOMIC); +	if (!skb) +		return -ENOMEM; + +	hci_dmp_cb(skb)->pkt_type = HCI_DEVCOREDUMP_PKT_ABORT; + +	skb_queue_tail(&hdev->dump.dump_q, skb); +	queue_work(hdev->workqueue, &hdev->dump.dump_rx); + +	return 0; +} +EXPORT_SYMBOL(hci_devcd_abort);  |