| // SPDX-License-Identifier: LGPL-2.1-or-later |
| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2022 Intel Corporation. All rights reserved. |
| * |
| */ |
| |
| #define _GNU_SOURCE |
| #include <inttypes.h> |
| #include <string.h> |
| #include <stdlib.h> |
| #include <stdbool.h> |
| #include <unistd.h> |
| #include <errno.h> |
| |
| #include <glib.h> |
| |
| #include "bluetooth/bluetooth.h" |
| #include "bluetooth/uuid.h" |
| |
| #include "src/shared/queue.h" |
| #include "src/shared/util.h" |
| #include "src/shared/timeout.h" |
| #include "src/shared/att.h" |
| #include "src/shared/gatt-db.h" |
| #include "src/shared/gatt-server.h" |
| #include "src/shared/gatt-client.h" |
| #include "src/shared/crypto.h" |
| #include "src/shared/csip.h" |
| |
| #define DBG(_csip, fmt, arg...) \ |
| csip_debug(_csip, "%s:%s() " fmt, __FILE__, __func__, ## arg) |
| |
| /* SIRK is now hardcoded in the code. This can be moved |
| * to a configuration file. Since the code is to validate |
| * the CSIP use case of set member |
| */ |
| #define SIRK "761FAE703ED681F0C50B34155B6434FB" |
| #define CSIS_SIZE 0x02 |
| #define CSIS_LOCK 0x01 |
| #define CSIS_RANK 0x01 |
| #define CSIS_PLAINTEXT 0x01 |
| #define CSIS_ENC 0x02 |
| |
| struct bt_csip_db { |
| struct gatt_db *db; |
| struct bt_csis *csis; |
| }; |
| |
| struct csis_sirk { |
| uint8_t type; |
| uint8_t val[16]; |
| } __packed; |
| |
| struct bt_csis { |
| struct bt_csip_db *cdb; |
| struct csis_sirk *sirk_val; |
| uint8_t size_val; |
| uint8_t lock_val; |
| uint8_t rank_val; |
| struct gatt_db_attribute *service; |
| struct gatt_db_attribute *sirk; |
| struct gatt_db_attribute *size; |
| struct gatt_db_attribute *lock; |
| struct gatt_db_attribute *lock_ccc; |
| struct gatt_db_attribute *rank; |
| bt_csip_encrypt_func_t encrypt; |
| }; |
| |
| struct bt_csip_cb { |
| unsigned int id; |
| bt_csip_func_t attached; |
| bt_csip_func_t detached; |
| void *user_data; |
| }; |
| |
| struct bt_csip_ready { |
| unsigned int id; |
| bt_csip_ready_func_t func; |
| bt_csip_destroy_func_t destroy; |
| void *data; |
| }; |
| |
| struct bt_csip { |
| int ref_count; |
| struct bt_csip_db *ldb; |
| struct bt_csip_db *rdb; |
| struct bt_gatt_client *client; |
| struct bt_att *att; |
| |
| unsigned int idle_id; |
| struct queue *ready_cbs; |
| |
| bt_csip_debug_func_t debug_func; |
| bt_csip_destroy_func_t debug_destroy; |
| void *debug_data; |
| |
| bt_csip_sirk_func_t sirk_func; |
| void *sirk_data; |
| |
| void *user_data; |
| }; |
| |
| static struct queue *csip_db; |
| static struct queue *csip_cbs; |
| static struct queue *sessions; |
| |
| static void csip_detached(void *data, void *user_data) |
| { |
| struct bt_csip_cb *cb = data; |
| struct bt_csip *csip = user_data; |
| |
| cb->detached(csip, cb->user_data); |
| } |
| |
| void bt_csip_detach(struct bt_csip *csip) |
| { |
| if (!queue_remove(sessions, csip)) |
| return; |
| |
| bt_gatt_client_idle_unregister(csip->client, csip->idle_id); |
| |
| bt_gatt_client_unref(csip->client); |
| csip->client = NULL; |
| |
| queue_foreach(csip_cbs, csip_detached, csip); |
| } |
| |
| static void csis_free(struct bt_csis *csis) |
| { |
| if (!csis) |
| return; |
| |
| free(csis->sirk_val); |
| free(csis); |
| } |
| |
| static void csip_db_free(void *data) |
| { |
| struct bt_csip_db *cdb = data; |
| |
| if (!cdb) |
| return; |
| |
| gatt_db_unref(cdb->db); |
| |
| csis_free(cdb->csis); |
| free(cdb); |
| } |
| |
| static void csip_ready_free(void *data) |
| { |
| struct bt_csip_ready *ready = data; |
| |
| if (ready->destroy) |
| ready->destroy(ready->data); |
| |
| free(ready); |
| } |
| |
| static void csip_free(void *data) |
| { |
| struct bt_csip *csip = data; |
| |
| bt_csip_detach(csip); |
| |
| csip_db_free(csip->rdb); |
| |
| queue_destroy(csip->ready_cbs, csip_ready_free); |
| |
| free(csip); |
| } |
| |
| struct bt_att *bt_csip_get_att(struct bt_csip *csip) |
| { |
| if (!csip) |
| return NULL; |
| |
| if (csip->att) |
| return csip->att; |
| |
| return bt_gatt_client_get_att(csip->client); |
| } |
| |
| struct bt_csip *bt_csip_ref(struct bt_csip *csip) |
| { |
| if (!csip) |
| return NULL; |
| |
| __sync_fetch_and_add(&csip->ref_count, 1); |
| |
| return csip; |
| } |
| |
| static struct bt_csip *bt_csip_ref_safe(struct bt_csip *csip) |
| { |
| if (!csip || !csip->ref_count) |
| return NULL; |
| |
| return bt_csip_ref(csip); |
| } |
| |
| void bt_csip_unref(struct bt_csip *csip) |
| { |
| if (!csip) |
| return; |
| |
| if (__sync_sub_and_fetch(&csip->ref_count, 1)) |
| return; |
| |
| csip_free(csip); |
| } |
| |
| static void csip_debug(struct bt_csip *csip, const char *format, ...) |
| { |
| va_list ap; |
| |
| if (!csip || !format || !csip->debug_func) |
| return; |
| |
| va_start(ap, format); |
| util_debug_va(csip->debug_func, csip->debug_data, format, ap); |
| va_end(ap); |
| } |
| |
| static void csis_sirk_read(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct bt_csis *csis = user_data; |
| struct csis_sirk sirk; |
| struct iovec iov; |
| |
| memcpy(&sirk, csis->sirk_val, sizeof(sirk)); |
| |
| if (sirk.type == BT_CSIP_SIRK_ENCRYPT && |
| !csis->encrypt(att, sirk.val)) { |
| gatt_db_attribute_read_result(attrib, id, BT_ATT_ERROR_UNLIKELY, |
| NULL, 0); |
| return; |
| } |
| |
| iov.iov_base = &sirk; |
| iov.iov_len = sizeof(sirk); |
| |
| gatt_db_attribute_read_result(attrib, id, 0, iov.iov_base, |
| iov.iov_len); |
| } |
| |
| static void csis_size_read(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct bt_csis *csis = user_data; |
| struct iovec iov; |
| |
| iov.iov_base = &csis->size_val; |
| iov.iov_len = sizeof(csis->size_val); |
| |
| gatt_db_attribute_read_result(attrib, id, 0, iov.iov_base, |
| iov.iov_len); |
| } |
| |
| static void csis_lock_read_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| uint8_t value = CSIS_LOCK; |
| |
| gatt_db_attribute_read_result(attrib, id, 0, &value, sizeof(value)); |
| } |
| |
| static void csis_lock_write_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| const uint8_t *value, size_t len, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| gatt_db_attribute_write_result(attrib, id, 0); |
| } |
| |
| static void csis_rank_read_cb(struct gatt_db_attribute *attrib, |
| unsigned int id, uint16_t offset, |
| uint8_t opcode, struct bt_att *att, |
| void *user_data) |
| { |
| struct bt_csis *csis = user_data; |
| struct iovec iov; |
| |
| iov.iov_base = &csis->rank_val; |
| iov.iov_len = sizeof(csis->rank_val); |
| |
| gatt_db_attribute_read_result(attrib, id, 0, iov.iov_base, iov.iov_len); |
| } |
| |
| static struct bt_csis *csis_new(struct gatt_db *db) |
| { |
| struct bt_csis *csis; |
| |
| if (!db) |
| return NULL; |
| |
| csis = new0(struct bt_csis, 1); |
| |
| return csis; |
| } |
| |
| static struct bt_csip_db *csip_db_new(struct gatt_db *db) |
| { |
| struct bt_csip_db *cdb; |
| |
| if (!db) |
| return NULL; |
| |
| cdb = new0(struct bt_csip_db, 1); |
| cdb->db = gatt_db_ref(db); |
| |
| if (!csip_db) |
| csip_db = queue_new(); |
| |
| cdb->csis = csis_new(db); |
| cdb->csis->cdb = cdb; |
| |
| queue_push_tail(csip_db, cdb); |
| |
| return cdb; |
| } |
| |
| bool bt_csip_set_user_data(struct bt_csip *csip, void *user_data) |
| { |
| if (!csip) |
| return false; |
| |
| csip->user_data = user_data; |
| |
| return true; |
| } |
| |
| static bool csip_db_match(const void *data, const void *match_data) |
| { |
| const struct bt_csip_db *cdb = data; |
| const struct gatt_db *db = match_data; |
| |
| return (cdb->db == db); |
| } |
| |
| static struct bt_csip_db *csip_get_db(struct gatt_db *db) |
| { |
| struct bt_csip_db *cdb; |
| |
| cdb = queue_find(csip_db, csip_db_match, db); |
| if (cdb) |
| return cdb; |
| |
| return csip_db_new(db); |
| } |
| |
| bool bt_csip_set_debug(struct bt_csip *csip, bt_csip_debug_func_t func, |
| void *user_data, bt_csip_destroy_func_t destroy) |
| { |
| if (!csip) |
| return false; |
| |
| if (csip->debug_destroy) |
| csip->debug_destroy(csip->debug_data); |
| |
| csip->debug_func = func; |
| csip->debug_destroy = destroy; |
| csip->debug_data = user_data; |
| |
| return true; |
| } |
| |
| unsigned int bt_csip_register(bt_csip_func_t attached, bt_csip_func_t detached, |
| void *user_data) |
| { |
| struct bt_csip_cb *cb; |
| static unsigned int id; |
| |
| if (!attached && !detached) |
| return 0; |
| |
| if (!csip_cbs) |
| csip_cbs = queue_new(); |
| |
| cb = new0(struct bt_csip_cb, 1); |
| cb->id = ++id ? id : ++id; |
| cb->attached = attached; |
| cb->detached = detached; |
| cb->user_data = user_data; |
| |
| queue_push_tail(csip_cbs, cb); |
| |
| return cb->id; |
| } |
| |
| static bool match_id(const void *data, const void *match_data) |
| { |
| const struct bt_csip_cb *cb = data; |
| unsigned int id = PTR_TO_UINT(match_data); |
| |
| return (cb->id == id); |
| } |
| |
| bool bt_csip_unregister(unsigned int id) |
| { |
| struct bt_csip_cb *cb; |
| |
| cb = queue_remove_if(csip_cbs, match_id, UINT_TO_PTR(id)); |
| if (!cb) |
| return false; |
| |
| free(cb); |
| |
| return true; |
| } |
| |
| struct bt_csip *bt_csip_new(struct gatt_db *ldb, struct gatt_db *rdb) |
| { |
| struct bt_csip *csip; |
| struct bt_csip_db *db; |
| |
| if (!ldb) |
| return NULL; |
| |
| db = csip_get_db(ldb); |
| if (!db) |
| return NULL; |
| |
| csip = new0(struct bt_csip, 1); |
| csip->ldb = db; |
| csip->ready_cbs = queue_new(); |
| |
| if (!rdb) |
| goto done; |
| |
| db = new0(struct bt_csip_db, 1); |
| db->db = gatt_db_ref(rdb); |
| |
| csip->rdb = db; |
| |
| done: |
| bt_csip_ref(csip); |
| |
| return csip; |
| } |
| |
| static struct bt_csis *csip_get_csis(struct bt_csip *csip) |
| { |
| if (!csip) |
| return NULL; |
| |
| if (csip->rdb->csis) |
| return csip->rdb->csis; |
| |
| csip->rdb->csis = new0(struct bt_csis, 1); |
| csip->rdb->csis->cdb = csip->rdb; |
| |
| return csip->rdb->csis; |
| } |
| |
| static void read_sirk(bool success, uint8_t att_ecode, const uint8_t *value, |
| uint16_t length, void *user_data) |
| { |
| struct bt_csip *csip = user_data; |
| struct bt_csis *csis; |
| struct csis_sirk *sirk; |
| struct iovec iov = { |
| .iov_base = (void *)value, |
| .iov_len = length |
| }; |
| |
| if (!success) { |
| DBG(csip, "Unable to read SIRK: error 0x%02x", att_ecode); |
| return; |
| } |
| |
| csis = csip_get_csis(csip); |
| if (!csis) |
| return; |
| |
| sirk = util_iov_pull_mem(&iov, sizeof(*sirk)); |
| if (!sirk) { |
| DBG(csip, "Invalid size for SIRK: len %u", length); |
| return; |
| } |
| |
| if (!csis->sirk_val) |
| csis->sirk_val = new0(struct csis_sirk, 1); |
| |
| memcpy(csis->sirk_val, sirk, sizeof(*sirk)); |
| } |
| |
| static void read_size(bool success, uint8_t att_ecode, const uint8_t *value, |
| uint16_t length, void *user_data) |
| { |
| struct bt_csip *csip = user_data; |
| struct bt_csis *csis; |
| |
| if (!success) { |
| DBG(csip, "Unable to read Size: error 0x%02x", att_ecode); |
| return; |
| } |
| |
| csis = csip_get_csis(csip); |
| if (!csis) |
| return; |
| |
| csis->size_val = *value; |
| } |
| |
| static void read_rank(bool success, uint8_t att_ecode, const uint8_t *value, |
| uint16_t length, void *user_data) |
| { |
| struct bt_csip *csip = user_data; |
| struct bt_csis *csis; |
| |
| if (!success) { |
| DBG(csip, "Unable to read Rank: error 0x%02x", att_ecode); |
| return; |
| } |
| |
| csis = csip_get_csis(csip); |
| if (!csis) |
| return; |
| |
| csis->rank_val = *value; |
| } |
| |
| static void csip_notify_ready(struct bt_csip *csip) |
| { |
| const struct queue_entry *entry; |
| |
| if (!bt_csip_ref_safe(csip)) |
| return; |
| |
| for (entry = queue_get_entries(csip->ready_cbs); entry; |
| entry = entry->next) { |
| struct bt_csip_ready *ready = entry->data; |
| |
| ready->func(csip, ready->data); |
| } |
| |
| bt_csip_unref(csip); |
| } |
| |
| static void foreach_csis_char(struct gatt_db_attribute *attr, void *user_data) |
| { |
| struct bt_csip *csip = user_data; |
| uint16_t value_handle; |
| bt_uuid_t uuid, uuid_sirk, uuid_size, uuid_rank; |
| struct bt_csis *csis; |
| |
| if (!gatt_db_attribute_get_char_data(attr, NULL, &value_handle, |
| NULL, NULL, &uuid)) |
| return; |
| |
| bt_uuid16_create(&uuid_sirk, CS_SIRK); |
| bt_uuid16_create(&uuid_size, CS_SIZE); |
| bt_uuid16_create(&uuid_rank, CS_RANK); |
| |
| if (!bt_uuid_cmp(&uuid, &uuid_sirk)) { |
| DBG(csip, "SIRK found: handle 0x%04x", value_handle); |
| |
| csis = csip_get_csis(csip); |
| if (!csis) |
| return; |
| |
| csis->sirk = attr; |
| |
| bt_gatt_client_read_value(csip->client, value_handle, read_sirk, |
| csip, NULL); |
| |
| return; |
| } |
| |
| if (!bt_uuid_cmp(&uuid, &uuid_size)) { |
| DBG(csip, "Size found: handle 0x%04x", value_handle); |
| |
| csis = csip_get_csis(csip); |
| if (!csis) |
| return; |
| |
| csis->size = attr; |
| |
| bt_gatt_client_read_value(csip->client, value_handle, read_size, |
| csip, NULL); |
| } |
| |
| if (!bt_uuid_cmp(&uuid, &uuid_rank)) { |
| DBG(csip, "Rank found: handle 0x%04x", value_handle); |
| |
| csis = csip_get_csis(csip); |
| if (!csis) |
| return; |
| |
| csis->rank = attr; |
| |
| bt_gatt_client_read_value(csip->client, value_handle, read_rank, |
| csip, NULL); |
| } |
| } |
| static void foreach_csis_service(struct gatt_db_attribute *attr, |
| void *user_data) |
| { |
| struct bt_csip *csip = user_data; |
| struct bt_csis *csis = csip_get_csis(csip); |
| |
| if (!csis) |
| return; |
| |
| csis->service = attr; |
| |
| gatt_db_service_set_claimed(attr, true); |
| |
| gatt_db_service_foreach_char(attr, foreach_csis_char, csip); |
| } |
| |
| static void csip_idle(void *data) |
| { |
| struct bt_csip *csip = data; |
| |
| csip->idle_id = 0; |
| |
| csip_notify_ready(csip); |
| } |
| |
| bool bt_csip_attach(struct bt_csip *csip, struct bt_gatt_client *client) |
| { |
| bt_uuid_t uuid; |
| |
| if (!sessions) |
| sessions = queue_new(); |
| |
| queue_push_tail(sessions, csip); |
| |
| if (!client) |
| return true; |
| |
| if (csip->client) |
| return false; |
| |
| csip->client = bt_gatt_client_clone(client); |
| if (!csip->client) |
| return false; |
| |
| csip->idle_id = bt_gatt_client_idle_register(csip->client, csip_idle, |
| csip, NULL); |
| |
| bt_uuid16_create(&uuid, CSIS_UUID); |
| gatt_db_foreach_service(csip->rdb->db, &uuid, foreach_csis_service, |
| csip); |
| |
| return true; |
| } |
| |
| static struct csis_sirk *sirk_new(struct bt_csis *csis, struct gatt_db *db, |
| uint8_t type, uint8_t k[16], |
| uint8_t size, uint8_t rank) |
| { |
| struct csis_sirk *sirk; |
| bt_uuid_t uuid; |
| struct gatt_db_attribute *cas; |
| |
| if (!csis) |
| return NULL; |
| |
| if (csis->sirk) |
| sirk = csis->sirk_val; |
| else |
| sirk = new0(struct csis_sirk, 1); |
| |
| sirk->type = type; |
| memcpy(sirk->val, k, sizeof(sirk->val)); |
| csis->sirk_val = sirk; |
| csis->size_val = size; |
| csis->lock_val = 1; |
| csis->rank_val = rank; |
| |
| /* Check if service already active as that means the attributes have |
| * already been registered. |
| */ |
| if (gatt_db_service_get_active(csis->service)) |
| return sirk; |
| |
| /* Populate DB with CSIS attributes */ |
| bt_uuid16_create(&uuid, CSIS_UUID); |
| csis->service = gatt_db_add_service(db, &uuid, true, 10); |
| |
| bt_uuid16_create(&uuid, CS_SIRK); |
| csis->sirk = gatt_db_service_add_characteristic(csis->service, |
| &uuid, |
| BT_ATT_PERM_READ | |
| BT_ATT_PERM_READ_ENCRYPT, |
| BT_GATT_CHRC_PROP_READ, |
| csis_sirk_read, NULL, |
| csis); |
| |
| bt_uuid16_create(&uuid, CS_SIZE); |
| csis->size = gatt_db_service_add_characteristic(csis->service, |
| &uuid, |
| BT_ATT_PERM_READ | |
| BT_ATT_PERM_READ_ENCRYPT, |
| BT_GATT_CHRC_PROP_READ, |
| csis_size_read, NULL, |
| csis); |
| |
| /* Lock */ |
| bt_uuid16_create(&uuid, CS_LOCK); |
| csis->lock = gatt_db_service_add_characteristic(csis->service, &uuid, |
| BT_ATT_PERM_READ | |
| BT_ATT_PERM_READ_ENCRYPT | |
| BT_ATT_PERM_WRITE | |
| BT_ATT_PERM_WRITE_ENCRYPT, |
| BT_GATT_CHRC_PROP_READ | |
| BT_GATT_CHRC_PROP_WRITE | |
| BT_GATT_CHRC_PROP_NOTIFY, |
| csis_lock_read_cb, |
| csis_lock_write_cb, |
| csis); |
| |
| csis->lock_ccc = gatt_db_service_add_ccc(csis->service, |
| BT_ATT_PERM_READ | BT_ATT_PERM_WRITE); |
| |
| /* Rank */ |
| bt_uuid16_create(&uuid, CS_RANK); |
| csis->rank = gatt_db_service_add_characteristic(csis->service, &uuid, |
| BT_ATT_PERM_READ | |
| BT_ATT_PERM_READ_ENCRYPT, |
| BT_GATT_CHRC_PROP_READ, |
| csis_rank_read_cb, |
| NULL, csis); |
| |
| /* Add the CAS service */ |
| bt_uuid16_create(&uuid, 0x1853); |
| cas = gatt_db_add_service(db, &uuid, true, 2); |
| gatt_db_service_add_included(cas, csis->service); |
| gatt_db_service_set_active(cas, true); |
| gatt_db_service_add_included(cas, csis->service); |
| |
| gatt_db_service_set_active(csis->service, true); |
| |
| return sirk; |
| } |
| |
| bool bt_csip_set_sirk(struct bt_csip *csip, bool encrypt, |
| uint8_t k[16], uint8_t size, uint8_t rank, |
| bt_csip_encrypt_func_t func) |
| { |
| uint8_t zero[16] = {}; |
| uint8_t type; |
| |
| if (!csip || !csip->ldb || !memcmp(k, zero, sizeof(zero))) |
| return false; |
| |
| type = encrypt ? BT_CSIP_SIRK_ENCRYPT : BT_CSIP_SIRK_CLEARTEXT; |
| |
| /* In case of encrypted type requires sef key function */ |
| if (type == BT_CSIP_SIRK_ENCRYPT && !func) |
| return false; |
| |
| if (!sirk_new(csip->ldb->csis, csip->ldb->db, type, k, size, rank)) |
| return false; |
| |
| csip->ldb->csis->encrypt = func; |
| |
| return true; |
| } |
| |
| bool bt_csip_get_sirk(struct bt_csip *csip, uint8_t *type, |
| uint8_t k[16], uint8_t *size, uint8_t *rank) |
| { |
| struct bt_csis *csis; |
| |
| if (!csip) |
| return false; |
| |
| csis = csip_get_csis(csip); |
| if (!csis) |
| return false; |
| |
| if (!csis->sirk_val) |
| return false; |
| |
| if (type) |
| *type = csis->sirk_val->type; |
| |
| memcpy(k, csis->sirk_val->val, sizeof(csis->sirk_val->val)); |
| |
| if (size) |
| *size = csis->size_val; |
| |
| if (rank) |
| *rank = csis->rank_val; |
| |
| return true; |
| } |
| |
| unsigned int bt_csip_ready_register(struct bt_csip *csip, |
| bt_csip_ready_func_t func, void *user_data, |
| bt_csip_destroy_func_t destroy) |
| { |
| struct bt_csip_ready *ready; |
| static unsigned int id; |
| |
| if (!csip) |
| return 0; |
| |
| ready = new0(struct bt_csip_ready, 1); |
| ready->id = ++id ? id : ++id; |
| ready->func = func; |
| ready->destroy = destroy; |
| ready->data = user_data; |
| |
| queue_push_tail(csip->ready_cbs, ready); |
| |
| return ready->id; |
| } |
| |
| static bool match_ready_id(const void *data, const void *match_data) |
| { |
| const struct bt_csip_ready *ready = data; |
| unsigned int id = PTR_TO_UINT(match_data); |
| |
| return (ready->id == id); |
| } |
| |
| bool bt_csip_ready_unregister(struct bt_csip *csip, unsigned int id) |
| { |
| struct bt_csip_ready *ready; |
| |
| ready = queue_remove_if(csip->ready_cbs, match_ready_id, |
| UINT_TO_PTR(id)); |
| if (!ready) |
| return false; |
| |
| csip_ready_free(ready); |
| |
| return true; |
| } |