RFC: Add USBDEVFS_TRY_DISCONNECT ioctl

From: Alan Stern
Date: Wed Aug 24 2011 - 16:32:35 EST


On Sun, 21 Aug 2011, Greg KH wrote:

> On Fri, Aug 19, 2011 at 10:56:23PM -0400, Alan Stern wrote:
> > > It's of course racey for userspace to check
> > > whether a device is busy and then disconnect the driver, but the "try
> > > disconnect" ioctl could cause the driver to disconnect itself. In the end there
> > > wasn't a very good solution to this problem.
> >
> > In principle it's quite doable. The question is whether people will
> > stand for it. Greg KH has already come out against the idea, although
> > perhaps he could be persuaded. It doesn't help that this new mechanism
> > would be used only for USB; if other subsystems could benefit from it
> > too then it would be easier to sell.
>
> I still don't think this would really work that well. But it wouldn't
> be just USB devices that need it, in the future we will have lots of
> detatched storage devices that guests want access to in "native" mode
> (Thunderbolt being one good example.)
>
> But feel free to change my mind with a patch showing just how this all
> would work :)

Okay, here's a sample patch. Actually it's three patches, listed one
after another, but people can apply it like a single patch.

1. Introduce the USBDEVFS_TRY_DISCONNECT ioctl and the check_busy
callback it uses. Implement the callback in the usbfs driver;
this gives a way for programs to unbind kernel drivers without
unbinding other userspace drivers.

2. Implement device-file reference tracking in the SCSI layer,
and the device_open and device_close callbacks it uses.

3. Implement both check_busy and device_{open,close} in
usb-storage.

Attached is a sample program that demonstrates how to use this. It
will unbind usb-storage from a USB interface if the storage device
isn't open or mounted, otherwise it will fail with a "device busy"
error. It will also unbind any other USB kernel driver from an
interface -- but it won't unbind a usbfs userspace driver that has
claimed an interface.

Alan Stern



Patch 1: USBDEVFS_TRY_DISCONNECT

drivers/usb/core/devio.c | 13 +++++++++++++
include/linux/usb.h | 5 +++++
include/linux/usbdevice_fs.h | 1 +
3 files changed, 19 insertions(+)

Index: usb-3.1/include/linux/usbdevice_fs.h
===================================================================
--- usb-3.1.orig/include/linux/usbdevice_fs.h
+++ usb-3.1/include/linux/usbdevice_fs.h
@@ -204,4 +204,5 @@ struct usbdevfs_ioctl32 {
#define USBDEVFS_CONNECT _IO('U', 23)
#define USBDEVFS_CLAIM_PORT _IOR('U', 24, unsigned int)
#define USBDEVFS_RELEASE_PORT _IOR('U', 25, unsigned int)
+#define USBDEVFS_TRY_DISCONNECT _IO('U', 26)
#endif /* _LINUX_USBDEVICE_FS_H */
Index: usb-3.1/include/linux/usb.h
===================================================================
--- usb-3.1.orig/include/linux/usb.h
+++ usb-3.1/include/linux/usb.h
@@ -812,6 +812,9 @@ struct usbdrv_wrap {
* post_reset method is called.
* @post_reset: Called by usb_reset_device() after the device
* has been reset
+ * @check_busy: Called to see whether the device is currently
+ * busy before doing a user-initiated unbind. The driver must not
+ * allow the device to become busy after this routine returns 0.
* @id_table: USB drivers use ID table to support hotplugging.
* Export this with MODULE_DEVICE_TABLE(usb,...). This must be set
* or your driver's probe function will never get called.
@@ -858,6 +861,8 @@ struct usb_driver {
int (*pre_reset)(struct usb_interface *intf);
int (*post_reset)(struct usb_interface *intf);

+ int (*check_busy)(struct usb_interface *intf);
+
const struct usb_device_id *id_table;

struct usb_dynids dynids;
Index: usb-3.1/drivers/usb/core/devio.c
===================================================================
--- usb-3.1.orig/drivers/usb/core/devio.c
+++ usb-3.1/drivers/usb/core/devio.c
@@ -516,12 +516,18 @@ static int driver_resume(struct usb_inte
return 0;
}

+static int driver_check_busy(struct usb_interface *intf)
+{
+ return -EBUSY;
+}
+
struct usb_driver usbfs_driver = {
.name = "usbfs",
.probe = driver_probe,
.disconnect = driver_disconnect,
.suspend = driver_suspend,
.resume = driver_resume,
+ .check_busy = driver_check_busy,
};

static int claimintf(struct dev_state *ps, unsigned int ifnum)
@@ -1647,9 +1653,16 @@ static int proc_ioctl(struct dev_state *
else switch (ctl->ioctl_code) {

/* disconnect kernel driver from interface */
+ case USBDEVFS_TRY_DISCONNECT:
case USBDEVFS_DISCONNECT:
if (intf->dev.driver) {
driver = to_usb_driver(intf->dev.driver);
+ if (ctl->ioctl_code == USBDEVFS_TRY_DISCONNECT &&
+ driver->check_busy) {
+ retval = driver->check_busy(intf);
+ if (retval)
+ break;
+ }
dev_dbg(&intf->dev, "disconnect by usbfs\n");
usb_driver_release_interface(driver, intf);
} else



Patch 2: SCSI device file open reference tracking

drivers/scsi/hosts.c | 32 ++++++++++++++++++++++++++++++++
drivers/scsi/sd.c | 7 +++++++
drivers/scsi/sg.c | 8 +++++++-
drivers/scsi/sr.c | 3 ++-
include/scsi/scsi_device.h | 2 ++
include/scsi/scsi_host.h | 19 +++++++++++++++++++
6 files changed, 69 insertions(+), 2 deletions(-)

Index: usb-3.1/include/scsi/scsi_host.h
===================================================================
--- usb-3.1.orig/include/scsi/scsi_host.h
+++ usb-3.1/include/scsi/scsi_host.h
@@ -356,6 +356,25 @@ struct scsi_host_template {
enum blk_eh_timer_return (*eh_timed_out)(struct scsi_cmnd *);

/*
+ * This function is called when a device file is about to be opened;
+ * if the function returns nonzero then the open fails. The low
+ * level driver can use this to track the number of open device file
+ * references (including mounts), and thereby know when it is safe to
+ * unregister the host without disrupting mounted filesystems or
+ * the like.
+ *
+ * Status: OPTIONAL
+ */
+ int (*device_open)(struct Scsi_Host *, struct scsi_device *);
+
+ /*
+ * This function is called after a device file is closed.
+ *
+ * Status: OPTIONAL
+ */
+ void (*device_close)(struct Scsi_Host *, struct scsi_device *);
+
+ /*
* Name of proc directory
*/
const char *proc_name;
Index: usb-3.1/include/scsi/scsi_device.h
===================================================================
--- usb-3.1.orig/include/scsi/scsi_device.h
+++ usb-3.1/include/scsi/scsi_device.h
@@ -361,6 +361,8 @@ extern struct scsi_event *sdev_evt_alloc
extern void sdev_evt_send(struct scsi_device *sdev, struct scsi_event *evt);
extern void sdev_evt_send_simple(struct scsi_device *sdev,
enum scsi_device_event evt_type, gfp_t gfpflags);
+extern int scsi_device_open(struct scsi_device *sdev);
+extern void scsi_device_close(struct scsi_device *sdev);
extern int scsi_device_quiesce(struct scsi_device *sdev);
extern void scsi_device_resume(struct scsi_device *sdev);
extern void scsi_target_quiesce(struct scsi_target *);
Index: usb-3.1/drivers/scsi/hosts.c
===================================================================
--- usb-3.1.orig/drivers/scsi/hosts.c
+++ usb-3.1/drivers/scsi/hosts.c
@@ -577,3 +577,35 @@ void scsi_flush_work(struct Scsi_Host *s
flush_workqueue(shost->work_q);
}
EXPORT_SYMBOL_GPL(scsi_flush_work);
+
+/**
+ * scsi_device_open - Check whether it's okay to open a device file
+ * @sdev: Device whose file will be opened.
+ *
+ * Return value:
+ * 0 if open is allowed; negative errno otherwise
+ **/
+int scsi_device_open(struct scsi_device *sdev)
+{
+ struct Scsi_Host *shost = sdev->host;
+ struct scsi_host_template *sht = shost->hostt;
+
+ if (!sht->device_open)
+ return 0;
+ return sht->device_open(shost, sdev);
+}
+EXPORT_SYMBOL_GPL(scsi_device_open);
+
+/**
+ * scsi_device_close - Announce that a device file has been closed
+ * @sdev: Device whose file has been closed.
+ **/
+void scsi_device_close(struct scsi_device *sdev)
+{
+ struct Scsi_Host *shost = sdev->host;
+ struct scsi_host_template *sht = shost->hostt;
+
+ if (sht->device_close)
+ sht->device_close(shost, sdev);
+}
+EXPORT_SYMBOL_GPL(scsi_device_close);
Index: usb-3.1/drivers/scsi/sd.c
===================================================================
--- usb-3.1.orig/drivers/scsi/sd.c
+++ usb-3.1/drivers/scsi/sd.c
@@ -935,6 +935,10 @@ static int sd_open(struct block_device *

sdev = sdkp->device;

+ retval = scsi_device_open(sdev);
+ if (retval)
+ goto error_open;
+
retval = scsi_autopm_get_device(sdev);
if (retval)
goto error_autopm;
@@ -985,6 +989,8 @@ static int sd_open(struct block_device *
error_out:
scsi_autopm_put_device(sdev);
error_autopm:
+ scsi_device_close(sdev);
+error_open:
scsi_disk_put(sdkp);
return retval;
}
@@ -1020,6 +1026,7 @@ static int sd_release(struct gendisk *di
*/

scsi_autopm_put_device(sdev);
+ scsi_device_close(sdev);
scsi_disk_put(sdkp);
return 0;
}
Index: usb-3.1/drivers/scsi/sr.c
===================================================================
--- usb-3.1.orig/drivers/scsi/sr.c
+++ usb-3.1/drivers/scsi/sr.c
@@ -623,7 +623,7 @@ static int sr_open(struct cdrom_device_i
if (!scsi_block_when_processing_errors(sdev))
goto error_out;

- return 0;
+ retval = scsi_device_open(sdev);

error_out:
return retval;
@@ -636,6 +636,7 @@ static void sr_release(struct cdrom_devi
if (cd->device->sector_size > 2048)
sr_set_blocklength(cd, 2048);

+ scsi_device_close(cd->device);
}

static int sr_probe(struct device *dev)
Index: usb-3.1/drivers/scsi/sg.c
===================================================================
--- usb-3.1.orig/drivers/scsi/sg.c
+++ usb-3.1/drivers/scsi/sg.c
@@ -247,10 +247,14 @@ sg_open(struct inode *inode, struct file
if (retval)
goto sg_put;

- retval = scsi_autopm_get_device(sdp->device);
+ retval = scsi_device_open(sdp->device);
if (retval)
goto sdp_put;

+ retval = scsi_autopm_get_device(sdp->device);
+ if (retval)
+ goto scsi_device_close;
+
if (!((flags & O_NONBLOCK) ||
scsi_block_when_processing_errors(sdp->device))) {
retval = -ENXIO;
@@ -310,6 +314,8 @@ sg_open(struct inode *inode, struct file
error_out:
if (retval) {
scsi_autopm_put_device(sdp->device);
+scsi_device_close:
+ scsi_device_close(sdp->device);
sdp_put:
scsi_device_put(sdp->device);
}



Patch 3: implementation for usb-storage

drivers/usb/storage/scsiglue.c | 30 ++++++++++++++++++++++++++++++
drivers/usb/storage/usb.c | 21 +++++++++++++++++++++
drivers/usb/storage/usb.h | 1 +
3 files changed, 52 insertions(+)

Index: usb-3.1/drivers/usb/storage/usb.h
===================================================================
--- usb-3.1.orig/drivers/usb/storage/usb.h
+++ usb-3.1/drivers/usb/storage/usb.h
@@ -135,6 +135,7 @@ struct us_data {
struct scsi_cmnd *srb; /* current srb */
unsigned int tag; /* current dCBWTag */
char scsi_name[32]; /* scsi_host name */
+ int open_count; /* open file refs */

/* control and bulk communications data */
struct urb *current_urb; /* USB requests */
Index: usb-3.1/drivers/usb/storage/usb.c
===================================================================
--- usb-3.1.orig/drivers/usb/storage/usb.c
+++ usb-3.1/drivers/usb/storage/usb.c
@@ -217,6 +217,26 @@ int usb_stor_post_reset(struct usb_inter
}
EXPORT_SYMBOL_GPL(usb_stor_post_reset);

+int usb_stor_check_busy(struct usb_interface *iface)
+{
+ struct us_data *us = usb_get_intfdata(iface);
+ struct Scsi_Host *host = us_to_host(us);
+ int rc = -EBUSY;
+
+ US_DEBUGP("%s\n", __func__);
+
+ /* If there are no open file references then we aren't busy */
+ scsi_lock(host);
+ if (us->open_count <= 0) {
+ us->open_count = -1; /* No more opens allowed */
+ rc = 0;
+ }
+ scsi_unlock(host);
+
+ return rc;
+}
+EXPORT_SYMBOL_GPL(usb_stor_check_busy);
+
/*
* fill_inquiry_response takes an unsigned char array (which must
* be at least 36 characters) and populates the vendor name,
@@ -1060,6 +1080,7 @@ static struct usb_driver usb_storage_dri
.reset_resume = usb_stor_reset_resume,
.pre_reset = usb_stor_pre_reset,
.post_reset = usb_stor_post_reset,
+ .check_busy = usb_stor_check_busy,
.id_table = usb_storage_usb_ids,
.supports_autosuspend = 1,
.soft_unbind = 1,
Index: usb-3.1/drivers/usb/storage/scsiglue.c
===================================================================
--- usb-3.1.orig/drivers/usb/storage/scsiglue.c
+++ usb-3.1/drivers/usb/storage/scsiglue.c
@@ -411,6 +411,32 @@ void usb_stor_report_bus_reset(struct us
scsi_unlock(host);
}

+/* Check whether to allow a device file to be opened. */
+static int device_open(struct Scsi_Host *host, struct scsi_device *sdev)
+{
+ struct us_data *us = host_to_us(host);
+ int rc = -EIO;
+
+ /* Allow the open if a disconnect hasn't been requested */
+ scsi_lock(host);
+ if (us->open_count >= 0) {
+ ++us->open_count;
+ rc = 0;
+ }
+ scsi_unlock(host);
+ return rc;
+}
+
+/* A device file has been closed. */
+static void device_close(struct Scsi_Host *host, struct scsi_device *sdev)
+{
+ struct us_data *us = host_to_us(host);
+
+ scsi_lock(host);
+ --us->open_count;
+ scsi_unlock(host);
+}
+
/***********************************************************************
* /proc/scsi/ functions
***********************************************************************/
@@ -537,6 +563,10 @@ struct scsi_host_template usb_stor_host_
.eh_device_reset_handler = device_reset,
.eh_bus_reset_handler = bus_reset,

+ /* open file reference notifiers */
+ .device_open = device_open,
+ .device_close = device_close,
+
/* queue commands only, only one command per LUN */
.can_queue = 1,
.cmd_per_lun = 1,
/* usb-try-discon -- try to claim a USB mass-storage interface */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>

#include <linux/usbdevice_fs.h>
#define USBDEVFS_TRY_DISCONNECT _IO('U', 26)


int main(int argc, char **argv)
{
const char *filename;
unsigned intf;
int fd;
int rc;
int c;
struct usbdevfs_ioctl uioctl;

if (argc != 3) {
fprintf(stderr, "Usage: usb-try-discon devpath intf-num\n");
return 1;
}
filename = argv[1];
intf = atoi(argv[2]);

fd = open(filename, O_WRONLY);
if (fd < 0) {
perror("Error opening device file");
return 1;
}

printf("Trying to disconnect driver of %s interface %d\n",
filename, intf);
uioctl.ifno = intf;
uioctl.ioctl_code = USBDEVFS_TRY_DISCONNECT;
uioctl.data = NULL;
rc = ioctl(fd, USBDEVFS_IOCTL, &uioctl);
if (rc < 0 && errno == ENODATA) {
printf("No driver was connected.\n");
} else if (rc < 0) {
perror("Error in ioctl");
return 1;
} else {
printf("Disconnect successful\n");
}

printf("Trying to claim the interface\n");
rc = ioctl(fd, USBDEVFS_CLAIMINTERFACE, &intf);
if (rc < 0) {
perror("Error in ioctl");
return 1;
}
printf("Claim successful\n");

printf("Press Enter to continue...");
do {
c = getc(stdin);
} while (c != '\n' && c != EOF);

printf("Releasing interface\n");
rc = ioctl(fd, USBDEVFS_RELEASEINTERFACE, &intf);
if (rc < 0) {
perror("Error in ioctl");
return 1;
}
printf("Release successful\n");

close(fd);
return 0;
}