|  | // SPDX-License-Identifier: GPL-2.0+ | 
|  | /* | 
|  | * Copyright (C) 2015 Karol Kosik <karo9@interia.eu> | 
|  | * Copyright (C) 2015-2016 Samsung Electronics | 
|  | *               Igor Kotrasinski <i.kotrasinsk@samsung.com> | 
|  | *               Krzysztof Opasiak <k.opasiak@samsung.com> | 
|  | */ | 
|  |  | 
|  | #include <linux/device.h> | 
|  | #include <linux/kernel.h> | 
|  | #include <linux/list.h> | 
|  | #include <linux/platform_device.h> | 
|  | #include <linux/usb.h> | 
|  | #include <linux/usb/gadget.h> | 
|  | #include <linux/usb/hcd.h> | 
|  | #include <linux/kthread.h> | 
|  | #include <linux/file.h> | 
|  | #include <linux/byteorder/generic.h> | 
|  |  | 
|  | #include "usbip_common.h" | 
|  | #include "vudc.h" | 
|  |  | 
|  | #define VIRTUAL_ENDPOINTS (1 /* ep0 */ + 15 /* in eps */ + 15 /* out eps */) | 
|  |  | 
|  | /* urb-related structures alloc / free */ | 
|  |  | 
|  |  | 
|  | static void free_urb(struct urb *urb) | 
|  | { | 
|  | if (!urb) | 
|  | return; | 
|  |  | 
|  | kfree(urb->setup_packet); | 
|  | urb->setup_packet = NULL; | 
|  |  | 
|  | kfree(urb->transfer_buffer); | 
|  | urb->transfer_buffer = NULL; | 
|  |  | 
|  | usb_free_urb(urb); | 
|  | } | 
|  |  | 
|  | struct urbp *alloc_urbp(void) | 
|  | { | 
|  | struct urbp *urb_p; | 
|  |  | 
|  | urb_p = kzalloc(sizeof(*urb_p), GFP_KERNEL); | 
|  | if (!urb_p) | 
|  | return urb_p; | 
|  |  | 
|  | urb_p->urb = NULL; | 
|  | urb_p->ep = NULL; | 
|  | INIT_LIST_HEAD(&urb_p->urb_entry); | 
|  | return urb_p; | 
|  | } | 
|  |  | 
|  | static void free_urbp(struct urbp *urb_p) | 
|  | { | 
|  | kfree(urb_p); | 
|  | } | 
|  |  | 
|  | void free_urbp_and_urb(struct urbp *urb_p) | 
|  | { | 
|  | if (!urb_p) | 
|  | return; | 
|  | free_urb(urb_p->urb); | 
|  | free_urbp(urb_p); | 
|  | } | 
|  |  | 
|  |  | 
|  | /* utilities ; almost verbatim from dummy_hcd.c */ | 
|  |  | 
|  | /* called with spinlock held */ | 
|  | static void nuke(struct vudc *udc, struct vep *ep) | 
|  | { | 
|  | struct vrequest	*req; | 
|  |  | 
|  | while (!list_empty(&ep->req_queue)) { | 
|  | req = list_first_entry(&ep->req_queue, struct vrequest, | 
|  | req_entry); | 
|  | list_del_init(&req->req_entry); | 
|  | req->req.status = -ESHUTDOWN; | 
|  |  | 
|  | spin_unlock(&udc->lock); | 
|  | usb_gadget_giveback_request(&ep->ep, &req->req); | 
|  | spin_lock(&udc->lock); | 
|  | } | 
|  | } | 
|  |  | 
|  | /* caller must hold lock */ | 
|  | static void stop_activity(struct vudc *udc) | 
|  | { | 
|  | int i; | 
|  | struct urbp *urb_p, *tmp; | 
|  |  | 
|  | udc->address = 0; | 
|  |  | 
|  | for (i = 0; i < VIRTUAL_ENDPOINTS; i++) | 
|  | nuke(udc, &udc->ep[i]); | 
|  |  | 
|  | list_for_each_entry_safe(urb_p, tmp, &udc->urb_queue, urb_entry) { | 
|  | list_del(&urb_p->urb_entry); | 
|  | free_urbp_and_urb(urb_p); | 
|  | } | 
|  | } | 
|  |  | 
|  | struct vep *vudc_find_endpoint(struct vudc *udc, u8 address) | 
|  | { | 
|  | int i; | 
|  |  | 
|  | if ((address & ~USB_DIR_IN) == 0) | 
|  | return &udc->ep[0]; | 
|  |  | 
|  | for (i = 1; i < VIRTUAL_ENDPOINTS; i++) { | 
|  | struct vep *ep = &udc->ep[i]; | 
|  |  | 
|  | if (!ep->desc) | 
|  | continue; | 
|  | if (ep->desc->bEndpointAddress == address) | 
|  | return ep; | 
|  | } | 
|  | return NULL; | 
|  | } | 
|  |  | 
|  | /* gadget ops */ | 
|  |  | 
|  | static int vgadget_get_frame(struct usb_gadget *_gadget) | 
|  | { | 
|  | struct timespec64 now; | 
|  | struct vudc *udc = usb_gadget_to_vudc(_gadget); | 
|  |  | 
|  | ktime_get_ts64(&now); | 
|  | return ((now.tv_sec - udc->start_time.tv_sec) * 1000 + | 
|  | (now.tv_nsec - udc->start_time.tv_nsec) / NSEC_PER_MSEC) | 
|  | & 0x7FF; | 
|  | } | 
|  |  | 
|  | static int vgadget_set_selfpowered(struct usb_gadget *_gadget, int value) | 
|  | { | 
|  | struct vudc *udc = usb_gadget_to_vudc(_gadget); | 
|  |  | 
|  | if (value) | 
|  | udc->devstatus |= (1 << USB_DEVICE_SELF_POWERED); | 
|  | else | 
|  | udc->devstatus &= ~(1 << USB_DEVICE_SELF_POWERED); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int vgadget_pullup(struct usb_gadget *_gadget, int value) | 
|  | { | 
|  | struct vudc *udc = usb_gadget_to_vudc(_gadget); | 
|  | unsigned long flags; | 
|  | int ret; | 
|  |  | 
|  |  | 
|  | spin_lock_irqsave(&udc->lock, flags); | 
|  | value = !!value; | 
|  | if (value == udc->pullup) | 
|  | goto unlock; | 
|  |  | 
|  | udc->pullup = value; | 
|  | if (value) { | 
|  | udc->gadget.speed = min_t(u8, USB_SPEED_HIGH, | 
|  | udc->driver->max_speed); | 
|  | udc->ep[0].ep.maxpacket = 64; | 
|  | /* | 
|  | * This is the first place where we can ask our | 
|  | * gadget driver for descriptors. | 
|  | */ | 
|  | ret = get_gadget_descs(udc); | 
|  | if (ret) { | 
|  | dev_err(&udc->gadget.dev, "Unable go get desc: %d", ret); | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | spin_unlock_irqrestore(&udc->lock, flags); | 
|  | usbip_start_eh(&udc->ud); | 
|  | } else { | 
|  | /* Invalidate descriptors */ | 
|  | udc->desc_cached = 0; | 
|  |  | 
|  | spin_unlock_irqrestore(&udc->lock, flags); | 
|  | usbip_event_add(&udc->ud, VUDC_EVENT_REMOVED); | 
|  | usbip_stop_eh(&udc->ud); /* Wait for eh completion */ | 
|  | } | 
|  |  | 
|  | return 0; | 
|  |  | 
|  | unlock: | 
|  | spin_unlock_irqrestore(&udc->lock, flags); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int vgadget_udc_start(struct usb_gadget *g, | 
|  | struct usb_gadget_driver *driver) | 
|  | { | 
|  | struct vudc *udc = usb_gadget_to_vudc(g); | 
|  | unsigned long flags; | 
|  |  | 
|  | spin_lock_irqsave(&udc->lock, flags); | 
|  | udc->driver = driver; | 
|  | udc->pullup = udc->connected = udc->desc_cached = 0; | 
|  | spin_unlock_irqrestore(&udc->lock, flags); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int vgadget_udc_stop(struct usb_gadget *g) | 
|  | { | 
|  | struct vudc *udc = usb_gadget_to_vudc(g); | 
|  | unsigned long flags; | 
|  |  | 
|  | spin_lock_irqsave(&udc->lock, flags); | 
|  | udc->driver = NULL; | 
|  | spin_unlock_irqrestore(&udc->lock, flags); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static const struct usb_gadget_ops vgadget_ops = { | 
|  | .get_frame	= vgadget_get_frame, | 
|  | .set_selfpowered = vgadget_set_selfpowered, | 
|  | .pullup		= vgadget_pullup, | 
|  | .udc_start	= vgadget_udc_start, | 
|  | .udc_stop	= vgadget_udc_stop, | 
|  | }; | 
|  |  | 
|  |  | 
|  | /* endpoint ops */ | 
|  |  | 
|  | static int vep_enable(struct usb_ep *_ep, | 
|  | const struct usb_endpoint_descriptor *desc) | 
|  | { | 
|  | struct vep	*ep; | 
|  | struct vudc	*udc; | 
|  | unsigned int	maxp; | 
|  | unsigned long	flags; | 
|  |  | 
|  | ep = to_vep(_ep); | 
|  | udc = ep_to_vudc(ep); | 
|  |  | 
|  | if (!_ep || !desc || ep->desc || _ep->caps.type_control | 
|  | || desc->bDescriptorType != USB_DT_ENDPOINT) | 
|  | return -EINVAL; | 
|  |  | 
|  | if (!udc->driver) | 
|  | return -ESHUTDOWN; | 
|  |  | 
|  | spin_lock_irqsave(&udc->lock, flags); | 
|  |  | 
|  | maxp = usb_endpoint_maxp(desc); | 
|  | _ep->maxpacket = maxp; | 
|  | ep->desc = desc; | 
|  | ep->type = usb_endpoint_type(desc); | 
|  | ep->halted = ep->wedged = 0; | 
|  |  | 
|  | spin_unlock_irqrestore(&udc->lock, flags); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int vep_disable(struct usb_ep *_ep) | 
|  | { | 
|  | struct vep *ep; | 
|  | struct vudc *udc; | 
|  | unsigned long flags; | 
|  |  | 
|  | ep = to_vep(_ep); | 
|  | udc = ep_to_vudc(ep); | 
|  | if (!_ep || !ep->desc || _ep->caps.type_control) | 
|  | return -EINVAL; | 
|  |  | 
|  | spin_lock_irqsave(&udc->lock, flags); | 
|  | ep->desc = NULL; | 
|  | nuke(udc, ep); | 
|  | spin_unlock_irqrestore(&udc->lock, flags); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static struct usb_request *vep_alloc_request(struct usb_ep *_ep, | 
|  | gfp_t mem_flags) | 
|  | { | 
|  | struct vrequest *req; | 
|  |  | 
|  | if (!_ep) | 
|  | return NULL; | 
|  |  | 
|  | req = kzalloc(sizeof(*req), mem_flags); | 
|  | if (!req) | 
|  | return NULL; | 
|  |  | 
|  | INIT_LIST_HEAD(&req->req_entry); | 
|  |  | 
|  | return &req->req; | 
|  | } | 
|  |  | 
|  | static void vep_free_request(struct usb_ep *_ep, struct usb_request *_req) | 
|  | { | 
|  | struct vrequest *req; | 
|  |  | 
|  | /* ep is always valid here - see usb_ep_free_request() */ | 
|  | if (!_req) | 
|  | return; | 
|  |  | 
|  | req = to_vrequest(_req); | 
|  | kfree(req); | 
|  | } | 
|  |  | 
|  | static int vep_queue(struct usb_ep *_ep, struct usb_request *_req, | 
|  | gfp_t mem_flags) | 
|  | { | 
|  | struct vep *ep; | 
|  | struct vrequest *req; | 
|  | struct vudc *udc; | 
|  | unsigned long flags; | 
|  |  | 
|  | if (!_ep || !_req) | 
|  | return -EINVAL; | 
|  |  | 
|  | ep = to_vep(_ep); | 
|  | req = to_vrequest(_req); | 
|  | udc = ep_to_vudc(ep); | 
|  |  | 
|  | spin_lock_irqsave(&udc->lock, flags); | 
|  | _req->actual = 0; | 
|  | _req->status = -EINPROGRESS; | 
|  |  | 
|  | list_add_tail(&req->req_entry, &ep->req_queue); | 
|  | spin_unlock_irqrestore(&udc->lock, flags); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int vep_dequeue(struct usb_ep *_ep, struct usb_request *_req) | 
|  | { | 
|  | struct vep *ep; | 
|  | struct vrequest *req; | 
|  | struct vudc *udc; | 
|  | struct vrequest *lst; | 
|  | unsigned long flags; | 
|  | int ret = -EINVAL; | 
|  |  | 
|  | if (!_ep || !_req) | 
|  | return ret; | 
|  |  | 
|  | ep = to_vep(_ep); | 
|  | req = to_vrequest(_req); | 
|  | udc = req->udc; | 
|  |  | 
|  | if (!udc->driver) | 
|  | return -ESHUTDOWN; | 
|  |  | 
|  | spin_lock_irqsave(&udc->lock, flags); | 
|  | list_for_each_entry(lst, &ep->req_queue, req_entry) { | 
|  | if (&lst->req == _req) { | 
|  | list_del_init(&lst->req_entry); | 
|  | _req->status = -ECONNRESET; | 
|  | ret = 0; | 
|  | break; | 
|  | } | 
|  | } | 
|  | spin_unlock_irqrestore(&udc->lock, flags); | 
|  |  | 
|  | if (ret == 0) | 
|  | usb_gadget_giveback_request(_ep, _req); | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | static int | 
|  | vep_set_halt_and_wedge(struct usb_ep *_ep, int value, int wedged) | 
|  | { | 
|  | struct vep *ep; | 
|  | struct vudc *udc; | 
|  | unsigned long flags; | 
|  | int ret = 0; | 
|  |  | 
|  | ep = to_vep(_ep); | 
|  | if (!_ep) | 
|  | return -EINVAL; | 
|  |  | 
|  | udc = ep_to_vudc(ep); | 
|  | if (!udc->driver) | 
|  | return -ESHUTDOWN; | 
|  |  | 
|  | spin_lock_irqsave(&udc->lock, flags); | 
|  | if (!value) | 
|  | ep->halted = ep->wedged = 0; | 
|  | else if (ep->desc && (ep->desc->bEndpointAddress & USB_DIR_IN) && | 
|  | !list_empty(&ep->req_queue)) | 
|  | ret = -EAGAIN; | 
|  | else { | 
|  | ep->halted = 1; | 
|  | if (wedged) | 
|  | ep->wedged = 1; | 
|  | } | 
|  |  | 
|  | spin_unlock_irqrestore(&udc->lock, flags); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | static int | 
|  | vep_set_halt(struct usb_ep *_ep, int value) | 
|  | { | 
|  | return vep_set_halt_and_wedge(_ep, value, 0); | 
|  | } | 
|  |  | 
|  | static int vep_set_wedge(struct usb_ep *_ep) | 
|  | { | 
|  | return vep_set_halt_and_wedge(_ep, 1, 1); | 
|  | } | 
|  |  | 
|  | static const struct usb_ep_ops vep_ops = { | 
|  | .enable		= vep_enable, | 
|  | .disable	= vep_disable, | 
|  |  | 
|  | .alloc_request	= vep_alloc_request, | 
|  | .free_request	= vep_free_request, | 
|  |  | 
|  | .queue		= vep_queue, | 
|  | .dequeue	= vep_dequeue, | 
|  |  | 
|  | .set_halt	= vep_set_halt, | 
|  | .set_wedge	= vep_set_wedge, | 
|  | }; | 
|  |  | 
|  |  | 
|  | /* shutdown / reset / error handlers */ | 
|  |  | 
|  | static void vudc_shutdown(struct usbip_device *ud) | 
|  | { | 
|  | struct vudc *udc = container_of(ud, struct vudc, ud); | 
|  | int call_disconnect = 0; | 
|  | unsigned long flags; | 
|  |  | 
|  | dev_dbg(&udc->pdev->dev, "device shutdown"); | 
|  | if (ud->tcp_socket) | 
|  | kernel_sock_shutdown(ud->tcp_socket, SHUT_RDWR); | 
|  |  | 
|  | if (ud->tcp_rx) { | 
|  | kthread_stop_put(ud->tcp_rx); | 
|  | ud->tcp_rx = NULL; | 
|  | } | 
|  | if (ud->tcp_tx) { | 
|  | kthread_stop_put(ud->tcp_tx); | 
|  | ud->tcp_tx = NULL; | 
|  | } | 
|  |  | 
|  | if (ud->tcp_socket) { | 
|  | sockfd_put(ud->tcp_socket); | 
|  | ud->tcp_socket = NULL; | 
|  | } | 
|  |  | 
|  | spin_lock_irqsave(&udc->lock, flags); | 
|  | stop_activity(udc); | 
|  | if (udc->connected && udc->driver->disconnect) | 
|  | call_disconnect = 1; | 
|  | udc->connected = 0; | 
|  | spin_unlock_irqrestore(&udc->lock, flags); | 
|  | if (call_disconnect) | 
|  | udc->driver->disconnect(&udc->gadget); | 
|  | } | 
|  |  | 
|  | static void vudc_device_reset(struct usbip_device *ud) | 
|  | { | 
|  | struct vudc *udc = container_of(ud, struct vudc, ud); | 
|  | unsigned long flags; | 
|  |  | 
|  | dev_dbg(&udc->pdev->dev, "device reset"); | 
|  | spin_lock_irqsave(&udc->lock, flags); | 
|  | stop_activity(udc); | 
|  | spin_unlock_irqrestore(&udc->lock, flags); | 
|  | if (udc->driver) | 
|  | usb_gadget_udc_reset(&udc->gadget, udc->driver); | 
|  | spin_lock_irqsave(&ud->lock, flags); | 
|  | ud->status = SDEV_ST_AVAILABLE; | 
|  | spin_unlock_irqrestore(&ud->lock, flags); | 
|  | } | 
|  |  | 
|  | static void vudc_device_unusable(struct usbip_device *ud) | 
|  | { | 
|  | unsigned long flags; | 
|  |  | 
|  | spin_lock_irqsave(&ud->lock, flags); | 
|  | ud->status = SDEV_ST_ERROR; | 
|  | spin_unlock_irqrestore(&ud->lock, flags); | 
|  | } | 
|  |  | 
|  | /* device setup / cleanup */ | 
|  |  | 
|  | struct vudc_device *alloc_vudc_device(int devid) | 
|  | { | 
|  | struct vudc_device *udc_dev = NULL; | 
|  |  | 
|  | udc_dev = kzalloc(sizeof(*udc_dev), GFP_KERNEL); | 
|  | if (!udc_dev) | 
|  | goto out; | 
|  |  | 
|  | INIT_LIST_HEAD(&udc_dev->dev_entry); | 
|  |  | 
|  | udc_dev->pdev = platform_device_alloc(GADGET_NAME, devid); | 
|  | if (!udc_dev->pdev) { | 
|  | kfree(udc_dev); | 
|  | udc_dev = NULL; | 
|  | } | 
|  |  | 
|  | out: | 
|  | return udc_dev; | 
|  | } | 
|  |  | 
|  | void put_vudc_device(struct vudc_device *udc_dev) | 
|  | { | 
|  | platform_device_put(udc_dev->pdev); | 
|  | kfree(udc_dev); | 
|  | } | 
|  |  | 
|  | static int init_vudc_hw(struct vudc *udc) | 
|  | { | 
|  | int i; | 
|  | struct usbip_device *ud = &udc->ud; | 
|  | struct vep *ep; | 
|  |  | 
|  | udc->ep = kcalloc(VIRTUAL_ENDPOINTS, sizeof(*udc->ep), GFP_KERNEL); | 
|  | if (!udc->ep) | 
|  | goto nomem_ep; | 
|  |  | 
|  | INIT_LIST_HEAD(&udc->gadget.ep_list); | 
|  |  | 
|  | /* create ep0 and 15 in, 15 out general purpose eps */ | 
|  | for (i = 0; i < VIRTUAL_ENDPOINTS; ++i) { | 
|  | int is_out = i % 2; | 
|  | int num = (i + 1) / 2; | 
|  |  | 
|  | ep = &udc->ep[i]; | 
|  |  | 
|  | sprintf(ep->name, "ep%d%s", num, | 
|  | i ? (is_out ? "out" : "in") : ""); | 
|  | ep->ep.name = ep->name; | 
|  |  | 
|  | ep->ep.ops = &vep_ops; | 
|  |  | 
|  | usb_ep_set_maxpacket_limit(&ep->ep, ~0); | 
|  | ep->ep.max_streams = 16; | 
|  | ep->gadget = &udc->gadget; | 
|  | INIT_LIST_HEAD(&ep->req_queue); | 
|  |  | 
|  | if (i == 0) { | 
|  | /* ep0 */ | 
|  | ep->ep.caps.type_control = true; | 
|  | ep->ep.caps.dir_out = true; | 
|  | ep->ep.caps.dir_in = true; | 
|  |  | 
|  | udc->gadget.ep0 = &ep->ep; | 
|  | } else { | 
|  | /* All other eps */ | 
|  | ep->ep.caps.type_iso = true; | 
|  | ep->ep.caps.type_int = true; | 
|  | ep->ep.caps.type_bulk = true; | 
|  |  | 
|  | if (is_out) | 
|  | ep->ep.caps.dir_out = true; | 
|  | else | 
|  | ep->ep.caps.dir_in = true; | 
|  |  | 
|  | list_add_tail(&ep->ep.ep_list, &udc->gadget.ep_list); | 
|  | } | 
|  | } | 
|  |  | 
|  | spin_lock_init(&udc->lock); | 
|  | spin_lock_init(&udc->lock_tx); | 
|  | INIT_LIST_HEAD(&udc->urb_queue); | 
|  | INIT_LIST_HEAD(&udc->tx_queue); | 
|  | init_waitqueue_head(&udc->tx_waitq); | 
|  |  | 
|  | spin_lock_init(&ud->lock); | 
|  | mutex_init(&ud->sysfs_lock); | 
|  | ud->status = SDEV_ST_AVAILABLE; | 
|  | ud->side = USBIP_VUDC; | 
|  |  | 
|  | ud->eh_ops.shutdown = vudc_shutdown; | 
|  | ud->eh_ops.reset    = vudc_device_reset; | 
|  | ud->eh_ops.unusable = vudc_device_unusable; | 
|  |  | 
|  | v_init_timer(udc); | 
|  | return 0; | 
|  |  | 
|  | nomem_ep: | 
|  | return -ENOMEM; | 
|  | } | 
|  |  | 
|  | static void cleanup_vudc_hw(struct vudc *udc) | 
|  | { | 
|  | kfree(udc->ep); | 
|  | } | 
|  |  | 
|  | /* platform driver ops */ | 
|  |  | 
|  | int vudc_probe(struct platform_device *pdev) | 
|  | { | 
|  | struct vudc *udc; | 
|  | int ret = -ENOMEM; | 
|  |  | 
|  | udc = kzalloc(sizeof(*udc), GFP_KERNEL); | 
|  | if (!udc) | 
|  | goto out; | 
|  |  | 
|  | udc->gadget.name = GADGET_NAME; | 
|  | udc->gadget.ops = &vgadget_ops; | 
|  | udc->gadget.max_speed = USB_SPEED_HIGH; | 
|  | udc->gadget.dev.parent = &pdev->dev; | 
|  | udc->pdev = pdev; | 
|  |  | 
|  | ret = init_vudc_hw(udc); | 
|  | if (ret) | 
|  | goto err_init_vudc_hw; | 
|  |  | 
|  | ret = usb_add_gadget_udc(&pdev->dev, &udc->gadget); | 
|  | if (ret < 0) | 
|  | goto err_add_udc; | 
|  |  | 
|  | platform_set_drvdata(pdev, udc); | 
|  |  | 
|  | return ret; | 
|  |  | 
|  | err_add_udc: | 
|  | cleanup_vudc_hw(udc); | 
|  | err_init_vudc_hw: | 
|  | kfree(udc); | 
|  | out: | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | int vudc_remove(struct platform_device *pdev) | 
|  | { | 
|  | struct vudc *udc = platform_get_drvdata(pdev); | 
|  |  | 
|  | usb_del_gadget_udc(&udc->gadget); | 
|  | cleanup_vudc_hw(udc); | 
|  | kfree(udc); | 
|  | return 0; | 
|  | } |