/* * Copyright (c) 2016-2021 Chuck Tuffli * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include #include "libsmart.h" #include "libsmart_priv.h" #include "libsmart_dev.h" /* Provide compatibility for FreeBSD 11.0 */ #if (__FreeBSD_version < 1101000) struct scsi_log_informational_exceptions { struct scsi_log_param_header hdr; #define SLP_IE_GEN 0x0000 uint8_t ie_asc; uint8_t ie_ascq; uint8_t temperature; }; #endif struct fbsd_smart { smart_t common; struct cam_device *camdev; }; static smart_protocol_e __device_get_proto(struct fbsd_smart *); static bool __device_proto_tunneled(struct fbsd_smart *); static int32_t __device_get_info(struct fbsd_smart *); smart_h device_open(smart_protocol_e protocol, char *devname) { struct fbsd_smart *h = NULL; h = malloc(sizeof(struct fbsd_smart)); if (h == NULL) return NULL; memset(h, 0, sizeof(struct fbsd_smart)); h->common.protocol = SMART_PROTO_MAX; h->camdev = cam_open_device(devname, O_RDWR); if (h->camdev == NULL) { printf("%s: error opening %s - %s\n", __func__, devname, cam_errbuf); free(h); h = NULL; } else { smart_protocol_e proto = __device_get_proto(h); if ((protocol == SMART_PROTO_AUTO) || (protocol == proto)) { h->common.protocol = proto; } else { printf("%s: protocol mismatch %d vs %d\n", __func__, protocol, proto); } if (proto == SMART_PROTO_SCSI) { if (__device_proto_tunneled(h)) { h->common.protocol = SMART_PROTO_ATA; h->common.info.tunneled = 1; } } __device_get_info(h); } return h; } void device_close(smart_h h) { struct fbsd_smart *fsmart = h; if (fsmart != NULL) { if (fsmart->camdev != NULL) { cam_close_device(fsmart->camdev); } free(fsmart); } } static const uint8_t smart_read_data[] = { 0xb0, 0xd0, 0x00, 0x4f, 0xc2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const uint8_t smart_return_status[] = { 0xb0, 0xda, 0x00, 0x4f, 0xc2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static int32_t __device_read_ata(smart_h h, uint32_t page, void *buf, size_t bsize, union ccb *ccb) { struct fbsd_smart *fsmart = h; const uint8_t *smart_fis; uint32_t smart_fis_size = 0; uint32_t flags = 0; uint16_t sector_count = 0; uint8_t protocol = 0; switch (page) { case PAGE_ID_ATA_SMART_READ_DATA: /* Support SMART READ DATA */ smart_fis = smart_read_data; smart_fis_size = sizeof(smart_read_data); flags = CAM_DIR_IN; sector_count = 1; protocol = AP_PROTO_PIO_IN; break; case PAGE_ID_ATA_SMART_RET_STATUS: /* Support SMART RETURN STATUS */ smart_fis = smart_return_status; smart_fis_size = sizeof(smart_return_status); /* Command has no data but uses the return status */ flags = CAM_DIR_NONE; protocol = AP_PROTO_NON_DATA; bsize = 0; break; default: return EINVAL; } if (fsmart->common.info.tunneled) { struct ata_pass_16 *cdb; uint8_t cdb_flags; if (bsize > 0) { cdb_flags = AP_FLAG_TDIR_FROM_DEV | AP_FLAG_BYT_BLOK_BLOCKS | AP_FLAG_TLEN_SECT_CNT; } else { cdb_flags = AP_FLAG_CHK_COND | AP_FLAG_TDIR_FROM_DEV | AP_FLAG_BYT_BLOK_BLOCKS; } cdb = (struct ata_pass_16 *)ccb->csio.cdb_io.cdb_bytes; memset(cdb, 0, sizeof(*cdb)); scsi_ata_pass_16(&ccb->csio, /*retries*/ 1, /*cbfcnp*/ NULL, /*flags*/ flags, /*tag_action*/ MSG_SIMPLE_Q_TAG, /*protocol*/ protocol, /*ata_flags*/ cdb_flags, /*features*/ page, /*sector_count*/sector_count, /*lba*/ 0, /*command*/ ATA_SMART_CMD, /*control*/ 0, /*data_ptr*/ buf, /*dxfer_len*/ bsize, /*sense_len*/ SSD_FULL_SIZE, /*timeout*/ 5000 ); cdb->lba_mid = 0x4f; cdb->lba_high = 0xc2; cdb->device = 0; /* scsi_ata_pass_16() sets this */ } else { memcpy(&ccb->ataio.cmd.command, smart_fis, smart_fis_size); cam_fill_ataio(&ccb->ataio, /* retries */1, /* cbfcnp */NULL, /* flags */flags, /* tag_action */0, /* data_ptr */buf, /* dxfer_len */bsize, /* timeout */5000); ccb->ataio.cmd.flags |= CAM_ATAIO_NEEDRESULT; ccb->ataio.cmd.control = 0; } return 0; } static int32_t __device_read_scsi(__attribute__((unused)) smart_h h, uint32_t page, void *buf, size_t bsize, union ccb *ccb) { scsi_log_sense(&ccb->csio, /* retries */1, /* cbfcnp */NULL, /* tag_action */0, /* page_code */SLS_PAGE_CTRL_CUMULATIVE, /* page */page, /* save_pages */0, /* ppc */0, /* paramptr */0, /* param_buf */buf, /* param_len */bsize, /* sense_len */0, /* timeout */5000); return 0; } static int32_t __device_read_nvme(__attribute__((unused)) smart_h h, uint32_t page, void *buf, size_t bsize, union ccb *ccb) { struct ccb_nvmeio *nvmeio = &ccb->nvmeio; uint32_t numd = 0; /* number of dwords */ /* * NVME CAM passthru * 1200000 > version > 1101510 uses nvmeio->cmd.opc * 1200059 > version > 1200038 uses nvmeio->cmd.opc * 1200081 > version > 1200058 uses nvmeio->cmd.opc_fuse * > 1200080 uses nvmeio->cmd.opc * This code doesn't support the brief 'opc_fuse' period. */ #if ((__FreeBSD_version > 1200038) || ((__FreeBSD_version > 1101510) && (__FreeBSD_version < 1200000))) switch (page) { case NVME_LOG_HEALTH_INFORMATION: numd = (sizeof(struct nvme_health_information_page) / sizeof(uint32_t)); break; default: /* Unsupported log page */ return EINVAL; } /* Subtract 1 because NUMD is a zero based value */ numd--; nvmeio->cmd.opc = NVME_OPC_GET_LOG_PAGE; nvmeio->cmd.nsid = NVME_GLOBAL_NAMESPACE_TAG; nvmeio->cmd.cdw10 = page | (numd << 16); cam_fill_nvmeadmin(&ccb->nvmeio, /* retries */1, /* cbfcnp */NULL, /* flags */CAM_DIR_IN, /* data_ptr */buf, /* dxfer_len */bsize, /* timeout */5000); #endif return 0; } /* * Retrieve the SMART RETURN STATUS * * SMART RETURN STATUS provides the reliability status of the * device and can be used as a high-level indication of health. */ static int32_t __device_status_ata(smart_h h, union ccb *ccb) { struct fbsd_smart *fsmart = h; uint8_t *buf = NULL; uint32_t page = 0; uint8_t lba_high = 0, lba_mid = 0, device = 0, status = 0; if (fsmart->common.info.tunneled) { struct ata_res_pass16 { u_int16_t reserved[5]; u_int8_t flags; u_int8_t error; u_int8_t sector_count_exp; u_int8_t sector_count; u_int8_t lba_low_exp; u_int8_t lba_low; u_int8_t lba_mid_exp; u_int8_t lba_mid; u_int8_t lba_high_exp; u_int8_t lba_high; u_int8_t device; u_int8_t status; } *res_pass16 = (struct ata_res_pass16 *)(uintptr_t) &ccb->csio.sense_data; buf = ccb->csio.data_ptr; page = ((struct ata_pass_16 *)ccb->csio.cdb_io.cdb_bytes)->features; lba_high = res_pass16->lba_high; lba_mid = res_pass16->lba_mid; device = res_pass16->device; status = res_pass16->status; /* * Note that this generates an expected CHECK CONDITION. * Mask it so the outer function doesn't print an error * message. */ ccb->ccb_h.status &= ~CAM_STATUS_MASK; ccb->ccb_h.status |= CAM_REQ_CMP; } else { struct ccb_ataio *ataio = (struct ccb_ataio *)&ccb->ataio; buf = ataio->data_ptr; page = ataio->cmd.features; lba_high = ataio->res.lba_high; lba_mid = ataio->res.lba_mid; device = ataio->res.device; status = ataio->res.status; } switch (page) { case PAGE_ID_ATA_SMART_RET_STATUS: /* * Typically, SMART related log pages return data, but this * command is different in that the data is encoded in the * result registers. * * Handle this in a UNIX-like way by writing a 0 (no errors) * or 1 (threshold exceeded condition) to the output buffer. */ dprintf("SMART_RET_STATUS: lba mid=%#x high=%#x device=%#x status=%#x\n", lba_mid, lba_high, device, status); if ((lba_high == 0x2c) && (lba_mid == 0xf4)) { buf[0] = 1; } else if ((lba_high == 0xc2) && (lba_mid == 0x4f)) { buf[0] = 0; } else { /* Ruh-roh ... */ buf[0] = 255; } break; default: ; } return 0; } int32_t device_read_log(smart_h h, uint32_t page, void *buf, size_t bsize) { struct fbsd_smart *fsmart = h; union ccb *ccb = NULL; int rc = 0; if (fsmart == NULL) return EINVAL; dprintf("read log page %#x\n", page); ccb = cam_getccb(fsmart->camdev); if (ccb == NULL) return ENOMEM; CCB_CLEAR_ALL_EXCEPT_HDR(ccb); switch (fsmart->common.protocol) { case SMART_PROTO_ATA: rc = __device_read_ata(h, page, buf, bsize, ccb); break; case SMART_PROTO_SCSI: rc = __device_read_scsi(h, page, buf, bsize, ccb); break; case SMART_PROTO_NVME: rc = __device_read_nvme(h, page, buf, bsize, ccb); break; default: warnx("unsupported protocol %d", fsmart->common.protocol); cam_freeccb(ccb); return ENODEV; } if (rc) { if (rc == EINVAL) warnx("unsupported page %#x", page); return rc; } if (((rc = cam_send_ccb(fsmart->camdev, ccb)) < 0) || ((ccb->ccb_h.status & CAM_STATUS_MASK) != CAM_REQ_CMP)) { if (rc < 0) warn("error sending command"); } /* * Most commands don't need any post-processing. But then there's * ATA. It's why we can't have nice things :( */ switch (fsmart->common.protocol) { case SMART_PROTO_ATA: __device_status_ata(h, ccb); break; default: ; } if ((ccb->ccb_h.status & CAM_STATUS_MASK) != CAM_REQ_CMP) { cam_error_print(fsmart->camdev, ccb, CAM_ESF_ALL, CAM_EPF_ALL, stderr); } cam_freeccb(ccb); return 0; } /* * The SCSI / ATA Translation (SAT) requires devices to support the ATA * Information VPD Page (T10/2126-D Revision 04). Use the existence of * this page to identify tunneled devices. */ static bool __device_proto_tunneled(struct fbsd_smart *fsmart) { union ccb *ccb = NULL; struct scsi_vpd_supported_page_list supportedp; uint32_t i; bool is_tunneled = false; if (fsmart->common.protocol != SMART_PROTO_SCSI) { return false; } ccb = cam_getccb(fsmart->camdev); if (!ccb) { warn("Allocation failure ccb=%p", ccb); goto __device_proto_tunneled_out; } scsi_inquiry(&ccb->csio, 3, // retries NULL, // callback function MSG_SIMPLE_Q_TAG, // tag action (uint8_t *)&supportedp, sizeof(struct scsi_vpd_supported_page_list), 1, // EVPD SVPD_SUPPORTED_PAGE_LIST, // page code SSD_FULL_SIZE, // sense length 5000); // timeout ccb->ccb_h.flags |= CAM_DEV_QFRZDIS; if ((cam_send_ccb(fsmart->camdev, ccb) >= 0) && ((ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP)) { dprintf("Looking for page %#x (total = %u):\n", SVPD_ATA_INFORMATION, supportedp.length); for (i = 0; i < supportedp.length; i++) { dprintf("\t[%u] = %#x\n", i, supportedp.list[i]); if (supportedp.list[i] == SVPD_ATA_INFORMATION) { is_tunneled = true; break; } } } cam_freeccb(ccb); __device_proto_tunneled_out: return is_tunneled; } /** * Retrieve the device protocol type via the transport settings * * @return protocol type or SMART_PROTO_MAX on error */ static smart_protocol_e __device_get_proto(struct fbsd_smart *fsmart) { smart_protocol_e proto = SMART_PROTO_MAX; union ccb *ccb; if (!fsmart || !fsmart->camdev) { warn("Bad handle %p", fsmart); return proto; } ccb = cam_getccb(fsmart->camdev); if (ccb != NULL) { CCB_CLEAR_ALL_EXCEPT_HDR(&ccb->cts); ccb->ccb_h.func_code = XPT_GET_TRAN_SETTINGS; ccb->cts.type = CTS_TYPE_CURRENT_SETTINGS; if (cam_send_ccb(fsmart->camdev, ccb) >= 0) { if ((ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) { struct ccb_trans_settings *cts = &ccb->cts; switch (cts->protocol) { case PROTO_ATA: proto = SMART_PROTO_ATA; break; case PROTO_SCSI: proto = SMART_PROTO_SCSI; break; case PROTO_NVME: proto = SMART_PROTO_NVME; break; default: printf("%s: unknown protocol %d\n", __func__, cts->protocol); } } } cam_freeccb(ccb); } return proto; } static int32_t __device_info_ata(struct fbsd_smart *fsmart, struct ccb_getdev *cgd) { smart_info_t *sinfo = NULL; if (!fsmart || !cgd) { return -1; } sinfo = &fsmart->common.info; sinfo->supported = cgd->ident_data.support.command1 & ATA_SUPPORT_SMART; dprintf("ATA command1 = %#x\n", cgd->ident_data.support.command1); cam_strvis((uint8_t *)sinfo->device, cgd->ident_data.model, sizeof(cgd->ident_data.model), sizeof(sinfo->device)); cam_strvis((uint8_t *)sinfo->rev, cgd->ident_data.revision, sizeof(cgd->ident_data.revision), sizeof(sinfo->rev)); cam_strvis((uint8_t *)sinfo->serial, cgd->ident_data.serial, sizeof(cgd->ident_data.serial), sizeof(sinfo->serial)); return 0; } static int32_t __device_info_scsi(struct fbsd_smart *fsmart, struct ccb_getdev *cgd) { smart_info_t *sinfo = NULL; union ccb *ccb = NULL; struct scsi_vpd_unit_serial_number *snum = NULL; struct scsi_log_informational_exceptions ie = {0}; if (!fsmart || !cgd) { return -1; } sinfo = &fsmart->common.info; cam_strvis((uint8_t *)sinfo->vendor, (uint8_t *)cgd->inq_data.vendor, sizeof(cgd->inq_data.vendor), sizeof(sinfo->vendor)); cam_strvis((uint8_t *)sinfo->device, (uint8_t *)cgd->inq_data.product, sizeof(cgd->inq_data.product), sizeof(sinfo->device)); cam_strvis((uint8_t *)sinfo->rev, (uint8_t *)cgd->inq_data.revision, sizeof(cgd->inq_data.revision), sizeof(sinfo->rev)); ccb = cam_getccb(fsmart->camdev); snum = malloc(sizeof(struct scsi_vpd_unit_serial_number)); if (!ccb || !snum) { warn("Allocation failure ccb=%p snum=%p", ccb, snum); goto __device_info_scsi_out; } /* Get the serial number */ CCB_CLEAR_ALL_EXCEPT_HDR(&ccb->csio); scsi_inquiry(&ccb->csio, 3, // retries NULL, // callback function MSG_SIMPLE_Q_TAG, // tag action (uint8_t *)snum, sizeof(struct scsi_vpd_unit_serial_number), 1, // EVPD SVPD_UNIT_SERIAL_NUMBER, // page code SSD_FULL_SIZE, // sense length 5000); // timeout ccb->ccb_h.flags |= CAM_DEV_QFRZDIS; if ((cam_send_ccb(fsmart->camdev, ccb) >= 0) && ((ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP)) { cam_strvis((uint8_t *)sinfo->serial, snum->serial_num, snum->length, sizeof(sinfo->serial)); sinfo->serial[sizeof(sinfo->serial) - 1] = '\0'; } memset(ccb, 0, sizeof(*ccb)); scsi_log_sense(&ccb->csio, /* retries */1, /* cbfcnp */NULL, /* tag_action */0, /* page_code */SLS_PAGE_CTRL_CUMULATIVE, /* page */SLS_IE_PAGE, /* save_pages */0, /* ppc */0, /* paramptr */0, /* param_buf */(uint8_t *)&ie, /* param_len */sizeof(ie), /* sense_len */0, /* timeout */5000); /* * Note: The existance of the Informational Exceptions (IE) log page * appears to be the litmus test for SMART support in SCSI * devices. Confusingly, smartctl will report SMART health * status as 'OK' if the device doesn't support the IE page. * For now, just report the facts. */ if ((cam_send_ccb(fsmart->camdev, ccb) >= 0) && ((ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP)) { if ((ie.hdr.param_len < 4) || ie.ie_asc || ie.ie_ascq) { printf("Log Sense, Informational Exceptions failed " "(length=%u asc=%#x ascq=%#x)\n", ie.hdr.param_len, ie.ie_asc, ie.ie_ascq); } else { sinfo->supported = true; } } __device_info_scsi_out: free(snum); if (ccb) cam_freeccb(ccb); return 0; } static int32_t __device_info_nvme(struct fbsd_smart *fsmart, struct ccb_getdev *cgd) { union ccb *ccb; smart_info_t *sinfo = NULL; struct nvme_controller_data cd; if (!fsmart || !cgd) { return -1; } sinfo = &fsmart->common.info; sinfo->supported = true; ccb = cam_getccb(fsmart->camdev); if (ccb != NULL) { struct ccb_dev_advinfo *cdai = &ccb->cdai; CCB_CLEAR_ALL_EXCEPT_HDR(cdai); cdai->ccb_h.func_code = XPT_DEV_ADVINFO; cdai->ccb_h.flags = CAM_DIR_IN; cdai->flags = CDAI_FLAG_NONE; #ifdef CDAI_TYPE_NVME_CNTRL cdai->buftype = CDAI_TYPE_NVME_CNTRL; #else cdai->buftype = 6; #endif cdai->bufsiz = sizeof(struct nvme_controller_data); cdai->buf = (uint8_t *)&cd; if (cam_send_ccb(fsmart->camdev, ccb) >= 0) { if ((ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) { cam_strvis((uint8_t *)sinfo->device, cd.mn, sizeof(cd.mn), sizeof(sinfo->device)); cam_strvis((uint8_t *)sinfo->rev, cd.fr, sizeof(cd.fr), sizeof(sinfo->rev)); cam_strvis((uint8_t *)sinfo->serial, cd.sn, sizeof(cd.sn), sizeof(sinfo->serial)); } } cam_freeccb(ccb); } return 0; } static int32_t __device_info_tunneled_ata(struct fbsd_smart *fsmart) { struct ata_params ident_data; union ccb *ccb = NULL; int32_t rc = -1; ccb = cam_getccb(fsmart->camdev); if (ccb == NULL) { goto __device_info_tunneled_ata_out; } memset(&ident_data, 0, sizeof(struct ata_params)); CCB_CLEAR_ALL_EXCEPT_HDR(ccb); scsi_ata_pass_16(&ccb->csio, /*retries*/ 1, /*cbfcnp*/ NULL, /*flags*/ CAM_DIR_IN, /*tag_action*/ MSG_SIMPLE_Q_TAG, /*protocol*/ AP_PROTO_PIO_IN, /*ata_flags*/ AP_FLAG_TLEN_SECT_CNT | AP_FLAG_BYT_BLOK_BLOCKS | AP_FLAG_TDIR_FROM_DEV, /*features*/ 0, /*sector_count*/sizeof(struct ata_params), /*lba*/ 0, /*command*/ ATA_ATA_IDENTIFY, /*control*/ 0, /*data_ptr*/ (uint8_t *)&ident_data, /*dxfer_len*/ sizeof(struct ata_params), /*sense_len*/ SSD_FULL_SIZE, /*timeout*/ 5000 ); rc = cam_send_ccb(fsmart->camdev, ccb); if (rc != 0) { warnx("%s: scsi_ata_pass_16() failed (programmer error?)", __func__); goto __device_info_tunneled_ata_out; } fsmart->common.info.supported = ident_data.support.command1 & ATA_SUPPORT_SMART; dprintf("ATA command1 = %#x\n", ident_data.support.command1); __device_info_tunneled_ata_out: if (ccb) { cam_freeccb(ccb); } return rc; } /** * Retrieve the device information and use to populate the info structure */ static int32_t __device_get_info(struct fbsd_smart *fsmart) { union ccb *ccb; int32_t rc = -1; if (!fsmart || !fsmart->camdev) { warn("Bad handle %p", fsmart); return -1; } ccb = cam_getccb(fsmart->camdev); if (ccb != NULL) { struct ccb_getdev *cgd = &ccb->cgd; CCB_CLEAR_ALL_EXCEPT_HDR(cgd); /* * GDEV_TYPE doesn't support NVMe. What we do get is: * - device (ata/model, scsi/product) * - revision (ata, scsi) * - serial (ata) * - vendor (scsi) * - supported (ata) * * Serial # for all proto via ccb_dev_advinfo (buftype CDAI_TYPE_SERIAL_NUM) */ ccb->ccb_h.func_code = XPT_GDEV_TYPE; if (cam_send_ccb(fsmart->camdev, ccb) >= 0) { if ((ccb->ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP) { switch (cgd->protocol) { case PROTO_ATA: rc = __device_info_ata(fsmart, cgd); break; case PROTO_SCSI: rc = __device_info_scsi(fsmart, cgd); if (!rc && fsmart->common.protocol == SMART_PROTO_ATA) { rc = __device_info_tunneled_ata(fsmart); } break; case PROTO_NVME: rc = __device_info_nvme(fsmart, cgd); break; default: printf("%s: unsupported protocol %d\n", __func__, cgd->protocol); } } } cam_freeccb(ccb); } return rc; }