Page MenuHomeFreeBSD

D8787.id22903.diff
No OneTemporary

D8787.id22903.diff

Index: share/man/man4/Makefile
===================================================================
--- share/man/man4/Makefile
+++ share/man/man4/Makefile
@@ -100,6 +100,7 @@
cd.4 \
cdce.4 \
cfi.4 \
+ cfumass.4 \
ch.4 \
ciss.4 \
cloudabi.4 \
Index: share/man/man4/cfumass.4
===================================================================
--- /dev/null
+++ share/man/man4/cfumass.4
@@ -0,0 +1,123 @@
+.\" Copyright (c) 2016 The FreeBSD Foundation
+.\" All rights reserved.
+.\"
+.\" This software was developed by Edward Tomasz Napierala under sponsorship
+.\" from the FreeBSD Foundation.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\" notice, this list of conditions and the following disclaimer in the
+.\" documentation and/or other materials provided with the distribution.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.\" $FreeBSD$
+.Dd June 8, 2016
+.Dt CFUMASS 4
+.Os
+.Sh NAME
+.Nm cfumass
+.Nd USB device side support for Mass Storage Class Bulk-Only (BBB) Transport
+.Sh SYNOPSIS
+To compile this driver into the kernel,
+place the following lines in your
+kernel configuration file:
+.Bd -ragged -offset indent
+.Cd "device usb"
+.Cd "device usb_template"
+.Cd "device iscsi"
+.Cd "device ctl"
+.Cd "device cfumass"
+.Ed
+.Pp
+Alternatively, to load the driver as a
+module at boot time, place the following line in
+.Xr loader.conf 5 :
+.Bd -literal -offset indent
+cfumass_load="YES"
+.Ed
+.Sh DESCRIPTION
+The
+.Nm
+driver provides device side support for emulating an USB mass storage
+device compliant with the USB Mass Storage Class Bulk-Only (BBB) Transport
+specification, implemented as a
+.Xr ctl 4
+frontend driver.
+.Pp
+In order to use this functionality, the
+.Xr usb_template 4
+driver needs to be loaded, the USB OTG port must be working in a
+USB device side mode (which happens automatically upon connecting to a
+USB host), and the USB Mass Storage template needs to be chosen by
+setting the
+.Va hw.usb.template
+sysctl to 0.
+.Pp
+Upon attach the driver creates a ctl(4) port named "umassX", where "X"
+is the USB interface number, usually 0, presenting LUN 0 to the USB host.
+See
+.Xr ctl.conf 5
+and
+.Xr ctld 8
+for details on configuring the LUN.
+.Sh SYSCTL VARIABLES
+The following variables are available as both
+.Xr sysctl 8
+variables and
+.Xr loader 8
+tunables:
+.Bl -tag -width indent
+.It Va hw.usb.cfumass.debug
+Verbosity level for log messages from the
+.Nm
+driver.
+Set to 0 to disable logging or 1 to warn about potential problems.
+Larger values enable debugging output.
+Defaults to 1.
+.It Va hw.usb.cfumass.ignore_stop
+Ignore START STOP UNIT SCSI commands with START and LOEJ bits cleared.
+Some initiators send that command to stop the target when the user
+attempts to gracefully eject the drive, but fail to start it when it's
+plugged back.
+Set to 0 to handle the command in a standards-compliant way,
+1 to ignore it and log a warning, and 2 to ignore it silently.
+Defaults to 1.
+.It Va hw.usb.cfumass.max_lun
+Max LUN number to report to the initiator (USB host).
+Must be between 0 and 15.
+Some initiators incorrectly handle values larger than 0.
+Defaults to 0.
+.El
+.Sh SEE ALSO
+.Xr ctl 4 ,
+.Xr umass 4 ,
+.Xr usb 4 ,
+.Xr usb_template 4 ,
+.Xr ctl.conf 5 ,
+.Xr ctld 8
+.Sh HISTORY
+The
+.Nm
+driver first appeared in
+.Fx 11.1 .
+.Sh AUTHORS
+The
+.Nm
+driver was developed by
+.An Edward Tomasz Napierala Aq Mt trasz@FreeBSD.org
+under sponsorship from the FreeBSD Foundation.
Index: share/man/man4/ctl.4
===================================================================
--- share/man/man4/ctl.4
+++ share/man/man4/ctl.4
@@ -172,6 +172,7 @@
Defaults to 5.
.El
.Sh SEE ALSO
+.Xr cfumass 4 ,
.Xr ctladm 8 ,
.Xr ctld 8 ,
.Xr ctlstat 8
Index: share/man/man4/umass.4
===================================================================
--- share/man/man4/umass.4
+++ share/man/man4/umass.4
@@ -239,6 +239,7 @@
when using
.Xr mount 8 .
.Sh SEE ALSO
+.Xr cfumass 4 ,
.Xr ehci 4 ,
.Xr ohci 4 ,
.Xr uhci 4 ,
Index: share/man/man4/usb_template.4
===================================================================
--- share/man/man4/usb_template.4
+++ share/man/man4/usb_template.4
@@ -84,6 +84,7 @@
.El
.
.Sh SEE ALSO
+.Xr cfumass 4 ,
.Xr usb 4 ,
.Xr usfs 4
.Sh STANDARDS
Index: share/man/man4/usfs.4
===================================================================
--- share/man/man4/usfs.4
+++ share/man/man4/usfs.4
@@ -58,6 +58,7 @@
.Pp
Upon attach the driver creates a RAM disk which can be read and written.
.Sh SEE ALSO
+.Xr cfumass 4 ,
.Xr umass 4 ,
.Xr usb 4 ,
.Xr usb_template 4
Index: sys/conf/files
===================================================================
--- sys/conf/files
+++ sys/conf/files
@@ -2929,6 +2929,7 @@
#
# USB storage drivers
#
+dev/usb/storage/cfumass.c optional cfumass ctl
dev/usb/storage/umass.c optional umass
dev/usb/storage/urio.c optional urio
dev/usb/storage/ustorage_fs.c optional usfs
Index: sys/dev/usb/storage/cfumass.c
===================================================================
--- /dev/null
+++ sys/dev/usb/storage/cfumass.c
@@ -0,0 +1,1059 @@
+/*-
+ * Copyright (c) 2016 The FreeBSD Foundation
+ * All rights reserved.
+ *
+ * This software was developed by Edward Tomasz Napierala under sponsorship
+ * from the FreeBSD Foundation.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ */
+/*
+ * USB Mass Storage Class Bulk-Only (BBB) Transport target.
+ *
+ * http://www.usb.org/developers/docs/devclass_docs/usbmassbulk_10.pdf
+ *
+ * This code implements the USB Mass Storage frontend driver for the CAM
+ * Target Layer (ctl(4)) subsystem.
+ */
+
+#include <sys/cdefs.h>
+__FBSDID("$FreeBSD$");
+
+#ifdef USB_GLOBAL_INCLUDE_FILE
+#include USB_GLOBAL_INCLUDE_FILE
+#else
+#include <sys/stdint.h>
+#include <sys/param.h>
+#include <sys/types.h>
+#include <sys/systm.h>
+#include <sys/kernel.h>
+#include <sys/bus.h>
+#include <sys/module.h>
+#include <sys/lock.h>
+#include <sys/mutex.h>
+#include <sys/refcount.h>
+#include <sys/sysctl.h>
+
+#include <dev/usb/usb.h>
+#include <dev/usb/usbdi.h>
+#include "usbdevs.h"
+#include "usb_if.h"
+
+#include <cam/scsi/scsi_all.h>
+#include <cam/scsi/scsi_da.h>
+#include <cam/ctl/ctl_io.h>
+#include <cam/ctl/ctl.h>
+#include <cam/ctl/ctl_backend.h>
+#include <cam/ctl/ctl_error.h>
+#include <cam/ctl/ctl_frontend.h>
+#include <cam/ctl/ctl_debug.h>
+#include <cam/ctl/ctl_ha.h>
+#include <cam/ctl/ctl_ioctl.h>
+#include <cam/ctl/ctl_private.h>
+
+#define USB_DEBUG_VAR cfumass_debug
+#include <dev/usb/usb_debug.h>
+#endif /* USB_GLOBAL_INCLUDE_FILE */
+
+SYSCTL_NODE(_hw_usb, OID_AUTO, cfumass, CTLFLAG_RW, 0,
+ "CAM Target Layer USB Mass Storage Frontend");
+static int debug = 1;
+SYSCTL_INT(_hw_usb_cfumass, OID_AUTO, debug, CTLFLAG_RWTUN,
+ &debug, 1, "Enable debug messages");
+static int max_lun = 0;
+SYSCTL_INT(_hw_usb_cfumass, OID_AUTO, max_lun, CTLFLAG_RWTUN,
+ &max_lun, 1, "Maximum advertised LUN number");
+static int ignore_stop = 1;
+SYSCTL_INT(_hw_usb_cfumass, OID_AUTO, ignore_stop, CTLFLAG_RWTUN,
+ &ignore_stop, 1, "Ignore START STOP UNIT with START and LOEJ bits cleared");
+
+#ifndef CFUMASS_BULK_SIZE
+#define CFUMASS_BULK_SIZE (1U << 17) /* bytes */
+#endif
+
+/*
+ * USB transfer definitions.
+ */
+#define CFUMASS_T_COMMAND 0
+#define CFUMASS_T_DATA_OUT 1
+#define CFUMASS_T_DATA_IN 2
+#define CFUMASS_T_STATUS 3
+#define CFUMASS_T_MAX 4
+
+/*
+ * USB interface specific control requests.
+ */
+#define UR_RESET 0xff /* Bulk-Only Mass Storage Reset */
+#define UR_GET_MAX_LUN 0xfe /* Get Max LUN */
+
+/*
+ * Command Block Wrapper.
+ */
+struct cfumass_cbw_t {
+ uDWord dCBWSignature;
+#define CBWSIGNATURE 0x43425355 /* "USBC" */
+ uDWord dCBWTag;
+ uDWord dCBWDataTransferLength;
+ uByte bCBWFlags;
+#define CBWFLAGS_OUT 0x00
+#define CBWFLAGS_IN 0x80
+ uByte bCBWLUN;
+ uByte bCDBLength;
+#define CBWCBLENGTH 16
+ uByte CBWCB[CBWCBLENGTH];
+} __packed;
+
+#define CFUMASS_CBW_SIZE 31
+CTASSERT(sizeof(struct cfumass_cbw_t) == CFUMASS_CBW_SIZE);
+
+/*
+ * Command Status Wrapper.
+ */
+struct cfumass_csw_t {
+ uDWord dCSWSignature;
+#define CSWSIGNATURE 0x53425355 /* "USBS" */
+ uDWord dCSWTag;
+ uDWord dCSWDataResidue;
+ uByte bCSWStatus;
+#define CSWSTATUS_GOOD 0x0
+#define CSWSTATUS_FAILED 0x1
+#define CSWSTATUS_PHASE 0x2
+} __packed;
+
+#define CFUMASS_CSW_SIZE 13
+CTASSERT(sizeof(struct cfumass_csw_t) == CFUMASS_CSW_SIZE);
+
+struct cfumass_softc {
+ device_t sc_dev;
+ struct usb_device *sc_udev;
+ struct usb_xfer *sc_xfer[CFUMASS_T_MAX];
+
+ struct cfumass_cbw_t *sc_cbw;
+ struct cfumass_csw_t *sc_csw;
+
+ struct mtx sc_mtx;
+ struct ctl_port sc_port;
+ int sc_online;
+ int sc_ctl_initid;
+
+ /*
+ * This is used to communicate between CTL callbacks
+ * and USB callbacks; basically, it holds the state
+ * for the current command ("the" command, since there
+ * is no queueing in USB Mass Storage).
+ */
+ bool sc_current_stalled;
+
+ /*
+ * The following are set upon receiving a SCSI command.
+ */
+ int sc_current_tag;
+ int sc_current_transfer_length;
+ int sc_current_flags;
+
+ /*
+ * The following are set in ctl_datamove().
+ */
+ int sc_current_residue;
+ union ctl_io *sc_ctl_io;
+
+ /*
+ * The following is set in cfumass_done().
+ */
+ int sc_current_status;
+
+ /*
+ * Number of requests queued to CTL.
+ */
+ volatile u_int sc_queued;
+};
+
+/*
+ * USB interface.
+ */
+static device_probe_t cfumass_probe;
+static device_attach_t cfumass_attach;
+static device_detach_t cfumass_detach;
+static device_suspend_t cfumass_suspend;
+static device_resume_t cfumass_resume;
+static usb_handle_request_t cfumass_handle_request;
+
+static usb_callback_t cfumass_t_command_callback;
+static usb_callback_t cfumass_t_data_out_callback;
+static usb_callback_t cfumass_t_data_in_callback;
+static usb_callback_t cfumass_t_status_callback;
+
+static device_method_t cfumass_methods[] = {
+
+ /* USB interface. */
+ DEVMETHOD(usb_handle_request, cfumass_handle_request),
+
+ /* Device interface. */
+ DEVMETHOD(device_probe, cfumass_probe),
+ DEVMETHOD(device_attach, cfumass_attach),
+ DEVMETHOD(device_detach, cfumass_detach),
+ DEVMETHOD(device_suspend, cfumass_suspend),
+ DEVMETHOD(device_resume, cfumass_resume),
+
+ DEVMETHOD_END
+};
+
+static driver_t cfumass_driver = {
+ .name = "cfumass",
+ .methods = cfumass_methods,
+ .size = sizeof(struct cfumass_softc),
+};
+
+static devclass_t cfumass_devclass;
+
+DRIVER_MODULE(cfumass, uhub, cfumass_driver, cfumass_devclass, NULL, 0);
+MODULE_VERSION(cfumass, 0);
+MODULE_DEPEND(cfumass, usb, 1, 1, 1);
+MODULE_DEPEND(cfumass, usb_template, 1, 1, 1);
+
+static struct usb_config cfumass_config[CFUMASS_T_MAX] = {
+
+ [CFUMASS_T_COMMAND] = {
+ .type = UE_BULK,
+ .endpoint = UE_ADDR_ANY,
+ .direction = UE_DIR_OUT,
+ .bufsize = sizeof(struct cfumass_cbw_t),
+ .callback = &cfumass_t_command_callback,
+ .usb_mode = USB_MODE_DEVICE,
+ },
+
+ [CFUMASS_T_DATA_OUT] = {
+ .type = UE_BULK,
+ .endpoint = UE_ADDR_ANY,
+ .direction = UE_DIR_OUT,
+ .bufsize = CFUMASS_BULK_SIZE,
+ .flags = {.proxy_buffer = 1, .short_xfer_ok = 1, .ext_buffer = 1},
+ .callback = &cfumass_t_data_out_callback,
+ .usb_mode = USB_MODE_DEVICE,
+ },
+
+ [CFUMASS_T_DATA_IN] = {
+ .type = UE_BULK,
+ .endpoint = UE_ADDR_ANY,
+ .direction = UE_DIR_IN,
+ .bufsize = CFUMASS_BULK_SIZE,
+ .flags = {.proxy_buffer = 1, .short_xfer_ok = 1, .ext_buffer = 1},
+ .callback = &cfumass_t_data_in_callback,
+ .usb_mode = USB_MODE_DEVICE,
+ },
+
+ [CFUMASS_T_STATUS] = {
+ .type = UE_BULK,
+ .endpoint = UE_ADDR_ANY,
+ .direction = UE_DIR_IN,
+ .bufsize = sizeof(struct cfumass_csw_t),
+ .flags = {.short_xfer_ok = 1},
+ .callback = &cfumass_t_status_callback,
+ .usb_mode = USB_MODE_DEVICE,
+ },
+};
+
+/*
+ * CTL frontend interface.
+ */
+static int cfumass_init(void);
+static void cfumass_online(void *arg);
+static void cfumass_offline(void *arg);
+static void cfumass_datamove(union ctl_io *io);
+static void cfumass_done(union ctl_io *io);
+
+static struct ctl_frontend cfumass_frontend = {
+ .name = "umass",
+ .init = cfumass_init,
+};
+CTL_FRONTEND_DECLARE(ctlcfumass, cfumass_frontend);
+
+#define CFUMASS_DEV_DEBUG(D, X, ...) \
+ do { \
+ if (debug > 1) { \
+ device_printf(D, "%s: " X "\n", \
+ __func__, ## __VA_ARGS__); \
+ } \
+ } while (0)
+
+#define CFUMASS_WARN(S, X, ...) \
+ do { \
+ if (debug > 0) { \
+ device_printf(S->sc_dev, "WARNING: %s: " X "\n",\
+ __func__, ## __VA_ARGS__); \
+ } \
+ } while (0)
+
+#define CFUMASS_LOCK(X) mtx_lock(&X->sc_mtx)
+#define CFUMASS_UNLOCK(X) mtx_unlock(&X->sc_mtx)
+
+static void cfumass_transfer_start(struct cfumass_softc *sc, uint8_t xfer_index);
+static void cfumass_terminate(struct cfumass_softc *sc);
+
+static int
+cfumass_probe(device_t dev)
+{
+ struct usb_attach_arg *uaa;
+ struct usb_interface_descriptor *id;
+
+ uaa = device_get_ivars(dev);
+
+ if (uaa->usb_mode != USB_MODE_DEVICE)
+ return (ENXIO);
+
+ /*
+ * Check for a compliant device.
+ */
+ id = usbd_get_interface_descriptor(uaa->iface);
+ if ((id == NULL) ||
+ (id->bInterfaceClass != UICLASS_MASS) ||
+ (id->bInterfaceSubClass != UISUBCLASS_SCSI) ||
+ (id->bInterfaceProtocol != UIPROTO_MASS_BBB)) {
+ return (ENXIO);
+ }
+
+ return (BUS_PROBE_GENERIC);
+}
+
+static int
+cfumass_attach(device_t dev)
+{
+ struct cfumass_softc *sc;
+ struct usb_attach_arg *uaa;
+ struct usb_interface_descriptor *id;
+ struct ctl_port *port;
+ int error;
+
+ /*
+ * NOTE: the softc struct is cleared in device_set_driver.
+ * We can safely call cfumass_detach without specifically
+ * initializing the struct.
+ */
+
+ CFUMASS_DEV_DEBUG(dev, "go");
+
+ sc = device_get_softc(dev);
+ uaa = device_get_ivars(dev);
+
+ sc->sc_dev = dev;
+ sc->sc_udev = uaa->device;
+
+ usbd_set_power_mode(uaa->device, USB_POWER_MODE_SAVE);
+ device_set_usb_desc(dev);
+
+ mtx_init(&sc->sc_mtx, "cfumass", NULL, MTX_DEF);
+
+ id = usbd_get_interface_descriptor(uaa->iface);
+ if (id == NULL) {
+ CFUMASS_WARN(sc, "usbd_get_interface_descriptor() failed");
+ goto detach;
+ }
+
+ error = usbd_transfer_setup(uaa->device,
+ &uaa->info.bIfaceIndex, sc->sc_xfer, cfumass_config,
+ CFUMASS_T_MAX, sc, &sc->sc_mtx);
+ if (error != 0) {
+ CFUMASS_WARN(sc, "usbd_transfer_setup() failed: %s",
+ usbd_errstr(error));
+ goto detach;
+ }
+
+ sc->sc_cbw = usbd_xfer_get_frame_buffer(sc->sc_xfer[
+ CFUMASS_T_COMMAND], 0);
+ sc->sc_csw = usbd_xfer_get_frame_buffer(sc->sc_xfer[
+ CFUMASS_T_STATUS], 0);
+
+ port = &sc->sc_port;
+
+ port->frontend = &cfumass_frontend;
+ port->port_type = CTL_PORT_ISCSI; // XXX: CTL_PORT_UMASS?
+ /* XXX KDM what should the real number be here? */
+ port->num_requested_ctl_io = 4096;
+ port->port_name = "cfumass";
+ port->physical_port = device_get_unit(dev);
+ port->virtual_port = 0;
+ port->port_online = cfumass_online;
+ port->port_offline = cfumass_offline;
+ port->onoff_arg = sc;
+ port->fe_datamove = cfumass_datamove;
+ port->fe_done = cfumass_done;
+
+ /* XXX KDM what should we report here? */
+ /* XXX These should probably be fetched from CTL. */
+ port->max_targets = 1;
+ port->max_target_id = 15;
+ port->targ_port = -1;
+
+ error = ctl_port_register(port);
+ if (error != 0) {
+ CFUMASS_WARN(sc, "ctl_port_register() failed with error %d", error);
+ return (error);
+ }
+
+ sc->sc_ctl_initid = ctl_add_initiator(&sc->sc_port, -1, 0, NULL);
+ if (sc->sc_ctl_initid < 0) {
+ CFUMASS_WARN(sc, "ctl_add_initiator() failed with error %d", sc->sc_ctl_initid);
+ return (1);
+ }
+
+ refcount_init(&sc->sc_queued, 0);
+
+ CFUMASS_LOCK(sc);
+ cfumass_transfer_start(sc, CFUMASS_T_COMMAND);
+ CFUMASS_UNLOCK(sc);
+
+ return (0);
+
+detach:
+ cfumass_detach(dev);
+ return (ENXIO);
+}
+
+static int
+cfumass_detach(device_t dev)
+{
+ struct cfumass_softc *sc;
+ int error;
+
+ sc = device_get_softc(dev);
+
+ CFUMASS_DEV_DEBUG(dev, "go");
+
+ CFUMASS_LOCK(sc);
+ cfumass_terminate(sc);
+ CFUMASS_UNLOCK(sc);
+ usbd_transfer_unsetup(sc->sc_xfer, CFUMASS_T_MAX);
+
+ if (sc->sc_ctl_initid != -1) {
+ error = ctl_remove_initiator(&sc->sc_port, sc->sc_ctl_initid);
+ if (error != 0)
+ CFUMASS_WARN(sc, "ctl_remove_initiator() failed with error %d", error);
+ sc->sc_ctl_initid = -1;
+ }
+
+ // XXX: What if it's not initialized yet?
+ error = ctl_port_deregister(&sc->sc_port);
+ if (error != 0)
+ CFUMASS_WARN(sc, "ctl_port_deregister() failed with error %d", error);
+
+ mtx_destroy(&sc->sc_mtx);
+
+ return (0);
+}
+
+static int
+cfumass_suspend(device_t dev)
+{
+
+ CFUMASS_DEV_DEBUG(dev, "go");
+ return (0);
+}
+
+static int
+cfumass_resume(device_t dev)
+{
+
+ CFUMASS_DEV_DEBUG(dev, "go");
+ return (0);
+}
+
+static void
+cfumass_transfer_start(struct cfumass_softc *sc, uint8_t xfer_index)
+{
+
+ usbd_transfer_start(sc->sc_xfer[xfer_index]);
+}
+
+static void
+cfumass_transfer_stop_and_drain(struct cfumass_softc *sc, uint8_t xfer_index)
+{
+
+ usbd_transfer_stop(sc->sc_xfer[xfer_index]);
+ CFUMASS_UNLOCK(sc);
+ usbd_transfer_drain(sc->sc_xfer[xfer_index]);
+ CFUMASS_LOCK(sc);
+}
+
+static void
+cfumass_terminate(struct cfumass_softc *sc)
+{
+ int last;
+
+ for (;;) {
+ cfumass_transfer_stop_and_drain(sc, CFUMASS_T_COMMAND);
+ cfumass_transfer_stop_and_drain(sc, CFUMASS_T_DATA_IN);
+ cfumass_transfer_stop_and_drain(sc, CFUMASS_T_DATA_OUT);
+
+ if (sc->sc_ctl_io != NULL) {
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "terminating CTL transfer");
+ ctl_set_data_phase_error(&sc->sc_ctl_io->scsiio);
+ sc->sc_ctl_io->scsiio.be_move_done(sc->sc_ctl_io);
+ sc->sc_ctl_io = NULL;
+ }
+
+ cfumass_transfer_stop_and_drain(sc, CFUMASS_T_STATUS);
+
+ refcount_acquire(&sc->sc_queued);
+ last = refcount_release(&sc->sc_queued);
+ if (last != 0)
+ break;
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "%d CTL tasks pending", sc->sc_queued);
+ msleep(__DEVOLATILE(void *, &sc->sc_queued), &sc->sc_mtx, 0, "cfumass_reset", hz / 100);
+ }
+}
+
+static int
+cfumass_handle_request(device_t dev,
+ const void *preq, void **pptr, uint16_t *plen,
+ uint16_t offset, uint8_t *pstate)
+{
+ static uint8_t max_lun_tmp;
+ struct cfumass_softc *sc;
+ const struct usb_device_request *req;
+ uint8_t is_complete;
+
+ sc = device_get_softc(dev);
+ req = preq;
+ is_complete = *pstate;
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "go");
+
+ if (is_complete)
+ return (ENXIO);
+
+ if ((req->bmRequestType == UT_WRITE_CLASS_INTERFACE) &&
+ (req->bRequest == UR_RESET)) {
+ CFUMASS_WARN(sc, "received Bulk-Only Mass Storage Reset");
+ *plen = 0;
+
+ CFUMASS_LOCK(sc);
+ cfumass_terminate(sc);
+ cfumass_transfer_start(sc, CFUMASS_T_COMMAND);
+ CFUMASS_UNLOCK(sc);
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "Bulk-Only Mass Storage Reset done");
+ return (0);
+ }
+
+ if ((req->bmRequestType == UT_READ_CLASS_INTERFACE) &&
+ (req->bRequest == UR_GET_MAX_LUN)) {
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "received Get Max LUN");
+ if (offset == 0) {
+ *plen = 1;
+ /*
+ * The protocol doesn't support LUN numbers higher
+ * than 15. Also, some initiators (namely Windows XP
+ * SP3 Version 2002) can't properly query the number
+ * of LUNs, resulting in inaccessible "fake" ones - thus
+ * the default limit of one LUN.
+ */
+ if (max_lun < 0 || max_lun > 15) {
+ CFUMASS_WARN(sc,
+ "invalid hw.usb.cfumass.max_lun, must be "
+ "between 0 and 15; defaulting to 0");
+ max_lun_tmp = 0;
+ } else {
+ max_lun_tmp = max_lun;
+ }
+ *pptr = &max_lun_tmp;
+ } else {
+ *plen = 0;
+ }
+ return (0);
+ }
+
+ return (ENXIO);
+}
+
+static int
+cfumass_quirk(struct cfumass_softc *sc, unsigned char *cdb, int cdb_len)
+{
+ struct scsi_start_stop_unit *sssu;
+
+ switch (cdb[0]) {
+ case START_STOP_UNIT:
+ /*
+ * Some initiators - eg OSX, Darwin Kernel Version 15.6.0,
+ * root:xnu-3248.60.11~2/RELEASE_X86_64 - attempt to stop
+ * the unit on eject, but fail to start it when it's plugged
+ * back. Just ignore the command.
+ */
+
+ if (cdb_len < sizeof(*sssu)) {
+ CFUMASS_DEV_DEBUG(sc->sc_dev,
+ "received START STOP UNIT with "
+ "bCDBLength %d, should be %zd",
+ cdb_len, sizeof(*sssu));
+ break;
+ }
+
+ sssu = (struct scsi_start_stop_unit *)cdb;
+ if ((sssu->how & SSS_PC_MASK) != 0)
+ break;
+
+ if ((sssu->how & SSS_START) != 0)
+ break;
+
+ if ((sssu->how & SSS_LOEJ) != 0)
+ break;
+
+ if (ignore_stop == 0)
+ break;
+ else if (ignore_stop == 1)
+ CFUMASS_WARN(sc, "ignoring START STOP UNIT request");
+ else
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "ignoring START STOP UNIT request");
+
+ sc->sc_current_status = 0;
+ cfumass_transfer_start(sc, CFUMASS_T_STATUS);
+
+ return (1);
+ default:
+ break;
+ }
+
+ return (0);
+}
+
+static void
+cfumass_t_command_callback(struct usb_xfer *xfer, usb_error_t usb_error)
+{
+ struct cfumass_softc *sc;
+ uint32_t signature;
+ union ctl_io *io;
+ int error = 0;
+
+ sc = usbd_xfer_softc(xfer);
+
+ KASSERT(sc->sc_ctl_io == NULL,
+ ("sc_ctl_io is %p, should be NULL", sc->sc_ctl_io));
+
+ switch (USB_GET_STATE(xfer)) {
+ case USB_ST_TRANSFERRED:
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ST_TRANSFERRED");
+
+ signature = UGETDW(sc->sc_cbw->dCBWSignature);
+ if (signature != CBWSIGNATURE) {
+ CFUMASS_WARN(sc, "invalid dCBWSignature 0x%08x, should be 0x%08x",
+ signature, CBWSIGNATURE);
+ break;
+ }
+
+ if (sc->sc_cbw->bCDBLength <= 0 ||
+ sc->sc_cbw->bCDBLength > sizeof(sc->sc_cbw->CBWCB)) {
+ CFUMASS_WARN(sc, "invalid bCDBLength %d, should be <= %zd",
+ sc->sc_cbw->bCDBLength, sizeof(sc->sc_cbw->CBWCB));
+ break;
+ }
+
+ sc->sc_current_stalled = false;
+ sc->sc_current_status = 0;
+ sc->sc_current_tag = UGETDW(sc->sc_cbw->dCBWTag);
+ sc->sc_current_transfer_length =
+ UGETDW(sc->sc_cbw->dCBWDataTransferLength);
+ sc->sc_current_flags = sc->sc_cbw->bCBWFlags;
+
+ /*
+ * Make sure to report proper residue if the datamove wasn't
+ * required, or wasn't called due to SCSI error.
+ */
+ sc->sc_current_residue = sc->sc_current_transfer_length;
+
+ if (cfumass_quirk(sc,
+ sc->sc_cbw->CBWCB, sc->sc_cbw->bCDBLength) != 0)
+ break;
+
+ /*
+ * Those CTL functions cannot be called with mutex held.
+ */
+ CFUMASS_UNLOCK(sc);
+ io = ctl_alloc_io(sc->sc_port.ctl_pool_ref);
+ ctl_zero_io(io);
+ io->io_hdr.ctl_private[CTL_PRIV_FRONTEND].ptr = sc;
+ io->io_hdr.io_type = CTL_IO_SCSI;
+ io->io_hdr.nexus.initid = sc->sc_ctl_initid;
+ io->io_hdr.nexus.targ_port = sc->sc_port.targ_port;
+ io->io_hdr.nexus.targ_lun = ctl_decode_lun(sc->sc_cbw->bCBWLUN);
+ io->scsiio.tag_num = UGETDW(sc->sc_cbw->dCBWTag);
+ io->scsiio.tag_type = CTL_TAG_UNTAGGED;
+ io->scsiio.cdb_len = sc->sc_cbw->bCDBLength;
+ memcpy(io->scsiio.cdb, sc->sc_cbw->CBWCB, sc->sc_cbw->bCDBLength);
+ refcount_acquire(&sc->sc_queued);
+ error = ctl_queue(io);
+ if (error != CTL_RETVAL_COMPLETE) {
+ CFUMASS_WARN(sc,
+ "ctl_queue() failed; error %d; stalling", error);
+ ctl_free_io(io);
+ refcount_release(&sc->sc_queued);
+ CFUMASS_LOCK(sc);
+ usbd_xfer_set_stall(xfer);
+ break;
+ }
+
+ CFUMASS_LOCK(sc);
+ break;
+
+ case USB_ST_SETUP:
+tr_setup:
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ST_SETUP");
+
+ usbd_xfer_set_frame_len(xfer, 0, sizeof(*sc->sc_cbw));
+ usbd_transfer_submit(xfer);
+ break;
+
+ default:
+ if (usb_error == USB_ERR_CANCELLED) {
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ERR_CANCELLED");
+ break;
+ }
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ST_ERROR: %s", usbd_errstr(usb_error));
+
+ goto tr_setup;
+ }
+}
+
+static void
+cfumass_t_data_out_callback(struct usb_xfer *xfer, usb_error_t usb_error)
+{
+ struct cfumass_softc *sc;
+ union ctl_io *io;
+ uint32_t max_bulk;
+ struct ctl_sg_entry ctl_sg_entry, *ctl_sglist;
+ int actlen, ctl_sg_count, sumlen;
+
+ sc = usbd_xfer_softc(xfer);
+ max_bulk = usbd_xfer_max_len(xfer);
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "go");
+
+ usbd_xfer_status(xfer, &actlen, &sumlen, NULL, NULL);
+
+ io = sc->sc_ctl_io;
+
+ switch (USB_GET_STATE(xfer)) {
+ case USB_ST_TRANSFERRED:
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ST_TRANSFERRED");
+
+ sc->sc_current_residue = 0;
+ io->scsiio.be_move_done(io);
+ sc->sc_ctl_io = NULL;
+ break;
+
+ case USB_ST_SETUP:
+tr_setup:
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ST_SETUP");
+
+ if (io->scsiio.kern_sg_entries > 0) {
+ ctl_sglist = (struct ctl_sg_entry *)io->scsiio.kern_data_ptr;
+ ctl_sg_count = io->scsiio.kern_sg_entries;
+ } else {
+ ctl_sglist = &ctl_sg_entry;
+ ctl_sglist->addr = io->scsiio.kern_data_ptr;
+ ctl_sglist->len = io->scsiio.kern_data_len;
+ ctl_sg_count = 1;
+ }
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "requested size %d, CTL segment size %zd",
+ sc->sc_current_transfer_length, ctl_sglist[0].len);
+
+ usbd_xfer_set_frame_data(xfer, 0, ctl_sglist[0].addr, ctl_sglist[0].len);
+ usbd_transfer_submit(xfer);
+ break;
+
+ default:
+ if (usb_error == USB_ERR_CANCELLED) {
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ERR_CANCELLED");
+ break;
+ }
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ST_ERROR: %s", usbd_errstr(usb_error));
+
+ goto tr_setup;
+ }
+}
+
+static void
+cfumass_t_data_in_callback(struct usb_xfer *xfer, usb_error_t usb_error)
+{
+ struct cfumass_softc *sc;
+ union ctl_io *io;
+ uint32_t max_bulk;
+ struct ctl_sg_entry ctl_sg_entry, *ctl_sglist;
+ int actlen, ctl_sg_count, sumlen;
+
+ sc = usbd_xfer_softc(xfer);
+ max_bulk = usbd_xfer_max_len(xfer);
+
+ usbd_xfer_status(xfer, &actlen, &sumlen, NULL, NULL);
+
+ io = sc->sc_ctl_io;
+
+ switch (USB_GET_STATE(xfer)) {
+ case USB_ST_TRANSFERRED:
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ST_TRANSFERRED");
+
+ io->scsiio.be_move_done(io);
+ sc->sc_ctl_io = NULL;
+ break;
+
+ case USB_ST_SETUP:
+tr_setup:
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ST_SETUP");
+
+ if (io->scsiio.kern_sg_entries > 0) {
+ ctl_sglist = (struct ctl_sg_entry *)io->scsiio.kern_data_ptr;
+ ctl_sg_count = io->scsiio.kern_sg_entries;
+ } else {
+ ctl_sglist = &ctl_sg_entry;
+ ctl_sglist->addr = io->scsiio.kern_data_ptr;
+ ctl_sglist->len = io->scsiio.kern_data_len;
+ ctl_sg_count = 1;
+ }
+
+ if (sc->sc_current_transfer_length > io->scsiio.kern_total_len) {
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "initiator requested %zd bytes, we will send %zd and stall",
+ sc->sc_current_transfer_length, io->scsiio.kern_total_len);
+ sc->sc_current_residue = sc->sc_current_transfer_length - io->scsiio.kern_total_len;
+ } else {
+ sc->sc_current_residue = 0;
+ }
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "max_bulk %d, requested size %d, CTL segment size %zd",
+ max_bulk, sc->sc_current_transfer_length, ctl_sglist[0].len);
+
+ if (max_bulk >= ctl_sglist[0].len)
+ max_bulk = ctl_sglist[0].len;
+
+#if 0
+ if (sc->sc_current_residue != 0) {
+ sc->sc_current_stalled = true;
+ usbd_xfer_set_stall(xfer);
+ }
+#endif
+
+ usbd_xfer_set_frame_data(xfer, 0, ctl_sglist[0].addr, max_bulk);
+ usbd_transfer_submit(xfer);
+
+ break;
+
+ default:
+ if (usb_error == USB_ERR_CANCELLED) {
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ERR_CANCELLED");
+ break;
+ }
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ST_ERROR: %s", usbd_errstr(usb_error));
+
+ goto tr_setup;
+ }
+}
+
+static void
+cfumass_t_status_callback(struct usb_xfer *xfer, usb_error_t usb_error)
+{
+ struct cfumass_softc *sc;
+
+ sc = usbd_xfer_softc(xfer);
+
+ KASSERT(sc->sc_ctl_io == NULL,
+ ("sc_ctl_io is %p, should be NULL", sc->sc_ctl_io));
+
+ switch (USB_GET_STATE(xfer)) {
+ case USB_ST_TRANSFERRED:
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ST_TRANSFERRED");
+
+ cfumass_transfer_start(sc, CFUMASS_T_COMMAND);
+ break;
+
+ case USB_ST_SETUP:
+tr_setup:
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ST_SETUP");
+
+ if (sc->sc_current_residue > 0 && !sc->sc_current_stalled) {
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "non-zero residue, stalling");
+ usbd_xfer_set_stall(xfer);
+ sc->sc_current_stalled = true;
+ }
+
+ USETDW(sc->sc_csw->dCSWSignature, CSWSIGNATURE);
+ USETDW(sc->sc_csw->dCSWTag, sc->sc_current_tag);
+ USETDW(sc->sc_csw->dCSWDataResidue, sc->sc_current_residue);
+ sc->sc_csw->bCSWStatus = sc->sc_current_status;
+
+ usbd_xfer_set_frame_len(xfer, 0, sizeof(*sc->sc_csw));
+ usbd_transfer_submit(xfer);
+ break;
+
+ default:
+ if (usb_error == USB_ERR_CANCELLED) {
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ERR_CANCELLED");
+ break;
+ }
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "USB_ST_ERROR: %s", usbd_errstr(usb_error));
+
+ goto tr_setup;
+ }
+}
+
+static void
+cfumass_online(void *arg)
+{
+ struct cfumass_softc *sc;
+
+ sc = (struct cfumass_softc *)arg;
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "go");
+
+ CFUMASS_LOCK(sc);
+ sc->sc_online = 1;
+ CFUMASS_UNLOCK(sc);
+}
+
+static void
+cfumass_offline(void *arg)
+{
+ struct cfumass_softc *sc;
+
+ sc = (struct cfumass_softc *)arg;
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "go");
+
+ CFUMASS_LOCK(sc);
+ sc->sc_online = 0;
+ CFUMASS_UNLOCK(sc);
+}
+
+static void
+cfumass_datamove(union ctl_io *io)
+{
+ struct cfumass_softc *sc;
+
+ sc = io->io_hdr.ctl_private[CTL_PRIV_FRONTEND].ptr;
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "go");
+
+ CFUMASS_LOCK(sc);
+
+ KASSERT(sc->sc_ctl_io == NULL,
+ ("sc_ctl_io is %p, should be NULL", sc->sc_ctl_io));
+ sc->sc_ctl_io = io;
+
+ if ((io->io_hdr.flags & CTL_FLAG_DATA_MASK) == CTL_FLAG_DATA_IN) {
+ /*
+ * Verify that CTL wants us to send the data in the direction
+ * expected by the initiator.
+ */
+ if (sc->sc_current_flags != CBWFLAGS_IN) {
+ CFUMASS_WARN(sc, "invalid bCBWFlags 0x%x, should be x0%x",
+ sc->sc_current_flags, CBWFLAGS_IN);
+ goto fail;
+ }
+
+ cfumass_transfer_start(sc, CFUMASS_T_DATA_IN);
+ } else {
+ if (sc->sc_current_flags != CBWFLAGS_OUT) {
+ CFUMASS_WARN(sc, "invalid bCBWFlags 0x%x, should be x0%x",
+ sc->sc_current_flags, CBWFLAGS_OUT);
+ goto fail;
+ }
+
+ /* We hadn't received anything during this datamove yet. */
+ io->scsiio.ext_data_filled = 0;
+ cfumass_transfer_start(sc, CFUMASS_T_DATA_OUT);
+ }
+
+ CFUMASS_UNLOCK(sc);
+ return;
+
+fail:
+ ctl_set_data_phase_error(&io->scsiio);
+ io->scsiio.be_move_done(io);
+ sc->sc_ctl_io = NULL;
+
+ // XXX: stall?
+}
+
+static void
+cfumass_done(union ctl_io *io)
+{
+ struct cfumass_softc *sc;
+
+ sc = io->io_hdr.ctl_private[CTL_PRIV_FRONTEND].ptr;
+
+ CFUMASS_DEV_DEBUG(sc->sc_dev, "go");
+
+ KASSERT(((io->io_hdr.status & CTL_STATUS_MASK) != CTL_STATUS_NONE),
+ ("invalid CTL status %#x", io->io_hdr.status));
+ KASSERT(sc->sc_ctl_io == NULL,
+ ("sc_ctl_io is %p, should be NULL", sc->sc_ctl_io));
+
+ if (io->io_hdr.io_type == CTL_IO_TASK &&
+ io->taskio.task_action == CTL_TASK_I_T_NEXUS_RESET) {
+ /*
+ * Implicit task termination has just completed; nothing to do.
+ */
+ ctl_free_io(io);
+ return;
+ }
+
+ /*
+ * Do not return status for aborted commands.
+ * There are exceptions, but none supported by CTL yet.
+ */
+ if (((io->io_hdr.flags & CTL_FLAG_ABORT) &&
+ (io->io_hdr.flags & CTL_FLAG_ABORT_STATUS) == 0) ||
+ (io->io_hdr.flags & CTL_FLAG_STATUS_SENT)) {
+ ctl_free_io(io);
+ return;
+ }
+
+ switch (io->scsiio.scsi_status) {
+ case SCSI_STATUS_OK:
+ sc->sc_current_status = 0;
+ break;
+ default:
+ sc->sc_current_status = 1;
+ break;
+ }
+
+ CFUMASS_LOCK(sc);
+ cfumass_transfer_start(sc, CFUMASS_T_STATUS);
+ CFUMASS_UNLOCK(sc);
+ ctl_free_io(io);
+
+ refcount_release(&sc->sc_queued);
+}
+
+int
+cfumass_init(void)
+{
+
+ /*
+ * Not much to do here; all the interesting stuff happens in cfumass_attach().
+ */
+
+ return (0);
+}
Index: sys/modules/usb/Makefile
===================================================================
--- sys/modules/usb/Makefile
+++ sys/modules/usb/Makefile
@@ -47,7 +47,7 @@
SUBDIR += ${_dwc_otg} ehci ${_musb} ohci uhci xhci ${_uss820dci} ${_at91dci} \
${_atmegadci} ${_avr32dci} ${_rsu} ${_rsufw} ${_saf1761otg}
SUBDIR += ${_rum} ${_run} ${_runfw} ${_uath} upgt usie ural ${_zyd} ${_urtw}
-SUBDIR += atp uhid ukbd ums udbp ufm uep wsp ugold uled
+SUBDIR += atp cfumass uhid ukbd ums udbp ufm uep wsp ugold uled
SUBDIR += ucom u3g uark ubsa ubser uchcom ucycom ufoma uftdi ugensa uipaq ulpt \
umct umcs umodem umoscom uplcom uslcom uvisor uvscom
SUBDIR += udl
Index: sys/modules/usb/cfumass/Makefile
===================================================================
--- /dev/null
+++ sys/modules/usb/cfumass/Makefile
@@ -0,0 +1,11 @@
+# $FreeBSD$
+
+S= ${.CURDIR}/../../..
+
+.PATH: $S/dev/usb/storage
+
+KMOD= cfumass
+SRCS= bus_if.h device_if.h opt_bus.h opt_usb.h usb_if.h usbdevs.h \
+ cfumass.c
+
+.include <bsd.kmod.mk>

File Metadata

Mime Type
text/plain
Expires
Tue, Nov 25, 6:26 PM (16 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
26159180
Default Alt Text
D8787.id22903.diff (34 KB)

Event Timeline