The Pedigree Project 0.1
mutex-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/LockGuard.h"
10#include "pedigree/kernel/Log.h"
11#include "pedigree/kernel/Spinlock.h"
12#include "pedigree/kernel/process/ConditionVariable.h"
13#include "pedigree/kernel/process/Mutex.h"
14#include "pedigree/kernel/process/Scheduler.h"
15#include "pedigree/kernel/process/Thread.h"
16#include "pedigree/kernel/processor/Processor.h"
17#include "pedigree/kernel/processor/ProcessorInformation.h"
18#include "pedigree/kernel/time/Time.h"
19
20namespace {
21struct MutexOwnershipContext {
22 MutexOwnershipContext(Mutex* mutex, ConditionVariable* condition)
23 : mutex(mutex),
24 condition(condition),
25 nonOwnerReleaseRejected(0),
26 conditionWaitRejected(0),
27 timedAcquireFinished(0),
28 timedAcquireTimedOut(0),
29 timedAcquireSucceeded(0) {}
30
31 Mutex* mutex;
32 ConditionVariable* condition;
33 Atomic<size_t> nonOwnerReleaseRejected;
34 Atomic<size_t> conditionWaitRejected;
35 Atomic<size_t> timedAcquireFinished;
36 Atomic<size_t> timedAcquireTimedOut;
37 Atomic<size_t> timedAcquireSucceeded;
38};
39
40struct MutexGuardContext {
41 MutexGuardContext(Mutex* mutex, bool constexprGuard)
42 : mutex(mutex),
43 worker(nullptr),
44 constexprGuard(constexprGuard),
45 phase(0),
46 entered(0),
47 ownedInCritical(0),
48 terminalPending(0),
49 deferredInCritical(0),
50 releaseTransitions(0),
51 releaseSawDeferral(0),
52 releasedAfterScope(0),
53 returned(0) {}
54
55 Mutex* mutex;
57 bool constexprGuard;
58 Atomic<size_t> phase;
59 Atomic<size_t> entered;
60 Atomic<size_t> ownedInCritical;
61 Atomic<size_t> terminalPending;
62 Atomic<size_t> deferredInCritical;
63 Atomic<size_t> releaseTransitions;
64 Atomic<size_t> releaseSawDeferral;
65 Atomic<size_t> releasedAfterScope;
66 Atomic<size_t> returned;
67};
68
69MutexGuardContext* g_MutexGuardContext = nullptr;
70
71Atomic<size_t> g_AcquireTransitionSeen(0);
72Atomic<size_t> g_ReleaseTransitionSeen(0);
73Atomic<size_t> g_TransitionInterruptFailures(0);
74
75void mutexTransitionHook(Semaphore::MutexTransitionWindow window) {
77 g_TransitionInterruptFailures += 1;
78 }
79
80 if (window == Semaphore::MutexCounterAcquired) {
81 g_AcquireTransitionSeen += 1;
82 } else if (window == Semaphore::MutexOwnerReleased) {
83 g_ReleaseTransitionSeen += 1;
84 }
85}
86
87void mutexGuardTransitionHook(Semaphore::MutexTransitionWindow window) {
88 MutexGuardContext* context = g_MutexGuardContext;
89 Thread* thread = Processor::information().getCurrentThread();
90 if (!context || thread != context->worker || window != Semaphore::MutexOwnerReleased ||
91 context->phase != static_cast<size_t>(1)) {
92 return;
93 }
94
95 context->releaseTransitions += 1;
96 if (thread->isTerminationDeferred()) {
97 context->releaseSawDeferral += 1;
98 }
99}
100
101bool check(bool condition, const char* detail, const char* test = "mutex-ownership") {
102 if (condition) {
103 return true;
104 }
105
106 ERROR("HOSTED-WAIT-TEST: FAIL " << test << ": " << detail);
107 return false;
108}
109
110bool mutexCompletionPreservesInterruption() {
111 constexpr const char* Test = "mutex-completion-preserves-interruption";
112 Thread* thread = Processor::information().getCurrentThread();
113 const Thread::InterruptionReason originalInterruption = thread->getInterruptionReason();
114 const bool originalInterrupts = Processor::getInterrupts();
115 const Thread::InterruptionReason reasons[] = {Thread::NotInterrupted, Thread::InterruptedBySignal,
116 Thread::InterruptedByTimeout};
117 const bool interruptStates[] = {false, true};
118 Mutex mutex;
119 bool passed = true;
120
121 for (bool interrupts : interruptStates) {
122 Processor::setInterrupts(interrupts);
123 for (Thread::InterruptionReason reason : reasons) {
124 thread->setInterruptionReason(reason);
125 const bool acquired = mutex.acquireForCompletion();
126 const bool owned = mutex.isOwnedByCurrentThread() && mutex.getValue() == 0;
127 const bool preserved =
128 thread->getInterruptionReason() == reason && Processor::getInterrupts() == interrupts;
129 if (acquired) {
130 mutex.release();
131 }
132 thread->setInterruptionReason(originalInterruption);
133 passed &=
134 check(acquired && owned && preserved && !mutex.isOwnedByCurrentThread() &&
135 mutex.getValue() == 1 && Processor::getInterrupts() == interrupts,
136 "immediate completion lost ownership, interruption, or interrupt state", Test);
137 }
138 }
139 Processor::setInterrupts(originalInterrupts);
140
141 if (passed) {
142 NOTICE("HOSTED-WAIT-TEST: PASS mutex-completion-preserves-interruption");
143 }
144 return passed;
145}
146
147void observeGuardedCriticalSection(MutexGuardContext* context) {
148 Thread* thread = Processor::information().getCurrentThread();
149 context->entered += 1;
150 context->ownedInCritical = context->mutex->isOwnedByCurrentThread() ? 1 : 0;
151 context->terminalPending = thread->getUnwindState() == Thread::TerminateThread ? 1 : 0;
152 context->deferredInCritical = thread->isTerminationDeferred() ? 1 : 0;
153 context->phase = 1;
154}
155
156int acquireTerminalMutexGuard(void* parameter) {
157 MutexGuardContext* context = reinterpret_cast<MutexGuardContext*>(parameter);
158 Thread* thread = Processor::information().getCurrentThread();
159 if (context->constexprGuard) {
160 ConstexprLockGuard<Mutex, true> guard(*context->mutex);
161 observeGuardedCriticalSection(context);
162 } else {
163 LockGuard<Mutex> guard(*context->mutex);
164 observeGuardedCriticalSection(context);
165 }
166
167 context->phase = 2;
168 context->releasedAfterScope =
169 !context->mutex->isOwnedByCurrentThread() && context->mutex->getValue() == 1 ? 1 : 0;
171 context->returned += 1;
172 return 0;
173}
174
175bool waitForMutexGuardBlock(Thread* thread) {
176 for (size_t i = 0; i < 10000; ++i) {
177 Thread::WaitDebugInfo wait = {};
178 uintptr_t debugAddress = 0;
179 if (thread->getWaitDebugInfo(wait) && wait.queued &&
180 thread->getDebugState(debugAddress) == Thread::SemWait) {
181 return true;
182 }
184 }
185 return false;
186}
187
188bool terminalMutexGuardScenario(bool constexprGuard) {
189 Mutex mutex;
190 MutexGuardContext context(&mutex, constexprGuard);
191 const bool supervisorAcquired = mutex.acquireForCompletion();
192
193 Thread* worker = new Thread(Scheduler::instance().getKernelProcess(), acquireTerminalMutexGuard,
194 &context, nullptr, false, true);
195 if (constexprGuard) {
196 worker->setName("hosted constexpr terminal Mutex guard");
197 } else {
198 worker->setName("hosted terminal Mutex guard");
199 }
200 context.worker = worker;
201
202 const bool queued = waitForMutexGuardBlock(worker);
203 const bool enteredBeforeRelease = context.entered != static_cast<size_t>(0);
204 g_MutexGuardContext = &context;
205 Semaphore::setMutexTransitionHook(mutexGuardTransitionHook);
206 worker->setUnwindState(Thread::TerminateThread);
207 if (supervisorAcquired) {
208 mutex.release();
209 }
210 const bool joined = worker->joinForCompletion();
211 Semaphore::setMutexTransitionHook(nullptr);
212 g_MutexGuardContext = nullptr;
213
214 const bool recoverable = mutex.tryAcquire();
215 if (recoverable) {
216 mutex.release();
217 }
218
219 return supervisorAcquired && queued && !enteredBeforeRelease && joined && context.entered == 1 &&
220 context.ownedInCritical == 1 && context.terminalPending == 1 &&
221 context.deferredInCritical == 1 && context.releaseTransitions == 1 &&
222 context.releaseSawDeferral == 1 && context.releasedAfterScope == 1 &&
223 context.returned == 1 && recoverable;
224}
225
226bool mutexGuardTerminalCompletion() {
227 constexpr const char* Test = "mutex-guard-terminal-completion";
228 bool passed = true;
229 passed &= check(terminalMutexGuardScenario(false),
230 "LockGuard did not retain ownership and teardown deferral", Test);
231 passed &= check(terminalMutexGuardScenario(true),
232 "ConstexprLockGuard did not retain ownership and teardown deferral", Test);
233
234 Mutex conditionalMutex;
235 {
236 LockGuard<Mutex> guard(conditionalMutex, false);
237 passed &= check(!guard.ownsLock() && !conditionalMutex.isOwnedByCurrentThread() &&
238 conditionalMutex.getValue() == 1,
239 "condition=false acquired or claimed the mutex", Test);
240 }
241
242 Mutex disownedMutex;
243 {
244 LockGuard<Mutex> guard(disownedMutex);
245 const bool acquired = guard.ownsLock() && disownedMutex.isOwnedByCurrentThread();
246 disownedMutex.release();
247 guard.disown();
248 passed &= check(acquired && !guard.ownsLock() && !disownedMutex.isOwnedByCurrentThread() &&
249 disownedMutex.getValue() == 1,
250 "disown did not transfer release responsibility", Test);
251 }
252
253 if (passed) {
254 NOTICE(
255 "HOSTED-WAIT-TEST: PASS "
256 "mutex-guard-terminal-completion");
257 }
258 return passed;
259}
260
261int attemptNonOwnerOperations(void* parameter) {
262 MutexOwnershipContext* context = reinterpret_cast<MutexOwnershipContext*>(parameter);
263
264 const bool ownedBeforeRelease = context->mutex->isOwnedByCurrentThread();
265 context->mutex->release();
266 if (!ownedBeforeRelease && !context->mutex->isOwnedByCurrentThread() &&
267 context->mutex->getValue() == 0) {
268 context->nonOwnerReleaseRejected += 1;
269 }
270
271 ConditionVariable::Error error = ConditionVariable::NoError;
272 const bool waited = context->condition->wait(*context->mutex, error);
273 if (!waited && error == ConditionVariable::MutexNotLocked &&
274 !context->mutex->isOwnedByCurrentThread() && context->mutex->getValue() == 0) {
275 context->conditionWaitRejected += 1;
276 }
277
278 return 0;
279}
280
281int attemptTimedMutexAcquire(void* parameter) {
282 MutexOwnershipContext* context = reinterpret_cast<MutexOwnershipContext*>(parameter);
283 Semaphore::SemaphoreError error = Semaphore::NoError;
284 const bool acquired = context->mutex->acquireWithError(1, 0, 20000, error);
285 if (acquired) {
286 context->timedAcquireSucceeded += 1;
287 context->mutex->release();
288 } else if (error == Semaphore::TimedOut) {
289 context->timedAcquireTimedOut += 1;
290 }
291 context->timedAcquireFinished += 1;
292 return 0;
293}
294} // namespace
295
296bool runHostedSpinlockRegressions() {
297 constexpr const char* Test = "spinlock-interrupt-state";
298 const bool originalInterrupts = Processor::getInterrupts();
299 const bool interruptStates[] = {true, false};
300 bool passed = true;
301
302 Spinlock lock;
303 for (bool interrupts : interruptStates) {
304 Processor::setInterrupts(interrupts);
305 const bool initiallyUnlocked = !lock.acquired();
306 const bool acquired = lock.acquire();
307 const bool held =
308 lock.acquired() && !Processor::getInterrupts() && lock.interrupts() == interrupts;
309 lock.release();
310 const bool restored = !lock.acquired() && Processor::getInterrupts() == interrupts;
311 Processor::setInterrupts(originalInterrupts);
312 passed &= check(initiallyUnlocked && acquired && held && restored,
313 "ordinary acquire/release lost ownership or interrupt state", Test);
314 }
315
316 Spinlock outer;
317 Spinlock inner;
318 for (bool interrupts : interruptStates) {
319 Processor::setInterrupts(interrupts);
320 outer.acquire();
321 inner.acquire();
322 const bool nested = outer.acquired() && inner.acquired() && !Processor::getInterrupts() &&
323 outer.interrupts() == interrupts && !inner.interrupts();
324 inner.release();
325 const bool outerHeld = outer.acquired() && !inner.acquired() && !Processor::getInterrupts();
326 outer.release();
327 const bool restored = !outer.acquired() && Processor::getInterrupts() == interrupts;
328 Processor::setInterrupts(originalInterrupts);
329 passed &= check(nested && outerHeld && restored,
330 "nested locks restored interrupts before the outer release", Test);
331 }
332
333 Spinlock recursive;
334 for (bool interrupts : interruptStates) {
335 Processor::setInterrupts(interrupts);
336 recursive.acquire(Spinlock::allow_recursion);
337 recursive.acquire(Spinlock::allow_recursion);
338 recursive.acquire(Spinlock::allow_recursion);
339 const bool nested =
340 recursive.acquired() && !Processor::getInterrupts() && recursive.interrupts() == interrupts;
341 recursive.release();
342 const bool innerHeld =
343 recursive.acquired() && !Processor::getInterrupts() && recursive.interrupts() == interrupts;
344 recursive.release();
345 const bool outerHeld =
346 recursive.acquired() && !Processor::getInterrupts() && recursive.interrupts() == interrupts;
347 recursive.release();
348 const bool restored = !recursive.acquired() && Processor::getInterrupts() == interrupts;
349 Processor::setInterrupts(originalInterrupts);
350 passed &= check(nested && innerHeld && outerHeld && restored,
351 "recursive nesting lost the outer ownership or interrupt state", Test);
352 }
353
355 lock.acquire();
356 const bool savedInterrupts = lock.interrupts();
357 lock.exit();
358 const bool exitedMasked = !lock.acquired() && !Processor::getInterrupts();
359 lock.acquire();
360 const bool reacquiredMasked = lock.acquired() && !lock.interrupts();
361 lock.release();
362 const bool releasedMasked = !lock.acquired() && !Processor::getInterrupts();
363 Processor::setInterrupts(originalInterrupts);
364 passed &=
365 check(savedInterrupts && exitedMasked && reacquiredMasked && releasedMasked,
366 "exit restored interrupts or retained stale state for the next acquisition", Test);
367
368 // A constructed-locked lock has no acquisition for the tracker to retire.
369 Spinlock initiallyLocked(true, true);
371 const bool constructedLocked = initiallyLocked.acquired() && !initiallyLocked.interrupts();
372 initiallyLocked.release();
373 const bool initialRelease = !initiallyLocked.acquired() && !Processor::getInterrupts();
374 initiallyLocked.acquire();
375 const bool reacquired = initiallyLocked.acquired() && !initiallyLocked.interrupts();
376 initiallyLocked.release();
377 const bool finalRelease = !initiallyLocked.acquired() && !Processor::getInterrupts();
378 Processor::setInterrupts(originalInterrupts);
379 passed &= check(constructedLocked && initialRelease && reacquired && finalRelease,
380 "an initially locked lock could not be released and acquired again", Test);
381
382 if (passed) {
383 NOTICE("HOSTED-WAIT-TEST: PASS spinlock-interrupt-state");
384 }
385 return passed;
386}
387
388bool runHostedMutexRegressions() {
389 Mutex mutex;
390 ConditionVariable condition;
391 MutexOwnershipContext context(&mutex, &condition);
392 bool passed = true;
393
394 g_AcquireTransitionSeen = 0;
395 g_ReleaseTransitionSeen = 0;
396 g_TransitionInterruptFailures = 0;
397
398 const bool initialInterruptState = Processor::getInterrupts();
399
400 Semaphore::setMutexTransitionHook(mutexTransitionHook);
401 passed &= check(mutex.acquire(), "the supervisor could not acquire the mutex");
402 Semaphore::setMutexTransitionHook(nullptr);
403 passed &= check(mutex.isOwnedByCurrentThread() && mutex.getValue() == 0,
404 "acquisition did not publish a single current-thread owner");
405 passed &= check(mutex.getDebugMutexOwner() == Processor::information().getCurrentThread(),
406 "the debugger owner snapshot did not identify the mutex owner");
407 passed &= check(g_AcquireTransitionSeen == 1 && g_TransitionInterruptFailures == 0 &&
408 Processor::getInterrupts() == initialInterruptState,
409 "acquisition exposed its counter/owner transition to interrupts");
410
411 Thread* peer = new Thread(Scheduler::instance().getKernelProcess(), attemptNonOwnerOperations,
412 &context, nullptr, false, true);
413 peer->setName("hosted mutex ownership regression");
414
415 passed &= check(peer->join(), "the non-owner peer could not be joined");
416 passed &= check(context.nonOwnerReleaseRejected == 1, "a non-owner release changed the mutex");
417 passed &= check(context.conditionWaitRejected == 1,
418 "condition wait accepted a mutex owned by another thread");
419 passed &= check(mutex.isOwnedByCurrentThread() && mutex.getValue() == 0,
420 "the non-owner peer disturbed the supervisor's ownership");
421
422 Thread* timedPeer = new Thread(Scheduler::instance().getKernelProcess(), attemptTimedMutexAcquire,
423 &context, nullptr, false, true);
424 timedPeer->setName("hosted timed mutex acquisition regression");
425
426 const Time::Timestamp timedAcquireDeadline =
427 Time::getTicks() + (500 * Time::Multiplier::Millisecond);
428 while (!context.timedAcquireFinished && Time::getTicks() < timedAcquireDeadline) {
430 }
431
432 // If the timeout path regresses, release the mutex so the peer and test
433 // suite can finish and report the failure instead of hanging indefinitely.
434 const bool timedAcquireNeededRescue = !context.timedAcquireFinished;
435 if (timedAcquireNeededRescue) {
436 mutex.release();
437 }
438
439 passed &= check(timedPeer->join(), "the timed-acquire peer could not be joined");
440 const bool timedAcquirePassed = !timedAcquireNeededRescue && context.timedAcquireTimedOut == 1 &&
441 context.timedAcquireSucceeded == 0;
442 passed &= check(timedAcquirePassed, "a held Mutex ignored its acquisition timeout");
443 if (timedAcquirePassed) {
444 NOTICE("HOSTED-WAIT-TEST: PASS hosted-timer-timeout-cleanup");
445 }
446
447 if (timedAcquireNeededRescue) {
448 passed &=
449 check(mutex.acquire(), "the supervisor could not restore ownership after timeout rescue");
450 }
451 passed &= check(mutex.isOwnedByCurrentThread() && mutex.getValue() == 0,
452 "the timed-acquire peer disturbed the supervisor's ownership");
453
454 const bool releaseInterruptState = Processor::getInterrupts();
455 Semaphore::setMutexTransitionHook(mutexTransitionHook);
456 mutex.release();
457 Semaphore::setMutexTransitionHook(nullptr);
458 passed &= check(!mutex.isOwnedByCurrentThread() && mutex.getValue() == 1 &&
459 mutex.getDebugMutexOwner() == nullptr,
460 "owner release did not restore one available item");
461 passed &= check(g_ReleaseTransitionSeen == 1 && g_TransitionInterruptFailures == 0 &&
462 Processor::getInterrupts() == releaseInterruptState,
463 "release exposed its owner/counter transition to interrupts");
464
465 mutex.release();
466 passed &= check(!mutex.isOwnedByCurrentThread() && mutex.getValue() == 1,
467 "double release changed the mutex's binary count");
468
469 Semaphore counting(1, false);
470 const size_t acquireTransitions = g_AcquireTransitionSeen;
471 const size_t releaseTransitions = g_ReleaseTransitionSeen;
472 const bool countingInterruptState = Processor::getInterrupts();
473 Semaphore::setMutexTransitionHook(mutexTransitionHook);
474 passed &= check(counting.tryAcquire(), "the counting-semaphore control acquisition failed");
475 counting.release();
476 Semaphore::setMutexTransitionHook(nullptr);
477 passed &= check(g_AcquireTransitionSeen == acquireTransitions &&
478 g_ReleaseTransitionSeen == releaseTransitions &&
479 Processor::getInterrupts() == countingInterruptState,
480 "counting Semaphore entered a Mutex transition window");
481 passed &= check(Processor::getInterrupts() == initialInterruptState,
482 "thread join or mutex teardown lost the caller interrupt state");
483
484 passed &= mutexCompletionPreservesInterruption();
485 passed &= mutexGuardTerminalCompletion();
486
487 if (passed) {
488 NOTICE("HOSTED-WAIT-TEST: PASS mutex-ownership");
489 }
490 return passed;
491}
Definition Mutex.h:56
bool isOwnedByCurrentThread() const
Definition Mutex.cc:30
static bool getInterrupts()
static ProcessorInformation & information()
static void setInterrupts(bool bEnable)
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226
ssize_t getValue()
Definition Semaphore.cc:598
void release(size_t n=1)
Definition Semaphore.cc:546
void release()
Definition Spinlock.cc:161
bool acquire(bool recurse=false, bool safe=true)
Definition Spinlock.cc:35
void exit(uintptr_t ra=0)
Definition Spinlock.cc:157
void setUnwindState(UnwindType ut)
Definition Thread.cc:3628
@ Continue
No unwind necessary, carry on as normal.
Definition Thread.h:513
@ TerminateThread
Exit only this thread during Process exit.
Definition Thread.h:515
bool getWaitDebugInfo(WaitDebugInfo &info)
Definition Thread.cc:3184
bool join()
Definition Thread.cc:2767
UnwindType getUnwindState()
Definition Thread.h:531
DebugState getDebugState(uintptr_t &address)
Definition Thread.h:570
bool isTerminationDeferred() const
Definition Thread.h:565