| // SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause |
| /* |
| * Copyright (C) 2010-2014, 2018-2023 Intel Corporation |
| * Copyright (C) 2013-2014 Intel Mobile Communications GmbH |
| * Copyright (C) 2016-2017 Intel Deutschland GmbH |
| */ |
| #include <linux/export.h> |
| #include <net/genetlink.h> |
| #include "iwl-drv.h" |
| #include "iwl-io.h" |
| #include "iwl-fh.h" |
| #include "iwl-prph.h" |
| #include "iwl-trans.h" |
| #include "iwl-op-mode.h" |
| #include "iwl-tm-gnl.h" |
| #include "iwl-tm-infc.h" |
| #include "iwl-dnt-cfg.h" |
| #include "iwl-dnt-dispatch.h" |
| #include "iwl-csr.h" |
| |
| /** |
| * iwl_tm_validate_fw_cmd() - Validates FW host command input data |
| * @data_in: Input to be validated |
| * Returns: an error code |
| */ |
| static int iwl_tm_validate_fw_cmd(struct iwl_tm_data *data_in) |
| { |
| struct iwl_tm_cmd_request *cmd_req; |
| u32 data_buf_size; |
| |
| if (!data_in->data || |
| (data_in->len < sizeof(struct iwl_tm_cmd_request))) |
| return -EINVAL; |
| |
| cmd_req = (struct iwl_tm_cmd_request *)data_in->data; |
| |
| data_buf_size = data_in->len - sizeof(struct iwl_tm_cmd_request); |
| if (data_buf_size < cmd_req->len) |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| /** |
| * iwl_tm_validate_reg_ops() - Checks the input data for registers operations |
| * @data_in: data is casted to iwl_tm_regs_request len in |
| * the size of the request struct in bytes. |
| * Returns: an error code |
| */ |
| static int iwl_tm_validate_reg_ops(struct iwl_tm_data *data_in) |
| { |
| struct iwl_tm_regs_request *request; |
| u32 request_size; |
| u32 idx; |
| |
| if (!data_in->data || |
| (data_in->len < sizeof(struct iwl_tm_regs_request))) |
| return -EINVAL; |
| |
| request = (struct iwl_tm_regs_request *)(data_in->data); |
| request_size = sizeof(struct iwl_tm_regs_request) + |
| request->num * sizeof(struct iwl_tm_reg_op); |
| if (data_in->len < request_size) |
| return -EINVAL; |
| |
| /* |
| * Calculate result size - result is returned only for read ops |
| * Also, verifying inputs |
| */ |
| for (idx = 0; idx < request->num; idx++) { |
| if (request->reg_ops[idx].op_type >= IWL_TM_REG_OP_MAX) |
| return -EINVAL; |
| |
| /* |
| * Allow access only to FH/CSR/HBUS in direct mode. |
| * Since we don't have the upper bounds for the CSR |
| * and HBUS segments, we will use only the upper |
| * bound of FH for sanity check. |
| */ |
| if (!IS_AL_ADDR(request->reg_ops[idx].address)) { |
| if (request->reg_ops[idx].address >= |
| FH_MEM_UPPER_BOUND) |
| return -EINVAL; |
| } |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * iwl_tm_trace_end - Ends debug trace. Common for all op modes. |
| * @dev: testmode device struct |
| * Returns: an error code |
| */ |
| static int iwl_tm_trace_end(struct iwl_tm_gnl_dev *dev) |
| { |
| struct iwl_trans *trans = dev->trans; |
| struct iwl_test_trace *trace = &dev->tst.trace; |
| |
| if (!trace->enabled) |
| return -EILSEQ; |
| |
| if (trace->cpu_addr && trace->dma_addr) |
| dma_free_coherent(trans->dev, trace->size, |
| trace->cpu_addr, trace->dma_addr); |
| memset(trace, 0, sizeof(struct iwl_test_trace)); |
| |
| return 0; |
| } |
| |
| /** |
| * iwl_tm_trace_begin() - Checks input data for trace request |
| * @dev: testmode device struct |
| * @data_in: Only size is relevant - Desired size of trace buffer. |
| * @data_out: Trace request data (address & size) |
| * Returns: an error code |
| */ |
| static int iwl_tm_trace_begin(struct iwl_tm_gnl_dev *dev, |
| struct iwl_tm_data *data_in, |
| struct iwl_tm_data *data_out) |
| { |
| struct iwl_tm_trace_request *req = data_in->data; |
| struct iwl_tm_trace_request *resp; |
| |
| if (!data_in->data || |
| data_in->len < sizeof(struct iwl_tm_trace_request)) |
| return -EINVAL; |
| |
| req = data_in->data; |
| |
| /* size zero means use the default */ |
| if (!req->size) |
| req->size = TRACE_BUFF_SIZE_DEF; |
| else if (req->size < TRACE_BUFF_SIZE_MIN || |
| req->size > TRACE_BUFF_SIZE_MAX) |
| return -EINVAL; |
| else if (!dev->dnt->mon_buf_cpu_addr) |
| return -ENOMEM; |
| |
| resp = kmalloc(sizeof(*resp), GFP_KERNEL); |
| if (!resp) { |
| return -ENOMEM; |
| } |
| resp->size = dev->dnt->mon_buf_size; |
| /* Casting to avoid compilation warnings when DMA address is 32bit */ |
| resp->addr = (u64)dev->dnt->mon_base_addr; |
| |
| data_out->data = resp; |
| data_out->len = sizeof(*resp); |
| |
| return 0; |
| } |
| |
| static bool iwl_tm_gnl_valid_hw_addr(u32 addr) |
| { |
| /* TODO need to implement */ |
| return true; |
| } |
| |
| /** |
| * iwl_tm_validate_sram_write_req() - Checks input data of SRAM write request |
| * @dev: testmode device struct |
| * @data_in: SRAM access request |
| * Returns: an error code |
| */ |
| static int iwl_tm_validate_sram_write_req(struct iwl_tm_gnl_dev *dev, |
| struct iwl_tm_data *data_in) |
| { |
| struct iwl_tm_sram_write_request *cmd_in; |
| u32 data_buf_size; |
| |
| if (!dev->trans->op_mode) { |
| IWL_ERR(dev->trans, "No op_mode!\n"); |
| return -ENODEV; |
| } |
| |
| if (!data_in->data || |
| data_in->len < sizeof(struct iwl_tm_sram_write_request)) |
| return -EINVAL; |
| |
| cmd_in = data_in->data; |
| |
| data_buf_size = |
| data_in->len - sizeof(struct iwl_tm_sram_write_request); |
| if (data_buf_size < cmd_in->len) |
| return -EINVAL; |
| |
| if (iwl_tm_gnl_valid_hw_addr(cmd_in->offset)) |
| return 0; |
| |
| return -EINVAL; |
| } |
| |
| /** |
| * iwl_tm_validate_sram_read_req() - Checks input data of SRAM read request |
| * @dev: testmode device struct |
| * @data_in: SRAM access request |
| * Returns: an error code |
| */ |
| static int iwl_tm_validate_sram_read_req(struct iwl_tm_gnl_dev *dev, |
| struct iwl_tm_data *data_in) |
| { |
| struct iwl_tm_sram_read_request *cmd_in; |
| |
| if (!dev->trans->op_mode) { |
| IWL_ERR(dev->trans, "No op_mode!\n"); |
| return -ENODEV; |
| } |
| |
| if (!data_in->data || |
| data_in->len < sizeof(struct iwl_tm_sram_read_request)) |
| return -EINVAL; |
| |
| cmd_in = data_in->data; |
| |
| if (iwl_tm_gnl_valid_hw_addr(cmd_in->offset)) |
| return 0; |
| |
| return -EINVAL; |
| } |
| |
| /** |
| * iwl_tm_notifications_en() - Checks input for enable test notifications |
| * @tst: Device's test data pointer |
| * @data_in: u32 notification (flag) |
| * Returns: an error code |
| */ |
| static int iwl_tm_notifications_en(struct iwl_test *tst, |
| struct iwl_tm_data *data_in) |
| { |
| u32 notification_en; |
| |
| if (!data_in->data || (data_in->len != sizeof(u32))) |
| return -EINVAL; |
| |
| notification_en = *(u32 *)data_in->data; |
| if ((notification_en != NOTIFICATIONS_ENABLE) && |
| (notification_en != NOTIFICATIONS_DISABLE)) |
| return -EINVAL; |
| |
| tst->notify = notification_en == NOTIFICATIONS_ENABLE; |
| |
| return 0; |
| } |
| |
| /** |
| * iwl_tm_validate_tx_cmd() - Validates FW host command input data |
| * @data_in: Input to be validated |
| * Returns: an error code |
| */ |
| static int iwl_tm_validate_tx_cmd(struct iwl_tm_data *data_in) |
| { |
| struct iwl_tm_mod_tx_request *cmd_req; |
| u32 data_buf_size; |
| |
| if (!data_in->data || |
| (data_in->len < sizeof(struct iwl_tm_mod_tx_request))) |
| return -EINVAL; |
| |
| cmd_req = (struct iwl_tm_mod_tx_request *)data_in->data; |
| |
| data_buf_size = data_in->len - |
| sizeof(struct iwl_tm_mod_tx_request); |
| if (data_buf_size < cmd_req->len) |
| return -EINVAL; |
| |
| if (cmd_req->sta_id >= IWL_TM_STATION_COUNT) |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| /** |
| * iwl_tm_validate_rx_hdrs_mode_req() - Validates RX headers mode request |
| * @data_in: Input to be validated |
| * Returns: an error code |
| */ |
| static int iwl_tm_validate_rx_hdrs_mode_req(struct iwl_tm_data *data_in) |
| { |
| if (!data_in->data || |
| (data_in->len < sizeof(struct iwl_xvt_rx_hdrs_mode_request))) |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| static int iwl_tm_validate_get_chip_id(struct iwl_trans *trans) |
| { |
| return 0; |
| } |
| |
| /** |
| * iwl_tm_validate_apmg_pd_mode_req() - Validates apmg rx mode request |
| * @data_in: Input to be validated |
| * Returns: an error code |
| */ |
| static int iwl_tm_validate_apmg_pd_mode_req(struct iwl_tm_data *data_in) |
| { |
| if (!data_in->data || |
| (data_in->len != sizeof(struct iwl_xvt_apmg_pd_mode_request))) |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| static int iwl_tm_get_device_status(struct iwl_tm_gnl_dev *dev, |
| struct iwl_tm_data *data_in, |
| struct iwl_tm_data *data_out) |
| { |
| __u32 *status; |
| |
| status = kmalloc(sizeof(__u32), GFP_KERNEL); |
| if (!status) |
| return -ENOMEM; |
| |
| *status = dev->dnt->iwl_dnt_status; |
| |
| data_out->data = status; |
| data_out->len = sizeof(__u32); |
| |
| return 0; |
| } |
| |
| #if IS_ENABLED(CPTCFG_IWLXVT) |
| static int iwl_tm_switch_op_mode(struct iwl_tm_gnl_dev *dev, |
| struct iwl_tm_data *data_in) |
| { |
| struct iwl_switch_op_mode *switch_cmd = data_in->data; |
| struct iwl_drv *drv; |
| int ret = 0; |
| |
| if (data_in->len < sizeof(*switch_cmd)) |
| return -EINVAL; |
| |
| drv = iwl_drv_get_dev_container(dev->trans->dev); |
| if (!drv) { |
| IWL_ERR(dev->trans, "Couldn't retrieve device information\n"); |
| return -ENODEV; |
| } |
| |
| /* Executing switch command */ |
| ret = iwl_drv_switch_op_mode(drv, switch_cmd->new_op_mode); |
| |
| if (ret < 0) |
| IWL_ERR(dev->trans, "Failed to switch op mode to %s (err:%d)\n", |
| switch_cmd->new_op_mode, ret); |
| |
| return ret; |
| } |
| #endif |
| |
| static int iwl_tm_gnl_get_sil_step(struct iwl_trans *trans, |
| struct iwl_tm_data *data_out) |
| { |
| struct iwl_sil_step *resp; |
| data_out->data = kmalloc(sizeof(struct iwl_sil_step), GFP_KERNEL); |
| if (!data_out->data) |
| return -ENOMEM; |
| data_out->len = sizeof(struct iwl_sil_step); |
| resp = (struct iwl_sil_step *)data_out->data; |
| resp->silicon_step = trans->hw_rev_step; |
| return 0; |
| } |
| |
| static int iwl_tm_gnl_get_build_info(struct iwl_trans *trans, |
| struct iwl_tm_data *data_out) |
| { |
| struct iwl_tm_build_info *resp; |
| |
| data_out->data = kmalloc(sizeof(*resp), GFP_KERNEL); |
| if (!data_out->data) |
| return -ENOMEM; |
| data_out->len = sizeof(struct iwl_tm_build_info); |
| resp = (struct iwl_tm_build_info *)data_out->data; |
| |
| memset(resp, 0 , sizeof(*resp)); |
| strncpy(resp->driver_version, BACKPORTS_GIT_TRACKED, |
| sizeof(resp->driver_version)); |
| |
| return 0; |
| } |
| |
| static int iwl_tm_gnl_get_sil_type(struct iwl_trans * trans,struct iwl_tm_data * data_out) |
| { |
| struct iwl_tm_sil_type *resp; |
| |
| resp = kzalloc(sizeof(*resp), GFP_KERNEL); |
| if (!resp) |
| return -ENOMEM; |
| |
| resp->silicon_type = CSR_HW_REV_TYPE(trans->hw_rev); |
| |
| data_out->data = resp; |
| data_out->len = sizeof(*resp); |
| |
| return 0; |
| } |
| |
| static int iwl_tm_gnl_get_rfid(struct iwl_trans *trans, |
| struct iwl_tm_data *data_out) |
| { |
| struct iwl_tm_rfid *resp; |
| |
| resp = kzalloc(sizeof(*resp), GFP_KERNEL); |
| if (!resp) |
| return -ENOMEM; |
| |
| IWL_DEBUG_INFO(trans, "HW RFID=0x08%X\n", trans->hw_rf_id); |
| |
| resp->flavor = CSR_HW_RFID_FLAVOR(trans->hw_rf_id); |
| resp->dash = CSR_HW_RFID_DASH(trans->hw_rf_id); |
| resp->step = CSR_HW_RFID_STEP(trans->hw_rf_id); |
| resp->type = CSR_HW_RFID_TYPE(trans->hw_rf_id); |
| resp->is_cdb = CSR_HW_RFID_IS_CDB(trans->hw_rf_id); |
| resp->is_jacket = CSR_HW_RFID_IS_JACKET(trans->hw_rf_id); |
| |
| data_out->data = resp; |
| data_out->len = sizeof(*resp); |
| |
| return 0; |
| } |
| |
| static int iwl_tm_gnl_get_rfid_v2(struct iwl_trans *trans, |
| struct iwl_tm_data *data_out) |
| { |
| struct iwl_tm_rfid *resp; |
| |
| resp = kzalloc(sizeof(*resp), GFP_KERNEL); |
| if (!resp) |
| return -ENOMEM; |
| |
| resp->flavor = CSR_HW_RFID_FLAVOR(trans->hw_crf_id); |
| resp->dash = CSR_HW_RFID_DASH(trans->hw_crf_id); |
| resp->step = CSR_HW_RFID_STEP(trans->hw_crf_id); |
| resp->type = CSR_HW_RFID_TYPE(trans->hw_rf_id); |
| resp->is_cdb = CSR_HW_RFID_IS_CDB(trans->hw_rf_id); |
| resp->is_jacket = CSR_HW_RFID_IS_JACKET(trans->hw_rf_id); |
| |
| data_out->data = resp; |
| data_out->len = sizeof(*resp); |
| |
| return 0; |
| } |
| |
| /* |
| * Testmode GNL family types (This NL family |
| * will eventually replace nl80211 support in |
| * iwl xVM modules) |
| */ |
| |
| #define IWL_TM_GNL_FAMILY_NAME "iwl_tm_gnl" |
| #define IWL_TM_GNL_MC_GRP_NAME "iwl_tm_mcgrp" |
| #define IWL_TM_GNL_VERSION_NR 1 |
| |
| #define IWL_TM_GNL_DEVNAME_LEN 256 |
| |
| |
| /** |
| * struct iwl_tm_gnl_cmd - Required data for command execution. |
| * @dev_name: Target device |
| * @cmd: Command index |
| * @data_in: Input data |
| * @data_out: Output data, to be sent when |
| * command is done. |
| */ |
| struct iwl_tm_gnl_cmd { |
| const char *dev_name; |
| u32 cmd; |
| struct iwl_tm_data data_in; |
| struct iwl_tm_data data_out; |
| }; |
| |
| static struct list_head dev_list; /* protected by mutex or RCU */ |
| static struct mutex dev_list_mtx; |
| |
| /* Testmode GNL family command attributes */ |
| enum iwl_tm_gnl_cmd_attr_t { |
| IWL_TM_GNL_MSG_ATTR_INVALID = 0, |
| IWL_TM_GNL_MSG_ATTR_DEVNAME, |
| IWL_TM_GNL_MSG_ATTR_CMD, |
| IWL_TM_GNL_MSG_ATTR_DATA, |
| IWL_TM_GNL_MSG_ATTR_MAX |
| }; |
| |
| /* TM GNL family definition */ |
| static struct genl_family iwl_tm_gnl_family; |
| static const struct genl_multicast_group iwl_tm_gnl_mcgrps[] = { |
| { .name = IWL_TM_GNL_MC_GRP_NAME, }, |
| }; |
| |
| /* TM GNL bus policy */ |
| static const struct nla_policy |
| iwl_tm_gnl_msg_policy[IWL_TM_GNL_MSG_ATTR_MAX] = { |
| [IWL_TM_GNL_MSG_ATTR_DEVNAME] = { |
| .type = NLA_NUL_STRING, |
| .len = IWL_TM_GNL_DEVNAME_LEN-1 }, |
| [IWL_TM_GNL_MSG_ATTR_CMD] = { .type = NLA_U32, }, |
| [IWL_TM_GNL_MSG_ATTR_DATA] = { .type = NLA_BINARY, }, |
| }; |
| |
| /** |
| * iwl_tm_gnl_get_dev() - Retrieve device information |
| * @dev_name: Device to be found |
| * |
| * Finds the device information according to device name, |
| * must be protected by list mutex when used (mutex is not |
| * locked inside the function to allow code flexibility) |
| * |
| * Returns: an error code |
| */ |
| static struct iwl_tm_gnl_dev *iwl_tm_gnl_get_dev(const char *dev_name) |
| { |
| struct iwl_tm_gnl_dev *dev_itr, *dev = NULL; |
| |
| lockdep_assert_held(&dev_list_mtx); |
| |
| list_for_each_entry(dev_itr, &dev_list, list) { |
| if (!strcmp(dev_itr->dev_name, dev_name)) { |
| dev = dev_itr; |
| break; |
| } |
| } |
| |
| return dev; |
| } |
| |
| /** |
| * iwl_tm_gnl_create_msg - Creates a genl message |
| * @pid: Netlink PID that the message is addressed to |
| * @seq: sequence number (usually the one of the sender) |
| * @cmd_data: Message's data |
| * @flags: Resources allocation flags |
| * |
| * Returns: an error code |
| */ |
| static struct sk_buff *iwl_tm_gnl_create_msg(u32 pid, u32 seq, |
| struct iwl_tm_gnl_cmd cmd_data, |
| gfp_t flags) |
| { |
| void *nlmsg_head; |
| struct sk_buff *skb; |
| int ret; |
| |
| skb = genlmsg_new(NLMSG_GOODSIZE, flags); |
| if (!skb) |
| goto send_msg_err; |
| |
| nlmsg_head = genlmsg_put(skb, pid, seq, |
| &iwl_tm_gnl_family, 0, |
| IWL_TM_GNL_CMD_EXECUTE); |
| if (!nlmsg_head) |
| goto send_msg_err; |
| |
| ret = nla_put_string(skb, IWL_TM_GNL_MSG_ATTR_DEVNAME, |
| cmd_data.dev_name); |
| if (ret) |
| goto send_msg_err; |
| |
| ret = nla_put_u32(skb, IWL_TM_GNL_MSG_ATTR_CMD, |
| cmd_data.cmd); |
| if (ret) |
| goto send_msg_err; |
| |
| if (cmd_data.data_out.len && cmd_data.data_out.data) { |
| ret = nla_put(skb, IWL_TM_GNL_MSG_ATTR_DATA, |
| cmd_data.data_out.len, cmd_data.data_out.data); |
| if (ret) |
| goto send_msg_err; |
| } |
| |
| genlmsg_end(skb, nlmsg_head); |
| |
| return skb; |
| |
| send_msg_err: |
| if (skb) |
| kfree_skb(skb); |
| |
| return NULL; |
| } |
| |
| /** |
| * iwl_tm_gnl_send_msg - Sends a message to mcast or userspace listener |
| * @trans: transport |
| * @cmd: Command index |
| * @check_notify: only send when notify is set |
| * @data_out: Data to be sent |
| * @data_len: data length |
| * @flags: allocation flags |
| * |
| * Initiate a message sending to user space (as apposed |
| * to replying to a message that was initiated by user |
| * space). Uses multicast broadcasting method. |
| * |
| * Returns: an error code |
| */ |
| int iwl_tm_gnl_send_msg(struct iwl_trans *trans, u32 cmd, bool check_notify, |
| void *data_out, u32 data_len, gfp_t flags) |
| { |
| struct iwl_tm_gnl_dev *dev; |
| struct iwl_tm_gnl_cmd cmd_data; |
| struct sk_buff *skb; |
| u32 nlportid; |
| |
| if (WARN_ON_ONCE(!trans)) |
| return -EINVAL; |
| |
| if (!trans->tmdev) |
| return 0; |
| dev = trans->tmdev; |
| |
| nlportid = READ_ONCE(dev->nl_events_portid); |
| |
| if (check_notify && !dev->tst.notify) |
| return 0; |
| |
| memset(&cmd_data, 0 , sizeof(struct iwl_tm_gnl_cmd)); |
| cmd_data.dev_name = dev_name(trans->dev); |
| cmd_data.cmd = cmd; |
| cmd_data.data_out.data = data_out; |
| cmd_data.data_out.len = data_len; |
| |
| skb = iwl_tm_gnl_create_msg(nlportid, 0, cmd_data, flags); |
| if (!skb) |
| return -EINVAL; |
| |
| if (nlportid) |
| return genlmsg_unicast(&init_net, skb, nlportid); |
| return genlmsg_multicast(&iwl_tm_gnl_family, skb, 0, 0, flags); |
| } |
| IWL_EXPORT_SYMBOL(iwl_tm_gnl_send_msg); |
| |
| /** |
| * iwl_tm_gnl_reply() - Sends command's results back to user space |
| * @info: info struct received in .doit callback |
| * @cmd_data: Data of command to be responded |
| * Returns: an error code |
| */ |
| static int iwl_tm_gnl_reply(struct genl_info *info, |
| struct iwl_tm_gnl_cmd cmd_data) |
| { |
| struct sk_buff *skb; |
| |
| skb = iwl_tm_gnl_create_msg(info->snd_portid, info->snd_seq, |
| cmd_data, GFP_KERNEL); |
| if (!skb) |
| return -EINVAL; |
| |
| return genlmsg_reply(skb, info); |
| } |
| |
| /** |
| * iwl_tm_gnl_cmd_execute() - Execute IWL testmode GNL command |
| * @cmd_data: Pointer to the data of command to be executed |
| * Returns: an error code |
| */ |
| static int iwl_tm_gnl_cmd_execute(struct iwl_tm_gnl_cmd *cmd_data) |
| { |
| struct iwl_tm_gnl_dev *dev; |
| bool common_op = false; |
| int ret = 0; |
| mutex_lock(&dev_list_mtx); |
| dev = iwl_tm_gnl_get_dev(cmd_data->dev_name); |
| mutex_unlock(&dev_list_mtx); |
| if (!dev) |
| return -ENODEV; |
| |
| IWL_DEBUG_INFO(dev->trans, "%s cmd=0x%X\n", __func__, cmd_data->cmd); |
| switch (cmd_data->cmd) { |
| |
| case IWL_TM_USER_CMD_HCMD: |
| ret = iwl_tm_validate_fw_cmd(&cmd_data->data_in); |
| break; |
| |
| case IWL_TM_USER_CMD_REG_ACCESS: |
| ret = iwl_tm_validate_reg_ops(&cmd_data->data_in); |
| break; |
| |
| case IWL_TM_USER_CMD_SRAM_WRITE: |
| ret = iwl_tm_validate_sram_write_req(dev, &cmd_data->data_in); |
| break; |
| |
| case IWL_TM_USER_CMD_BEGIN_TRACE: |
| ret = iwl_tm_trace_begin(dev, |
| &cmd_data->data_in, |
| &cmd_data->data_out); |
| common_op = true; |
| break; |
| |
| case IWL_TM_USER_CMD_END_TRACE: |
| ret = iwl_tm_trace_end(dev); |
| common_op = true; |
| break; |
| |
| case IWL_XVT_CMD_MOD_TX: |
| ret = iwl_tm_validate_tx_cmd(&cmd_data->data_in); |
| break; |
| |
| case IWL_XVT_CMD_RX_HDRS_MODE: |
| ret = iwl_tm_validate_rx_hdrs_mode_req(&cmd_data->data_in); |
| break; |
| |
| case IWL_XVT_CMD_APMG_PD_MODE: |
| ret = iwl_tm_validate_apmg_pd_mode_req(&cmd_data->data_in); |
| break; |
| |
| case IWL_TM_USER_CMD_NOTIFICATIONS: |
| ret = iwl_tm_notifications_en(&dev->tst, &cmd_data->data_in); |
| common_op = true; |
| break; |
| |
| case IWL_TM_USER_CMD_GET_DEVICE_STATUS: |
| ret = iwl_tm_get_device_status(dev, &cmd_data->data_in, |
| &cmd_data->data_out); |
| common_op = true; |
| break; |
| #if IS_ENABLED(CPTCFG_IWLXVT) |
| case IWL_TM_USER_CMD_SWITCH_OP_MODE: |
| ret = iwl_tm_switch_op_mode(dev, &cmd_data->data_in); |
| common_op = true; |
| break; |
| #endif |
| case IWL_XVT_CMD_GET_CHIP_ID: |
| ret = iwl_tm_validate_get_chip_id(dev->trans); |
| break; |
| |
| case IWL_TM_USER_CMD_GET_SIL_STEP: |
| ret = iwl_tm_gnl_get_sil_step(dev->trans, &cmd_data->data_out); |
| common_op = true; |
| break; |
| |
| case IWL_TM_USER_CMD_GET_DRIVER_BUILD_INFO: |
| ret = iwl_tm_gnl_get_build_info(dev->trans, |
| &cmd_data->data_out); |
| common_op = true; |
| break; |
| |
| case IWL_TM_USER_CMD_GET_SIL_TYPE: |
| ret = iwl_tm_gnl_get_sil_type(dev->trans, &cmd_data->data_out); |
| common_op = true; |
| break; |
| |
| case IWL_TM_USER_CMD_GET_RFID: |
| ret = iwl_tm_gnl_get_rfid(dev->trans, &cmd_data->data_out); |
| common_op = true; |
| break; |
| |
| case IWL_TM_USER_CMD_GET_RFID_V2: |
| ret = iwl_tm_gnl_get_rfid_v2(dev->trans, &cmd_data->data_out); |
| common_op = true; |
| break; |
| } |
| |
| if (ret) { |
| IWL_ERR(dev->trans, "%s Error=%d\n", __func__, ret); |
| return ret; |
| } |
| |
| if (!common_op) |
| ret = iwl_tm_execute_cmd(&dev->trans->testmode, cmd_data->cmd, |
| &cmd_data->data_in, |
| &cmd_data->data_out); |
| |
| if (ret) |
| IWL_ERR(dev->trans, "%s ret=%d\n", __func__, ret); |
| else |
| IWL_DEBUG_INFO(dev->trans, "%s ended Ok\n", __func__); |
| return ret; |
| } |
| |
| /** |
| * iwl_tm_mem_dump() - Returns memory buffer data |
| * @dev: testmode device struct |
| * @data_in: input data |
| * @data_out: Dump data |
| * Returns: an error code |
| */ |
| static int iwl_tm_mem_dump(struct iwl_tm_gnl_dev *dev, |
| struct iwl_tm_data *data_in, |
| struct iwl_tm_data *data_out) |
| { |
| int ret; |
| |
| ret = iwl_tm_validate_sram_read_req(dev, data_in); |
| if (ret) |
| return ret; |
| |
| return iwl_tm_execute_cmd(&dev->trans->testmode, |
| IWL_TM_USER_CMD_SRAM_READ, |
| data_in, data_out); |
| } |
| |
| /** |
| * iwl_tm_trace_dump - Returns trace buffer data |
| * @dev: Device pointer |
| * @data_out: Dump data |
| * Returns: an error code |
| */ |
| static int iwl_tm_trace_dump(struct iwl_tm_gnl_dev *dev, |
| struct iwl_tm_data *data_out) |
| { |
| int ret; |
| u32 buf_size; |
| |
| if (!(dev->dnt->iwl_dnt_status & IWL_DNT_STATUS_MON_CONFIGURED)) { |
| IWL_ERR(dev->trans, "Invalid monitor status\n"); |
| return -EINVAL; |
| } |
| |
| if (dev->dnt->mon_buf_size == 0) { |
| IWL_ERR(dev->trans, "No available monitor buffer\n"); |
| return -ENOMEM; |
| } |
| |
| buf_size = dev->dnt->mon_buf_size; |
| data_out->data = kmalloc(buf_size, GFP_KERNEL); |
| if (!data_out->data) |
| return -ENOMEM; |
| |
| ret = iwl_dnt_dispatch_pull(dev->trans, data_out->data, |
| buf_size, MONITOR); |
| if (ret < 0) { |
| kfree(data_out->data); |
| return ret; |
| } |
| data_out->len = ret; |
| |
| return 0; |
| } |
| |
| /** |
| * iwl_tm_gnl_command_dump() - Returns dump buffer data |
| * @cmd_data: Pointer to the data of dump command. |
| * Only device name and command index are the relevant input. |
| * Data out is the start address of the buffer, and it's size. |
| * Returns: an error code |
| */ |
| static int iwl_tm_gnl_command_dump(struct iwl_tm_gnl_cmd *cmd_data) |
| { |
| struct iwl_tm_gnl_dev *dev; |
| int ret = 0; |
| |
| mutex_lock(&dev_list_mtx); |
| dev = iwl_tm_gnl_get_dev(cmd_data->dev_name); |
| mutex_unlock(&dev_list_mtx); |
| if (!dev) |
| return -ENODEV; |
| |
| switch (cmd_data->cmd) { |
| |
| case IWL_TM_USER_CMD_TRACE_DUMP: |
| ret = iwl_tm_trace_dump(dev, &cmd_data->data_out); |
| break; |
| |
| case IWL_TM_USER_CMD_SRAM_READ: |
| ret = iwl_tm_mem_dump(dev, |
| &cmd_data->data_in, |
| &cmd_data->data_out); |
| break; |
| |
| default: |
| return -EOPNOTSUPP; |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * iwl_tm_gnl_parse_msg - Extract input cmd data out of netlink attributes |
| * @attrs: Input netlink attributes |
| * @cmd_data: Command |
| * Returns: an error code |
| */ |
| static int iwl_tm_gnl_parse_msg(struct nlattr **attrs, |
| struct iwl_tm_gnl_cmd *cmd_data) |
| { |
| memset(cmd_data, 0 , sizeof(struct iwl_tm_gnl_cmd)); |
| |
| if (!attrs[IWL_TM_GNL_MSG_ATTR_DEVNAME] || |
| !attrs[IWL_TM_GNL_MSG_ATTR_CMD]) |
| return -EINVAL; |
| |
| cmd_data->dev_name = nla_data(attrs[IWL_TM_GNL_MSG_ATTR_DEVNAME]); |
| cmd_data->cmd = nla_get_u32(attrs[IWL_TM_GNL_MSG_ATTR_CMD]); |
| |
| if (attrs[IWL_TM_GNL_MSG_ATTR_DATA]) { |
| cmd_data->data_in.data = |
| nla_data(attrs[IWL_TM_GNL_MSG_ATTR_DATA]); |
| cmd_data->data_in.len = |
| nla_len(attrs[IWL_TM_GNL_MSG_ATTR_DATA]); |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * iwl_tm_gnl_cmd_do() - Executes IWL testmode GNL command |
| * @skb: SKB with the command data |
| * @info: generic netlink info |
| * Returns: an error code |
| */ |
| static int iwl_tm_gnl_cmd_do(struct sk_buff *skb, struct genl_info *info) |
| { |
| struct iwl_tm_gnl_cmd cmd_data; |
| int ret; |
| |
| ret = iwl_tm_gnl_parse_msg(info->attrs, &cmd_data); |
| if (ret) |
| return ret; |
| |
| ret = iwl_tm_gnl_cmd_execute(&cmd_data); |
| if (!ret && cmd_data.data_out.len) { |
| ret = iwl_tm_gnl_reply(info, cmd_data); |
| /* |
| * In this case, data out should be allocated in |
| * iwl_tm_gnl_cmd_execute so it should be freed |
| * here |
| */ |
| kfree(cmd_data.data_out.data); |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * iwl_tm_gnl_dump() - Executes IWL testmode GNL command |
| * @skb: SKB to fill |
| * @cb: netlink callback data |
| * |
| * cb->args[0]: Command number, incremented by one (as it may be zero) |
| * We're keeping the command so we can tell if is it the |
| * first dump call or not. |
| * cb->args[1]: Buffer data to dump |
| * cb->args[2]: Buffer size |
| * cb->args[3]: Buffer offset from where to dump in the next round |
| * Returns: an error code |
| */ |
| static int iwl_tm_gnl_dump(struct sk_buff *skb, struct netlink_callback *cb) |
| { |
| struct iwl_tm_gnl_cmd cmd_data; |
| void *nlmsg_head = NULL; |
| struct nlattr *attrs[IWL_TM_GNL_MSG_ATTR_MAX]; |
| u8 *dump_addr; |
| unsigned long dump_offset; |
| int dump_size, chunk_size, ret; |
| |
| if (!cb->args[0]) { |
| /* |
| * This is the first part of the dump - Parse dump data |
| * out of the data in the netlink header and set up the |
| * dump in cb->args[]. |
| */ |
| ret = nlmsg_parse(cb->nlh, GENL_HDRLEN, attrs, |
| IWL_TM_GNL_MSG_ATTR_MAX - 1, |
| iwl_tm_gnl_msg_policy, NULL); |
| if (ret) |
| return ret; |
| |
| ret = iwl_tm_gnl_parse_msg(attrs, &cmd_data); |
| if (ret) |
| return ret; |
| |
| ret = iwl_tm_gnl_command_dump(&cmd_data); |
| if (ret) |
| return ret; |
| |
| /* Incrementing command since command number may be zero */ |
| cb->args[0] = cmd_data.cmd + 1; |
| cb->args[1] = (unsigned long)cmd_data.data_out.data; |
| cb->args[2] = cmd_data.data_out.len; |
| cb->args[3] = 0; |
| |
| if (!cb->args[2]) |
| return -ENODATA; |
| } |
| |
| dump_addr = (u8 *)cb->args[1]; |
| dump_size = cb->args[2]; |
| dump_offset = cb->args[3]; |
| |
| nlmsg_head = genlmsg_put(skb, NETLINK_CB(cb->skb).portid, |
| cb->nlh->nlmsg_seq, |
| &iwl_tm_gnl_family, NLM_F_MULTI, |
| IWL_TM_GNL_CMD_EXECUTE); |
| |
| /* |
| * Reserve some room for NL attribute header, |
| * 16 bytes should be enough. |
| */ |
| chunk_size = skb_tailroom(skb) - 16; |
| if (chunk_size <= 0) { |
| ret = -ENOMEM; |
| goto dump_err; |
| } |
| |
| if (chunk_size > dump_size - dump_offset) |
| chunk_size = dump_size - dump_offset; |
| |
| if (chunk_size) { |
| ret = nla_put(skb, IWL_TM_GNL_MSG_ATTR_DATA, |
| chunk_size, dump_addr + dump_offset); |
| if (ret) |
| goto dump_err; |
| } |
| |
| genlmsg_end(skb, nlmsg_head); |
| |
| /* move offset */ |
| cb->args[3] += chunk_size; |
| |
| return cb->args[2] - cb->args[3]; |
| |
| dump_err: |
| genlmsg_cancel(skb, nlmsg_head); |
| return ret; |
| } |
| |
| static int iwl_tm_gnl_done(struct netlink_callback *cb) |
| { |
| switch (cb->args[0] - 1) { |
| case IWL_TM_USER_CMD_SRAM_READ: |
| case IWL_TM_USER_CMD_TRACE_DUMP: |
| kfree((void *)cb->args[1]); |
| return 0; |
| } |
| |
| return -EOPNOTSUPP; |
| } |
| |
| static int iwl_tm_gnl_cmd_subscribe(struct sk_buff *skb, struct genl_info *info) |
| { |
| struct iwl_tm_gnl_dev *dev; |
| const char *dev_name; |
| int ret; |
| |
| if (!info->attrs[IWL_TM_GNL_MSG_ATTR_DEVNAME]) |
| return -EINVAL; |
| |
| dev_name = nla_data(info->attrs[IWL_TM_GNL_MSG_ATTR_DEVNAME]); |
| |
| mutex_lock(&dev_list_mtx); |
| dev = iwl_tm_gnl_get_dev(dev_name); |
| if (!dev) { |
| ret = -ENODEV; |
| goto unlock; |
| } |
| |
| if (dev->nl_events_portid) { |
| ret = -EBUSY; |
| goto unlock; |
| } |
| |
| dev->nl_events_portid = info->snd_portid; |
| ret = 0; |
| |
| unlock: |
| mutex_unlock(&dev_list_mtx); |
| return ret; |
| } |
| |
| /* |
| * iwl_tm_gnl_ops - GNL Family commands. |
| * There is only one NL command, and only one callback, |
| * which handles all NL messages. |
| */ |
| static const struct genl_ops iwl_tm_gnl_ops[] = { |
| { |
| .cmd = IWL_TM_GNL_CMD_EXECUTE, |
| #if LINUX_VERSION_IS_GEQ(5,2,0) |
| .validate = GENL_DONT_VALIDATE_STRICT | GENL_DONT_VALIDATE_DUMP, |
| #endif |
| |
| .doit = iwl_tm_gnl_cmd_do, |
| #if CFG80211_VERSION < KERNEL_VERSION(5,2,0) |
| .policy = iwl_tm_gnl_msg_policy, |
| #endif |
| .dumpit = iwl_tm_gnl_dump, |
| .done = iwl_tm_gnl_done, |
| }, |
| { |
| .cmd = IWL_TM_GNL_CMD_SUBSCRIBE_EVENTS, |
| #if LINUX_VERSION_IS_GEQ(5,2,0) |
| .validate = GENL_DONT_VALIDATE_STRICT | GENL_DONT_VALIDATE_DUMP, |
| #endif |
| |
| .doit = iwl_tm_gnl_cmd_subscribe, |
| #if CFG80211_VERSION < KERNEL_VERSION(5,2,0) |
| .policy = iwl_tm_gnl_msg_policy, |
| #endif |
| }, |
| }; |
| |
| static struct genl_family iwl_tm_gnl_family __genl_ro_after_init = { |
| .hdrsize = 0, |
| .name = IWL_TM_GNL_FAMILY_NAME, |
| .version = IWL_TM_GNL_VERSION_NR, |
| .maxattr = IWL_TM_GNL_MSG_ATTR_MAX, |
| #if CFG80211_VERSION >= KERNEL_VERSION(5,2,0) |
| .policy = iwl_tm_gnl_msg_policy, |
| #endif |
| .module = THIS_MODULE, |
| .ops = iwl_tm_gnl_ops, |
| .n_ops = ARRAY_SIZE(iwl_tm_gnl_ops), |
| #if LINUX_VERSION_IS_GEQ(6,1,0) |
| .resv_start_op = IWL_TM_GNL_CMD_SUBSCRIBE_EVENTS + 1, |
| #endif |
| .mcgrps = iwl_tm_gnl_mcgrps, |
| .n_mcgrps = ARRAY_SIZE(iwl_tm_gnl_mcgrps), |
| }; |
| |
| /** |
| * iwl_tm_gnl_add() - Registers a devices/op-mode in the iwl-tm-gnl list |
| * @trans: transport struct for the device to register for |
| */ |
| void iwl_tm_gnl_add(struct iwl_trans *trans) |
| { |
| struct iwl_tm_gnl_dev *dev; |
| |
| if (!trans) |
| return; |
| |
| if (trans->tmdev) |
| return; |
| |
| mutex_lock(&dev_list_mtx); |
| |
| if (iwl_tm_gnl_get_dev(dev_name(trans->dev))) |
| goto unlock; |
| |
| dev = kzalloc(sizeof(*dev), GFP_KERNEL); |
| if (!dev) |
| goto unlock; |
| |
| dev->dev_name = dev_name(trans->dev); |
| trans->tmdev = dev; |
| dev->trans = trans; |
| list_add_tail_rcu(&dev->list, &dev_list); |
| |
| unlock: |
| mutex_unlock(&dev_list_mtx); |
| } |
| |
| /** |
| * iwl_tm_gnl_remove() - Unregisters a devices/op-mode in the iwl-tm-gnl list |
| * @trans: transport struct for the device |
| */ |
| void iwl_tm_gnl_remove(struct iwl_trans *trans) |
| { |
| struct iwl_tm_gnl_dev *dev_itr, *tmp; |
| |
| if (WARN_ON_ONCE(!trans)) |
| return; |
| |
| /* Searching for operation mode in list */ |
| mutex_lock(&dev_list_mtx); |
| list_for_each_entry_safe(dev_itr, tmp, &dev_list, list) { |
| if (dev_itr->trans == trans) { |
| /* |
| * Device found. Removing it from list |
| * and releasing it's resources |
| */ |
| list_del_rcu(&dev_itr->list); |
| synchronize_rcu(); |
| kfree(dev_itr); |
| break; |
| } |
| } |
| |
| trans->tmdev = NULL; |
| mutex_unlock(&dev_list_mtx); |
| } |
| |
| static int iwl_tm_gnl_netlink_notify(struct notifier_block *nb, |
| unsigned long state, |
| void *_notify) |
| { |
| struct netlink_notify *notify = _notify; |
| struct iwl_tm_gnl_dev *dev; |
| |
| rcu_read_lock(); |
| list_for_each_entry_rcu(dev, &dev_list, list) { |
| if (dev->nl_events_portid == notify->portid) |
| dev->nl_events_portid = 0; |
| } |
| rcu_read_unlock(); |
| |
| return NOTIFY_OK; |
| } |
| |
| static struct notifier_block iwl_tm_gnl_netlink_notifier = { |
| .notifier_call = iwl_tm_gnl_netlink_notify, |
| }; |
| |
| |
| /** |
| * iwl_tm_gnl_init() - Registers tm-gnl module |
| * Returns: an error code |
| * |
| * Registers Testmode GNL family and initializes |
| * TM GNL global variables |
| */ |
| int iwl_tm_gnl_init(void) |
| { |
| int ret; |
| |
| INIT_LIST_HEAD(&dev_list); |
| mutex_init(&dev_list_mtx); |
| |
| ret = genl_register_family(&iwl_tm_gnl_family); |
| if (ret) |
| return ret; |
| ret = netlink_register_notifier(&iwl_tm_gnl_netlink_notifier); |
| if (ret) |
| genl_unregister_family(&iwl_tm_gnl_family); |
| return ret; |
| } |
| |
| /** |
| * iwl_tm_gnl_exit() - Unregisters Testmode GNL family |
| * Returns: an error code |
| */ |
| int iwl_tm_gnl_exit(void) |
| { |
| netlink_unregister_notifier(&iwl_tm_gnl_netlink_notifier); |
| return genl_unregister_family(&iwl_tm_gnl_family); |
| } |
| |
| /** |
| * iwl_tm_gnl_send_rx - Send a spontaneous rx message to user |
| * @trans: Pointer to the transport layer |
| * @rxb: Contains rx packet to be sent |
| */ |
| void iwl_tm_gnl_send_rx(struct iwl_trans *trans, struct iwl_rx_cmd_buffer *rxb) |
| { |
| struct iwl_rx_packet *pkt = rxb_addr(rxb); |
| int length = iwl_rx_packet_len(pkt); |
| |
| /* the length doesn't include len_n_flags field, so add it manually */ |
| length += sizeof(__le32); |
| |
| iwl_tm_gnl_send_msg(trans, IWL_TM_USER_CMD_NOTIF_UCODE_RX_PKT, true, |
| (void *)pkt, length, GFP_ATOMIC); |
| } |
| IWL_EXPORT_SYMBOL(iwl_tm_gnl_send_rx); |