While run_task() iterates through the host command queue during a scan,
the interface may get reset via run_stop() and run_init() when switching
bands (2GHz -> 5GHz) in run_media_change().

The list state gets corrupted because run_init() resets the list counters
while run_task() is still iterating the list. run_task() will now attempt
to call garbage or NULL callback pointers.

run_newstate_cb SCAN -> INIT
run_newstate_cb SCAN -> SCAN
run_newstate_cb SCAN -> SCAN
run_task ring->next=7
run_task ring->cur=0
run_task ring->queued=-7   <-- negative nonesense
run_task cmd=0xffff800000651110
run_task cmd->cb=0x0

The kernel now explodes trying to call cmd->cb.

This can be reproduced by running 'ifconfig run0 mediaopt monitor up'.
Only happens with dual-band devices.

run_init() can't know whether one or more run_task() tasks are scheduled.
So I cannot think of a better fix but make run_task() abort itself in
this situation, muck like iwm(4) does.

Also complain about host command ring overflow in dmesg.

ok?

Index: if_run.c
===================================================================
RCS file: /cvs/src/sys/dev/usb/if_run.c,v
retrieving revision 1.109
diff -u -p -r1.109 if_run.c
--- if_run.c    12 Jun 2015 15:47:31 -0000      1.109
+++ if_run.c    11 Jul 2015 07:51:29 -0000
@@ -1720,19 +1720,23 @@ run_task(void *arg)
        struct run_softc *sc = arg;
        struct run_host_cmd_ring *ring = &sc->cmdq;
        struct run_host_cmd *cmd;
-       int s;
+       int s, generation;
 
        if (usbd_is_dying(sc->sc_udev))
                return;
 
        /* process host commands */
        s = splusb();
+       generation = ring->generation;
        while (ring->next != ring->cur) {
                cmd = &ring->cmd[ring->next];
                splx(s);
                /* callback */
                cmd->cb(sc, cmd->data);
                s = splusb();
+               /* Abort this task if interface was stopped meanwhile. */
+               if (generation != ring->generation)
+                       break;
                ring->queued--;
                ring->next = (ring->next + 1) % RUN_HOST_CMD_RING_COUNT;
        }
@@ -1751,6 +1755,11 @@ run_do_async(struct run_softc *sc, void 
                return;
 
        s = splusb();
+       if (ring->queued == RUN_HOST_CMD_RING_COUNT) {
+               printf("%s: host command ring overflow\n", sc->sc_dev.dv_xname);
+               splx(s);
+               return;
+       }
        cmd = &ring->cmd[ring->cur];
        cmd->cb = cb;
        KASSERT(len <= sizeof (cmd->data));
@@ -4739,6 +4748,10 @@ run_stop(struct ifnet *ifp, int disable)
        timeout_del(&sc->calib_to);
 
        s = splusb();
+
+       /* Abort run_task thread. */
+       sc->cmdq.generation++;
+
        ieee80211_new_state(ic, IEEE80211_S_INIT, -1);
        /* wait for all queued asynchronous commands to complete */
        usb_wait_task(sc->sc_udev, &sc->sc_task);
Index: if_runvar.h
===================================================================
RCS file: /cvs/src/sys/dev/usb/if_runvar.h,v
retrieving revision 1.10
diff -u -p -r1.10 if_runvar.h
--- if_runvar.h 24 May 2014 10:10:17 -0000      1.10
+++ if_runvar.h 11 Jul 2015 07:26:41 -0000
@@ -123,6 +123,7 @@ struct run_host_cmd_ring {
        int                     cur;
        int                     next;
        int                     queued;
+       int                     generation;
 };
 
 struct run_node {

Reply via email to