The Pedigree Project 0.1
scheduler-regressions.cc
1/*
2 * Copyright (c) 2026, Pedigree Developers
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted.
6 */
7
8#include "pedigree/kernel/Atomic.h"
9#include "pedigree/kernel/Log.h"
10#include "pedigree/kernel/Subsystem.h"
11#include "pedigree/kernel/machine/IrqHandler.h"
12#include "pedigree/kernel/machine/IrqManager.h"
13#include "pedigree/kernel/machine/Machine.h"
14#include "pedigree/kernel/machine/SchedulerTimer.h"
15#include "pedigree/kernel/machine/SchedulerTimerDispatchCleanup.h"
16#include "pedigree/kernel/machine/SchedulerTimerHandler.h"
17#include "pedigree/kernel/process/Event.h"
18#include "pedigree/kernel/process/Process.h"
19#include "pedigree/kernel/process/Scheduler.h"
20#include "pedigree/kernel/process/Thread.h"
21#include "pedigree/kernel/processor/Processor.h"
22#include "pedigree/kernel/processor/ProcessorInformation.h"
23#include "pedigree/kernel/processor/VirtualAddressSpace.h"
24#include "pedigree/kernel/time/Time.h"
25
26#include <signal.h>
27#include <time.h>
28
29#include "system/kernel/machine/hosted/IrqManager.h"
30#include "system/kernel/machine/hosted/SchedulerTimer.h"
31#include "system/kernel/machine/hosted/Timer.h"
32
33#if !PEDIGREE_HOSTED_CORE_SMOKE
34extern "C" int hostedSchedulerExitUserProbe(void* parameter);
35extern "C" void hostedSchedulerExitUserProbeTimedOut(void* parameter);
36
37#if HOSTED && BITS_64
38asm(".text\n"
39 ".globl hostedSchedulerExitUserProbe\n"
40 ".type hostedSchedulerExitUserProbe,@function\n"
41 "hostedSchedulerExitUserProbe:\n"
42 "movq %rdi,%r12\n"
43 "movq $1,0(%r12)\n"
44 "subq $16,%rsp\n"
45 "movl $228,%eax\n"
46 "movl $1,%edi\n"
47 "movq %rsp,%rsi\n"
48 "syscall\n"
49 "testq %rax,%rax\n"
50 "js 2f\n"
51 "movq 0(%rsp),%r13\n"
52 "addq $3,%r13\n"
53 "movq $100000,%r14\n"
54 "1:\n"
55 "pause\n"
56 "decq %r14\n"
57 "jnz 1b\n"
58 "movq $100000,%r14\n"
59 "movl $228,%eax\n"
60 "movl $1,%edi\n"
61 "movq %rsp,%rsi\n"
62 "syscall\n"
63 "testq %rax,%rax\n"
64 "js 2f\n"
65 "cmpq %r13,0(%rsp)\n"
66 "jl 1b\n"
67 "2:\n"
68 "movq $1,8(%r12)\n"
69 "subq $16,%rsp\n"
70 "movq $0xa00,0(%rsp)\n"
71 "movl $14,%eax\n"
72 "xorl %edi,%edi\n"
73 "movq %rsp,%rsi\n"
74 "xorl %edx,%edx\n"
75 "movl $8,%r10d\n"
76 "syscall\n"
77 "call hostedSetKernelFs@PLT\n"
78 "movq 24(%r12),%rsp\n"
79 "andq $-16,%rsp\n"
80 "movq %r12,%rdi\n"
81 "call hostedSchedulerExitUserProbeTimedOut@PLT\n"
82 "ud2\n"
83 ".size hostedSchedulerExitUserProbe,.-hostedSchedulerExitUserProbe\n");
84#endif
85#endif
86
87extern bool runHostedAccountingRegressions();
88
89namespace {
90struct TlsResetContext {
91 TlsResetContext()
92 : hookCalls(0),
93 beforeClearCalls(0),
94 clearedCalls(0),
95 remappedCalls(0),
96 failures(0),
97 interruptsInitiallyEnabled(0),
98 initialBaseValid(0),
99 remappedBase(0),
100 mapped(0),
101 interruptsRestored(0),
102 returned(0) {}
103
104 Atomic<size_t> hookCalls;
105 Atomic<size_t> beforeClearCalls;
106 Atomic<size_t> clearedCalls;
107 Atomic<size_t> remappedCalls;
108 Atomic<size_t> failures;
109 Atomic<size_t> interruptsInitiallyEnabled;
110 Atomic<size_t> initialBaseValid;
111 Atomic<size_t> remappedBase;
112 Atomic<size_t> mapped;
113 Atomic<size_t> interruptsRestored;
114 Atomic<size_t> returned;
115};
116
117TlsResetContext* g_TlsResetContext = nullptr;
118
119void observeTlsReset(Thread* thread, Thread::TlsResetPhase phase, uintptr_t base) {
120 TlsResetContext* context = __atomic_load_n(&g_TlsResetContext, __ATOMIC_ACQUIRE);
121 if (!context) {
122 return;
123 }
124
125 context->hookCalls += 1;
126 if (thread != Processor::information().getCurrentThread() || Processor::getInterrupts() ||
127 Processor::executionContext() != ExecutionContext::AtomicThread) {
128 context->failures += 1;
129 }
130
131 if (phase == Thread::TlsResetBeforeClear && !base) {
132 context->beforeClearCalls += 1;
133 } else if (phase == Thread::TlsResetCleared && !base) {
134 context->clearedCalls += 1;
135 } else if (phase == Thread::TlsResetRemapped && base) {
136 context->remappedCalls += 1;
137 context->remappedBase = base;
138 } else {
139 context->failures += 1;
140 }
141
142 // Keep the controlled missing-guard regression out of the real race window.
145 }
146}
147
148int resetTlsBaseThread(void* parameter) {
149 TlsResetContext* context = reinterpret_cast<TlsResetContext*>(parameter);
150 Thread* current = Processor::information().getCurrentThread();
151 const bool interruptsWereEnabled = Processor::getInterrupts();
152 context->interruptsInitiallyEnabled = interruptsWereEnabled ? 1 : 0;
153 const uintptr_t initialBase = current->getTlsBase();
154 context->initialBaseValid = initialBase && current->getParent()->getAddressSpace()->isMapped(
155 reinterpret_cast<void*>(initialBase));
156
157 Process* process = current->getParent();
158 process->resetUserReservations();
160
161 current->resetTlsBase();
162
163 context->interruptsRestored = Processor::getInterrupts() == interruptsWereEnabled ? 1 : 0;
164 if (Processor::getInterrupts() != interruptsWereEnabled) {
165 Processor::setInterrupts(interruptsWereEnabled);
166 }
167 const uintptr_t tlsBase = context->remappedBase;
168 context->mapped =
169 tlsBase && process->getAddressSpace()->isMapped(reinterpret_cast<void*>(tlsBase));
170 context->returned = 1;
171 return 0;
172}
173
174bool tlsResetAtomicRemap() {
175 Process* process = new Process(Scheduler::instance().getKernelProcess());
176 TlsResetContext context;
177 Thread* target = new Thread(process, resetTlsBaseThread, &context, nullptr, true, true, true);
178 target->setName("hosted TLS reset atomicity probe");
179
180 __atomic_store_n(&g_TlsResetContext, &context, __ATOMIC_RELEASE);
181 Thread::setTlsResetHookForHostedTest(target, observeTlsReset);
182 const bool started = target->start();
183 const bool joined = started && target->joinForCompletion();
184 Thread::setTlsResetHookForHostedTest(nullptr, nullptr);
185 __atomic_store_n(&g_TlsResetContext, static_cast<TlsResetContext*>(nullptr), __ATOMIC_RELEASE);
186
187 if (!started) {
188 delete target;
189 }
190 delete process;
191
192 const bool passed = started && joined && context.interruptsInitiallyEnabled &&
193 context.initialBaseValid && context.hookCalls == 3 &&
194 context.beforeClearCalls == 1 && context.clearedCalls == 1 &&
195 context.remappedCalls == 1 && !context.failures && context.mapped &&
196 context.interruptsRestored && context.returned;
197 if (!passed) {
198 ERROR(
199 "HOSTED-WAIT-TEST: FAIL thread-tls-reset-interrupt-atomicity: "
200 "TLS reset escaped its non-preemptible mapping window");
201 } else {
202 NOTICE("HOSTED-WAIT-TEST: PASS thread-tls-reset-interrupt-atomicity");
203 }
204 return passed;
205}
206
207struct ContextSwitchContext {
208 explicit ContextSwitchContext(Thread* driver)
209 : driver(driver),
210 phase(0),
211 switchReturns(0),
212 bookkeepingCalls(0),
213 restoreBoundaries(0),
214 tickCalls(0),
215 targetCalls(0),
216 failures(0) {}
217
218 Thread* driver;
219 Atomic<size_t> phase;
220 Atomic<size_t> switchReturns;
221 Atomic<size_t> bookkeepingCalls;
222 Atomic<size_t> restoreBoundaries;
223 Atomic<size_t> tickCalls;
224 Atomic<size_t> targetCalls;
225 Atomic<size_t> failures;
226};
227
228ContextSwitchContext* g_ContextSwitchContext = nullptr;
229
230struct SchedulerTimerContext {
231 SchedulerTimerContext() : calls(0), failures(0) {}
232
233 Atomic<size_t> calls;
234 Atomic<size_t> failures;
235};
236
237SchedulerTimerContext* g_SchedulerTimerContext = nullptr;
238
239constexpr int DeferredTimerExitCode = 73;
240
241#if !PEDIGREE_HOSTED_CORE_SMOKE
242struct SchedulerExitContext;
243
244struct SchedulerExitUserProbeState {
245 uintptr_t ready;
246 uintptr_t timedOut;
247 SchedulerExitContext* context;
248 uintptr_t kernelStackTop;
249};
250
251static_assert(__builtin_offsetof(SchedulerExitUserProbeState, ready) == 0 &&
252 __builtin_offsetof(SchedulerExitUserProbeState, timedOut) == 8 &&
253 __builtin_offsetof(SchedulerExitUserProbeState, kernelStackTop) == 24,
254 "hosted scheduler user probe assembly layout changed");
255
256struct SchedulerExitContext {
257 SchedulerExitContext()
258 : user{0, 0, this, 0},
259 target(nullptr),
260 event(nullptr),
261 userStackBase(0),
262 userStackTop(0),
263 hookCalls(0),
264 targetHookCalls(0),
265 userHookCalls(0),
266 tickCalls(0),
267 queued(0),
268 eventCalls(0),
269 exitCalls(0),
270 failures(0) {}
271
272 SchedulerExitUserProbeState user;
273 Thread* target;
274 Event* event;
275 uintptr_t userStackBase;
276 uintptr_t userStackTop;
277 Atomic<size_t> hookCalls;
278 Atomic<size_t> targetHookCalls;
279 Atomic<size_t> userHookCalls;
280 Atomic<size_t> tickCalls;
281 Atomic<size_t> queued;
282 Atomic<size_t> eventCalls;
283 Atomic<size_t> exitCalls;
284 Atomic<size_t> failures;
285};
286
287SchedulerExitContext* g_SchedulerExitContext = nullptr;
288
289void schedulerExitEventHandler(size_t) {
290 SchedulerExitContext* context = __atomic_load_n(&g_SchedulerExitContext, __ATOMIC_ACQUIRE);
291 if (!context) {
292 return;
293 }
294 Thread* current = Processor::information().getCurrentThread();
295 if (current != context->target || !Processor::getInterrupts() || Processor::inDeviceHardIrq() ||
296 current->getHostedSignalDepth() ||
297 current->currentTimeAccountingMode() != CpuTimeMode::Kernel) {
298 context->failures += 1;
299 }
300 context->eventCalls += 1;
301 current->deferProcessExit(DeferredTimerExitCode);
302}
303
304class SchedulerExitEvent : public Event {
305 public:
306 SchedulerExitEvent() : Event(reinterpret_cast<uintptr_t>(&schedulerExitEventHandler), false) {}
307
308 size_t serialize(uint8_t*) override {
309 return 0;
310 }
311
312 size_t getNumber() override {
313 return 0x45584954;
314 }
315};
316
317class SchedulerExitSubsystem : public Subsystem {
318 public:
319 explicit SchedulerExitSubsystem(SchedulerExitContext& context)
320 : Subsystem(None), m_Context(context) {}
321
322 void exit(int code, ExitCause cause) override {
323 Thread* current = Processor::information().getCurrentThread();
324 if (code != DeferredTimerExitCode || current != m_Context.target || m_Context.eventCalls != 1 ||
326 current->getHostedSignalDepth() ||
327 current->currentTimeAccountingMode() != CpuTimeMode::Kernel || cause != ExitCause::Normal) {
328 m_Context.failures += 1;
329 }
330 m_Context.exitCalls += 1;
332 }
333
334 bool kill(KillReason, Thread*) override {
335 return false;
336 }
337
338 bool invoke(const char*, Vector<String>&, Vector<String>&) override {
339 return false;
340 }
341
342 bool invoke(const char*, Vector<String>&, Vector<String>&, SyscallState&) override {
343 return false;
344 }
345
346 bool invoke(File*, const String&, Vector<String>&, Vector<String>&) override {
347 return false;
348 }
349
350 bool invoke(File*, const String&, Vector<String>&, Vector<String>&, SyscallState&) override {
351 return false;
352 }
353
354 private:
355 SchedulerExitContext& m_Context;
356};
357
358class SchedulerExitProcess : public Process {
359 public:
360 SchedulerExitProcess(SchedulerExitContext& context, Process* parent)
361 : Process(DeferredPublication(), parent) {
362 setSubsystem(new SchedulerExitSubsystem(context));
363 description() += "hosted scheduler return-tail exit probe";
364 publish();
365 }
366
367 ~SchedulerExitProcess() override {
369 }
370};
371
372void queueExitEventFromSchedulerTick(uint64_t delta, InterruptState& state) {
373 SchedulerExitContext* context = __atomic_load_n(&g_SchedulerExitContext, __ATOMIC_ACQUIRE);
374 Thread* current = Processor::information().getCurrentThread();
375 if (!context) {
376 return;
377 }
378 context->hookCalls += 1;
379 if (!__atomic_load_n(&context->user.ready, __ATOMIC_ACQUIRE)) {
380 return;
381 }
382 if (current != context->target) {
383 return;
384 }
385 context->targetHookCalls += 1;
386 if (state.kernelMode() || current->getStateLevel() ||
387 state.getStackPointer() < context->userStackBase ||
388 state.getStackPointer() > context->userStackTop) {
389 return;
390 }
391 context->userHookCalls += 1;
392 if (!context->queued.compareAndSwap(0, 1)) {
393 return;
394 }
395
396 const uint64_t interval = 100 * Time::Multiplier::Millisecond;
397 if (delta < interval || (delta % interval) || Processor::getInterrupts() ||
398 current->getHostedSignalDepth() != 1 || Processor::inDeviceHardIrq() || state.kernelMode() ||
399 state.getStackPointer() < context->userStackBase ||
400 state.getStackPointer() > context->userStackTop ||
401 current->currentTimeAccountingMode() != CpuTimeMode::Kernel ||
402 state.getInterruptNumber() != SIGUSR2 ||
403 state.getInterruptSource() != HostedSchedulerTimer::sourceForTest()) {
404 context->failures += 1;
405 }
406
407 // Test-only injection at the exact historical window: the old timer path
408 // dispatched this preallocated Event before returning from the hard IRQ.
409 if (!current->sendEvent(context->event)) {
410 context->failures += 1;
411 }
412 context->tickCalls += 1;
413}
414
415bool schedulerTimerExitDeferral() {
416 constexpr const char* Test = "scheduler-timer-exit-return-tail";
417 SchedulerExitContext context;
418 SchedulerExitEvent event;
419 context.event = &event;
420 const bool timerSlowed = HostedTimer::setSignalIntervalForTest(4 * Time::Multiplier::Second);
421 Thread* driver = Processor::information().getCurrentThread();
422 SchedulerExitProcess* process =
423 driver ? new SchedulerExitProcess(context, driver->getParent()) : nullptr;
424 VirtualAddressSpace::Stack* userStack =
425 process ? process->getAddressSpace()->allocateStack() : nullptr;
426 Thread* target = nullptr;
427 if (userStack) {
428 context.userStackBase = reinterpret_cast<uintptr_t>(userStack->getBase());
429 context.userStackTop = reinterpret_cast<uintptr_t>(userStack->getTop());
430 target = new Thread(process, hostedSchedulerExitUserProbe, &context.user, userStack->getTop(),
431 false, true, true);
432 context.target = target;
433 context.user.kernelStackTop = reinterpret_cast<uintptr_t>(target->getKernelStack());
434 target->setName("hosted scheduler exit return-tail probe");
435 }
436
437 const bool interruptsWereEnabled = Processor::getInterrupts();
439 __atomic_store_n(&g_SchedulerExitContext, &context, __ATOMIC_RELEASE);
440 HostedSchedulerTimer::setHardContextHookForTest(queueExitEventFromSchedulerTick);
441 Processor::setInterrupts(interruptsWereEnabled);
442
443 const bool started = target && target->start();
444 bool reapable = false;
445 timespec startedAt = {};
446 timespec now = {};
447 clock_gettime(CLOCK_MONOTONIC, &startedAt);
448 while (started && !reapable) {
449 reapable = target->isReapableForHostedTest();
450 if (reapable) {
451 break;
452 }
454 clock_gettime(CLOCK_MONOTONIC, &now);
455 if (now.tv_sec - startedAt.tv_sec >= 6) {
456 break;
457 }
458 }
459 if (started && !reapable) {
460 ERROR("HOSTED-WAIT-TEST: DETAIL "
461 << Test << ": ready=" << context.user.ready << " timed-out=" << context.user.timedOut
462 << " hooks=" << context.hookCalls.value() << " target-hooks="
463 << context.targetHookCalls.value() << " user-hooks=" << context.userHookCalls.value()
464 << " queued=" << context.queued.value() << " event-calls=" << context.eventCalls.value()
465 << " exit-calls=" << context.exitCalls.value() << " target-status="
466 << static_cast<size_t>(target->getStatus()) << " target-level=" << target->getStateLevel()
467 << " target-signal-depth=" << target->getHostedSignalDepth()
468 << " timer-admissions=" << HostedSchedulerTimer::activeDispatchesForTest());
469 FATAL("Hosted scheduler exit probe made no bounded progress");
470 }
471 const bool joined = reapable && target->joinForCompletion();
472 if (target && !started) {
473 delete target;
474 }
475
477 HostedSchedulerTimer::setHardContextHookForTest(nullptr);
478 __atomic_store_n(&g_SchedulerExitContext, static_cast<SchedulerExitContext*>(nullptr),
479 __ATOMIC_RELEASE);
480 const bool timerRestored = HostedTimer::setSignalIntervalForTest(Time::Multiplier::Millisecond);
481 Processor::setInterrupts(interruptsWereEnabled);
482
483 event.waitForDeliveries();
484 if (userStack) {
485 process->getAddressSpace()->freeStack(userStack);
486 }
487 if (process) {
488 delete process;
489 }
490
491 const bool passed = timerSlowed && timerRestored && started && joined && context.user.ready &&
492 !context.user.timedOut && context.tickCalls == 1 && context.queued == 1 &&
493 context.eventCalls == 1 && context.exitCalls == 1 && !context.failures;
494 if (!passed) {
495 ERROR("HOSTED-WAIT-TEST: FAIL " << Test
496 << ": process exit ran before the "
497 "IRQ return-to-user tail");
498 } else {
499 NOTICE(
500 "HOSTED-WAIT-TEST: PASS "
501 "scheduler-timer-exit-return-tail");
502 }
503 return passed;
504}
505#endif
506
507struct HostedSignalSwitchContext {
508 explicit HostedSignalSwitchContext(Thread* driver)
509 : driver(driver),
510 target(nullptr),
511 armed(0),
512 waiting(0),
513 computing(0),
514 driverTicks(0),
515 targetTicks(0),
516 ran(0),
517 failures(0),
518 failureMask(0) {}
519
520 Thread* driver;
521 Thread* target;
522 Atomic<size_t> armed;
523 Atomic<size_t> waiting;
524 Atomic<size_t> computing;
525 Atomic<size_t> driverTicks;
526 Atomic<size_t> targetTicks;
527 Atomic<size_t> ran;
528 Atomic<size_t> failures;
529 Atomic<size_t> failureMask;
530};
531
532HostedSignalSwitchContext* g_HostedSignalSwitchContext = nullptr;
533
534void observeHostedAutodisarmTick(uint64_t delta, InterruptState& state) {
535 HostedSignalSwitchContext* context =
536 __atomic_load_n(&g_HostedSignalSwitchContext, __ATOMIC_ACQUIRE);
537 if (!context || !context->armed) {
538 return;
539 }
540
541 Thread* current = Processor::information().getCurrentThread();
542 if (current == context->driver) {
543 context->driverTicks += 1;
544 return;
545 }
546 if (current != context->target || !context->computing ||
547 !context->targetTicks.compareAndSwap(0, 1)) {
548 return;
549 }
550
551 const uint64_t interval = 100 * Time::Multiplier::Millisecond;
552 size_t failureMask = 0;
553 failureMask |= delta < interval ? 1 : 0;
554 failureMask |= (delta % interval) ? 2 : 0;
555 failureMask |= !state.kernelMode() ? 4 : 0;
556 failureMask |= !current->getHostedSignalDepth() ? 8 : 0;
557 failureMask |= Processor::hostedSignalFrameDepthForTest() < 2 ? 16 : 0;
558 failureMask |= Processor::getInterrupts() ? 32 : 0;
559 failureMask |= Processor::inDeviceHardIrq() ? 64 : 0;
560 failureMask |= current->currentTimeAccountingMode() != CpuTimeMode::Kernel ? 128 : 0;
561 failureMask |= state.getInterruptNumber() != SIGUSR2 ? 256 : 0;
562 failureMask |= state.getInterruptSource() != HostedSchedulerTimer::sourceForTest() ? 512 : 0;
563 failureMask |= Processor::executionContext() != ExecutionContext::SchedulerIrq ? 1024 : 0;
564 if (failureMask) {
565 context->failureMask |= failureMask;
566 context->failures += 1;
567 }
568}
569
570int hostedSignalSwitchTarget(void* parameter) {
571 HostedSignalSwitchContext* context = reinterpret_cast<HostedSignalSwitchContext*>(parameter);
572 context->waiting = 1;
573 while (!context->armed) {
575 }
576
577 // This continuation is selected by the real scheduler signal while its
578 // frame remains live on another Pedigree stack.
580 sigset_t mask;
581 sigprocmask(0, nullptr, &mask);
582 Thread* current = Processor::information().getCurrentThread();
583 size_t failureMask = 0;
584 failureMask |= !current ? 1024 : 0;
585 failureMask |= current && current->getHostedSignalDepth() ? 2048 : 0;
586 failureMask |= !Processor::hostedSignalFrameDepthForTest() ? 4096 : 0;
587 failureMask |= !Processor::getInterrupts() ? 8192 : 0;
588 failureMask |= sigismember(&mask, SIGUSR1) ? 16384 : 0;
589 failureMask |= sigismember(&mask, SIGUSR2) ? 32768 : 0;
590 failureMask |= Processor::executionContext() != ExecutionContext::WaitableThread ? 131072 : 0;
591 if (failureMask) {
592 context->failureMask |= failureMask;
593 context->failures += 1;
594 }
595
596 context->computing = 1;
597 timespec startedAt = {};
598 timespec now = {};
599 clock_gettime(CLOCK_MONOTONIC, &startedAt);
600 while (!context->targetTicks) {
602 clock_gettime(CLOCK_MONOTONIC, &now);
603 if (now.tv_sec - startedAt.tv_sec >= 3) {
604 context->failureMask |= 65536;
605 context->failures += 1;
606 break;
607 }
608 }
609 context->ran = 1;
610 return 0;
611}
612
613bool hostedSignalMaskSpansContextSwitch() {
614 constexpr const char* Test = "hosted-signal-autodisarm-preemption";
615 HostedSignalSwitchContext context(Processor::information().getCurrentThread());
616 Thread* target = new Thread(Scheduler::instance().getKernelProcess(), hostedSignalSwitchTarget,
617 &context, nullptr, false, true, true);
618 target->setName("hosted signal-mask context-switch probe");
619 context.target = target;
620 const bool started = target->start();
621
622 constexpr size_t Attempts = 10000;
623 for (size_t attempt = 0; started && !context.waiting && attempt < Attempts; ++attempt) {
625 }
626
627 const bool interruptsWereEnabled = Processor::getInterrupts();
629 __atomic_store_n(&g_HostedSignalSwitchContext, &context, __ATOMIC_RELEASE);
630 HostedSchedulerTimer::setHardContextHookForTest(observeHostedAutodisarmTick);
631 context.armed = 1;
632 Processor::setInterrupts(interruptsWereEnabled);
633 const Time::Timestamp deadline = Time::getTicks() + (3 * Time::Multiplier::Second);
634 while (!context.ran && Time::getTicks() < deadline) {
636 }
637
639 HostedSchedulerTimer::setHardContextHookForTest(nullptr);
640 __atomic_store_n(&g_HostedSignalSwitchContext, static_cast<HostedSignalSwitchContext*>(nullptr),
641 __ATOMIC_RELEASE);
642 Processor::setInterrupts(interruptsWereEnabled);
643
644 // A failed preemption must not leave the probe stack live while its
645 // context record goes out of scope.
646 for (size_t attempt = 0; started && !context.ran && attempt < Attempts; ++attempt) {
648 }
649
650 const bool joined = context.ran && target->joinForCompletion();
651 if (!started) {
652 delete target;
653 }
654
655 const bool passed = started && context.waiting && context.computing && context.driverTicks &&
656 context.targetTicks == 1 && context.ran && joined && !context.failures;
657 if (!passed) {
658 ERROR("HOSTED-WAIT-TEST: FAIL "
659 << Test << ": s=" << started << " w=" << static_cast<size_t>(context.waiting)
660 << " c=" << static_cast<size_t>(context.computing)
661 << " d=" << static_cast<size_t>(context.driverTicks) << " t="
662 << static_cast<size_t>(context.targetTicks) << " r=" << static_cast<size_t>(context.ran)
663 << " j=" << joined << " f=" << static_cast<size_t>(context.failures)
664 << " m=" << context.failureMask.value());
665 } else {
666 NOTICE(
667 "HOSTED-WAIT-TEST: PASS "
668 "hosted-signal-autodisarm-preemption");
669 }
670 return passed;
671}
672
673void observeSchedulerTimerHardContext(uint64_t delta, InterruptState& state) {
674 SchedulerTimerContext* context = __atomic_load_n(&g_SchedulerTimerContext, __ATOMIC_ACQUIRE);
675 if (!context) {
676 return;
677 }
678
679 Thread* current = Processor::information().getCurrentThread();
680 const uint64_t interval = 100 * Time::Multiplier::Millisecond;
681 if (delta < interval || (delta % interval) || !current || !current->getHostedSignalDepth() ||
682 !Processor::onHostedExecutionThread() ||
683 Processor::executionContext() != ExecutionContext::SchedulerIrq ||
684 Processor::inDeviceHardIrq() || Processor::deviceHardIrqDepthForTest() != 0 ||
685 state.getInterruptNumber() != SIGUSR2 ||
686 state.getInterruptSource() != HostedSchedulerTimer::sourceForTest()) {
687 context->failures += 1;
688 }
689 context->calls += 1;
690}
691
692bool schedulerTimerHardContext() {
693 constexpr const char* Test = "hosted-scheduler-timer-hard-context";
694 SchedulerTimerContext context;
695 const bool directRoute = HostedSchedulerTimer::directRoutePublishedForTest();
696
697 const bool interruptsWereEnabled = Processor::getInterrupts();
699 __atomic_store_n(&g_SchedulerTimerContext, &context, __ATOMIC_RELEASE);
700 HostedSchedulerTimer::setHardContextHookForTest(observeSchedulerTimerHardContext);
701 Processor::setInterrupts(interruptsWereEnabled);
702
703 const Time::Timestamp deadline = Time::getTicks() + (2 * Time::Multiplier::Second);
704 while (!context.calls && Time::getTicks() < deadline) {
706 }
707
709 HostedSchedulerTimer::setHardContextHookForTest(nullptr);
710 __atomic_store_n(&g_SchedulerTimerContext, static_cast<SchedulerTimerContext*>(nullptr),
711 __ATOMIC_RELEASE);
712 Processor::setInterrupts(interruptsWereEnabled);
713
714 const bool passed = directRoute && context.calls && !context.failures;
715 if (!passed) {
716 ERROR("HOSTED-WAIT-TEST: FAIL " << Test
717 << ": the scheduler callback did not "
718 "use its dedicated controller route");
719 } else {
720 NOTICE(
721 "HOSTED-WAIT-TEST: PASS "
722 "hosted-scheduler-timer-hard-context");
723 }
724 return passed;
725}
726
727class ConflictingSchedulerTimerHandler : public SchedulerTimerHandler {
728 public:
729 ConflictingSchedulerTimerHandler() : calls(0) {}
730
731 void timer(uint64_t, InterruptState&) override {
732 calls += 1;
733 }
734
735 Atomic<size_t> calls;
736};
737
738class SelfRemovingSchedulerTimerHandler : public SchedulerTimerHandler {
739 public:
740 explicit SelfRemovingSchedulerTimerHandler(SchedulerTimer* timer)
741 : m_Timer(timer), calls(0), removalSucceeded(0), continuedAfterRemoval(0), wrongContext(0) {}
742
743 void timer(uint64_t, InterruptState&) override {
744 if (Processor::executionContext() != ExecutionContext::SchedulerIrq) {
745 wrongContext += 1;
746 }
747 calls += 1;
748 if (m_Timer && m_Timer->removeHandler(this)) {
749 removalSucceeded = 1;
750 }
751 // A true removal must be impossible here: this statement is still in
752 // the callback whose lifetime removeHandler promises to drain.
753 continuedAfterRemoval = 1;
754 }
755
756 SchedulerTimer* m_Timer;
757 Atomic<size_t> calls;
758 Atomic<size_t> removalSucceeded;
759 Atomic<size_t> continuedAfterRemoval;
760 Atomic<size_t> wrongContext;
761};
762
763bool schedulerTimerAbandonedAdmissionCleanup() {
764 constexpr const char* Test = "hosted-scheduler-timer-abandoned-admission-cleanup";
765 Thread* current = Processor::information().getCurrentThread();
766 ConflictingSchedulerTimerHandler handler;
768 const size_t owner = Processor::id();
769 const size_t initialLevel = current ? current->getStateLevel() : 0;
770 const bool published = current && slot.publish(owner, &handler);
771 SchedulerState* previous = published ? current->pushState() : nullptr;
772 const bool pushed = previous && current->getStateLevel() == initialLevel + 1;
773
774 bool admitted = false;
775 bool counted = false;
776 bool abandoned = false;
777 if (pushed) {
779 admitted = slot.beginDispatch(owner, dispatch);
780 if (admitted) {
781 // Models the old leak: the scheduler frame owns an admission, but
782 // its Thread state is discarded without C++ stack unwinding.
783 {
784 SchedulerTimerDispatchCleanup cleanup(dispatch);
785 counted = slot.activeDispatches() == 1;
786 current->abandonCurrentState(false);
787 abandoned = current->getStateLevel() == initialLevel && slot.activeDispatches() == 0;
788 }
789 // Both cleanup and the raw guard destruct after the modelled
790 // abandonment; idempotent release must prevent an underflow.
791 } else {
792 current->popState(false);
793 }
794 }
795
796 const bool removed = abandoned && slot.unpublish(owner, &handler);
797 const bool republished = removed && slot.publish(owner, &handler);
798 bool readmitted = false;
799 if (republished) {
801 readmitted = slot.beginDispatch(owner, dispatch);
802 dispatch.release();
803 }
804 const bool drained = slot.activeDispatches() == 0;
805 const bool finallyRemoved = republished && drained && slot.unpublish(owner, &handler);
806
807 const bool passed = published && pushed && admitted && counted && abandoned && removed &&
808 republished && readmitted && drained && finallyRemoved;
809 if (!passed) {
810 ERROR("HOSTED-WAIT-TEST: FAIL " << Test
811 << ": an abandoned scheduler frame "
812 "stranded its callback admission");
813 } else {
814 NOTICE(
815 "HOSTED-WAIT-TEST: PASS "
816 "hosted-scheduler-timer-abandoned-admission-cleanup");
817 }
818 return passed;
819}
820
821bool schedulerTimerSingleOwner() {
822 constexpr const char* Test = "hosted-scheduler-timer-single-owner";
823 SchedulerTimer* timer = Machine::instance().getSchedulerTimer();
824 SchedulerTimerHandler* owner = HostedSchedulerTimer::publishedHandlerForTest();
825 ConflictingSchedulerTimerHandler conflicting;
826
827 const bool ownerPublished = owner != nullptr;
828 const bool nullRegistrationRejected = timer && !timer->registerHandler(nullptr);
829 const bool duplicateRejected = timer && !timer->registerHandler(owner);
830 const bool conflictRejected = timer && !timer->registerHandler(&conflicting);
831 const bool nullRemovalRejected = timer && !timer->removeHandler(nullptr);
832 const bool wrongOwnerRejected = timer && !timer->removeHandler(&conflicting);
833 const bool ownerPreserved = HostedSchedulerTimer::publishedHandlerForTest() == owner;
834
835 const bool passed = timer && ownerPublished && nullRegistrationRejected && duplicateRejected &&
836 conflictRejected && nullRemovalRejected && wrongOwnerRejected &&
837 ownerPreserved && !conflicting.calls;
838 if (!passed) {
839 ERROR("HOSTED-WAIT-TEST: FAIL " << Test
840 << ": handler ownership was replaced "
841 "or removed by a non-owner");
842 } else {
843 NOTICE(
844 "HOSTED-WAIT-TEST: PASS "
845 "hosted-scheduler-timer-single-owner");
846 }
847 return passed;
848}
849
850bool schedulerTimerSelfRemovalRejected() {
851 constexpr const char* Test = "hosted-scheduler-timer-self-removal-rejected";
852 SchedulerTimer* timer = Machine::instance().getSchedulerTimer();
853 SchedulerTimerHandler* owner = HostedSchedulerTimer::publishedHandlerForTest();
854 SelfRemovingSchedulerTimerHandler probe(timer);
855
856 auto removeWithRetry = [timer](SchedulerTimerHandler* handler, size_t& attempts) {
857 constexpr size_t RemovalAttemptLimit = 256;
858 while (timer && handler && attempts < RemovalAttemptLimit) {
859 ++attempts;
860 if (timer->removeHandler(handler)) {
861 return true;
862 }
863 // The regression driver can resume inside the scheduler tick whose
864 // dispatch admission makes removal retryable. Yield until that older
865 // hard frame returns instead of treating a live admission as failure.
867 }
868 return false;
869 };
870
871 size_t ownerRemovalAttempts = 0;
872 const bool ownerRemoved = removeWithRetry(owner, ownerRemovalAttempts);
873 const bool probeRegistered = ownerRemoved && timer->registerHandler(&probe);
874 const Time::Timestamp deadline = Time::getTicks() + (2 * Time::Multiplier::Second);
875 while (probeRegistered && !probe.calls && Time::getTicks() < deadline) {
877 }
878
879 size_t probeRemovalAttempts = 0;
880 const bool probeRemoved =
881 probeRegistered && !probe.removalSucceeded && removeWithRetry(&probe, probeRemovalAttempts);
882 if (probeRegistered && !probeRemoved && !probe.removalSucceeded) {
883 FATAL("Hosted scheduler-timer regression could not retire its stack probe");
884 }
885
886 const bool probeQuiesced = !probeRegistered || probeRemoved || probe.removalSucceeded;
887 const bool ownerRestored = ownerRemoved && probeQuiesced && timer->registerHandler(owner);
888 if (ownerRemoved && !ownerRestored) {
889 FATAL("Hosted scheduler-timer regression could not restore the real owner");
890 }
891 const bool passed = ownerRemoved && probeRegistered && probe.calls.value() >= 1 &&
892 !probe.removalSucceeded && probe.continuedAfterRemoval == 1 &&
893 !probe.wrongContext && probeRemoved && ownerRestored &&
894 HostedSchedulerTimer::publishedHandlerForTest() == owner;
895 if (!passed) {
896 ERROR("HOSTED-WAIT-TEST: FAIL " << Test << ": owner-removed=" << ownerRemoved << " registered="
897 << probeRegistered << " calls=" << probe.calls.value());
898 ERROR("HOSTED-WAIT-TEST: DETAIL "
899 << Test << ": callback-remove=" << probe.removalSucceeded.value()
900 << " continued=" << probe.continuedAfterRemoval.value()
901 << " wrong-context=" << probe.wrongContext.value() << " probe-removed=" << probeRemoved);
902 ERROR("HOSTED-WAIT-TEST: DETAIL "
903 << Test << ": owner-restored=" << ownerRestored
904 << " published-owner=" << (HostedSchedulerTimer::publishedHandlerForTest() == owner)
905 << " owner-remove-attempts=" << ownerRemovalAttempts
906 << " probe-remove-attempts=" << probeRemovalAttempts
907 << " active-dispatches=" << HostedSchedulerTimer::activeDispatchesForTest());
908 } else {
909 NOTICE(
910 "HOSTED-WAIT-TEST: PASS "
911 "hosted-scheduler-timer-self-removal-rejected");
912 }
913 return passed;
914}
915
916class RejectedHostedDeviceHandler : public HardIrqHandler {
917 public:
918 HardIrqDisposition irq(irq_id_t, InterruptState&) override {
919 return HardIrqDisposition::Handled;
920 }
921};
922
923bool schedulerRouteIsDedicated() {
924 constexpr const char* Test = "hosted-scheduler-route-dedicated";
925 IrqManager* manager = Machine::instance().getIrqManager();
926 SchedulerIrqHandler* handler = HostedIrqManager::schedulerIrqHandlerForTest(1);
927 RejectedHostedDeviceHandler deviceHandler;
928 const irq_id_t rejected =
929 manager ? manager->registerHardIsaIrqHandler(1, &deviceHandler, IrqPolicy::syntheticHard())
930 : 0;
931 const bool passed =
932 manager && handler && !rejected && HostedIrqManager::schedulerIrqHandlerForTest(1) == handler;
933 if (!passed) {
934 ERROR("HOSTED-WAIT-TEST: FAIL " << Test
935 << ": hosted admitted a device handler "
936 "on its dedicated scheduler line");
937 } else {
938 NOTICE(
939 "HOSTED-WAIT-TEST: PASS "
940 "hosted-scheduler-route-dedicated");
941 }
942 return passed;
943}
944
945void observeQueuedSchedulerTick(uint64_t, InterruptState&) {
946 ContextSwitchContext* context = __atomic_load_n(&g_ContextSwitchContext, __ATOMIC_ACQUIRE);
947 if (!context || Processor::information().getCurrentThread() != context->driver) {
948 return;
949 }
950
951 const size_t phase = context->phase;
952 if (phase && phase < 4) {
953 context->tickCalls += 1;
954 if (phase != 3 || Processor::getInterrupts() || Processor::inDeviceHardIrq() ||
955 Processor::executionContext() != ExecutionContext::SchedulerIrq) {
956 context->failures += 1;
957 }
958 context->phase = 4;
959 }
960}
961
962void contextSwitchHook(ProcessorBase::HostedContextSwitchStage stage) {
963 ContextSwitchContext* context = __atomic_load_n(&g_ContextSwitchContext, __ATOMIC_ACQUIRE);
964 if (!context || Processor::information().getCurrentThread() != context->driver) {
965 return;
966 }
967
968 switch (stage) {
969 case ProcessorBase::HostedContextSwitchStage::SwitchStateReturnedMasked:
970 if (!context->phase.compareAndSwap(0, 1)) {
971 return;
972 }
973 context->switchReturns += 1;
974 if (Processor::getInterrupts() || !HostedSchedulerTimer::queueTickForTest()) {
975 context->failures += 1;
976 }
977 break;
978 case ProcessorBase::HostedContextSwitchStage::SchedulerBookkeepingComplete:
979 if (context->phase == static_cast<size_t>(1)) {
980 context->bookkeepingCalls += 1;
981 if (Processor::getInterrupts() || !context->phase.compareAndSwap(1, 2)) {
982 context->failures += 1;
983 }
984 }
985 break;
986 case ProcessorBase::HostedContextSwitchStage::SchedulerRestoringInterrupts:
987 if (context->phase == static_cast<size_t>(2)) {
988 context->restoreBoundaries += 1;
989 if (Processor::getInterrupts() || !context->phase.compareAndSwap(2, 3)) {
990 context->failures += 1;
991 }
992 }
993 break;
994 }
995}
996
997int contextSwitchTarget(void* parameter) {
998 ContextSwitchContext* context = reinterpret_cast<ContextSwitchContext*>(parameter);
999 context->targetCalls += 1;
1000 return 0;
1001}
1002
1003bool check(bool condition, const char* detail) {
1004 if (condition) {
1005 return true;
1006 }
1007
1008 ERROR("HOSTED-WAIT-TEST: FAIL context-switch-interrupt-restore: " << detail);
1009 return false;
1010}
1011} // namespace
1012
1013#if !PEDIGREE_HOSTED_CORE_SMOKE
1014extern "C" void hostedSchedulerExitUserProbeTimedOut(void* parameter) {
1015 SchedulerExitUserProbeState* state = reinterpret_cast<SchedulerExitUserProbeState*>(parameter);
1016 if (state && state->context) {
1017 state->context->failures += 1;
1018 }
1020}
1021#endif
1022
1023bool runHostedSchedulerRegressions() {
1024 if (!tlsResetAtomicRemap()) {
1025 return false;
1026 }
1027
1028 if (!hostedSignalMaskSpansContextSwitch() || !schedulerTimerSingleOwner() ||
1029 !schedulerTimerAbandonedAdmissionCleanup() || !schedulerTimerSelfRemovalRejected() ||
1030 !schedulerRouteIsDedicated() || !schedulerTimerHardContext()) {
1031 return false;
1032 }
1033
1034#if !PEDIGREE_HOSTED_CORE_SMOKE
1035 // This probe intentionally enters Linux userspace and exercises the
1036 // syscall-return tail. Darwin hosted execution is kernel-only.
1037 if (!schedulerTimerExitDeferral()) {
1038 return false;
1039 }
1040#endif
1041
1042 if (!runHostedAccountingRegressions()) {
1043 return false;
1044 }
1045
1046 Thread* driver = Processor::information().getCurrentThread();
1047 ContextSwitchContext context(driver);
1048 const bool directRoutePreserved = HostedSchedulerTimer::directRoutePublishedForTest();
1049
1050 Thread* target = new Thread(Scheduler::instance().getKernelProcess(), contextSwitchTarget,
1051 &context, nullptr, false, true, true);
1052 target->setName("hosted context-switch IRQ target");
1053
1054 __atomic_store_n(&g_ContextSwitchContext, &context, __ATOMIC_RELEASE);
1055 Processor::setHostedContextSwitchHook(contextSwitchHook);
1056 HostedSchedulerTimer::setHardContextHookForTest(observeQueuedSchedulerTick);
1057 const bool started = target->start();
1058
1059 constexpr size_t Attempts = 10000;
1060 bool completed = false;
1061 for (size_t attempt = 0; started && attempt < Attempts; ++attempt) {
1062 if (context.phase == static_cast<size_t>(4) && target->isReapableForHostedTest()) {
1063 completed = true;
1064 break;
1065 }
1067 }
1068
1069 HostedSchedulerTimer::setHardContextHookForTest(nullptr);
1070 Processor::setHostedContextSwitchHook(nullptr);
1071 __atomic_store_n(&g_ContextSwitchContext, static_cast<ContextSwitchContext*>(nullptr),
1072 __ATOMIC_RELEASE);
1073
1074 const bool targetJoined = target->isReapableForHostedTest() && target->joinForCompletion();
1075 const bool directRouteAfterRemoval = HostedSchedulerTimer::directRoutePublishedForTest();
1076
1077 const bool passed =
1078 check(directRoutePreserved && started && completed && targetJoined &&
1079 directRouteAfterRemoval && context.switchReturns == 1 &&
1080 context.bookkeepingCalls == 1 && context.restoreBoundaries == 1 &&
1081 context.tickCalls == 1 && context.targetCalls == 1 && context.failures == 0,
1082 "the queued scheduler IRQ escaped the masked post-switch boundary");
1083 if (passed) {
1084 NOTICE("HOSTED-WAIT-TEST: PASS context-switch-interrupt-restore");
1085 }
1086 return passed;
1087}
Definition Event.h:49
virtual size_t getNumber()=0
virtual size_t serialize(uint8_t *pBuffer)=0
Definition File.h:74
virtual HardIrqDisposition irq(irq_id_t number, InterruptState &state)=0
virtual irq_id_t registerHardIsaIrqHandler(uint8_t irq, HardIrqHandler *handler, const IrqPolicy &policy)=0
virtual SchedulerTimer * getSchedulerTimer()=0
VirtualAddressSpace * getAddressSpace()
Definition Process.h:478
void publish()
Definition Process.cc:832
LargeStaticString & description()
Definition Process.h:473
void prepareForDestruction()
Definition Process.cc:914
static bool getInterrupts()
static ProcessorId id()
static ProcessorInformation & information()
static void pause()
static bool inDeviceHardIrq()
Definition Processor.h:559
static ExecutionContext executionContext()
Definition Processor.cc:109
static void setInterrupts(bool bEnable)
bool beginDispatch(size_t owner, DispatchGuard &guard)
bool publish(size_t owner, SchedulerTimerHandler *handler)
bool unpublish(size_t owner, SchedulerTimerHandler *handler)
virtual void timer(uint64_t delta, InterruptState &state)=0
virtual bool registerHandler(SchedulerTimerHandler *handler)=0
virtual bool removeHandler(SchedulerTimerHandler *handler)=0
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226
virtual bool invoke(const char *name, Vector< String > &argv, Vector< String > &env)=0
virtual bool kill(KillReason killReason, Thread *pThread=0)=0
CpuTimeMode currentTimeAccountingMode() const
Definition Thread.cc:457
static void threadExited() NORETURN
Definition Thread.cc:1073
bool joinForCompletion()
Definition Thread.cc:2771
void * getKernelStack()
Definition Thread.cc:1113
Status getStatus() const
Definition Thread.h:431
Process * getParent() const
Definition Thread.h:338
void popState(bool clean=true)
Definition Thread.cc:956
SchedulerState * pushState()
Definition Thread.cc:884
bool start()
Definition Thread.cc:794
bool sendEvent(Event *pEvent)
Definition Thread.cc:1158
uintptr_t getTlsBase()
Definition Thread.cc:2665
size_t getStateLevel() const
Definition Thread.h:314
void resetTlsBase()
Definition Thread.cc:2712
void deferProcessExit(int code)
Definition Thread.cc:3653
void abandonCurrentState(bool clean=false)
Definition Thread.cc:999
A vector / dynamic array.
Definition Vector.h:33
virtual void freeStack(Stack *pStack)=0
virtual Stack * allocateStack()=0
virtual bool isMapped(void *virtualAddress)=0
virtual void revertToKernelAddressSpace()=0
HardIrqDisposition
Definition IrqHandler.h:44