summaryrefslogtreecommitdiff
path: root/sys/dev
diff options
context:
space:
mode:
Diffstat (limited to 'sys/dev')
-rw-r--r--sys/dev/usb/usbnet.c102
1 files changed, 97 insertions, 5 deletions
diff --git a/sys/dev/usb/usbnet.c b/sys/dev/usb/usbnet.c
index 1712d2f687f..8a6f0ad67f6 100644
--- a/sys/dev/usb/usbnet.c
+++ b/sys/dev/usb/usbnet.c
@@ -1,4 +1,4 @@
-/* $NetBSD: usbnet.c,v 1.43 2021/12/11 19:24:21 mrg Exp $ */
+/* $NetBSD: usbnet.c,v 1.44 2022/01/29 21:37:07 riastradh Exp $ */
/*
* Copyright (c) 2019 Matthew R. Green
@@ -31,7 +31,7 @@
*/
#include <sys/cdefs.h>
-__KERNEL_RCSID(0, "$NetBSD: usbnet.c,v 1.43 2021/12/11 19:24:21 mrg Exp $");
+__KERNEL_RCSID(0, "$NetBSD: usbnet.c,v 1.44 2022/01/29 21:37:07 riastradh Exp $");
#include <sys/param.h>
#include <sys/kernel.h>
@@ -72,6 +72,7 @@ struct usbnet_private {
struct ethercom unp_ec;
struct mii_data unp_mii;
+ struct usb_task unp_mcasttask;
struct usb_task unp_ticktask;
struct callout unp_stat_ch;
struct usbd_pipe *unp_ep[USBNET_ENDPT_MAX];
@@ -1035,12 +1036,64 @@ usbnet_if_ioctl(struct ifnet *ifp, u_long cmd, void *data)
return uno_override_ioctl(un, ifp, cmd, data);
error = ether_ioctl(ifp, cmd, data);
- if (error == ENETRESET)
- error = uno_ioctl(un, ifp, cmd, data);
+ if (error == ENETRESET) {
+ switch (cmd) {
+ case SIOCADDMULTI:
+ case SIOCDELMULTI:
+ usb_add_task(un->un_udev, &unp->unp_mcasttask,
+ USB_TASKQ_DRIVER);
+ error = 0;
+ break;
+ default:
+ error = uno_ioctl(un, ifp, cmd, data);
+ }
+ }
return error;
}
+static void
+usbnet_mcast_task(void *arg)
+{
+ USBNETHIST_FUNC();
+ struct usbnet * const un = arg;
+ struct usbnet_private * const unp = un->un_pri;
+ struct ifnet * const ifp = usbnet_ifp(un);
+ bool dying;
+ struct ifreq ifr;
+
+ USBNETHIST_CALLARGSN(10, "%jd: enter", unp->unp_number, 0, 0, 0);
+
+ /*
+ * If we're detaching, we must check unp_dying _before_
+ * touching IFNET_LOCK -- the ifnet may have been detached by
+ * the time this task runs. This is racy -- unp_dying may be
+ * set immediately after we test it -- but nevertheless safe,
+ * because usbnet_detach waits for the task to complete before
+ * issuing if_detach, and necessary, so that we don't touch
+ * IFNET_LOCK after if_detach. See usbnet_detach for details.
+ */
+ mutex_enter(&unp->unp_core_lock);
+ dying = unp->unp_dying;
+ mutex_exit(&unp->unp_core_lock);
+ if (dying)
+ return;
+
+ /*
+ * Pass a bogus ifr with SIOCDELMULTI -- the goal is to just
+ * notify the driver to reprogram any hardware multicast
+ * filter, according to what's already stored in the ethercom.
+ * None of the drivers actually examine this argument, so it
+ * doesn't change the ABI as far as they can tell.
+ */
+ IFNET_LOCK(ifp);
+ if (ifp->if_flags & IFF_RUNNING) {
+ memset(&ifr, 0, sizeof(ifr));
+ (void)uno_ioctl(un, ifp, SIOCDELMULTI, &ifr);
+ }
+ IFNET_UNLOCK(ifp);
+}
+
/*
* Generic stop network function:
* - mark as stopping
@@ -1373,7 +1426,10 @@ usbnet_attach(struct usbnet *un,
un->un_pri = kmem_zalloc(sizeof(*un->un_pri), KM_SLEEP);
struct usbnet_private * const unp = un->un_pri;
- usb_init_task(&unp->unp_ticktask, usbnet_tick_task, un, USB_TASKQ_MPSAFE);
+ usb_init_task(&unp->unp_mcasttask, usbnet_mcast_task, un,
+ USB_TASKQ_MPSAFE);
+ usb_init_task(&unp->unp_ticktask, usbnet_tick_task, un,
+ USB_TASKQ_MPSAFE);
callout_init(&unp->unp_stat_ch, CALLOUT_MPSAFE);
callout_setfunc(&unp->unp_stat_ch, usbnet_tick, un);
@@ -1506,6 +1562,8 @@ usbnet_detach(device_t self, int flags)
callout_halt(&unp->unp_stat_ch, NULL);
usb_rem_task_wait(un->un_udev, &unp->unp_ticktask, USB_TASKQ_DRIVER,
NULL);
+ usb_rem_task_wait(un->un_udev, &unp->unp_mcasttask, USB_TASKQ_DRIVER,
+ NULL);
mutex_enter(&unp->unp_core_lock);
unp->unp_refcnt--;
@@ -1534,6 +1592,40 @@ usbnet_detach(device_t self, int flags)
}
usbnet_ec(un)->ec_mii = NULL;
+ /*
+ * We have already waited for the multicast task to complete.
+ * Unfortunately, until if_detach, nothing has prevented it
+ * from running again -- another thread might issue if_mcast_op
+ * between the time of our first usb_rem_task_wait and the time
+ * we actually get around to if_detach.
+ *
+ * Fortunately, the first usb_rem_task_wait ensures that if the
+ * task is scheduled again, it will witness our setting of
+ * unp_dying to true[*]. So after that point, if the task is
+ * scheduled again, it will decline to touch IFNET_LOCK and do
+ * nothing. But we still need to wait for it to complete.
+ *
+ * It would be nice if we could write
+ *
+ * if_pleasestopissuingmcastopsthanks(ifp);
+ * usb_rem_task_wait(..., &unp->unp_mcasttask, ...);
+ * if_detach(ifp);
+ *
+ * and then we would need only one usb_rem_task_wait.
+ *
+ * Unfortunately, there is no such operation available in
+ * sys/net at the moment, and it would require a bit of
+ * coordination with if_mcast_op and doifioctl probably under a
+ * new lock. So we'll use this kludge until that mechanism is
+ * invented.
+ *
+ * [*] This is not exactly a documented property of the API,
+ * but it is implied by the single lock in the task queue
+ * serializing changes to the task state.
+ */
+ usb_rem_task_wait(un->un_udev, &unp->unp_mcasttask, USB_TASKQ_DRIVER,
+ NULL);
+
cv_destroy(&unp->unp_detachcv);
mutex_destroy(&unp->unp_core_lock);
mutex_destroy(&unp->unp_rxlock);