diff options
Diffstat (limited to 'scsinvme.cpp')
-rw-r--r-- | scsinvme.cpp | 441 |
1 files changed, 441 insertions, 0 deletions
diff --git a/scsinvme.cpp b/scsinvme.cpp new file mode 100644 index 0000000..3f23da4 --- /dev/null +++ b/scsinvme.cpp @@ -0,0 +1,441 @@ +/* + * scsinvme.cpp + * + * Home page of code is: https://www.smartmontools.org + * + * Copyright (C) 2020-21 Christian Franke + * Copyright (C) 2018 Harry Mallon <hjmallon@gmail.com> + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "config.h" + +#include "dev_interface.h" +#include "dev_tunnelled.h" +#include "nvmecmds.h" +#include "scsicmds.h" +#include "sg_unaligned.h" +#include "utility.h" + +#include <errno.h> + +const char * scsinvme_cpp_svnid = "$Id: scsinvme.cpp 5337 2022-02-27 07:53:55Z dpgilbert $"; + +// SNT (SCSI NVMe Translation) namespace and prefix +namespace snt { + +///////////////////////////////////////////////////////////////////////////// +// sntasmedia_device + +class sntasmedia_device +: public tunnelled_device< + /*implements*/ nvme_device, + /*by tunnelling through a*/ scsi_device + > +{ +public: + sntasmedia_device(smart_interface * intf, scsi_device * scsidev, + const char * req_type, unsigned nsid); + + virtual ~sntasmedia_device(); + + virtual bool nvme_pass_through(const nvme_cmd_in & in, nvme_cmd_out & out) override; +}; + +sntasmedia_device::sntasmedia_device(smart_interface * intf, scsi_device * scsidev, + const char * req_type, unsigned nsid) +: smart_device(intf, scsidev->get_dev_name(), "sntasmedia", req_type), + tunnelled_device<nvme_device, scsi_device>(scsidev, nsid) +{ + set_info().info_name = strprintf("%s [USB NVMe ASMedia]", scsidev->get_info_name()); +} + +sntasmedia_device::~sntasmedia_device() +{ +} + +bool sntasmedia_device::nvme_pass_through(const nvme_cmd_in & in, nvme_cmd_out & /* out */) +{ + unsigned size = in.size; + unsigned cdw10_hi = in.cdw10 >> 16; + switch (in.opcode) { + case smartmontools::nvme_admin_identify: + if (in.cdw10 == 0x0000001) // Identify controller + break; + if (in.cdw10 == 0x0000000) { // Identify namespace + if (in.nsid == 1) + break; + return set_err(ENOSYS, "NVMe Identify Namespace 0x%x not supported", in.nsid); + } + return set_err(ENOSYS, "NVMe Identify with CDW10=0x%08x not supported", in.cdw10); + case smartmontools::nvme_admin_get_log_page: + if (!(in.nsid == 0xffffffff || !in.nsid)) + return set_err(ENOSYS, "NVMe Get Log Page with NSID=0x%x not supported", in.nsid); + if (size > 0x200) { // Reading more results in command timeout + // TODO: Add ability to return short reads to caller + size = 0x200; + cdw10_hi = (size / 4) - 1; + pout("Warning: NVMe Get Log truncated to 0x%03x bytes, 0x%03x bytes zero filled\n", size, in.size - size); + } + break; + default: + return set_err(ENOSYS, "NVMe admin command 0x%02x not supported", in.opcode); + break; + } + if (in.cdw11 || in.cdw12 || in.cdw13 || in.cdw14 || in.cdw15) + return set_err(ENOSYS, "Nonzero NVMe command dwords 11-15 not supported"); + + uint8_t cdb[16] = {0, }; + cdb[0] = 0xe6; + cdb[1] = in.opcode; + //cdb[2] = ? + cdb[3] = (uint8_t)in.cdw10; + //cdb[4..6] = ? + cdb[7] = (uint8_t)cdw10_hi; + //cdb[8..15] = ? + + scsi_cmnd_io io_hdr = {}; + io_hdr.cmnd = cdb; + io_hdr.cmnd_len = sizeof(cdb); + io_hdr.dxfer_dir = DXFER_FROM_DEVICE; + io_hdr.dxferp = (uint8_t *)in.buffer; + io_hdr.dxfer_len = size; + memset(in.buffer, 0, in.size); + + scsi_device * scsidev = get_tunnel_dev(); + if (!scsidev->scsi_pass_through_and_check(&io_hdr, "sntasmedia_device::nvme_pass_through: ")) + return set_err(scsidev->get_err()); + + //out.result = ?; + return true; +} + +///////////////////////////////////////////////////////////////////////////// +// sntjmicron_device + +#define SNT_JMICRON_NVME_SIGNATURE 0x454d564eu // 'NVME' reversed (little endian) +#define SNT_JMICRON_CDB_LEN 12 +#define SNT_JMICRON_NVM_CMD_LEN 512 + +class sntjmicron_device +: public tunnelled_device< + /*implements*/ nvme_device, + /*by tunnelling through a*/ scsi_device + > +{ +public: + sntjmicron_device(smart_interface * intf, scsi_device * scsidev, + const char * req_type, unsigned nsid); + + virtual ~sntjmicron_device(); + + virtual bool open() override; + + virtual bool nvme_pass_through(const nvme_cmd_in & in, nvme_cmd_out & out) override; + +private: + enum { + proto_nvm_cmd = 0x0, proto_non_data = 0x1, proto_dma_in = 0x2, + proto_dma_out = 0x3, proto_response = 0xF + }; +}; + +sntjmicron_device::sntjmicron_device(smart_interface * intf, scsi_device * scsidev, + const char * req_type, unsigned nsid) +: smart_device(intf, scsidev->get_dev_name(), "sntjmicron", req_type), + tunnelled_device<nvme_device, scsi_device>(scsidev, nsid) +{ + set_info().info_name = strprintf("%s [USB NVMe JMicron]", scsidev->get_info_name()); +} + +sntjmicron_device::~sntjmicron_device() +{ +} + +bool sntjmicron_device::open() +{ + // Open USB first + if (!tunnelled_device<nvme_device, scsi_device>::open()) + return false; + + // No sure how multiple namespaces come up on device so we + // cannot detect e.g. /dev/sdX is NSID 2. + // Set to broadcast if not available + if (!get_nsid()) { + set_nsid(0xFFFFFFFF); + } + + return true; +} + +// cdb[0]: ATA PASS THROUGH (12) SCSI command opcode byte (0xa1) +// cdb[1]: [ is admin cmd: 1 ] [ protocol : 7 ] +// cdb[2]: reserved +// cdb[3]: parameter list length (23:16) +// cdb[4]: parameter list length (15:08) +// cdb[5]: parameter list length (07:00) +// cdb[6]: reserved +// cdb[7]: reserved +// cdb[8]: reserved +// cdb[9]: reserved +// cdb[10]: reserved +// cdb[11]: CONTROL (?) +bool sntjmicron_device::nvme_pass_through(const nvme_cmd_in & in, nvme_cmd_out & out) +{ + /* Only admin commands used */ + constexpr bool admin = true; + + // 1: "NVM Command Set Payload" + { + unsigned char cdb[SNT_JMICRON_CDB_LEN] = { 0 }; + cdb[0] = SAT_ATA_PASSTHROUGH_12; + cdb[1] = (admin ? 0x80 : 0x00) | proto_nvm_cmd; + sg_put_unaligned_be24(SNT_JMICRON_NVM_CMD_LEN, &cdb[3]); + + unsigned nvm_cmd[SNT_JMICRON_NVM_CMD_LEN / sizeof(unsigned)] = { 0 }; + nvm_cmd[0] = SNT_JMICRON_NVME_SIGNATURE; + // nvm_cmd[1]: reserved + nvm_cmd[2] = in.opcode; // More of CDW0 may go in here in future + nvm_cmd[3] = in.nsid; + // nvm_cmd[4-5]: reserved + // nvm_cmd[6-7]: metadata pointer + // nvm_cmd[8-11]: data ptr (?) + nvm_cmd[12] = in.cdw10; + nvm_cmd[13] = in.cdw11; + nvm_cmd[14] = in.cdw12; + nvm_cmd[15] = in.cdw13; + nvm_cmd[16] = in.cdw14; + nvm_cmd[17] = in.cdw15; + // nvm_cmd[18-127]: reserved + + if (isbigendian()) + for (unsigned i = 0; i < (SNT_JMICRON_NVM_CMD_LEN / sizeof(uint32_t)); i++) + swapx(&nvm_cmd[i]); + + scsi_cmnd_io io_nvm = {}; + + io_nvm.cmnd = cdb; + io_nvm.cmnd_len = SNT_JMICRON_CDB_LEN; + io_nvm.dxfer_dir = DXFER_TO_DEVICE; + io_nvm.dxferp = (uint8_t *)nvm_cmd; + io_nvm.dxfer_len = SNT_JMICRON_NVM_CMD_LEN; + + scsi_device * scsidev = get_tunnel_dev(); + if (!scsidev->scsi_pass_through_and_check(&io_nvm, + "sntjmicron_device::nvme_pass_through:NVM: ")) + return set_err(scsidev->get_err()); + } + + // 2: DMA or Non-Data + { + unsigned char cdb[SNT_JMICRON_CDB_LEN] = { 0 }; + cdb[0] = SAT_ATA_PASSTHROUGH_12; + + scsi_cmnd_io io_data = {}; + io_data.cmnd = cdb; + io_data.cmnd_len = SNT_JMICRON_CDB_LEN; + + switch (in.direction()) { + case nvme_cmd_in::no_data: + cdb[1] = (admin ? 0x80 : 0x00) | proto_non_data; + io_data.dxfer_dir = DXFER_NONE; + break; + case nvme_cmd_in::data_out: + cdb[1] = (admin ? 0x80 : 0x00) | proto_dma_out; + sg_put_unaligned_be24(in.size, &cdb[3]); + io_data.dxfer_dir = DXFER_TO_DEVICE; + io_data.dxferp = (uint8_t *)in.buffer; + io_data.dxfer_len = in.size; + break; + case nvme_cmd_in::data_in: + cdb[1] = (admin ? 0x80 : 0x00) | proto_dma_in; + sg_put_unaligned_be24(in.size, &cdb[3]); + io_data.dxfer_dir = DXFER_FROM_DEVICE; + io_data.dxferp = (uint8_t *)in.buffer; + io_data.dxfer_len = in.size; + memset(in.buffer, 0, in.size); + break; + case nvme_cmd_in::data_io: + default: + return set_err(EINVAL); + } + + scsi_device * scsidev = get_tunnel_dev(); + if (!scsidev->scsi_pass_through_and_check(&io_data, + "sntjmicron_device::nvme_pass_through:Data: ")) + return set_err(scsidev->get_err()); + } + + // 3: "Return Response Information" + { + unsigned char cdb[SNT_JMICRON_CDB_LEN] = { 0 }; + cdb[0] = SAT_ATA_PASSTHROUGH_12; + cdb[1] = (admin ? 0x80 : 0x00) | proto_response; + sg_put_unaligned_be24(SNT_JMICRON_NVM_CMD_LEN, &cdb[3]); + + unsigned nvm_reply[SNT_JMICRON_NVM_CMD_LEN / sizeof(unsigned)] = { 0 }; + + scsi_cmnd_io io_reply = {}; + + io_reply.cmnd = cdb; + io_reply.cmnd_len = SNT_JMICRON_CDB_LEN; + io_reply.dxfer_dir = DXFER_FROM_DEVICE; + io_reply.dxferp = (uint8_t *)nvm_reply; + io_reply.dxfer_len = SNT_JMICRON_NVM_CMD_LEN; + + scsi_device * scsidev = get_tunnel_dev(); + if (!scsidev->scsi_pass_through_and_check(&io_reply, + "sntjmicron_device::nvme_pass_through:Reply: ")) + return set_err(scsidev->get_err()); + + if (isbigendian()) + for (unsigned i = 0; i < (SNT_JMICRON_NVM_CMD_LEN / sizeof(uint32_t)); i++) + swapx(&nvm_reply[i]); + + if (nvm_reply[0] != SNT_JMICRON_NVME_SIGNATURE) + return set_err(EIO, "Out of spec JMicron NVMe reply"); + + int status = nvm_reply[5] >> 17; + + if (status > 0) + return set_nvme_err(out, status); + + out.result = nvm_reply[2]; + } + + return true; +} + +///////////////////////////////////////////////////////////////////////////// +// sntrealtek_device + +class sntrealtek_device +: public tunnelled_device< + /*implements*/ nvme_device, + /*by tunnelling through a*/ scsi_device + > +{ +public: + sntrealtek_device(smart_interface * intf, scsi_device * scsidev, + const char * req_type, unsigned nsid); + + virtual ~sntrealtek_device(); + + virtual bool nvme_pass_through(const nvme_cmd_in & in, nvme_cmd_out & out) override; +}; + +sntrealtek_device::sntrealtek_device(smart_interface * intf, scsi_device * scsidev, + const char * req_type, unsigned nsid) +: smart_device(intf, scsidev->get_dev_name(), "sntrealtek", req_type), + tunnelled_device<nvme_device, scsi_device>(scsidev, nsid) +{ + set_info().info_name = strprintf("%s [USB NVMe Realtek]", scsidev->get_info_name()); +} + +sntrealtek_device::~sntrealtek_device() +{ +} + +bool sntrealtek_device::nvme_pass_through(const nvme_cmd_in & in, nvme_cmd_out & /* out */) +{ + unsigned size = in.size; + switch (in.opcode) { + case smartmontools::nvme_admin_identify: + if (in.cdw10 == 0x0000001) // Identify controller + break; + if (in.cdw10 == 0x0000000) { // Identify namespace + if (in.nsid == 1) + break; + return set_err(ENOSYS, "NVMe Identify Namespace 0x%x not supported", in.nsid); + } + return set_err(ENOSYS, "NVMe Identify with CDW10=0x%08x not supported", in.cdw10); + case smartmontools::nvme_admin_get_log_page: + if (!(in.nsid == 0xffffffff || !in.nsid)) + return set_err(ENOSYS, "NVMe Get Log Page with NSID=0x%x not supported", in.nsid); + if (size > 0x200) { // Reading more apparently returns old data from previous command + // TODO: Add ability to return short reads to caller + size = 0x200; + pout("Warning: NVMe Get Log truncated to 0x%03x bytes, 0x%03x bytes zero filled\n", size, in.size - size); + } + break; + default: + return set_err(ENOSYS, "NVMe admin command 0x%02x not supported", in.opcode); + break; + } + if (in.cdw11 || in.cdw12 || in.cdw13 || in.cdw14 || in.cdw15) + return set_err(ENOSYS, "Nonzero NVMe command dwords 11-15 not supported"); + + uint8_t cdb[16] = {0, }; + cdb[0] = 0xe4; + sg_put_unaligned_le16(size, cdb+1); + cdb[3] = in.opcode; + cdb[4] = (uint8_t)in.cdw10; + + scsi_cmnd_io io_hdr = {}; + io_hdr.cmnd = cdb; + io_hdr.cmnd_len = sizeof(cdb); + io_hdr.dxfer_dir = DXFER_FROM_DEVICE; + io_hdr.dxferp = (uint8_t *)in.buffer; + io_hdr.dxfer_len = size; + memset(in.buffer, 0, in.size); + + scsi_device * scsidev = get_tunnel_dev(); + if (!scsidev->scsi_pass_through_and_check(&io_hdr, "sntrealtek_device::nvme_pass_through: ")) + return set_err(scsidev->get_err()); + + //out.result = ?; // TODO + return true; +} + + +} // namespace snt + +using namespace snt; + +nvme_device * smart_interface::get_snt_device(const char * type, scsi_device * scsidev) +{ + if (!scsidev) + throw std::logic_error("smart_interface: get_snt_device() called with scsidev=0"); + + // Take temporary ownership of 'scsidev' to delete it on error + scsi_device_auto_ptr scsidev_holder(scsidev); + nvme_device * sntdev = 0; + + // TODO: Remove this and adjust drivedb entry accordingly when no longer EXPERIMENTAL + if (!strcmp(type, "sntjmicron#please_try")) { + set_err(EINVAL, "USB to NVMe bridge [please try '-d sntjmicron' and report result to: " + PACKAGE_BUGREPORT "]"); + return 0; + } + + if (!strcmp(type, "sntasmedia")) { + // No namespace supported + sntdev = new sntasmedia_device(this, scsidev, type, 0xffffffff); + } + + else if (!strncmp(type, "sntjmicron", 10)) { + int n1 = -1, n2 = -1, len = strlen(type); + unsigned nsid = 0; // invalid namespace id -> use default + sscanf(type, "sntjmicron%n,0x%x%n", &n1, &nsid, &n2); + if (!(n1 == len || n2 == len)) { + set_err(EINVAL, "Invalid NVMe namespace id in '%s'", type); + return 0; + } + sntdev = new sntjmicron_device(this, scsidev, type, nsid); + } + + else if (!strcmp(type, "sntrealtek")) { + // No namespace supported + sntdev = new sntrealtek_device(this, scsidev, type, 0xffffffff); + } + + else { + set_err(EINVAL, "Unknown SNT device type '%s'", type); + return 0; + } + + // 'scsidev' is now owned by 'sntdev' + scsidev_holder.release(); + return sntdev; +} |