Suspending an RTOS Task That Holds a Mutex: Deadlock Risks and Safer Pause Protocols

Diagnose the scheduling operation before redesigning the pause

A worker owns a device mutex when another task calls vTaskSuspend(worker). A diagnostic task later waits for that mutex indefinitely. The first useful evidence is an ordered trace: successful mutex acquisition, suspension of the owner, a competing take, and the absence of a subsequent owner release. This article focuses on the exact suspend/resume semantics and race timelines in upstream FreeRTOS Kernel V11.1.0, with single-core examples. It does not repeat a complete shutdown or OTA quiescence protocol.

Suspending a task prevents its execution; it does not unwind its call stack or release application resources. The official task-suspension reference states that a suspended task receives no processing time regardless of priority. Consequently, priority inheritance cannot make an explicitly suspended mutex owner execute its release path. The diagnostic distinction is between a runnable low-priority owner and an owner removed from normal scheduling.

Worker checks for work, an interrupt publishes and resumes before suspension, then the worker suspends with no retained wake event.
Original conceptual diagram. Arrows show ownership or lifecycle dependencies, not measured timing.

Suspension has no nesting count

Consider two independent controllers. Controller A suspends the worker for calibration. Controller B suspends it for a debug operation. When A resumes it, the worker becomes eligible to run even though B still believes its request is active. vTaskSuspend() calls are not cumulative; a single vTaskResume() can ready the task after multiple suspensions. These semantics appear in the V11.1.0 task API.

Therefore a wrapper named pause_acquire() cannot implement reference-counted pause ownership merely by forwarding every acquire to suspend and every release to resume. If multiple clients need a common pause condition, the application must arbitrate those clients explicitly. For diagnosis, log the caller and reason for every suspend and resume, not just the worker handle. An unexpected restart after the “wrong” client’s resume is often a semantic mismatch rather than memory corruption.

Reconstruct the early-resume race

A second failure does not require a mutex. At time A, the worker checks a shared flag and decides there is no work. At time B, an interrupt publishes work and resumes the worker. At time C, the worker calls vTaskSuspend(NULL). Because the resume occurred before suspension, it did not create a token for that future wait. The task can remain suspended with work already available.

The pinned implementation checks whether the target is actually suspended before applying resume. This differs from a suitable semaphore or notification scheme that can retain an event until the receiver consumes it. The task API specifically warns against xTaskResumeFromISR() as interrupt synchronization when the interrupt might arrive first. Reproduce the ordering with controlled gates in a test build; random delays are less useful than deliberately placing the event between the check and suspend.

A state query does not close the race

Adding eTaskGetState() before resume or suspend does not make the pair atomic. Its documented result is a snapshot that may become stale before the caller tests it. If one controller observes a suspended task, another context can resume it before the first controller acts. Likewise, observing a runnable worker says nothing about whether it will enter suspension immediately afterward.

Use state queries as diagnostic observations and correlate them with event order. Do not make “observed suspended” stand in for an application-safe condition such as “device transaction completed.” Also distinguish explicit suspension from blocking on a queue, mutex or notification. A task that is not executing may simply be waiting on its proper synchronization object. Inspect the selected kernel’s state-reporting logic and wait reason rather than guessing from a debugger’s list label.

Task suspension and scheduler suspension are different

vTaskSuspendAll() suspends scheduling without disabling interrupts; the current task continues executing. xTaskResumeAll() resumes scheduling and does not resume tasks explicitly suspended by vTaskSuspend(). The API warns against calls that can cause a context switch while scheduling is suspended. These rules are different from pausing one worker, and the similarly named functions should not be substituted for one another.

In a single-core diagnostic example, a controller that suspends the scheduler and then waits for a mutex held by another task prevents the very owner it needs from making progress. Interrupts can still modify shared hardware state, so scheduler suspension is also not a general interrupt-exclusion mechanism. Do not extend this single-core reasoning to SMP without examining that port’s implementation and concurrency guarantees. Record whether the trace shows task suspension, scheduler suspension, interrupt masking, or an ordinary blocking call.

Respect the ISR resume boundary

Use xTaskResumeFromISR() rather than vTaskResume() in an interrupt that is permitted to call the kernel. Its BaseType_t return indicates whether resuming the task should result in a context switch; apply the selected port’s ISR-exit yield mechanism accordingly. It is not a “message delivered” acknowledgement. The exact priority restriction belongs to the processor port, and an ISR above the permitted API-call priority must not call it simply because the function name ends in FromISR.

A deferred ready transition can also interact with scheduler suspension, as the implementation’s pending-ready path shows. Therefore distinguish “resume requested,” “made ready,” and “actually ran” in timing evidence. After a successful wake, execution still depends on scheduling. Never interpret an ISR timestamp alone as the time the worker resumed its device operation.

Validate the mechanism and choose the smallest repair

Use a proposed test matrix: suspend while holding a mutex, two suspends followed by one resume, resume before self-suspend, a state-query race, and ISR resume at an allowed priority. Add a scheduler-suspension case without introducing an illegal blocking API call. Capture task identity, caller, lock ownership, scheduler state and ordered timestamps. No target tests were executed for this article.

If the goal is event delivery, replace suspend/resume with an appropriate retained event primitive and a validated wait predicate. If the goal is an operational pause, request that the worker reach a resource-safe point and then wait cooperatively; keep that application protocol separate from scheduler semantics. Never “repair” the failure by releasing another task’s mutex externally. Acceptance means each observed stall is explained by a reproducible ordering, and the selected mechanism matches event delivery, pause ownership or scheduling control as actually required.

Similar Posts