commit db5a173f6e7f14ebaa6feae230d027d042052afb Author: Ryan Stone Date: Wed Jan 6 17:58:17 2021 Fix race condition in linuxkpi workqueue Consider the following scenario: 1. A delayed_work struct in the WORK_ST_TIMER state. 2. Thread A calls mod_delayed_work() 3. Thread B (a callout thread) simultaneously calls linux_delayed_work_timer_fn() The following sequence of events is possible: A: Call linux_cancel_delayed_work() A: Change state from TIMER TO CANCEL B: Change state from CANCEL to TASK B: taskqueue_enqueue() the task A: taskqueue_cancel() the task A: Call linux_queue_delayed_work_on(). This is a no-op because the state is WORK_ST_TASK. As a result, the delayed_work struct will never be invoked. This is causing address resolution in ib_addr.c to stop permanently, as it never tries to reschedule a task that it thinks is already scheduled. Fix this by introducing locking into the cancel path (which corresponds with the lock held while the callout runs). This will prevent the callout from changing the state of the task until the cancel is complete, preventing the race. Bug: https://jira.cec.lab.emc.com/browse/PSCALE-24757 diff --git a/sys/compat/linuxkpi/common/src/linux_work.c b/sys/compat/linuxkpi/common/src/linux_work.c index e01e507c6be8..61e2105f5412 100644 --- a/sys/compat/linuxkpi/common/src/linux_work.c +++ b/sys/compat/linuxkpi/common/src/linux_work.c @@ -383,27 +383,44 @@ linux_cancel_delayed_work(struct delayed_work *dwork) }; struct taskqueue *tq; + /* Begin Isilon */ + bool cancelled; + + mtx_lock(&dwork->timer.mtx); + /* End Isilon */ + switch (linux_update_state(&dwork->work.state, states)) { case WORK_ST_TIMER: case WORK_ST_CANCEL: - if (linux_cancel_timer(dwork, 0)) { + /* Begin Isilon */ + cancelled = (callout_stop(&dwork->timer.callout) == 1); + if (cancelled) { atomic_cmpxchg(&dwork->work.state, WORK_ST_CANCEL, WORK_ST_IDLE); + mtx_unlock(&dwork->timer.mtx); return (1); } + /* End Isilon */ /* FALLTHROUGH */ case WORK_ST_TASK: tq = dwork->work.work_queue->taskqueue; if (taskqueue_cancel(tq, &dwork->work.work_task, NULL) == 0) { atomic_cmpxchg(&dwork->work.state, WORK_ST_CANCEL, WORK_ST_IDLE); + /* Begin Isilon */ + mtx_unlock(&dwork->timer.mtx); + /* End Isilon */ return (1); } /* FALLTHROUGH */ default: + /* Begin Isilon */ + mtx_unlock(&dwork->timer.mtx); + /* End Isilon */ return (0); } } +/* End Isilon */ /* * This function cancels the given work structure in a synchronous @@ -421,36 +438,47 @@ linux_cancel_delayed_work_sync(struct delayed_work *dwork) [WORK_ST_CANCEL] = WORK_ST_IDLE, /* cancel and drain */ }; struct taskqueue *tq; + /* Begin Isilon */ + int ret, state; + bool cancelled; + /* End Isilon */ WITNESS_WARN(WARN_GIANTOK | WARN_SLEEPOK, NULL, "linux_cancel_delayed_work_sync() might sleep"); - switch (linux_update_state(&dwork->work.state, states)) { + /* Begin Isilon */ + mtx_lock(&dwork->timer.mtx); + + state = linux_update_state(&dwork->work.state, states); + switch (state) { case WORK_ST_IDLE: return (0); - case WORK_ST_EXEC: - tq = dwork->work.work_queue->taskqueue; - if (taskqueue_cancel(tq, &dwork->work.work_task, NULL) != 0) - taskqueue_drain(tq, &dwork->work.work_task); - return (0); case WORK_ST_TIMER: case WORK_ST_CANCEL: - if (linux_cancel_timer(dwork, 1)) { - /* - * Make sure taskqueue is also drained before - * returning: - */ - tq = dwork->work.work_queue->taskqueue; - taskqueue_drain(tq, &dwork->work.work_task); - return (1); - } - /* FALLTHROUGH */ + cancelled = (callout_stop(&dwork->timer.callout) == 1); + + tq = dwork->work.work_queue->taskqueue; + taskqueue_cancel(tq, &dwork->work.work_task, NULL); + mtx_unlock(&dwork->timer.mtx); + + callout_drain(&dwork->timer.callout); + taskqueue_drain(tq, &dwork->work.work_task); + + return (cancelled); default: tq = dwork->work.work_queue->taskqueue; - if (taskqueue_cancel(tq, &dwork->work.work_task, NULL) != 0) + ret = taskqueue_cancel(tq, &dwork->work.work_task, NULL); + mtx_unlock(&dwork->timer.mtx); + if (ret != 0) taskqueue_drain(tq, &dwork->work.work_task); - return (1); + + /* + * If we're in the EXEC state then the work already ran so + * we did not cancel anything. + */ + return (state != WORK_ST_EXEC); } + /* End Isilon */ } /*