The Pedigree Project 0.1
IrqClosureRegressions.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, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 */
8
9#include "pedigree/kernel/Atomic.h"
10#include "pedigree/kernel/Log.h"
11#include "pedigree/kernel/core/SlamAllocator.h"
12#include "pedigree/kernel/machine/IrqHandler.h"
13#include "pedigree/kernel/machine/IrqHandlerRegistry.h"
14#include "pedigree/kernel/machine/Machine.h"
15#include "pedigree/kernel/machine/SplitIrqHandler.h"
16#include "pedigree/kernel/machine/ThreadedIrqDispatcher.h"
17#include "pedigree/kernel/machine/Timer.h"
18#include "pedigree/kernel/machine/TimerHandler.h"
19#include "pedigree/kernel/process/Scheduler.h"
20#include "pedigree/kernel/process/Semaphore.h"
21#include "pedigree/kernel/process/Thread.h"
22#include "pedigree/kernel/process/WaitQueue.h"
23#include "pedigree/kernel/processor/Processor.h"
24#include "pedigree/kernel/processor/ProcessorInformation.h"
25#include "pedigree/kernel/time/Time.h"
26
27#include "system/kernel/core/processor/DeviceHardIrqContext.h"
28
29extern bool runHostedInterruptManagerRegressions();
30extern bool runHostedSchedulerRegressions();
31extern void system_reset();
32
33namespace {
34constexpr Time::Timestamp TestTimeout = 2 * Time::Multiplier::Second;
35
36bool check(bool condition, const char* test, const char* detail) {
37 if (condition) {
38 return true;
39 }
40
41 ERROR("HOSTED-IRQ-CLOSURE: FAIL " << test << ": " << detail);
42 return false;
43}
44
45template <class Predicate>
46bool waitUntil(Predicate predicate) {
47 const Time::Timestamp deadline = Time::getTicks() + TestTimeout;
48 while (!predicate() && Time::getTicks() < deadline) {
50 }
51 return predicate();
52}
53
54struct DispatcherContext {
55 DispatcherContext() : calls(0), lastCookie(0), contextFailures(0), driver(nullptr) {}
56
57 Atomic<size_t> calls;
58 Atomic<size_t> lastCookie;
59 Atomic<size_t> contextFailures;
60 Thread* driver;
61};
62
63void dispatchThreaded(void* opaque, uint8_t line, size_t cookie) {
64 DispatcherContext* context = reinterpret_cast<DispatcherContext*>(opaque);
65 Thread* current = Processor::information().getCurrentThread();
66 if (line != 0 || !current || current == context->driver || !Processor::getInterrupts() ||
68 Processor::executionContext() != ExecutionContext::WaitableThread ||
69 current->getHostedSignalDepth()) {
70 context->contextFailures += 1;
71 }
72 context->lastCookie = cookie;
73 context->calls += 1;
74}
75
76bool threadedDispatcherLifecycle() {
77 constexpr const char* Test = "threaded-dispatcher-lifecycle";
78 DispatcherContext context;
79 context.driver = Processor::information().getCurrentThread();
80 ThreadedIrqDispatcher dispatcher(MakeConstantString("IRQ closure dispatcher"), 1,
81 dispatchThreaded, &context);
82
83 bool passed = check(dispatcher.initialise(), Test, "initialise failed");
84 passed &= check(!dispatcher.publishFromInterrupt(1, 1) && !dispatcher.publishFromInterrupt(0, 0),
85 Test, "invalid publications were accepted");
86
87 const bool interruptsWereEnabled = Processor::getInterrupts();
89 const bool firstPublished = dispatcher.publishFromInterrupt(0, 7);
90 const bool newerPublished = dispatcher.publishFromInterrupt(0, 9);
91 Processor::setInterrupts(interruptsWereEnabled);
92
93 passed &= check(firstPublished && newerPublished &&
94 waitUntil([&dispatcher] { return dispatcher.completedCookie(0) == 9; }),
95 Test, "the worker did not consume the published high-water cookie");
96
97 dispatcher.rejectNextPublicationForTest();
99 const bool rejectionObserved = !dispatcher.publishFromInterrupt(0, 11);
100 const bool recoveryPublished = dispatcher.publishFromInterrupt(0, 12);
101 Processor::setInterrupts(interruptsWereEnabled);
102 passed &= check(rejectionObserved && recoveryPublished &&
103 waitUntil([&dispatcher] { return dispatcher.completedCookie(0) == 12; }),
104 Test, "a rejected publication poisoned the next occurrence");
105
106 const size_t callsBeforeShutdown = context.calls.value();
107 const bool shutdown = dispatcher.shutdown();
108 passed &= check(shutdown && callsBeforeShutdown && !context.contextFailures &&
109 !dispatcher.isInitialised() && !dispatcher.publishFromInterrupt(0, 13),
110 Test, "shutdown or callback context invariants failed");
111
112 if (passed) {
113 NOTICE("HOSTED-IRQ-CLOSURE: PASS threaded-dispatcher-lifecycle");
114 }
115 return passed;
116}
117
118class CallbackIdProbe final : public IrqHandler {
119 public:
120 CallbackIdProbe() : seen(0) {}
121 IrqDisposition irq(irq_id_t id) override {
122 seen = id;
123 return IrqDisposition::Handled;
124 }
125 irq_id_t seen;
126};
127
128bool registryCallbackId() {
129 constexpr const char* Test = "registry-callback-id";
130 constexpr uint8_t Key = 2;
131 constexpr irq_id_t PublicId = 35;
132 static IrqHandlerRegistry registry;
133 CallbackIdProbe probe;
134 if (!check(registry.registerThreadedHandler(Key, &probe, IrqPolicy::edgeThreaded()), Test,
135 "registration failed")) {
136 return false;
137 }
138 const bool interruptsWereEnabled = Processor::getInterrupts();
140 const bool published = registry.publishThreadedDispatch(Key, 1);
141 Processor::setInterrupts(interruptsWereEnabled);
143 const bool admitted = published && registry.dispatchThreaded(Key, 1, result, nullptr, PublicId);
144 registry.invalidateThreadedLine(Key, 1);
145 const bool retired =
146 registry.unregisterHandler(Key, &probe) == IrqHandlerRegistry::UnregisterResult::Completed;
147 const bool passed =
148 check(admitted && result.handled && result.allowRearm && probe.seen == PublicId && retired,
149 Test, "callback did not receive the public IRQ ID");
150 if (passed) {
151 NOTICE("HOSTED-IRQ-CLOSURE: PASS " << Test);
152 }
153 return passed;
154}
155
156class SplitLifecycleProbe final : public SplitIrqHandler {
157 public:
158 explicit SplitLifecycleProbe(Thread* driver)
159 : SplitIrqHandler(MakeConstantString("IRQ closure split handler")),
160 threadedWork(0),
161 rearmedWork(0),
162 quiesceCalls(0),
163 contextFailures(0),
164 quiesceContextFailures(0),
165 rearmContextFailures(0),
166 workerCalls(0),
167 m_Driver(driver),
168 m_Active(false) {}
169
170 ~SplitLifecycleProbe() override {
171 if (m_Active) {
173 }
174 }
175
176 bool start() {
177 m_Active = initialiseSplitIrq();
178 return m_Active;
179 }
180
181 bool publish(size_t work) {
182 return publishWorkForTest(work);
183 }
184
185 bool stop() {
186 const bool stopped = shutdownSplitIrq();
187 if (stopped) {
188 m_Active = false;
189 }
190 return stopped;
191 }
192
193 Atomic<size_t> threadedWork;
194 Atomic<size_t> rearmedWork;
195 Atomic<size_t> quiesceCalls;
196 Atomic<size_t> contextFailures;
197 Atomic<size_t> quiesceContextFailures;
198 Atomic<size_t> rearmContextFailures;
199 Atomic<size_t> workerCalls;
200
201 using SplitIrqHandler::completedBatchesForTest;
202 using SplitIrqHandler::deferredIrqsForTest;
203 using SplitIrqHandler::pendingWorkForTest;
204 using SplitIrqHandler::publicationFailuresForTest;
205 using SplitIrqHandler::rejectNextPublicationForTest;
206
207 private:
208 HardStageDisposition hardIrq(irq_id_t, InterruptState&, size_t&) override {
209 return HardStageDisposition::NotHandled;
210 }
211
212 void threadedIrq(size_t work) override {
213 Thread* current = Processor::information().getCurrentThread();
215 Processor::executionContext() != ExecutionContext::WaitableThread ||
216 current->getHostedSignalDepth()) {
217 contextFailures += 1;
218 }
219 if (current != m_Driver) {
220 workerCalls += 1;
221 }
222 threadedWork |= work;
223 }
224
225 bool quiesceIrqSources() override {
227 Processor::executionContext() != ExecutionContext::WaitableThread) {
228 quiesceContextFailures += 1;
229 }
230 quiesceCalls += 1;
231 return true;
232 }
233
234 void rearmIrqSources(size_t work) override {
236 Processor::executionContext() != ExecutionContext::AtomicThread) {
237 rearmContextFailures += 1;
238 }
239 rearmedWork |= work;
240 }
241
242 Thread* m_Driver;
243 bool m_Active;
244};
245
246bool splitHandlerLifecycle() {
247 constexpr const char* Test = "split-handler-lifecycle";
248 SplitLifecycleProbe probe(Processor::information().getCurrentThread());
249 if (!check(probe.start(), Test, "initialise failed")) {
250 return false;
251 }
252 bool passed = true;
253
254 const bool interruptsWereEnabled = Processor::getInterrupts();
256 bool firstPublished = false;
257 bool secondPublished = false;
258 {
259 size_t previousDepth = 0;
260 bool restorationArmed = false;
261 DeviceHardIrqContext hardIrq(previousDepth, restorationArmed);
262 firstPublished = probe.publish(1);
263 secondPublished = probe.publish(2);
264 }
265 Processor::setInterrupts(interruptsWereEnabled);
266
267 passed &= check(firstPublished && secondPublished && waitUntil([&probe] {
268 return probe.completedBatchesForTest() && !probe.pendingWorkForTest();
269 }) &&
270 probe.threadedWork.value() == 3 && probe.rearmedWork.value() == 3 &&
271 probe.workerCalls.value() && probe.deferredIrqsForTest() == 2 &&
272 !probe.publicationFailuresForTest(),
273 Test, "accepted hard work did not drain and rearm on the worker");
274
275 probe.rejectNextPublicationForTest();
277 bool rejectionObserved = false;
278 {
279 size_t previousDepth = 0;
280 bool restorationArmed = false;
281 DeviceHardIrqContext hardIrq(previousDepth, restorationArmed);
282 rejectionObserved = !probe.publish(4);
283 }
284 Processor::setInterrupts(interruptsWereEnabled);
285
286 const size_t rearmsBeforeShutdown = probe.rearmedWork.value();
287 const bool orphanRecorded = probe.pendingWorkForTest() == 4;
288 const bool stopped = probe.stop();
289 passed &= check(rejectionObserved && orphanRecorded && probe.publicationFailuresForTest() == 1,
290 Test, "the forced rejection did not leave one orphan batch");
291 passed &= check(stopped, Test, "shutdown rejected an ordinary caller");
292 passed &= check(!probe.pendingWorkForTest() && probe.threadedWork.value() == 7, Test,
293 "shutdown did not drain the orphan batch");
294 passed &= check(probe.rearmedWork.value() == rearmsBeforeShutdown, Test,
295 "shutdown rearmed a quiesced source");
296 passed &= check(probe.quiesceCalls.value() == 2, Test,
297 "shutdown did not reassert source quiescence after draining");
298 passed &= check(!probe.contextFailures, Test,
299 "the threaded callback ran outside ordinary waitable context");
300 passed &= check(!probe.quiesceContextFailures, Test,
301 "source quiescence ran outside ordinary waitable context");
302 passed &=
303 check(!probe.rearmContextFailures, Test, "source rearm did not run with interrupts disabled");
304
305 if (passed) {
306 NOTICE("HOSTED-IRQ-CLOSURE: PASS split-handler-lifecycle");
307 }
308 return passed;
309}
310
311class TimerContextProbe final : public TimerHandler {
312 public:
313 explicit TimerContextProbe(Thread* driver)
314 : calls(0), elapsed(0), contextFailures(0), m_Driver(driver) {}
315
316 void timer(uint64_t delta) override {
317 Thread* current = Processor::information().getCurrentThread();
318 if (!delta || !current || current == m_Driver || !Processor::getInterrupts() ||
320 Processor::executionContext() != ExecutionContext::WaitableThread ||
321 current->getHostedSignalDepth()) {
322 contextFailures += 1;
323 }
324 elapsed += delta;
325 calls += 1;
326 }
327
328 Atomic<size_t> calls;
329 Atomic<uint64_t> elapsed;
330 Atomic<size_t> contextFailures;
331
332 private:
333 Thread* m_Driver;
334};
335
336bool hostedTimerSplitDelivery() {
337 constexpr const char* Test = "hosted-timer-split-delivery";
338 Timer* timer = Machine::instance().getTimer();
339 TimerContextProbe probe(Processor::information().getCurrentThread());
340 bool passed =
341 check(timer && timer->registerHandler(&probe), Test, "timer handler registration failed");
342 passed &= check(waitUntil([&probe] { return probe.calls.value() >= 3; }), Test,
343 "the hard timer source did not reach its threaded callback");
344
345 const bool removed = timer && timer->unregisterHandler(&probe);
346 const size_t callsAfterRemoval = probe.calls.value();
347 const Time::Timestamp drainDeadline = Time::getTicks() + (10 * Time::Multiplier::Millisecond);
348 while (Time::getTicks() < drainDeadline) {
350 }
351 passed &= check(removed && callsAfterRemoval >= 3 && probe.calls.value() == callsAfterRemoval &&
352 probe.elapsed.value() && !probe.contextFailures,
353 Test, "threaded delivery context or unregister drain failed");
354
355 if (passed) {
356 NOTICE("HOSTED-IRQ-CLOSURE: PASS hosted-timer-split-delivery");
357 }
358 return passed;
359}
360
361constexpr size_t ExpectedGuardDenials = 7;
362
363struct GuardContext {
364 GuardContext() : calls(0), overflow(0), operations() {}
365
366 size_t calls;
367 size_t overflow;
368 DeviceHardIrqOperation operations[ExpectedGuardDenials];
369};
370
371GuardContext* g_GuardContext = nullptr;
372
373bool denyHardIrqOperation(DeviceHardIrqOperation operation) {
374 GuardContext* context = __atomic_load_n(&g_GuardContext, __ATOMIC_ACQUIRE);
375 if (!context) {
376 return false;
377 }
378 const size_t call = __atomic_fetch_add(&context->calls, static_cast<size_t>(1), __ATOMIC_RELAXED);
379 if (call < ExpectedGuardDenials) {
380 context->operations[call] = operation;
381 } else {
382 context->overflow = 1;
383 }
384 return true;
385}
386
387bool hardIrqOperationGuards() {
388 constexpr const char* Test = "hard-irq-operation-guards";
389 GuardContext context;
390 Semaphore semaphore(0);
391 WaitQueue waitQueue;
392 __atomic_store_n(&g_GuardContext, &context, __ATOMIC_RELEASE);
393 Processor::setDeviceHardIrqOperationHookForTest(denyHardIrqOperation);
394 const size_t denialsBefore = Processor::deviceHardIrqOperationDenialsForTest();
395
396 bool marked = false;
397 bool hardContext = false;
398 bool scheduleReturned = false;
399 bool semaphoreAcquireDenied = false;
400 bool semaphoreReleaseDenied = false;
401 bool waitDenied = false;
402 bool wakeDenied = false;
403 bool allocateDenied = false;
404 const bool interruptsWereEnabled = Processor::getInterrupts();
406 {
407 size_t previousDepth = 0;
408 bool restorationArmed = false;
409 DeviceHardIrqContext hardIrq(previousDepth, restorationArmed);
410 marked = Processor::inDeviceHardIrq() && Processor::deviceHardIrqDepthForTest() == 1;
411 hardContext = Processor::executionContext() == ExecutionContext::HardDeviceIrq;
412
414 scheduleReturned = true;
415
416 Semaphore::SemaphoreError error = Semaphore::NoError;
417 semaphoreAcquireDenied =
418 !semaphore.acquireWithError(1, 0, 0, error) && error == Semaphore::Interrupted;
419 semaphore.release(1);
420 semaphoreReleaseDenied = semaphore.getValue() == 0;
421
422 {
423 auto guard = waitQueue.acquire();
424 waitDenied = guard.wait() == WaitQueue::WakeReason::Spurious;
425 }
426 wakeDenied = waitQueue.wakeAll(WaitQueue::WakeReason::Signalled) == 0;
427 allocateDenied = SlamAllocator::guardedAllocateForTest(64) == 0;
428 SlamAllocator::guardedFreeForTest(0);
429 }
430 Processor::setInterrupts(interruptsWereEnabled);
431
432 Processor::setDeviceHardIrqOperationHookForTest(nullptr);
433 __atomic_store_n(&g_GuardContext, static_cast<GuardContext*>(nullptr), __ATOMIC_RELEASE);
434 const size_t denialsAfter = Processor::deviceHardIrqOperationDenialsForTest();
435
436 const DeviceHardIrqOperation expected[ExpectedGuardDenials] = {
437 DeviceHardIrqOperation::Schedule, DeviceHardIrqOperation::SemaphoreAcquire,
438 DeviceHardIrqOperation::SemaphoreRelease, DeviceHardIrqOperation::WaitQueueAccess,
439 DeviceHardIrqOperation::WaitQueueAccess, DeviceHardIrqOperation::HeapAllocate,
440 DeviceHardIrqOperation::HeapFree,
441 };
442 bool operationsMatch = context.calls == ExpectedGuardDenials && !context.overflow;
443 for (size_t i = 0; i < ExpectedGuardDenials && operationsMatch; ++i) {
444 operationsMatch = context.operations[i] == expected[i];
445 }
446
447 const bool passed = check(
448 marked && hardContext && scheduleReturned && semaphoreAcquireDenied &&
449 semaphoreReleaseDenied && waitDenied && wakeDenied && allocateDenied && operationsMatch &&
450 denialsAfter - denialsBefore == ExpectedGuardDenials && !Processor::inDeviceHardIrq() &&
451 Processor::executionContext() == ExecutionContext::WaitableThread &&
452 waitQueue.waiterCount() == 0 && semaphore.getValue() == 0,
453 Test, "a forbidden operation escaped or the hard context leaked");
454 if (passed) {
455 NOTICE("HOSTED-IRQ-CLOSURE: PASS hard-irq-operation-guards");
456 }
457 return passed;
458}
459} // namespace
460
461bool runHostedIrqClosureRegressions() {
462 NOTICE("HOSTED-IRQ-CLOSURE: BEGIN");
463 bool passed = hardIrqOperationGuards();
464 passed &= threadedDispatcherLifecycle();
465 passed &= registryCallbackId();
466 passed &= splitHandlerLifecycle();
467 passed &= hostedTimerSplitDelivery();
468 passed &= runHostedInterruptManagerRegressions();
469 passed &= runHostedSchedulerRegressions();
470 if (passed) {
471 NOTICE("HOSTED-IRQ-CLOSURE: PASS all");
472 } else {
473 ERROR("HOSTED-IRQ-CLOSURE: FAIL suite");
474 }
475 system_reset();
476 return passed;
477}
bool publishThreadedDispatch(uint8_t irq, size_t dispatchGeneration)
bool registerThreadedHandler(uint8_t irq, IrqHandler *handler)
UnregisterResult unregisterHandler(uint8_t irq, IrqHandlerBase *handler)
void invalidateThreadedLine(uint8_t irq, size_t throughGeneration)
bool dispatchThreaded(uint8_t irq, size_t dispatchGeneration, ThreadedDispatchResult &result, IrqHandler *onlyHandler=nullptr, irq_id_t callbackId=0)
virtual IrqDisposition irq(irq_id_t number)=0
virtual Timer * getTimer()=0
static bool getInterrupts()
static ProcessorInformation & information()
static bool inDeviceHardIrq()
Definition Processor.h:581
static ExecutionContext executionContext()
Definition Processor.cc:109
static void setInterrupts(bool bEnable)
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:236
virtual void rearmIrqSources(size_t work)=0
virtual bool quiesceIrqSources()=0
virtual HardStageDisposition hardIrq(irq_id_t number, InterruptState &state, size_t &work)=0
virtual void threadedIrq(size_t work)=0
virtual void timer(uint64_t delta)=0
MUST_USE_RESULT WakeReason wait(const Channel &channel=Channel(), size_t debugState=0, uintptr_t debugAddress=0, StackDiscardCleanup onStackDiscard=nullptr, void *stackDiscardContext=nullptr)
Definition WaitQueue.cc:104
IrqDisposition
Definition IrqHandler.h:31
DeviceHardIrqOperation
Definition Processor.h:46