Fix deinit of async_contet_freertos. improve API docs for deinit. Add todo for bugs in threadsafe_background
diff --git a/src/rp2_common/pico_async_context/async_context_freertos.c b/src/rp2_common/pico_async_context/async_context_freertos.c
index 02c688e..aff70e8 100644
--- a/src/rp2_common/pico_async_context/async_context_freertos.c
+++ b/src/rp2_common/pico_async_context/async_context_freertos.c
@@ -74,21 +74,32 @@
         __sev(); // it is possible regular code is waiting on a WFE on the other core
     } while (!self->task_should_exit);
     xSemaphoreGive(self->task_complete_sem);
-    vTaskDelete(NULL);
+    // Previously we called vTaskDelete(NULL) here, however this (self-delete) is always asynchronous and deferred
+    // to the idle task, which is a problem with static allocation, as async_context_freertos_deinit zeroes
+    // the context which actually contains the TCB, thus stomping on FreeRTOS's own internal (active) data structures,
+    // before the idle task gets a chance to clean it up. async_context_freertos_deinit must therefore be responsible
+    // for destroying this task in that case (and indeed it is simpler to do so there in all cases to make sure
+    // the timer is quiesced too)
+
+    // We must however do something other than return from a task, so we suspend ourself. Note however, that
+    // there is no guarantee this code actually executes before async_context_freertos_deinit proceeds since
+    // we have posted it a semaphore above, so the de-init code must not rely on this having happened.
+    vTaskSuspend(NULL);
 }
 
 static void async_context_freertos_wake_up(async_context_t *self_base) {
     async_context_freertos_t *self = (async_context_freertos_t *)self_base;
-    if (self->task_handle) {
+    TaskHandle_t task_handle = self->task_handle;
+    if (task_handle) {
         if (portCHECK_IF_IN_ISR()) {
-            vTaskNotifyGiveFromISR(self->task_handle, NULL);
+            vTaskNotifyGiveFromISR(task_handle, NULL);
             xSemaphoreGiveFromISR(self->work_needed_sem, NULL);
         } else {
             // we don't want to wake ourselves up (we will only ever be called
             // from the async_context_task if we own the lock, in which case processing
             // will already happen when the lock is finally unlocked
-            if (xTaskGetCurrentTaskHandle() != self->task_handle) {
-                xTaskNotifyGive(self->task_handle);
+            if (xTaskGetCurrentTaskHandle() != task_handle) {
+                xTaskNotifyGive(task_handle);
                 xSemaphoreGive(self->work_needed_sem);
             } else {
 #ifndef NDEBUG
@@ -195,23 +206,54 @@
 
 void async_context_freertos_deinit(async_context_t *self_base) {
     async_context_freertos_t *self = (async_context_freertos_t *)self_base;
-    if (self->task_handle) {
+    TaskHandle_t task_handle = self->task_handle;
+    // Ask the task to exit its loop, and wait for it to do so.
+    if (task_handle) {
         async_context_execute_sync(self_base, end_task_func, self_base);
         if (self->task_complete_sem) {
             xSemaphoreTake(self->task_complete_sem, portMAX_DELAY);
         }
     }
+    // Now the task has exited its loop, it will neither call worker functions nor re-arm
+    // the timer. At this point though both:
+    // a. The task may still be running on core (having not yet made it to the vTaskSuspend(NULL).
+    // b. Be targeted for notifications by the timer which is still active. This is fine though
+    //    as the task either is, or is about to be in vTaskSuspend(NULL) which isn't woken by notifications.
+
+    // First things first, let's now synchronously stop the timer...
     if (self->timer_handle) {
         xTimerDelete(self->timer_handle, 0);
 
-        // slight hoops to jump thru to make sure the timer is actually deleted before we
-        // free the remaining items below. this is needed for SMP and also if
-        // the current task has a higher/equal priority to the timer task
+        // Slight hoops to jump thru to make sure the timer has actually been deleted BEFORE we proceed
 
-        // 1. queue function which will notify us back to the timer task queue
+        // 1. Queue function which will notify us back to the timer task queue
         xTimerPendFunctionCall(timer_delete_sync_helper, (void *)xTaskGetCurrentTaskHandle(), 0, portMAX_DELAY);
-        // 2. wait for that function to execute (which will be after the timer deletion completes)
+        // 2. Wait for that function to execute (which will be after the timer deletion completes)
         ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
+        self->timer_handle = NULL;
+    }
+
+    // With the timer now stopped, there are no other current/future execution units referencing self->task_handle
+    // and we can proceed to delete the task...
+    if (task_handle) {
+        // ... however vTaskDelete is only synchronous if the task is not currently executing (on core) during the call
+
+        // 1. We don't care if not using static allocation, since nothing related to the task is stored in our
+        //    soon to be zeroed context.
+        // 2. We don't care if not using SMP since if the only core is currently executing vTaskDelete from this task,
+        //   then clearly the other task is not currently executing
+#if configSUPPORT_STATIC_ALLOCATION && ( configNUMBER_OF_CORES > 1 )
+        // Make sure the task cannot be re-scheduled again. This is asynchronous across cores...
+        vTaskSuspend(task_handle);
+        // ... so actually wait until it actually leaves the core if it was on it
+        while (xTaskGetCurrentTaskHandleForCore((BaseType_t)self->core.core_num) == task_handle) {
+            taskYIELD();
+        }
+#endif
+        // Now we can call vTaskDelete because in the static allocation case we know the task is
+        // no longer executing, so this call is guaranteed to complete synchronously, and in the non
+        // static allocation case we don't care if the call is asynchronous anyway.
+        vTaskDelete(task_handle);
     }
     if (self->lock_mutex) {
         vSemaphoreDelete(self->lock_mutex);
@@ -222,6 +264,7 @@
     if (self->task_complete_sem) {
         vSemaphoreDelete(self->task_complete_sem);
     }
+    // Finally clear the context now we know nothing is referencing it.
     memset(self, 0, sizeof(*self));
 }
 
diff --git a/src/rp2_common/pico_async_context/async_context_threadsafe_background.c b/src/rp2_common/pico_async_context/async_context_threadsafe_background.c
index 50cb4ef..b90d1a4 100644
--- a/src/rp2_common/pico_async_context/async_context_threadsafe_background.c
+++ b/src/rp2_common/pico_async_context/async_context_threadsafe_background.c
@@ -233,10 +233,24 @@
 
 static void async_context_threadsafe_background_deinit(async_context_t *self_base) {
     async_context_threadsafe_background_t *self = (async_context_threadsafe_background_t *)self_base;
-    // todo we do not currently handle this correctly; we could, but seems like a rare case
+    // todo we do not currently handle this correctly; we could, but it seems like a rare use case
     assert(get_core_num() == self_base->core_num);
+
+    // todo this cleanup orderd is incorrect, because:
+    //      - low_priority_irq_handler() -> process_under_lock() re-arms the alarm
+    //      - alarm_handler() -> wake_up() set ths irq pending
+    //
+    //      i.e. there is no ordering of these two calls that is correct, and they must be split
+    //
+
     low_prio_irq_deinit(self);
+
+    // alarm_pool_cancel_alarm() is asynchronous when called from a different core to the pool owner,
+    // so we rely on the fact that the alarm_pool core is the same as our core (which is asserted on during init)
+    // to make this synchronous
     if (self->alarm_id > 0) alarm_pool_cancel_alarm(self->alarm_pool, self->alarm_id);
+
+    // todo also force_alarm_id is never cancelled, which is a leak if we dont own the alarm pool
 #if ASYNC_CONTEXT_THREADSAFE_BACKGROUND_MULTI_CORE
     if (self->alarm_pool_owned) {
         alarm_pool_destroy(self->alarm_pool);
@@ -373,5 +387,3 @@
         .wait_for_work_until = async_context_threadsafe_background_wait_for_work_until,
         .deinit = async_context_threadsafe_background_deinit,
 };
-
-
diff --git a/src/rp2_common/pico_async_context/include/pico/async_context.h b/src/rp2_common/pico_async_context/include/pico/async_context.h
index f544147..5ffac91 100644
--- a/src/rp2_common/pico_async_context/include/pico/async_context.h
+++ b/src/rp2_common/pico_async_context/include/pico/async_context.h
@@ -449,14 +449,16 @@
 }
 
 /*!
- * \brief End async_context processing, and free any resources
+ * \brief End async_context processing, and free any resources owned by the context
  * \ingroup pico_async_context
  *
- * \note The user should clean up any resources associated with workers
- * in the async_context themselves.
+ * It is the callers responsibility to ensure that no external async_context_ calls
+ * to this context (i.e. those not originating as a direct synchronous result of active
+ * pending/at_time work within this context) are made after (or concurrently with) calling this method.
  *
- * Asynchronous (non-polled) async_contexts guarantee that no
- * callback is being called once this method returns.
+ * The context, in turn, guarantees that on return, no active calls are being made, and no
+ * subsequent calls will be made, to any workers registered in this context
+ * so the caller can free any resources associated with those workers.
  *
  * \param context the async_context
  */