Skip to content

signal: don't disable preemption before handling SIGSTOP - #72

Open
greatbridf wants to merge 1 commit into
masterfrom
fix/sigcont-panic
Open

greatbridf wants to merge 1 commit into
masterfrom
fix/sigcont-panic

Conversation

@greatbridf

Copy link
Copy Markdown
Owner

Summary

Stopping a task — SIGSTOP, or SIGTSTP/SIGTTIN/SIGTTOU taking their default
action — panicked the kernel instead of stopping the task:

[kernel:fatal] panicked at src/kernel/task.rs:147:21
[kernel:fatal] assertion `left == right` failed: Blocking in stackful futures
                is not allowed.: Preemption is not enabled

This removes the stray eonix_preempt::disable() that caused it.

Fixes #52.

Root cause

SignalList::handle implemented the default stop action by disabling preemption
and then awaiting a blocking future:

eonix_preempt::disable();
Runtime::block_till_woken(|waker| { ... }).await;

Runtime::block_till_woken always returns Pending on its first poll, so the
stackful-future runner reached assert_preempt_enabled!("Blocking in stackful futures is not allowed.") in src/kernel/task.rs and panicked. The panic was
therefore deterministic, not a race: it fired on the first stop signal delivered
to any task, regardless of SMP.

This disable() is a leftover from when context switches went through
Scheduler::schedule(), which did require preemption to be off. It was simply
missed when the task sleep/wakeup mechanism was reworked in
e23c9eb1f24c ("runtime: new task sleep-wakeup method and some adaption"), and
it is actively wrong under the new scheme: a task that blocks must have
preemption enabled.

Fix

Delete the eonix_preempt::disable() call, and correct the adjacent comment,
which claimed SAFETY: Preempt disabled above. and so no longer described the
code.

Verification

Reproduced with a local headless expect reproducer (not included in this PR):
boot the kernel, start a CPU-bound child, then kill -STOP it followed by
kill -CONT. A CPU-bound target is required, because a task that is already
blocked does not take a pending stop signal.

Tree Command Result
base 7baa97689d5f --expect bug, SMP=4 exit 0, REPRO bug-observed
base 7baa97689d5f --expect fixed, SMP=4 exit 1 (negative control)
this branch --expect fixed, SMP=4 exit 0, REPRO fixed-observed, 0 panics
this branch --expect bug, SMP=4 exit 1 (negative control)

The base panic matches the issue report exactly:

[kernel:fatal] panicked at src/kernel/task.rs:147:21
[kernel:fatal] assertion `left == right` failed: Blocking in stackful futures
                is not allowed.: Preemption is not enabled

Additional checks:

  • Baseline reproduced on 3/3 consecutive runs at SMP=4 (not flaky).
  • Reproduced at SMP=1 as well, confirming this is preemption accounting rather
    than a multi-CPU race.
  • make build MODE=release ARCH=x86_64 — pass; ARCH=x86_64 script/test.sh
    smoke test — pass.
  • The fixed tree keeps the shell usable after the stop/continue round trip
    (echo still responds), so the target is not merely "unpanicked but wedged".

Risks / notes

Removing this call makes the stop path reachable for the first time, so the
remaining defects in that path become observable. They are pre-existing and not
introduced here, but they are worth a follow-up:

  1. A skipped stop leaks stop_waker. BlockTillWoken::poll returns Ready
    on any second poll, while stop_waker is only cleared by a SIGCONT
    raise. If another wake completes the "stop" first, stop_waker stays
    Some, and the next stop signal trips
    assert!(old_waker.is_none(), "We should not have a waker here") at
    signal.rs:238. The stop can also silently become a no-op.
  2. SIGKILL does not wake a stopped task. raise hands SIGCONT to
    stop_waker, but every other signal — SIGKILL included — goes to
    signal_waker, which the stop path never installs. A stopped task is
    therefore not killable until a SIGCONT arrives, contrary to the
    "SIGSTOP can only be waken up by SIGCONT or SIGKILL" comment retained
    above.
  3. There is a lost-wakeup window between deciding to stop and installing
    stop_waker: a SIGCONT raised in between is pushed to pending and wakes
    nobody, leaving the task parked. The window is not instruction-sized, because
    the preceding ProcessList::get().read().await can genuinely suspend.

These are not regressions — before this change every stop signal panicked
earlier, at task.rs:147 — but this PR should not be read as claiming that
SIGSTOP/SIGCONT is fully correct. A follow-up should fix the waker
protocol (re-check pending for a stop-ending signal inside the set_waker
closure, and treat a spurious Ready as a re-park rather than a continue).

For some reason, we disabled preemptions before we set the waker and
yield. This is prohibited since we can't go to sleep with preemption
disabled.

After examining the previous commit, I think we used to call
Scheduler::schedule() to do context switches, which needs preemptions
disabled. When we move on to the new method, we just forgot to change
it. This should fix the issue.

Fixes: e23c9eb ("runtime: new task sleep-wakeup method and some adaption")
Fixes #52
Signed-off-by: greatbridf <greatbridf@icloud.com>
@greatbridf greatbridf linked an issue Nov 5, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kernel panic when process receives SIGSTOP and then SIGCONT

1 participant