| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * Copyright (C) 2023 Google Corporation | |
| */ | |
| #include <linux/devcoredump.h> | |
| #include <linux/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 %s.", \ | |
| hci_dmp_cb(skb)->pkt_type, \ | |
| hci_devcd_state_name(hdev->dump.state)) | |
| 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 %s to %s.", | |
| hci_devcd_state_name(hdev->dump.state), | |
| hci_devcd_state_name(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 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, HCI_DEVCD_HDR_END_MARKER, strlen(HCI_DEVCD_HDR_END_MARKER)); | |
| 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) | |
| { | |
| vfree(hdev->dump.head); | |
| hci_devcd_reset(hdev); | |
| } | |
| void hci_devcd_shutdown(struct hci_dev *hdev) | |
| { | |
| unsigned long flags; | |
| spin_lock_irqsave(&hdev->dump.dump_q.lock, flags); | |
| hdev->dump.supported = false; | |
| spin_unlock_irqrestore(&hdev->dump.dump_q.lock, flags); | |
| disable_work_sync(&hdev->dump.dump_rx); | |
| disable_delayed_work_sync(&hdev->dump.dump_timeout); | |
| hci_dev_lock(hdev); | |
| hci_devcd_free(hdev); | |
| hci_dev_unlock(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(HCI_DEVCD_HDR_SIZE_MAX, 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_dump(struct hci_dev *hdev) | |
| { | |
| struct sk_buff *skb; | |
| u32 size; | |
| bt_dev_dbg(hdev, "state %s", hci_devcd_state_name(hdev->dump.state)); | |
| size = hdev->dump.tail - hdev->dump.head; | |
| /* Send a copy to monitor as a diagnostic packet */ | |
| skb = bt_skb_alloc(size, GFP_ATOMIC); | |
| if (skb) { | |
| skb_put_data(skb, hdev->dump.head, size); | |
| hci_recv_diag(hdev, skb); | |
| } | |
| /* Emit a devcoredump with the available data */ | |
| dev_coredumpv(&hdev->dev, hdev->dump.head, size, GFP_KERNEL); | |
| } | |
| 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); | |
| hci_devcd_dump(hdev); | |
| } | |
| 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); | |
| hci_devcd_dump(hdev); | |
| } | |
| /* 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 %s.", | |
| hci_dmp_cb(skb)->pkt_type, | |
| hci_devcd_state_name(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); | |
| } | |
| } | |
| 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); | |
| hci_devcd_dump(hdev); | |
| hci_devcd_reset(hdev); | |
| hci_dev_unlock(hdev); | |
| } | |
| 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 READ_ONCE(hdev->dump.supported); | |
| } | |
| static int hci_devcd_queue(struct hci_dev *hdev, struct sk_buff *skb) | |
| { | |
| unsigned long flags; | |
| int err = 0; | |
| spin_lock_irqsave(&hdev->dump.dump_q.lock, flags); | |
| if (!hdev->dump.supported) | |
| err = -EOPNOTSUPP; | |
| else | |
| __skb_queue_tail(&hdev->dump.dump_q, skb); | |
| spin_unlock_irqrestore(&hdev->dump.dump_q.lock, flags); | |
| if (err) { | |
| kfree_skb(skb); | |
| return err; | |
| } | |
| queue_work(hdev->workqueue, &hdev->dump.dump_rx); | |
| return 0; | |
| } | |
| 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)); | |
| return hci_devcd_queue(hdev, skb); | |
| } | |
| 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; | |
| return hci_devcd_queue(hdev, skb); | |
| } | |
| 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)); | |
| return hci_devcd_queue(hdev, skb); | |
| } | |
| 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; | |
| return hci_devcd_queue(hdev, skb); | |
| } | |
| 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; | |
| return hci_devcd_queue(hdev, skb); | |
| } | |
| EXPORT_SYMBOL(hci_devcd_abort); | |
| const char *hci_devcd_state_name(enum devcoredump_state state) | |
| { | |
| const char *state_name = "Unknown"; | |
| switch (state) { | |
| case HCI_DEVCOREDUMP_IDLE: | |
| state_name = "IDLE"; | |
| break; | |
| case HCI_DEVCOREDUMP_ACTIVE: | |
| state_name = "ACTIVE"; | |
| break; | |
| case HCI_DEVCOREDUMP_DONE: | |
| state_name = "DONE"; | |
| break; | |
| case HCI_DEVCOREDUMP_ABORT: | |
| state_name = "ABORT"; | |
| break; | |
| case HCI_DEVCOREDUMP_TIMEOUT: | |
| state_name = "TIMEOUT"; | |
| break; | |
| default: | |
| break; | |
| } | |
| return state_name; | |
| } | |
| EXPORT_SYMBOL(hci_devcd_state_name); |