Skip to content

task: support multiple waiters in WaitQueue#1114

Open
mvanhorn wants to merge 1 commit into
coconut-svsm:mainfrom
mvanhorn:fix/1104-waitqueue-multiple-waiters
Open

task: support multiple waiters in WaitQueue#1114
mvanhorn wants to merge 1 commit into
coconut-svsm:mainfrom
mvanhorn:fix/1104-waitqueue-multiple-waiters

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

Summary

WaitQueue used Option<TaskPointer> to hold at most one waiter and panicked if a second task called wait_for_event while one was already queued. This made it impossible for multiple tasks to call wait_for_termination on the same target task simultaneously.

Why this matters

As described in #1104, the wait_for_termination API is the natural way to join a task, but any scenario where two tasks join the same target — e.g., a parent and a monitor both waiting on a worker — triggers an unconditional panic. The fix unlocks that usage pattern without any heap allocation in the wake path.

Changes

  • kernel/src/task/waiting.rs: Replace waiter: Option<TaskPointer> with waiters: Vec<TaskPointer>. Remove the assert!(self.waiter.is_none()) guard. wait_for_event now pushes onto the vec; wakeup drains it (alloc-free at wake time) and returns the full list.
  • kernel/src/task/tasks.rs: set_task_terminated now returns Vec<TaskPointer> instead of Option<TaskPointer>.
  • kernel/src/task/schedule.rs: TaskList::terminate returns Vec<TaskPointer>; current_task_terminated iterates over all returned waiters and calls enqueue_task for each. Adds test_wait_for_termination_multiple_waiters: two tasks each call wait_for_termination on a third task; both must resume after it terminates.

Fixes #1104

Signed-off-by: Matt Van Horn mvanhorn@gmail.com

@msft-jlange

Copy link
Copy Markdown
Collaborator

Unfortunately, this design won't work because it requires allocation of memory when entering a wait state (as a result of Vec::push). This makes it impossible for the memory allocator to participate in wait operations, which may be required as a result of system-wide synchronization (this obviously has nothing to with task termination, but WaitQueue will ultimately be used for all types of wait operations).

Please reconsider your design to implement a queue without requiring memory allocation.

@msft-jlange msft-jlange self-assigned this Jun 13, 2026
Comment thread kernel/src/task/schedule.rs Outdated
/// The caller must ensure that `task` is already a member of this task
/// list.
unsafe fn terminate(&mut self, task: TaskPointer) -> Option<TaskPointer> {
unsafe fn terminate(&mut self, task: TaskPointer) -> alloc::vec::Vec<TaskPointer> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general pattern, we prefer a use statement in the file so that types in declarations do not require namespace decoration. So you would want use alloc::vec::Vec at the top of the file and then this just becomes Vec<TaskPointer>.

Comment thread kernel/src/task/schedule.rs
Comment thread kernel/src/task/schedule.rs Outdated
if let Some(task) = target {
wait_for_termination(task);
}
MULTI_WAITER_COUNTER.fetch_add(10, Ordering::Relaxed);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does 10 mean? Is it a magic value?

Comment thread kernel/src/task/waiting.rs Outdated
self.waiter.take()
/// Wake all waiting tasks. Returns the drained list of waiters so the
/// caller can schedule each one. The drain itself does not allocate.
pub fn wakeup(&mut self) -> Vec<TaskPointer> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, some wake operations will want to wake the entire queue and some operations will only want to wake the head of the queue. Waiting for task termination clearly falls into former category but turnstile-type wait operations can only wake one at a time. While it's not necessary to implement this now, it would be nice to anticipate this by defining a parameter to this function (e.g. wake_all: bool) that permits both types. If you don't want to implement both now, then you can just assert!(wake_all) for now and make it a problem for the future, but defining this now at least requires callers to be conscious of the type of wakeup so that we don't have to reconsider the behavior of entire code base when this single wakeup is finally implemented.

@msft-jlange msft-jlange left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please reimplement as described without using memory allocation.

@joergroedel joergroedel added the in-review PR is under active review and not yet approved label Jun 16, 2026
@joergroedel

Copy link
Copy Markdown
Member

Unfortunately, this design won't work because it requires allocation of memory when entering a wait state (as a result of Vec::push). This makes it impossible for the memory allocator to participate in wait operations, which may be required as a result of system-wide synchronization (this obviously has nothing to with task termination, but WaitQueue will ultimately be used for all types of wait operations).

Please reconsider your design to implement a queue without requiring memory allocation.

I second this, the prefered data structure to use here is a linked list from intrusive_collections.

@mvanhorn

mvanhorn commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

Reworked in d2626cd per both of your points: the Vec is gone. The waiters queue is now an intrusive linked list from intrusive_collections, with the LinkedListAtomicLink embedded in Task next to the existing runlist link, so wait_for_event() performs no allocation and the allocator can safely participate in waits. wakeup() takes the list and hands it back to the scheduler for wake-up. cargo check for the svsm kernel target passes locally.

@stefano-garzarella

Copy link
Copy Markdown
Member

@mvanhorn please rebase since there are conflicts and also check all comments and resolve ones fixed or explain why they are not fixed.

@mvanhorn mvanhorn force-pushed the fix/1104-waitqueue-multiple-waiters branch from d2626cd to 7d11685 Compare July 6, 2026 17:37
@mvanhorn

mvanhorn commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main. There was one conflict in current_task_terminated: main moved task wakeup to rq.prepare_run_task, while this branch changed terminate() to wake multiple waiters, so I kept the multi-waiter loop and scheduled each via rq.prepare_run_task. cargo check for x86_64-unknown-none passes, clippy and fmt clean. (Couldn't run the host unit tests locally - this is an arm64 box and the syscall crate uses x86 asm - so CI will cover those.)

Still working through @msft-jlange's review comments (the Vec import, the #[cfg(test)] guard, the magic value, and the wake-all vs wake-head question); I'll push those separately.

@msft-jlange

Copy link
Copy Markdown
Collaborator

Still working through @msft-jlange's review comments (the Vec import, the #[cfg(test)] guard, the magic value, and the wake-all vs wake-head question); I'll push those separately.

Thanks for continuing to work through these. I haven't had a chance to look at your updates yet, but your description is certainly headed in the right direction.

One other issue - we require each commit in a PR to be separately stable, and your updates have not resolved the Vec issue in the first commit (they are resolved in a separate commit). You should plan to squash those changes into a single commit so the PR has no intermediate unstable commits.

@mvanhorn

mvanhorn commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

All four items addressed as of 8fd94da:

  • Vec import / namespace decoration: moot after the previous push - waiters now use an intrusive linked list, so there's no Vec left in the file.
  • cfg(test) guard: the counter and helpers live inside the #[cfg(all(test, test_in_svsm))] test module.
  • Magic 10: now MULTI_WAITER_TASK_INCREMENT, and the final assertion is derived from it.
  • Wake semantics: wakeup() takes wake_all: bool as you suggested. Termination passes true (wake the whole queue); false removes only the head waiter, so turnstile-type waits are supported without revisiting callers later.

Verified: cargo check and clippy clean for x86_64-unknown-none, fmt clean. The multi-waiter test is test_in_svsm, so it runs in-guest via CI.

Comment thread kernel/src/task/waiting.rs Outdated
Comment on lines 48 to 52
impl Default for WaitQueue {
fn default() -> Self {
Self::new()
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed after #1162 and should be removed.

)
.expect("Failed to start waiter 2");

// Wait for the target task from this task too, ensuring it has

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing here ensures that waiter 1 and waiter 2 enter the wait state while the target task is still executing so your tests cannot prove that the waiters actually join the wait queue. In fact, because your target task runs on the same CPU that the test function runs on, it is virtually guaranteed that the target task will terminate before either waiter thread is even created, so it is overwhelmingly likely that there is no wait queue testing occurring here. A proper test would require the following sequence:

  • Change the target task to set affinity to some processor other than the originating processor (use the same logic that is in empty_task()).
  • Add a barrier to the target task so that it will spin-wait for the barrier to be signaled until it proceeds to termination.
  • Signal the barrier from the test task after both waiters have been created but before the test task performs its own wait. This will guarantee that both waiter 1 and waiter 2 have entered wait states and will also guarantee that the target task has not terminated before both waiters have entered their wait states.

@msft-jlange

Copy link
Copy Markdown
Collaborator

@mvanhorn More actions are still required.

  • All commits must be squashed before merging to maintain our stable commit rule.
  • All comments must be resolved. There are new comments since your last push that identify additional issues. In addition, comments that appear to be addressed by your latest updates still need to be resolved.
  • Rebasing is required to eliminate merge conflicts.

@mvanhorn mvanhorn force-pushed the fix/1104-waitqueue-multiple-waiters branch from 8fd94da to 379df30 Compare July 9, 2026 15:05
Comment thread kernel/src/task/schedule.rs Outdated
if let Some(wake_task) = wakeup {
// Wake all tasks that were waiting for this task to terminate, scheduling
// each on the current runqueue.
for wake_task in wakeup {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is subject to race conditions. Once a task has been woken, it can immediately attempt to enter another wait state. However, for the duration of this loop, the task's wait list link is still inserted into the wakeup list here. That means that an attempt by the task to enter a new wait state will panic when it tries to insert itself into a different wait queue. I believe it is necessary to ensure that the task being woken is removed from the local wakeup list before the wakeup can be attempted.

WaitQueue previously supported only a single waiter; a second waiter
would overwrite the first and strand it forever. Store waiters in a
queue so every waiter is woken.

Fixes coconut-svsm#1104

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
@mvanhorn

Copy link
Copy Markdown
Contributor Author

Review feedback addressed and amended into the signed commit (9166b00, history kept squashed per repo convention). Verified locally: targeted x86_64 svsm build and clippy pass offline.

@mvanhorn mvanhorn force-pushed the fix/1104-waitqueue-multiple-waiters branch from 379df30 to 9166b00 Compare July 10, 2026 04:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in-review PR is under active review and not yet approved

Projects

None yet

Development

Successfully merging this pull request may close these issues.

kernel/waitqueue: improve handling of multiple tasks

4 participants