--- /dev/null
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2022 Google Corporation
+ */
+
+#ifndef __COREDUMP_H
+#define __COREDUMP_H
+
+#define DEVCOREDUMP_TIMEOUT    msecs_to_jiffies(10000) /* 10 sec */
+
+typedef void (*coredump_t)(struct hci_dev *hdev);
+typedef void (*dmp_hdr_t)(struct hci_dev *hdev, struct sk_buff *skb);
+typedef void (*notify_change_t)(struct hci_dev *hdev, int state);
+
+/* struct hci_devcoredump - Devcoredump state
+ *
+ * @supported: Indicates if FW dump collection is supported by driver
+ * @state: Current state of dump collection
+ * @timeout: Indicates a timeout for collecting the devcoredump
+ *
+ * @alloc_size: Total size of the dump
+ * @head: Start of the dump
+ * @tail: Pointer to current end of dump
+ * @end: head + alloc_size for easy comparisons
+ *
+ * @dump_q: Dump queue for state machine to process
+ * @dump_rx: Devcoredump state machine work
+ * @dump_timeout: Devcoredump timeout work
+ *
+ * @coredump: Called from the driver's .coredump() function.
+ * @dmp_hdr: Create a dump header to identify controller/fw/driver info
+ * @notify_change: Notify driver when devcoredump state has changed
+ */
+struct hci_devcoredump {
+       bool            supported;
+
+       enum devcoredump_state {
+               HCI_DEVCOREDUMP_IDLE,
+               HCI_DEVCOREDUMP_ACTIVE,
+               HCI_DEVCOREDUMP_DONE,
+               HCI_DEVCOREDUMP_ABORT,
+               HCI_DEVCOREDUMP_TIMEOUT,
+       } state;
+
+       unsigned long   timeout;
+
+       size_t          alloc_size;
+       char            *head;
+       char            *tail;
+       char            *end;
+
+       struct sk_buff_head     dump_q;
+       struct work_struct      dump_rx;
+       struct delayed_work     dump_timeout;
+
+       coredump_t              coredump;
+       dmp_hdr_t               dmp_hdr;
+       notify_change_t         notify_change;
+};
+
+#ifdef CONFIG_DEV_COREDUMP
+
+void hci_devcd_reset(struct hci_dev *hdev);
+void hci_devcd_rx(struct work_struct *work);
+void hci_devcd_timeout(struct work_struct *work);
+
+int hci_devcd_register(struct hci_dev *hdev, coredump_t coredump,
+                      dmp_hdr_t dmp_hdr, notify_change_t notify_change);
+int hci_devcd_init(struct hci_dev *hdev, u32 dump_size);
+int hci_devcd_append(struct hci_dev *hdev, struct sk_buff *skb);
+int hci_devcd_append_pattern(struct hci_dev *hdev, u8 pattern, u32 len);
+int hci_devcd_complete(struct hci_dev *hdev);
+int hci_devcd_abort(struct hci_dev *hdev);
+
+#else
+
+static inline void hci_devcd_reset(struct hci_dev *hdev) {}
+static inline void hci_devcd_rx(struct work_struct *work) {}
+static inline void hci_devcd_timeout(struct work_struct *work) {}
+
+static inline int hci_devcd_register(struct hci_dev *hdev, coredump_t coredump,
+                                    dmp_hdr_t dmp_hdr,
+                                    notify_change_t notify_change)
+{
+       return -EOPNOTSUPP;
+}
+
+static inline int hci_devcd_init(struct hci_dev *hdev, u32 dump_size)
+{
+       return -EOPNOTSUPP;
+}
+
+static inline int hci_devcd_append(struct hci_dev *hdev, struct sk_buff *skb)
+{
+       return -EOPNOTSUPP;
+}
+
+static inline int hci_devcd_append_pattern(struct hci_dev *hdev,
+                                          u8 pattern, u32 len)
+{
+       return -EOPNOTSUPP;
+}
+
+static inline int hci_devcd_complete(struct hci_dev *hdev)
+{
+       return -EOPNOTSUPP;
+}
+
+static inline int hci_devcd_abort(struct hci_dev *hdev)
+{
+       return -EOPNOTSUPP;
+}
+
+#endif /* CONFIG_DEV_COREDUMP */
+
+#endif /* __COREDUMP_H */
 
--- /dev/null
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2023 Google Corporation
+ */
+
+#include <linux/devcoredump.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 = skb_pull_data(skb, sizeof(*dump_size));
+       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;
+       skb_put_data(skb, &dump_size, sizeof(dump_size));
+
+       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);